First-boot provisioning
Bring up a fully configured install from one YAML file, and how to re-apply it.
On this page
Provisioning brings up a fully-configured install from a single YAML file - settings, custom roles, teams, zone templates, PowerDNS clusters and servers, OIDC / SAML / LDAP providers, and demo zones - with no clicking. It's infrastructure-as-code for the app's own configuration, ideal for reproducible deployments.
The canonical, every-field reference is
provisioning.example.yaml. This page explains
how it runs and how to think about it; reach for the example file for exact
keys.
How it runs#
- On boot, if
PROVISIONING_FILEpoints at a YAML file and thesettings.provisioned_atrow is absent, the app applies the file. - Blocks are processed in order:
settings → roles → teams → zone_templates → clusters → pdns_servers → oidc → saml → ldap → demo_zones. References resolve by slug, so anoidcgroup mapping can point at a role defined earlier in the same file (settings.auth_default_provideris the one exception: it's written last so a bare slug can name a provider declared further down). - On success it writes
settings.provisioned_at = <timestamp>. Subsequent boots skip the file - from then on the admin UI is the source of truth. - Parse errors abort the boot. A malformed file means the app refuses to start rather than half-applying - fail loud, fix the YAML, retry. Unknown keys at any level are rejected, so typos fail fast instead of silently no-op'ing.
# in your compose/k8s env
PROVISIONING_FILE=/etc/powerdns-authadmin/provisioning.yaml
PROVISION_ON_BOOT=true # default; set false to skip# mount it read-only where PROVISIONING_FILE points
volumes:
- ./provisioning.yaml:/etc/powerdns-authadmin/provisioning.yaml:roIt runs once - how to re-apply#
The provisioned_at sentinel makes provisioning a one-shot. To force a re-apply,
delete the sentinel row and restart:
# Postgres
psql "$DATABASE_URL" -c "DELETE FROM settings WHERE key='provisioned_at';"
# SQLite
sqlite3 /data/powerdns_authadmin.db "DELETE FROM settings WHERE key='provisioned_at';"On re-apply, providers/servers/roles are created or updated by slug - the applier never deletes. Anything you added in the UI that isn't in the file stays.
Secrets in the file#
PowerDNS api_keys, OIDC client_secrets, SAML SP private keys and LDAP bind
passwords live in this file in plaintext and are encrypted with
APP_ENCRYPTION_KEY before they hit the database. Treat the file as sensitive:
chmod 600, owned by the app's runtime user, mounted read-only.- Prefer mounting from a secret store (Docker secret, K8s
Secret, Vault).
The blocks at a glance#
Every block is optional - drop what you don't need.
| Block | What it creates | Notes |
|---|---|---|
version | (guard) | If set, the applier refuses to run unless the app version matches the prefix. |
settings | Global settings rows | Every key the admin Settings page knows: site_name, brand_logo_url, support_contact, login_intro, login_lockout_threshold, login_lockout_seconds, allow_password_reset, auth_default_provider (local, <type>:<slug>, or a bare provider slug declared in this file), default_record_ttl. |
roles | Custom roles | Entries whose slug is a seeded system role (super-admin, team-owner, operator, zone-editor, read-only) are refused - the applier fails rather than overwrite a system role. See RBAC. |
teams | Teams | Ownership boundaries for scoped roles. |
zone_templates | New-zone scaffolding | SOA timers, kind, NS, prelude records, zone settings, metadata. |
clusters | Backend groups | A multi-primary cluster, or a primary with its secondaries. Peer-selection strategy. See Backends. |
pdns_servers | The PowerDNS backends | Group membership via cluster_slug; optional write_mode: read_only. Encrypted API keys. |
oidc | OIDC providers (+ group mappings) | Same oidc_providers table the UI writes. See OIDC. |
saml | SAML providers (+ group mappings) | Same saml_providers table the UI writes. See SAML. |
ldap | LDAP providers (+ group mappings) | Same ldap_providers table the UI writes. See LDAP. |
demo_zones | Generated demo zones | For showcasing a fresh stack; omit in production. |
Cross-block rules worth knowing#
pdns_serverscarry no role - a backend's primary/secondary nature is observed from its/config(ADR-0014), never declared. Group a primary with its secondaries, or the peers of a multi-primary cluster, by giving every member the samecluster_slug(a cluster declared inclusters:or already in the database). Exactly one row should beis_default.pdns_servers:write_mode: read_onlymarks a backend that must never receive writes even though its/configlooks writable - the database-replicated public nameserver case. It's excluded from peer selection and from every backend picker, and can't beis_default. Defaults toauto.clusters: whether a group renders as a multi-primary cluster or as primary + secondaries is derived from what its members report - every member writable means cluster; one writable plus mirrors means primary + secondaries.oidc/saml/ldap: these are the same mechanism as the Admin UI (rows inoidc_providers/saml_providers/ldap_providers). Slugs are unique across all three. Theoidcblock coexists with the read-only env (OIDC_*) provider; a DB provider with the same slug shadows the env one. Group mappings reference roles/teams/servers by slug. See the OIDC configuration paths.
Minimal example#
A standalone primary, one custom role, and one SSO provider:
settings:
site_name: "Acme DNS"
support_contact: "ops@acme.example"
roles:
- slug: zone-noc
name: NOC Zone Operator
requires_mfa: true
permissions: [zone.read, record.create, record.update, record.delete, audit.read]
pdns_servers:
- slug: primary-1
name: "Primary (prod)"
base_url: "https://pdns.acme.example/api/v1"
api_key: "REPLACE_ME_api_key"
is_default: true
oidc:
- slug: company-sso
name: "Company SSO"
issuer_url: "https://auth.acme.example/realms/acme"
client_id: "powerdns-authadmin"
client_secret: "REPLACE_ME_client_secret"
scopes: "openid profile email groups"
enabled: true
group_mappings:
- { group: pdns-admins, role: super-admin, scope: global }
- { group: pdns-noc, role: zone-noc, scope: global }For every available field with inline documentation, copy
provisioning.example.yaml.
Topology examples#
The repo ships ready-to-run Postgres-backed stacks demonstrating each topology, each with a matching provisioning file:
docker-compose-primary-secondaries.yml+provisioning.primary-secondary.example.yamldocker-compose-multi-primary.yml+provisioning.multi-primary.example.yamldocker-compose-combined.yml+provisioning.combined.example.yaml