Upgrading
Move to a new version safely, with the notes that apply to each release.
On this page
PowerDNS-AuthAdmin ships as a single image and runs its own database migrations on boot, so upgrading is usually "pull a new tag and recreate the container." This page covers doing it safely.
Before you upgrade#
- Read the CHANGELOG for the target version - note any breaking changes.
- Back up the database (and your
APP_ENCRYPTION_KEY):- SQLite: copy the DB file / snapshot the data volume.
- Postgres:
pg_dump. See Installation → Backups.
- Pin a version tag in production rather than
:latest, so deploys are deterministic and you know exactly what you're moving from and to.
Upgrade#
docker compose pull # fetch the new image tag
docker compose up -d # recreate the app container
docker compose logs -f app # watch migrations applyOn boot the entrypoint runs migrations, then the seed (idempotent), then any provisioning (skipped after first boot), then starts the server. Migration logs are intentionally loud - you'll see the pending list and the applied names. If a migration fails, the container refuses to start rather than running on a half-migrated schema; fix the cause and restart.
Verify#
GET /readyzreturns 200 (the server only starts listening after the entrypoint has applied migrations, so a 200 means the new schema is in place and the database is reachable).- Sign in; check Admin → PowerDNS servers shows backends Reachable, and the dashboard has no unexpected attention banners.
Version-specific notes#
Upgrading to 1.9.1 (from 1.9.0)#
No migration, no schema change, no config change - pull the new tag and recreate the container. The Zone name input on Create zone keeps focus while you type (it dropped it after the first character). See the CHANGELOG.
Upgrading to 1.9.0 (from 1.8.4)#
No schema change, no migration. Pull the new tag and recreate the container. Three things to know:
- New: MCP server.
POST /api/mcpis live for any user with a personal access token; it exposes only what the token's owner may already do. Nothing to configure - see Natural-language DNS with MCP for client setup. - SQLite installs that upgraded through 1.1.0 may have lost data at that
upgrade: the
pdns_serverstable rebuild in migration 0003 cascaded intozone_grants(per-zone permissions for users and teams),metric_samplesandpdns_server_stats(dashboard history), and detachedpdns_requestsrows from their backend. Postgres installs are unaffected. The migration runner is fixed so this cannot recur; the lost rows cannot be recovered by the app. Check Admin → Users (and Teams) for per-zone grants you expect to see and re-create any that are missing. Metrics history refills over the retention window. - Backup / restore now works.
GET /api/admin/backup/exportreturned 500 on Postgres whenever the audit log had rows, andPOST /api/admin/backup/restoreinserted nothing while reporting success. Backups taken with earlier versions are empty or incomplete - take a fresh export after upgrading.
Behaviour changes worth knowing: backends with write_mode = read_only now
reject every write (previously only the pickers steered away from them);
passwordless passkey sign-in requires user verification; a lost session bounces
straight to the default OIDC/SAML provider instead of showing the local form
(/login?force-local=1 still does). New optional setting
PDNS_REQUEST_LOG_RETENTION_DAYS (default 7). See the
CHANGELOG.
Upgrading to 1.8.4 (from 1.8.3)#
No migration, no schema change, no config change - pull the new tag and recreate the container. Fixes the misaligned inline edit strip on MX / SRV / CAA and other structured record types. See the CHANGELOG.
Upgrading to 1.8.3 (from 1.8.2)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. Records can now be edited in place (Enter on a row),
the records table and all select menus are keyboard-drivable (type T on the
type menu to jump to TXT), and the record editor's number fields no longer snap
to 0 when cleared. See the CHANGELOG.
Upgrading to 1.8.2 (from 1.8.1)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. GET .../dnssec now reports mirror sync reliably when
nobody has the app open, and returns LUA/ALIAS warnings for unsigned zones so
it works as a pre-flight check. See the
CHANGELOG.
Upgrading to 1.8.1 (from 1.8.0)#
No migration, no schema change, no config change - pull the new tag and recreate the container. Enabling DNSSEC now also gets secondaries onto the new serial on PowerDNS 4.6-4.8 (a follow-up NOTIFY once the metadata cache expires), and the DS set to publish no longer includes SHA-1. See the CHANGELOG.
Upgrading to 1.8.0 (from 1.7.0)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. Sync checks now compare each secondary against the
serial the primary actually serves (after SOA-EDIT), so SOA-EDIT and signed
zones stop showing as DESYNCED. A zone whose secondary is only behind on the
weekly SOA-EDIT rollover shows as "refresh due" until its next SOA refresh. The
DNSSEC tab gains Enable / Rectify / Disable, and adding a single key now
rectifies the zone. New API routes:
GET|POST|DELETE /api/admin/pdns/zones/{zone}/dnssec,
PUT .../rectify and GET .../cryptokeys. See
04-BACKENDS § DNSSEC for the rollout order and the
CHANGELOG.
Upgrading to 1.7.0 (from 1.6.2)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. Patches the open dependency advisories (including a
critical next RCE), lets a team-scoped Team Owner open the Teams list, and
adds an optional Default TTL for new records setting (Admin → Settings, or
default_record_ttl in the provisioning YAML). Unset, new records still start
at 3600. A zone can override the setting with X-AUTHADMIN-DEFAULT-TTL
metadata. See the CHANGELOG.
Upgrading to 1.6.2 (from 1.6.1)#
No migration, no schema change, no config change - pull the new tag and recreate the container. Fixes a hydration race that made sign-in fail with "Invalid request body." on a first visit when the visitor typed faster than the page's JavaScript loaded. See the CHANGELOG.
Upgrading to 1.6.1 (from 1.6.0)#
No migration, no schema change, no config change - pull the new tag and recreate the container. Fixes misleading wording on two record-editor validation warnings (underscore labels, unrecognized CAA tags). See the CHANGELOG.
Upgrading to 1.6.0 (from 1.5.6)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. Clears every open Dependabot alert: next
(two critical RCEs), sharp, nodemailer (four advisories), js-yaml,
@simplewebauthn/server and the vitest dev-dependency trio all move to their
patched releases. See the CHANGELOG for
the full list.
Upgrading to 1.5.6 (from 1.5.5)#
No schema change, but there IS a migration -
0007_granular_zone_permissions_backfill is data-only: it rewrites permission
lists, adds no tables and no columns. Pull and recreate:
docker compose pull
docker compose up -d
docker compose logs app | grep migrate # expect 0007_… in the applied listWhat changed. Editing a zone's records and editing the zone itself are no
longer the same permission. soa.read / soa.update gate the SOA tab,
zone.settings.read gates the Zone settings tab (writing it stays
zone.update), and record.update.apex-ns is now required - on top of the
matching record.* - to write the zone's apex NS. Missing a read permission
hides the tab outright. See
RBAC → Splitting record editing from a zone's authority.
Your own roles keep working. The migration backfills every operator-defined role, zone grant and API token that held the permission which used to imply the new one:
| Held before | Gains |
|---|---|
zone.read | soa.read, zone.settings.read |
record.update | soa.update |
any record.* write | record.update.apex-ns |
An API token with an EMPTY scope list is untouched - empty already means
"everything the user currently holds". Nothing you have configured loses access
on upgrade; tightening a role is now an edit you make deliberately, by
unticking soa.update (and record.update.apex-ns, and zone.settings.read)
on it.
The seeded system roles DO change, because they are re-upserted from
lib/rbac/default-roles.ts on every boot:
- Zone Editor loses
soa.updateandrecord.update.apex-ns. It keepssoa.readandzone.settings.read, so both tabs stay visible, read-only. This is the role to assign a hosting customer. - Operator and above gain both, alongside the
zone.updatethey already held.
If you rely on Zone Editor being able to rewrite the SOA, copy it to a custom
role with soa.update ticked and reassign - system-role permissions can't be
edited in place by design.
Rolling back. 1.5.5 has the same schema, so a downgrade is a straight image swap. The backfilled permissions simply become strings 1.5.5 doesn't recognise (the ability builder logs and skips unknown names), and the old implicit behaviour returns.
Upgrading to 1.5.5 (from 1.5.4)#
No schema change - nothing to watch in the boot log beyond the usual "no pending migrations". Pull and recreate:
docker compose pull
docker compose up -dIf you have imported zonefiles before, check what landed. Every version up
to and including 1.5.4 stripped parentheses out of quoted record data on import
and collapsed runs of whitespace inside quoted strings, silently and without a
diagnostic. PowerDNS LUA records are the ones to look at first - a Lua
expression is nothing but function calls, so "ifportup(443, {...})" was stored
as "ifportup 443, {...} " and does not work. Quoted TXT values containing
parentheses were affected the same way.
Re-importing an affected zone on 1.5.5 parses it correctly, so the fix for a mangled record is to re-import from the original zonefile (or fix the record by hand in the editor). Nothing is rewritten automatically on upgrade - AuthAdmin has no way to tell a corrupted value from one an operator meant to type.
LUA records now need Lua enabled on the backend to import. Zonefile import
previously skipped the enable-lua-records check that the record editor
enforces. If you import a zonefile containing LUA records onto a backend
without enable-lua-records set, that zone now fails with an explanatory
message instead of silently creating records PowerDNS will not serve. Other
zones in the same import are unaffected.
Rolling back. 1.5.4 has the same schema, so a downgrade is a straight image swap - though it reintroduces the import corruption.
Upgrading to 1.5.4 (from 1.5.3)#
This release adds a table - zone_horizons, for the new split-horizon zone
classification. It's the first schema change since 1.5.0, so unlike the last few
upgrades there is a migration to watch:
docker compose pull
docker compose up -d
docker compose logs -f app # expect: 0006_nice_butterfly (Postgres) or
# 0006_petite_the_santerians (SQLite)The migration creates an empty table and touches nothing else - no existing row
is read, rewritten, or locked, so it applies in milliseconds on any size of
install. On Postgres the entrypoint takes a pg_advisory_lock first, so
multi-replica deployments serialize their boots as usual.
Rolling back. 1.5.3 doesn't know about zone_horizons and never queries it,
so a downgrade works: the table sits unused and your classifications are still
there if you roll forward again. The boot log will show one more applied
migration than that version's journal expects, which is logged but not treated
as an error (the migrator only refuses to start on pending migrations, never
on extra ones). Drop the table manually only if you're sure you won't roll
forward.
Everything else is a pull-and-recreate:
- Split-horizon zones. Nothing to configure - every existing zone is
public, exactly as it behaved before. Tick This is an internal zone on a zone's Zone settings tab (or when adding one) to classify it; the zones list then shows an internal and a public zone of the same name as separate rows instead of hiding one as a duplicate. - Lua capability badge. Backends now show a
lua recordsbadge whenenable-lua-recordsis on. It reads from the cached capability snapshot, so after changingpdns.confre-probe the backend (Admin → PowerDNS servers → Refresh) for the badge - and the record editor'sLUAtype - to update. Any backend probed before this upgrade shows nothing until its next probe. js-yaml4.3.1 andpostcss8.5.25 for the advisories noted in the CHANGELOG.
Upgrading to 1.5.3 (from 1.5.2)#
No migration and no code change to your data - pull the new tag and recreate the
container. Adds PowerDNS Lua record editing (only where the daemon has Lua
enabled) and bumps next to 16.2.12 and postcss to 8.5.18 for the advisories
noted in the CHANGELOG.
To use Lua records, enable them on the PowerDNS host - either globally with
enable-lua-records in pdns.conf, or per zone with
pdnsutil set-meta <zone> ENABLE-LUA-RECORDS 1. AuthAdmin cannot set that flag
itself (PowerDNS does not expose it for writing over the API) and reflects
whichever you chose.
Upgrading to 1.5.2 (from 1.5.1)#
No migration and no code change - pull the new tag and recreate the container.
Pins sharp forward to ^0.35.3 to clear the inherited libvips CVEs in
GHSA-f88m-g3jw-g9cj.
Upgrading to 1.5.1 (from 1.5.0)#
No migration, no schema change, no config change - pull the new tag and
recreate the container. Fixes three defects that predate 1.5.0: Test
connection crashing the result panel on a reachable backend, the favicon
rendering inconsistently in Safari, and recurring /favicon.ico prerender-cache
errors in the container logs.
Upgrading to 1.5.0 (from 1.4.3)#
One additive migration, no breaking changes - pull the new tag and recreate the
container as above. The migration adds pdns_servers.write_mode with a default of
auto, so every existing backend keeps its current behaviour and no backend
becomes read-only on its own.
Act on this release only if you run a hidden primary whose zones are Native
and replicate to public nameservers through the database rather than AXFR. Those
public nodes are indistinguishable from standalone primaries over the PowerDNS
API, so until now the peer-selection strategy could route writes to them and the
read-only database user would reject the change. Open each public node under
Admin → PowerDNS servers, tick Never write to this backend (read-only),
and save - writes then land only on the primary. In provisioning YAML the
equivalent is write_mode: read_only on the server row.
Every other topology needs no action.
This release also bumps js-yaml, dompurify, and brace-expansion to clear
open advisories, and tightens the OIDC icon-URL preview in the admin form. See
the CHANGELOG for the impact assessment on
each.
Upgrading to 1.4.3 (from 1.4.2)#
No migration and no breaking changes - pull the new tag and recreate the container as above. The release improves TSIG key workflows by showing and editing zone correlations on TSIG keys, selecting eligible keys when creating or importing Primary / Master zones, and applying import-time transfer keys through the same primary-plus-secondaries validation used by Add Zone.
SETTINGS_RO=true now also blocks Settings Backup & Restore. Export and restore
return 403 from the API while the UI shows the read-only lock and disables the
download, upload, and restore controls. Normal deployments with SETTINGS_RO
unset keep existing behaviour.
Upgrading to 1.4.2 (from 1.4.1)#
No migration, no breaking changes, and no config or permission changes - pull the
new tag and recreate the container as above. The release only swaps the in-app
favicon to the static {} SVG the marketing site uses so the browser-tab icon
matches; nothing operational changes.
Upgrading to 1.4.1 (from 1.4.0)#
No migration and no breaking changes - pull the new tag and recreate the container as above. The release fixes create-zone template auto-selection for clustered / grouped primaries, clarifies the backend picker's group label, and adds an opt-in Settings read-only lock.
Optional - lock the Settings page for public demos (SETTINGS_RO)#
If you run a publicly-reachable demo where visitors may hold a settings-capable
role, set SETTINGS_RO=true to make the entire admin Settings page read-only: every
runtime-mutable setting is frozen and PATCH /api/admin/settings returns 403 even
for holders of settings.write. Leave it unset (false, the default) for normal
deployments - existing behaviour is unchanged. It needs no companion variable.
Upgrading to 1.4.0 (from 1.3.x)#
No migration and no breaking changes - pull the new tag and recreate the container as above. The release adds an opt-in demo guard rail and dashboard fixes.
Optional - lock the bootstrap admin for public demos (BOOTSTRAP_ADMIN_RO)#
If you run a publicly-reachable demo with a published login, set
BOOTSTRAP_ADMIN_RO=true (and make sure BOOTSTRAP_ADMIN_EMAIL is set). This
freezes that account's own password, email, name, MFA / passkey, enable/disable,
delete, and role assignments, so a visitor can't hijack or lock out the shared
login. It changes nothing else the account can do. Leave it unset (false, the
default) for normal deployments - existing behaviour is unchanged. When enabled,
the seed provisions the account already password-compliant, so the first login
isn't trapped on a change-password screen it can never clear.
Upgrading to 1.3.0 (from 1.2.x)#
A feature-pile release: WebAuthn (passkeys, primary + 2FA), SAML 2.0 SP,
LDAP direct-bind, teams per-zone grants, session-scoped IdP-derived
permissions with live token recompute, the unified /admin/authentication
admin surface, and a super-admin Backup & Restore wizard. The migration is a
single SQL file per dialect (drizzle/0004_low_luminals.sql +
drizzle-sqlite/0004_black_storm.sql) and runs automatically at app boot -
no manual schema steps.
The items below need operator attention.
Permission rename - oidc.* → auth.*#
The oidc.read / oidc.manage permission strings are renamed to auth.read /
auth.manage since the same gates now cover OIDC, SAML, and LDAP at the
unified /admin/authentication surface.
What the migration does for you: existing roles.permissions arrays are
rewritten in place - every "oidc.read" becomes "auth.read", every
"oidc.manage" becomes "auth.manage". Seeded system roles + custom
operator-defined roles both get the update.
What you need to do: nothing in the typical case. If you provision roles
declaratively via provisioning.yaml, update your YAML to use the new
permission names - the applier won't fail on the old names, but they'll be
silently dropped (they're no longer in the master vocabulary).
Admin URL renames - /admin/oidc-providers → /admin/authentication/oidc#
| Old | New |
|---|---|
/admin/oidc-providers | /admin/authentication/oidc |
/admin/oidc-providers/<id> | /admin/authentication/oidc/<id> |
/admin/saml-providers | /admin/authentication/saml |
/admin/saml-providers/<id> | /admin/authentication/saml/<id> |
/admin/ldap-providers | /admin/authentication/ldap |
/admin/ldap-providers/<id> | /admin/authentication/ldap/<id> |
The old URLs are not redirected - they 404. Update bookmarks, runbooks and
any external links to the new paths. The internal API routes
(/api/admin/oidc-providers/... etc.) are not moved - they're a stable
contract for external automation.
IdP-derived permissions move from role_assignments to sessions#
The breaking-ish bit: rows in role_assignments tagged with provider_id
(i.e. they came from an OIDC group-sync) are deleted by the migration. They
re-materialise into the user's new sessions.derived_permissions JSONB column
on their next sign-in.
Why: persisting IdP-derived rows left stale state for users removed from
groups who never signed in again. The new model keeps a derived-perms snapshot
on the session row; tokens live-recompute against the IdP at use time (LDAP
service-account search, OIDC refresh-token → userinfo) bounded by
IDP_PERMS_CACHE_TTL_SECONDS (default 60s) or fall back to the latest session
snapshot up to TOKEN_IDP_FALLBACK_TTL_SECONDS (default 24h) when the IdP
can't be reached. See issue #85 for the full design.
What operators experience:
- Local-auth users: nothing changes.
- SSO users with an active session at upgrade time: the session keeps its admin-issued permissions. They temporarily lose IdP-derived perms until they sign in again, which re-materialises the snapshot. Sign-out / sign-in once after the upgrade.
- SSO users with API tokens: same - token use falls back to admin-issued perms until the user re-signs-in (then within 60s the live recompute kicks in for LDAP / OIDC sessions).
New env vars (both optional, sensible defaults):
# How old the latest session's IdP-derived snapshot may be before tokens drop
# the IdP-derived slice (default 24h).
TOKEN_IDP_FALLBACK_TTL_SECONDS=86400
# Cache window for the live IdP-perms recompute (LDAP/OIDC). Lower → tighter
# freshness, more IdP load. Default 60s.
IDP_PERMS_CACHE_TTL_SECONDS=60OIDC sessions: enable offline_access for live token recompute#
To get the OIDC live-recompute path (refresh-token → userinfo at API-token use
time), the IdP must include offline_access in the scope on the authorization
request - already the default for new OIDC providers configured under
/admin/authentication/oidc. Existing OIDC sessions created before the
upgrade have no refresh token stored; their tokens use the session-snapshot
fallback until the user signs in fresh. Existing OIDC operators should sign
out + sign in once after the upgrade to enjoy the live-recompute path.
Session TTL is now bounds-checked#
SESSION_TTL_SECONDS is clamped to [300, 2592000] (5 minutes – 30 days) at
boot. If you'd set a value outside that window (almost certainly a typo), the
app now refuses to start with a clear error rather than silently logging
everyone out or accumulating session rows without a ceiling. Default is
unchanged (43200 = 12h).
Audit-action vocabulary changes#
| Old | New |
|---|---|
auth.oidc.group_sync.assignment_added | removed (no per-row events now) |
auth.oidc.group_sync.assignment_removed | removed |
auth.oidc.group_sync.mapping_unresolved | auth.group_sync.mapping_unresolved |
auth.oidc.linked | auth.idp.linked |
auth.oidc.rejected_provisioning | auth.idp.rejected_provisioning |
auth.saml.linked | auth.idp.linked |
auth.saml.rejected_provisioning | auth.idp.rejected_provisioning |
auth.ldap.rejected_provisioning | auth.idp.rejected_provisioning |
Existing audit rows are untouched - old action names stay on the rows written under them. The change only affects new rows. Audit dashboards that filter on the old names should be updated.
Backup & Restore (super-admin only)#
A new Backup & Restore wizard at /admin/settings/backup exposes a JSON
export of the app DB and a merge-mode restore (insert-missing-rows only,
guarded by a typed confirmation phrase). Permission: the new system.backup,
default-granted only to the seeded super-admin role. The export excludes
PDNS zone data and the symmetric secrets (APP_SECRET_KEY /
APP_ENCRYPTION_KEY); encrypted columns export as ciphertext, useless without
the encryption key on the restore target.
Zone Import / Export#
A new Import / Export hub at /admin/import-export (under PowerDNS →
Zones) lets operators paste/upload BIND zonefiles to create zones in bulk, and
download selected zones as a single BIND-format bundle. zone.read to view,
zone.create to import. No operator action required - it's additive.
Upgrading to 1.2.1 (from 1.2.x)#
A build-pipeline patch - drop-in upgrade. No schema, no API, no operator-config changes. Pull-and-recreate the container:
docker compose pull && docker compose up -dWhat changes:
- Image is significantly smaller. ~1.18 GB → ~290 MB local (-75%); the compressed pull from GHCR drops from ~225 MB to ~80 MB (-65%). Pull time on a rolling deploy drops accordingly.
- No
tsx, no/app/scripts/,/app/lib/, or/app/tsconfig.jsonin the runtime image. Boot stages run pre-bundled ESM files under/app/boot/{migrate,seed,provision}.js. - Runner is now distroless. Base swapped from
node:24-bookworm-slimtogcr.io/distroless/nodejs24-debian12:nonroot. No shell, no apt, no package manager in the runtime image - onlynode, glibc, openssl, ca-certificates. - Container user is
nonroot(uid 65532), replacingnode(uid 1000). - Next.js built-in image optimizer is disabled (
images.unoptimized: true);<Image>tags still render at intrinsic size. :latestfollows releases, notmain.:edgetracksmain. See Installation → Image tags.
NOTE - operator-facing trade-offs (read before upgrading a production deployment):
- No shell in the runtime container.
docker exec <id> shis unavailable; the distroless base ships only the Node binary. For incident triage that needs a shell, build a:debugtag against the same builder stage with a bookworm-slim runner. Day-to-day operations (logs,/healthz,/readyz, env reload, image upgrades) are unaffected.- Container user changed from
node(uid 1000) tononroot(uid 65532). If you'd manually chown'd a host-mounted/datavolume to uid 1000 prior, re-chown it to 65532 before the first boot of v1.2.1. The compose examples shipped in this repo don't pin a uid and need no change.
If you mounted anything inside the image at one of the dropped paths
(/app/scripts, /app/lib, /app/tsconfig.json) for custom tooling,
that's the only meaningful breakage - those are build-time artefacts
and shouldn't be mounted in production, but the upgrade now enforces
it.
Upgrading to 1.2.0 (from 1.1.x)#
A minor release - no schema migration, no API changes, drop-in upgrade. There is one new env var with a behaviour-change default, and a fix for operators running standalone PowerDNS.
NOTE: This release introduces PDNS_BACKGROUND_POLLING which defaults to false.
If you currently rely on the SYNCED/DESYNCED chip, the dashboard
"PowerDNS metrics" tab, the per-zone "Sync" or "Statistics" tabs, the
servers-list Sync column, the zones-list mirror column, or drift-derived
advisories in the bell, you MUST NOW ENABLE PDNS_BACKGROUND_POLLING=true
in your environment and restart the app to keep those surfaces working.
With the flag at its default (false), all of those features hide;
the rest of AuthAdmin (zones, records, DNSSEC, TSIG, autoprimaries,
audit, RBAC, OIDC, signup) is unchanged.
What also changes after the upgrade:
- Standalone PDNS Auth instances (no
primary=yesorsecondary=yesinpdns.conf) become usable as zone-create targets. Until 1.2.0, AuthAdmin was treating "no replication flag set" as "not writable" - hiding the backend from/zones/new's picker even though Test went green. After upgrading those backends appear on the create-zone page automatically; nothing to reconfigure. (Closes #57.) - Capability badge
none→standalonefor those backends. Same neutral tone, accurate label. - If you applied the
primary=yesworkaround mentioned in #57 to unblock zone creation on a standalone, you can now revert it frompdns.conf. The override was operationally harmless either way - no slaves to NOTIFY - but the badge will readstandalonecorrectly once it's gone.
Choosing your PDNS_BACKGROUND_POLLING value#
| Deployment shape | Set PDNS_BACKGROUND_POLLING to |
|---|---|
| Single PowerDNS server / standalone / homelab - no AXFR replication. | false (the new default - leave it) |
| Multiple standalone PowerDNS instances, none acting as primary/secondary. | false |
| One primary with one or more secondaries replicating its zones via AXFR. | true (strongly recommended) |
| Two or more PowerDNS instances forming a multi-primary cluster. | true (strongly recommended) |
With PDNS_BACKGROUND_POLLING=false (default):
- AuthAdmin contacts PDNS only in direct response to operator actions: page renders, Test, Refresh All, zone create/edit/delete, DNSSEC/TSIG/autoprimaries actions. No background traffic.
- The supplementary "replication-awareness" surfaces are hidden - see the list under "MUST NOW ENABLE" above.
- The dashboard renders the Admin view only; a small
(i)icon by the page heading explains how to enable polling. - Operator clicks on a direct URL to a gated feature get a red error toast naming the env var.
With PDNS_BACKGROUND_POLLING=true:
- The unified background poller runs on its 30 s / 60 s / 5 min cadences against every configured backend.
- All replication-awareness features are visible and live:
- SYNCED / DESYNCED chip in the top header (per-page where relevant; fleet-wide on most pages).
- Sync + Statistics tabs on every zone-detail page.
- Sync column on
/admin/servers. - Mirror column on
/zones. - PowerDNS metrics tab on the dashboard, including the live time-series charts (query rate, latency, cache hit, response composition by qtype/rcode/size).
- Drift-derived advisories surfaced in the bell.
- A boot-time log line confirms the mode and (when off) whether the configured fleet looks like it would benefit from being on.
At the moment of the upgrade decision: most single-PowerDNS / small
deployments will get a quieter, friendlier experience by leaving the
flag at its default. Multi-replica and clustered operators will want
to set it true immediately and restart so AuthAdmin's full
operational surface is back.
Upgrading to 1.1.5 (from 1.1.x)#
A security-hygiene patch. No app-code changes; no schema, API, or config changes. Pull-and-recreate.
Adds a defensive package.json overrides pin keeping size-sensor at
1.0.3 (the last clean version) - closes the resolver attack window for
the Mini Shai-Hulud npm campaign that hijacked size-sensor 1.0.4
/ 1.1.4 / 1.2.4 on 2026-05-19. PowerDNS-AuthAdmin was never
shipped with the affected versions; this release just locks the door.
See the Security Advisory
and MAL-2026-4153 / GHSA-gx6x-v325-85g4 for the threat detail.
Upgrading to 1.1.4 (from 1.1.x)#
A major operator-UX release - drop-in upgrade. No schema migration, no API changes, no config changes. Pull-and-recreate the container and you're done.
What changes when operators sign in:
- Mobile-first responsive shell. Every page is usable on a phone now.
Drawer-style navigation under
md; full sidebar frommd+. - Live status chip in the top bar - connection state plus a fleet-wide
sync verdict (
SYNCED/DESYNCED) on every page, with the per-page override on zone-detail / zones-list / servers preserved. - New animated SyncIndicator - concentric-ring glyph used everywhere a
sync state is displayed. Honours
prefers-reduced-motion. - Diff-before-apply on every record edit. Save is now two clicks: an inline edit, then a Review changes modal with the BIND-style diff, then Save.
- One-button theme toggle cycles light → dark → system.
- Every list is one DataTable. Mobile reflows to labelled cards; the desktop chrome is uniform across audit / zones / users / roles / teams / PDNS requests / OIDC / TSIG / autoprimaries / zone templates.
- CSS-only changes - no client-side breaking changes; saved theme preferences, sessions, MFA enrolments, API tokens all carry over.
If you've embedded our screenshots in your own docs, note that several
filenames changed in this release - the canonical set is now
screenshots/README.md.
Upgrading to 1.1.3 (from 1.1.x)#
A maintenance release - no schema migration, a plain pull-and-recreate, and
nothing operator-facing to change. It fixes per-zone grants on multi-primary
clusters (a grant on one peer now authorizes the zone on every peer), renames the
internal request middleware to the Next 16 proxy convention (transparent), and
re-pins the CI GitHub Actions to Node 24 (CI-only).
Upgrading to 1.1.2 (from 1.1.x)#
A security release - no schema migration, a plain pull-and-recreate. Two
behavioural changes that close enforcement gaps (they may surface as a new 403
for accounts that were previously able to bypass a gate via the API):
- Required-MFA and forced-password-change are now enforced on the API, not just in the browser. If a user's role requires MFA, or the user is flagged "must change password", their session is now refused on write routes until they enroll TOTP / change their password (the enrollment, change-password, and logout endpoints stay reachable). This previously only redirected the browser, so a direct API caller could skip it. No action needed unless you rely on required-MFA accounts driving the API without having enrolled - have them enroll.
- Privilege ceilings are enforced on more admin paths. Creating a user with an initial role, resetting another user's password, and removing another user's MFA now refuse to act beyond the actor's own global permissions. Operators using only the built-in roles are unaffected.
Upgrading to 1.1.1 (from 1.1.0)#
A maintenance + security release - no schema migration, so it's a plain pull-and-recreate. One behavioural change to be aware of:
- OIDC
requireEmailVerifiednow defaults totruefor newly-created DB-backed providers (account-takeover hardening). Existing provider rows keep their stored value, so nothing changes for them on upgrade - but if you have a provider withrequireEmailVerifiedset tofalse, audit it: confirm the IdP genuinely never emits theemail_verifiedclaim before keeping it off. See OIDC.
Everything else in this release (the security batch, the CSP and supply-chain
hardening, and the opt-in additions like self-service signup) is transparent on
upgrade - signup stays off unless you set SIGNUP_ENABLED=true.
Upgrading to 1.1.0 (from 1.0.x)#
One schema migration applies on first boot (0003_backend_caps_advisories_squash).
It is data-preserving - verified end-to-end against a real 1.0.x SQLite
deployment (servers, users, roles, audit history all retained).
Two behavioural points specific to this release (ADR-0014 / ADR-0017):
- Per-server
roleandprimary_idare retired. The migration drops those columns; a backend's primary/secondary nature is now observed from its/configand its primary↔secondary links derived from each zone'smasters[]. On the first boot after upgrade, every backend'scapabilitiesstart empty and are re-populated by the first poll (seconds), so backends may briefly show as "unknown" until that completes. A primary + secondaries that was wired only by the oldprimary_idpin re-derives automatically when the secondary's zones list the primary's advertised address - otherwise group them under Admin → Clusters (or set the primary's advertised addresses). - Keep your
APP_ENCRYPTION_KEY. Stored PDNS API keys and OIDC client secrets are decrypted with it; an upgrade that loses the key can't decrypt them (you'd seeUnsupported state or unable to authenticate datain the logs). This isn't new in 1.1.0, but the backup step above is the reminder.
If you run SQLite, this release also requires no manual steps - the squashed migration handles the column add/drop in one table rebuild (see ADR-0017 for the SQLite-specific detail).
Rollback#
Migrations are forward-only - there are no automated down-migrations. To roll back the application image you must also restore the database to its pre-upgrade backup, because a newer migration may have changed the schema in ways the older image doesn't understand.
# 1. stop the app 2. restore the DB backup 3. pin the previous image tag
docker compose down
# …restore Postgres dump / SQLite file…
docker compose up -dThis is why the pre-upgrade backup is non-negotiable: it's your only rollback path.
Multi-replica notes (Postgres)#
Several replicas can boot at once - the migration step takes a pg_advisory_lock
so exactly one applies migrations while the others wait. Because the server only
starts listening after that step, a replica that passes /readyz already has the
current schema. To run migrations as a separate pipeline step instead, set
MIGRATE_ON_BOOT=false and run npm run db:migrate before rolling the app.
For replicas > 1 you also need REDIS_URL set so auth rate limiting, reveal-once
tokens, and the realtime event-bus coordinate across replicas (sessions already do
- they're in Postgres). See ADR-0016 and the High availability compose example.