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.
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:
- 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.
- Real RBAC. Five system roles plus org-defined custom roles. Permissions span ~60 actions across zones, records, DNSSEC, TSIG, metadata, autoprimaries, templates, users, teams, servers, API tokens, audit, and OIDC providers. Assignments scope to global / team / zone / server.
- Auth. Local accounts (Argon2id), generic OIDC with PKCE + per-provider group→role mapping,
TOTP MFA (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_hintflow lands you on the IdP's signed-out screen. - 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.yamlapplied on first boot: settings, custom roles, teams, zone templates, PDNS clusters + servers, demo zones, OIDC providers (with group mappings). Seeprovisioning.example.yamlfor an exhaustive reference. - Observability. Pino structured logs (secret-redacted), Prometheus
/metrics,/healthzliveness,/readyzreadiness (gated on DB + migration version). - 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.
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 — 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.
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.
Full gallery — every page, four variants: screenshots/README.md.
The app ships as a single image — ghcr.io/powerdns-authadmin/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 gets you clicking around in ~2 minutes; the Installation guide covers a real production deploy.
A throwaway SQLite stack with a bundled PowerDNS and 10 pre-seeded demo zones:
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.exampledirectly, which ships public throwaway secrets. Don't expose it.
For a real deployment — SQLite or Postgres, TLS, backups, and the boot sequence — follow the Installation guide. It's four copy-paste steps and the canonical source of truth (the demo above is evaluation-only).
Store
APP_SECRET_KEY/APP_ENCRYPTION_KEYonce in a persistent.envnext to your compose file — never shellexports. Exports vanish when the shell closes, and a regeneratedAPP_ENCRYPTION_KEYmakes every stored PowerDNS API key, OIDC secret, and MFA secret undecryptable. Generate once, back the.envup, never change them.
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.
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.
# 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.
Every variable is documented in .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. For SSO setup (env vs provisioning vs UI), see OIDC single sign-on.
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-primary-secondaries.yml,
docker-compose-multi-primary.yml.
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 + testFull workflow + troubleshooting in docs/dev-setup.md.
Full guides live in docs/ — start at the
documentation index.
| Guide | Purpose |
|---|---|
| Quickstart | Run the demo stack end-to-end in ~2 minutes. |
| Installation | Production install — SQLite or Postgres, TLS, backups, upgrades. |
| Configuration | Every environment variable, grouped and explained. |
| Connecting PowerDNS backends | Primaries, secondaries, and multi-primary clusters. |
| OIDC single sign-on | SSO with group → role mapping (env vs provisioning vs UI). |
| First-boot provisioning | Bring up a configured install from one YAML file. |
| Roles & permissions (RBAC) | Roles, the permission vocabulary, and scopes. |
| Hardening & best practices | Lock down a production deployment. |
| Upgrading | Move to a new version safely. |
| Troubleshooting | Fix startup errors and backend connectivity. |
| Doc | Purpose |
|---|---|
docs/FEATURES.md |
The full feature catalog with module pointers. |
docs/dev-setup.md |
Local development workflow. |
docs/adr/ |
Architecture Decision Records — why the codebase is shaped this way. |
provisioning.example.yaml |
Exhaustive provisioning reference. |
.env.example |
Documented environment variables. |
CONTRIBUTING.md |
Code standards, testing, security, perf budgets. |
SECURITY.md |
Vulnerability reporting policy. |
CLAUDE.md |
Guidance for AI coding agents working on this repo. |
Production-ready. Deploy on SQLite or Postgres with the published image; see
Run it. Released versions and changes are in
CHANGELOG.md; the roadmap is tracked in GitHub issues —
contributions welcome, read CONTRIBUTING.md first.