A PowerDNS web interface — fork of PowerDNS-Admin/PowerDNS-Admin, re-targeted at bare-metal Debian 13 with a tighter source tree, a strict-typed library layer, and a faster local-dev loop.
Originally PowerDNS-Admin by the upstream maintainers. All the hard work on the zone editor, the Knockout zone-record table, the OAuth/SAML/LDAP backends, and the JSON API came from there. This fork is downstream cleanup, URL polish, and operability changes; the underlying product is still PowerDNS-Admin and full credit goes to that project.
Project page: https://github.com/kynoci/powerdns-admin
Tested and developed on the latest Debian — Debian 13 (trixie)
with Python 3.13 (the trixie default). Upstream PowerDNS-Admin
predates the trixie release and several of its assumptions
(distutils, older requests, older Flask) needed updates to run
cleanly on the new stack. This fork tracks trixie as its primary
target. Newer PowerDNS Authoritative Server releases (4.x and 5.x)
are both supported via the configurable pdns_version setting.
Older OS releases — Debian 12, Ubuntu 22.04 etc. — should still work but aren't part of CI; if something is broken on them the fix is welcome but the master branch won't hold for them.
- Three-command local dev flow —
cd /opt/powerdns-admin && source .venv/bin/activate && ./run.py.run.pyauto-applies Alembic migrations on boot, andcreate_appauto-loads<repo_root>/config.pyif present, so noFLASK_APP/FLASK_CONFplumbing is needed for ordinary use. - Headless install / provisioning CLIs:
flask init-secrets— generates per-install randomSECRET_KEY/SALTto<instance>/secrets.py.flask create-admin --username … --email … --password …— create or promote a local Administrator (also resets the password if the user already exists).flask set-server-setting --api-url … --api-key … --pdns-ver …— seed the PowerDNS API connection settings without a human logging in.
- First-run security gate — the app refuses to serve traffic if
SECRET_KEY/SALT/SQLA_DB_PASSWORDstill match the source-tree defaults. Bypass for local dev withPDA_ALLOW_INSECURE_DEFAULTS=1orALLOW_INSECURE_DEFAULTS = Trueinconfig.py(logged loudly when active). - Scripted bare-metal install —
docs/install-001-powerdns-admin.shdoes PDA itself (system deps, venv, frontend assets, db upgrade, admin user, PowerDNS API settings, systemd unit). Pair it with one of the PowerDNS-auth scripts (install-002-pdns-auth-sqlite.shorinstall-004-pdns-auth-mysql.sh); addinstall-003-pdns-recursor.shif you want a local recursor. All idempotent.
The visible URL surface was reorganised so paths read like the UI labels. Old paths return 404; bookmarks need updating.
| Before | After |
|---|---|
/dashboard |
/zone/dashboard |
/domain/add |
/zone/create |
/domain/remove |
/zone/remove |
/admin/global-search |
/admin/search |
/admin/history |
/admin/activity |
/admin/server/statistics |
/admin/statistics |
/admin/server/configuration |
/admin/configuration |
/admin/manage-account |
/admin/account |
/admin/manage-user |
/admin/user |
/admin/manage-keys |
/admin/keys |
/admin/setting/basic |
/setting/config |
/admin/setting/dns-records |
/setting/record |
/admin/setting/pdns |
/setting/server |
/admin/setting/authentication |
/setting/auth |
- Sidebar redone as a Bootstrap-4 accordion: the three sections (Zone Management / Administration / Server Settings) auto-collapse each other so only one is open at a time. Section headers are bold; children are visually indented (drops to icon-only mode under the pushmenu toggle).
- Auto-derived breadcrumbs — every page renders its breadcrumb
from the first two URL segments (
Section ▸ Page). No more hand-written<ol class="breadcrumb">per template. - HTMX shipped in the asset bundle; the zone-editor changelog
navigation already uses
hx-boost. Wider HTMX adoption is staged.
- Python package renamed
powerdnsadmin→pdnsadmin. Every import and configuration reference moved; the product name ("PowerDNS-Admin") is preserved everywhere it was a label rather than an identifier. - Splits:
routes/api.py(1270 LOC) split intoroutes/api/{__init__,zones,apikeys,users,accounts,dnssec,proxy}.py. Settings panes moved offadmin_bpinto a dedicatedsetting_bp. Zone CRUD entry points moved offadmin/domaininto azone_bp. - OAuth/SAML providers stored under
current_app.extensions, not module-level globals. - Schema layer migrated from
limatomarshmallow; the generated OpenAPI spec at/api/v1/openapi.{json,yaml}now publishes typed schemas viaapispec. - Strict typing on
lib/—mypy --strictruns in CI on the pure-helper layer (errors, record diff, record builder, the external-provisioning helper,to_idna); the rest oflib/is in a relaxed list pending follow-up. - Setting model: explicit
ENV_PINNED_KEYSregistry. Keys that belong inapp.configonly (Flask-Mail, Flask-Session, bootstrap secrets, SAML transport, etc.) skip the DB lookup; the admin UI refuses to write them. - Test harness:
Makefile+scripts/setup-pdns-test.shbrings up pdns-auth (sqlite backend) for the integration suite. Tests partition cleanly intopytest -m unit(152 pure-function tests, ~0.3 s) andpytest -m integration(56 tests; needs live pdns). Wired into.github/workflows/test.yml.
A more detailed change log is in
docs/ROADMAP.md (each commit prefixed T1.X /
T2.X / T3.X maps to a tier in that plan).
Bring up PowerDNS authoritative (sqlite backend) and PDA:
sudo ./docs/install-002-pdns-auth-sqlite.sh # or install-004-pdns-auth-mysql.sh
sudo ./docs/install-001-powerdns-admin.shThe first script installs PowerDNS auth + the chosen DB backend on port 5300 with the API on 8081. The second installs system deps, creates the venv, builds the frontend, applies migrations, creates a local admin user, and writes the PDNS API connection settings.
Then:
# 1. Stop the running dev server
kill $(pgrep -f 'run.py')
# 2. Wipe the relocated venv
cd /opt/powerdns-admin/
rm -rf .venv
# 3. Recreate venv + install deps (uses the Makefile)
make install
# 4. Activate it
source .venv/bin/activate
# 5. Sanity check — shebang should now point at /opt/powerdns-admin
head -1 .venv/bin/flask
# 6. Create the admin
export FLASK_APP=pdnsadmin
flask create-admin --username admin --email admin@powerdns-admin.com --password adminpass
# 7. Start the dev server
./run.pyOpen http://127.0.0.1:9191/login and sign in with the credentials
the installer printed (admin / adminpass by default — change them
in docs/install-001-powerdns-admin.sh before re-running, or via
the admin UI).
docs/install-001-powerdns-admin.sh also installs a systemd unit
(pdnsadmin.service) that runs PDA under the venv on port 9191. For
a public deployment, terminate TLS at nginx and proxy to that port —
see docs/wiki/web-server/Running-PowerDNS-Admin-with-Systemd-Gunicorn-and-Nginx.md
for a worked example (the gunicorn launch command should target
pdnsadmin:create_app() instead of upstream's powerdnsadmin).
config.py at the repo root is your local config (gitignored). It
auto-loads after the in-tree defaults and before any FLASK_CONF
override. A minimal one for local dev:
import os
basedir = os.path.abspath(os.path.dirname(__file__))
SECRET_KEY = '<run flask init-secrets and paste from instance/secrets.py>'
SALT = '<same>'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
SESSION_TYPE = 'sqlalchemy'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')Or for a one-off local test:
ALLOW_INSECURE_DEFAULTS = True(Don't ship that to production.)
flask init-secrets writes random SECRET_KEY / SALT to
instance/secrets.py, which create_app loads automatically.
make install # one-shot venv + deps + dev tools
make test # unit + integration
make test-unit # pure-function tests (no Flask, no DB, no pdns)
make test-integration # needs live pdns-auth (run `make setup-pdns` once)
make lint # ruff
make lint-fix # ruff --fix
make typecheck # mypy --strict on pdnsadmin/lib/CI runs ruff + mypy + pytest -m unit on every PR; the
integration job spins up pdns-auth via a service container and runs
pytest -m integration.
- Upstream: PowerDNS-Admin/PowerDNS-Admin — the original project, where every feature in this fork's UI and API came from.
- This fork: https://github.com/kynoci/powerdns-admin
MIT — same as upstream. See LICENSE.
