# PowerDNS-AuthAdmin
> A modern, self-hosted DNS administration UI for PowerDNS Authoritative - first-class RBAC,
> audit log with diffs, SSO with group-driven role mapping, optimistic concurrency in the editor,
> and a UI built for teams that actually run multi-backend infrastructure.
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/ci.yml?query=branch%3Amain)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/releases/latest)
[](https://scorecard.dev/viewer/?uri=github.com/PowerDNS-AuthAdmin/powerdns-authadmin)
[](https://www.bestpractices.dev/projects/12974)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/pkgs/container/powerdns-authadmin)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/pkgs/container/powerdns-authadmin)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/pkgs/container/powerdns-authadmin)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/pkgs/container/powerdns-authadmin)
[](.nvmrc)
[](https://nextjs.org)
[](tsconfig.json)
**[Website](https://powerdns-authadmin.org/)**
**[Docs](https://powerdns-authadmin.org/docs/)**
**[Live demo](https://demo.powerdns-authadmin.org/)** (sign in with `admin@example.com` / `change-me-now`)
PowerDNS-AuthAdmin manages one or many PowerDNS Authoritative backends from a single web app. It
ships with a permissive RBAC engine, OIDC single sign-on with group→role mapping, transactional
zone editing, NOTIFY-aware sync probes for cluster + primary/secondary topologies, an append-only
audit log, scoped API tokens, and a YAML-driven first-boot provisioning system that brings up a
ready-to-use install without a single click.
**PowerDNS Auth version compatibility tests:**
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-46.yml)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-47.yml)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-48.yml)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-49.yml)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-50.yml)
[](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/actions/workflows/pdns-compat-51.yml)
## At a glance
- **Multi-backend.** One install fronts standalone primaries, primary + secondaries groups, and
multi-primary clusters. Backends are visible side-by-side; zones merge into one amalgamated
list. Per-cluster peer-selection strategies (round-robin / random / lowest-latency / least-load)
route reads and writes to a member of the cluster. Any backend can be marked **read-only** so
writes skip it - the escape hatch for public nameservers fed by database replication, which
PowerDNS itself reports as ordinary writable primaries.
- **Real RBAC.** Five system roles plus org-defined custom roles. Permissions span 54 actions
across zones, records, DNSSEC, TSIG, metadata, autoprimaries, templates, users, teams, servers,
API tokens, audit, and auth providers. Assignments scope to global / team / zone / server; a
role scoped to a zone (e.g. from an IdP group) grants that zone and nothing else.
A zone's authority is held apart from its records - the SOA, the apex NS and the Zone
settings tab each answer to their own permission, so a hosting customer can manage their
records without ever being shown the knobs that break the zone.
- **Auth.** Local accounts (Argon2id), generic OIDC with PKCE, SAML 2.0 and LDAP direct-bind
providers - each with per-provider group→role mapping - TOTP or passkey / security-key MFA
(WebAuthn, also usable as passwordless sign-in; greyed out for SSO-only users - the IdP is
the trust root), `pda_pat_` API tokens with per-token permission scopes.
- **RP-initiated logout.** OIDC sessions sign you out at the IdP, not just locally; the
`end_session_endpoint` + `id_token_hint` flow lands you on the IdP's signed-out screen.
- **Reverse DNS.** Optional auto-PTR: saving an A/AAAA record keeps its PTR in a hosted
reverse zone in sync (global, per-zone or per-save; never creates zones, never clobbers a
PTR that points elsewhere), plus a fleet-wide Reverse DNS page that diffs every address
against its PTR and rectifies the rows you pick. **Opt-in:** auto-PTR is the `auto_ptr`
setting; the Reverse DNS page and PTR chips need `REVERSE_DNS_ENABLED=true`.
- **Zone monitoring.** Every hosted zone checked from the outside on a schedule: delegation
at the parent (walked from the root servers, not through a resolver), nameserver
reachability, authority, serials and NS sets, open AXFR, DS versus DNSKEY, what public or
internal resolvers answer, SOA sanity and reverse-DNS drift. Stable finding codes with a
what-to-do, horizon-aware, bounded load, findings in the bell and a count in the sidebar.
**Opt-in:** set `ZONE_MONITOR_ENABLED=true` to turn it on (it sends DNS queries outside
your network).
- **Zones + records.** Per-RRset editor with diff-before-apply, zone cloning, zone templates
(NS + SOA timers + prelude records + zone-object settings + per-kind metadata), per-type
record validators, optimistic concurrency at the RRset level.
- **DNSSEC.** Cryptokey create / update / delete with per-key activity surfaced from the audit
log. Zone metadata management with diff history.
- **TSIG + autoprimaries.** Manage TSIG keys (read vs reveal split into separate permissions),
configure autoprimary registrations.
- **Cluster + sync.** A "cluster" is N writable PDNS peers behind a replicated store. Sync probe
for primary+secondaries: compare every secondary's serial against the primary, record-for-record
diff on demand. Sync probe for clusters: same UI shape, all peers compared against the
highest-serial peer as anchor.
- **Audit.** Append-only log of every write. Before/after JSONB snapshots redacted for known
secret fields. Per-zone history feed with chip-coloured action types. Operator-driven export.
- **Provisioning.** `provisioning.yaml` applied on first boot: settings, custom roles, teams,
zone templates, PDNS clusters + servers, OIDC / SAML / LDAP providers (with group mappings),
demo zones. See [`provisioning.example.yaml`](./provisioning.example.yaml) for an exhaustive
reference.
- **Observability.** Pino structured logs (secret-redacted), bearer-gated Prometheus `/metrics`,
`/healthz` liveness, `/readyz` readiness (200 when the database is reachable, 503 otherwise).
- **Natural-language DNS (MCP).** A built-in [Model Context Protocol](./docs/14-MCP.md) server
lets Claude Code, Codex, Cursor and any MCP client manage zones and records from a prompt,
with every call scoped to a personal access token and written to the audit log.
- **Self-contained.** One Docker image, no CDN, no telemetry phone-home. Migrations run inside
the app entrypoint; on Postgres they're serialized by an advisory lock so multi-replica boots
are safe.
The full feature catalog with module-level docs is in [`docs/FEATURES.md`](./docs/FEATURES.md).
## Screenshots
**Dashboard** - live PowerDNS stats, active sessions, and operator-attention surfaces.
**Multi-backend** - clusters, primary + secondaries groups, and standalone primaries side by side. Live sync state, drift advisories, and the dashboard PowerDNS-metrics tab are opt-in via [`PDNS_BACKGROUND_POLLING=true`](./docs/03-CONFIGURATION.md#pdns_background_polling) - recommended for replication topologies, off by default for standalone installs.
**Amalgamated zones** - every backend's zones in one searchable list with serial + per-row sync state.
**Per-RRset editor** - per-type structured editors with inline validation.
**Diff-before-apply** - every change previewed as a BIND-style before / after diff before it's written.
**Backend health** - bell-driven advisories for unreachable hosts, replication drift, missing TSIG keys, daemon-config drift.
**Append-only audit log** - redacted before/after snapshots, per-row PDNS HTTP trail, CSV export.
### Mobile-first
Every page is responsive down to a phone viewport - the off-canvas hamburger
drawer, the bell + theme + avatar cluster, and the record table all reflow
cleanly. Screenshots are rendered inside an iPhone 16 Pro bezel by
[`scripts/screenshots.mjs`](./scripts/screenshots.mjs).
Full gallery - every page, four variants:
[**screenshots/README.md**](./screenshots/README.md).
## Talk to your DNS
PowerDNS-AuthAdmin ships an **MCP server** at `/api/mcp`, so an AI assistant can manage your
zones the way you would describe it to a colleague:
> "Point `www` on example.com at 192.0.2.10 and add an AAAA for 2001:db8::10."
> "Which zones still have the old mail server? Replace their MX with mail.example.net."
> "Create staging.example.com from the template and give it the same NS set as example.com."
Three steps:
1. **Profile → API tokens → Create**, pick the scopes the assistant may use (for example
`zone.read` + `record.*`, or a single per-zone grant).
2. Connect your client to `https:///api/mcp` with `Authorization: Bearer pda_pat_…`:
```sh
claude mcp add --transport http powerdns https://dns.example.com/api/mcp \
--header "Authorization: Bearer pda_pat_xxxxxxxx"
```
Codex, Cursor and others take the same URL and header - see
[docs/14-MCP.md](./docs/14-MCP.md) for each client.
3. Ask.
The assistant gets nine task-shaped tools (`list_zones`, `list_records`, `set_records`,
`add_record`, `delete_records`, `create_zone`, …), never more power than the token's owner,
and every change lands in the audit log under that owner's name with the client's user agent.
Read-only tokens see a read-only server. Destructive tools are annotated so clients can ask
before running them.
## Run it
The app ships as a single image - **[`ghcr.io/powerdns-authadmin/powerdns-authadmin`](https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin/pkgs/container/powerdns-authadmin)**.
It runs on **SQLite** (single instance: homelab, eval, small teams) or **Postgres** (multi-instance,
write-concurrent). Migrations and the system-role seed run automatically on boot.
> New here? The [Quickstart](./docs/01-QUICKSTART.md) gets you clicking around in ~2 minutes; the
> [Installation guide](./docs/02-INSTALLATION.md) covers a real production deploy.
### Try it instantly - the minimal-demo stack
A throwaway SQLite stack with a bundled PowerDNS and 10 pre-seeded demo zones:
```sh
git clone https://github.com/PowerDNS-AuthAdmin/powerdns-authadmin.git
cd powerdns-authadmin
docker compose up -d
# → http://localhost:3000 (login: admin@example.com / change-me-now)
```
> ⚠️ Demo only: it reads `.env.example` directly, which ships public throwaway secrets. Don't expose it.
### Production
For a real deployment - SQLite or Postgres, TLS, backups, and the boot sequence -
follow the **[Installation guide](./docs/02-INSTALLATION.md)**. It's four copy-paste
steps and the canonical source of truth (the demo above is evaluation-only).
> Store `APP_SECRET_KEY` / `APP_ENCRYPTION_KEY` once in a persistent **`.env`** next to
> your compose file - **never** shell `export`s. Exports vanish when the shell closes, and
> a regenerated `APP_ENCRYPTION_KEY` makes every stored PowerDNS API key, OIDC secret, and
> MFA secret undecryptable. Generate once, back the `.env` up, never change them.
### High availability (replicas > 1)
To run more than one app replica, use **Postgres + Redis** and put a load balancer
in front. Sessions are already shared (they live in Postgres); setting `REDIS_URL`
makes the three remaining per-process pieces - auth rate limiting, one-time reveal
tokens, and the realtime SSE event bus - coordinate across replicas. Without Redis
those degrade to per-replica behaviour (looser rate limits, reveal tokens that only
work on their origin replica, live updates that don't cross replicas), so Redis is
**required** past one replica. See [ADR-0016](./docs/adr/0016-redis-horizontal-scale.md).
> SQLite is single-instance only - a file-backed DB isn't shared storage. HA means
> Postgres. The boot log says so if it detects the combination.
```yaml
# docker-compose.ha.yml - Postgres + Redis, app fronted by your load balancer.
services:
app:
image: ghcr.io/powerdns-authadmin/powerdns-authadmin:latest
restart: unless-stopped
# No host port - your load balancer (nginx/Traefik/cloud LB) fronts the replicas.
expose: ["3000"]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
environment:
APP_URL: https://dns.example.com
DATABASE_URL: postgres://pdns:${POSTGRES_PASSWORD}@postgres:5432/powerdns_authadmin
REDIS_URL: redis://redis:6379 # ← enables cross-replica coordination
APP_SECRET_KEY: ${APP_SECRET_KEY}
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY}
BOOTSTRAP_ADMIN_EMAIL: admin@example.com
BOOTSTRAP_ADMIN_PASSWORD: ${BOOTSTRAP_ADMIN_PASSWORD}
# Plain compose: `docker compose -f docker-compose.ha.yml up -d --scale app=3`.
# Swarm / k8s: set replicas in your orchestrator instead.
deploy:
replicas: 3
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: pdns
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: powerdns_authadmin
volumes: ["pg-data:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pdns -d powerdns_authadmin"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7-alpine
restart: unless-stopped
command: ["redis-server", "--save", "", "--appendonly", "no"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
pg-data:
```
> Migrations are serialized across replica boots by a Postgres advisory lock
> (ADR-0011), so starting N replicas at once is safe - only one applies migrations.
> Redis here is a coordination cache, not a datastore: persistence is off
> (`--save "" --appendonly no`) because nothing it holds needs to survive a restart.
### Configuration
Every variable is documented in [`.env.example`](./.env.example). The essentials:
| Variable | Required | What |
| ---------------------------------------------------- | ----------- | ---------------------------------------------------------------------- |
| `APP_URL` | ✅ | Public URL the app is served from, no trailing slash. |
| `APP_SECRET_KEY` | ✅ | Session / CSRF / token HMAC secret. `openssl rand -base64 32`. |
| `APP_ENCRYPTION_KEY` | ✅ | AES-256 key (base64, decodes to ≥32 bytes). `openssl rand -base64 32`. |
| `DATABASE_URL` | ✅ | `file:/data/powerdns_authadmin.db` (SQLite) or `postgres://…`. |
| `REDIS_URL` | optional | Enables cross-replica coordination - **required** for replicas > 1. |
| `BOOTSTRAP_ADMIN_EMAIL` / `_PASSWORD` | ⭐ | First SuperAdmin (password ≥12 chars). |
| `OIDC_*` | optional | SSO - or add providers in the UI instead. |
| `SMTP_*` | optional | Transactional email (verify-email, password reset). |
| `APP_PDNS_ALLOW_PRIVATE_NETWORKS` / `_INSECURE_HTTP` | situational | Allow internal-network / `http://` PDNS backends. |
| `APP_OIDC_ALLOW_PRIVATE_NETWORKS` / `_INSECURE_HTTP` | situational | Allow internal-network / `http://` OIDC issuers (SSRF guard). |
Full reference with every variable: **[Configuration](./docs/03-CONFIGURATION.md)**. For SSO setup
(env vs provisioning vs UI), see **[OIDC single sign-on](./docs/05-OIDC.md)**.
### Advanced topologies
To see a primary + secondaries group or a multi-primary cluster wired up end-to-end (Postgres-backed,
all official PowerDNS images), use the topology compose files:
[`docker-compose-combined.yml`](./docker-compose-combined.yml),
[`docker-compose-primary-secondaries.yml`](./docker-compose-primary-secondaries.yml),
[`docker-compose-multi-primary.yml`](./docker-compose-multi-primary.yml).
### Local development
```sh
nvm use && npm ci # Node 24 from .nvmrc
cp .env.example .env.local # set APP_SECRET_KEY + APP_ENCRYPTION_KEY
docker compose up pdns -d # a local PowerDNS to talk to
npm run dev # http://localhost:3000
npm run validate # lint + typecheck + format + test
```
Full workflow + troubleshooting in [`docs/dev-setup.md`](./docs/dev-setup.md).
## Documentation
Full guides live in **[`docs/`](./docs/)** - start at the
[documentation index](./docs/README.md).
| Guide | Purpose |
| ----------------------------------------------------- | ---------------------------------------------------------------- |
| [Quickstart](./docs/01-QUICKSTART.md) | Run the demo stack end-to-end in ~2 minutes. |
| [Installation](./docs/02-INSTALLATION.md) | Production install - SQLite or Postgres, TLS, backups, upgrades. |
| [Configuration](./docs/03-CONFIGURATION.md) | Every environment variable, grouped and explained. |
| [Connecting PowerDNS backends](./docs/04-BACKENDS.md) | Primaries, secondaries, and multi-primary clusters. |
| [OIDC single sign-on](./docs/05-OIDC.md) | SSO with group → role mapping (env vs provisioning vs UI). |
| [First-boot provisioning](./docs/06-PROVISIONING.md) | Bring up a configured install from one YAML file. |
| [Roles & permissions (RBAC)](./docs/07-RBAC.md) | Roles, the permission vocabulary, and scopes. |
| [Hardening & best practices](./docs/08-HARDENING.md) | Lock down a production deployment. |
| [Upgrading](./docs/09-UPGRADING.md) | Move to a new version safely. |
| [Troubleshooting](./docs/10-TROUBLESHOOTING.md) | Fix startup errors and backend connectivity. |
| [Passkeys & security keys](./docs/11-PASSKEYS.md) | WebAuthn as passwordless sign-in or second factor. |
| [LDAP sign-in](./docs/12-LDAP.md) | Authenticate against Active Directory / OpenLDAP. |
| [SAML 2.0 single sign-on](./docs/13-SAML.md) | SAML SSO with AD FS, Authentik, Keycloak. |
### Reference
| Doc | Purpose |
| ---------------------------------------------------------- | -------------------------------------------------------------------- |
| [`docs/FEATURES.md`](./docs/FEATURES.md) | The full feature catalog with module pointers. |
| [`docs/dev-setup.md`](./docs/dev-setup.md) | Local development workflow. |
| [`docs/adr/`](./docs/adr/) | Architecture Decision Records - why the codebase is shaped this way. |
| [`provisioning.example.yaml`](./provisioning.example.yaml) | Exhaustive provisioning reference. |
| [`.env.example`](./.env.example) | Documented environment variables. |
| [`CONTRIBUTING.md`](./CONTRIBUTING.md) | Code standards, testing, security, perf budgets. |
| [`SECURITY.md`](./SECURITY.md) | Vulnerability reporting policy. |
| [`CLAUDE.md`](./CLAUDE.md) | Guidance for AI coding agents working on this repo. |
## Status
**Production-ready.** Deploy on SQLite or Postgres with the published image; see
[Run it](#run-it). Released versions and changes are in
[`CHANGELOG.md`](./CHANGELOG.md); the roadmap is tracked in GitHub issues -
contributions welcome, read [`CONTRIBUTING.md`](./CONTRIBUTING.md) first.
## License
[](./LICENSE)