First-boot provisioning

Bring up a fully configured install from one YAML file, and how to re-apply it.

Updated 4 min read Edit on GitHub
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#

  1. On boot, if PROVISIONING_FILE points at a YAML file and the settings.provisioned_at row is absent, the app applies the file.
  2. Blocks are processed in order: settings → roles → teams → zone_templates → clusters → pdns_servers → oidc → saml → ldap → demo_zones. References resolve by slug, so an oidc group mapping can point at a role defined earlier in the same file (settings.auth_default_provider is the one exception: it's written last so a bare slug can name a provider declared further down).
  3. On success it writes settings.provisioned_at = <timestamp>. Subsequent boots skip the file - from then on the admin UI is the source of truth.
  4. 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.
bash
# in your compose/k8s env
PROVISIONING_FILE=/etc/powerdns-authadmin/provisioning.yaml
PROVISION_ON_BOOT=true   # default; set false to skip
yaml
# mount it read-only where PROVISIONING_FILE points
volumes:
  - ./provisioning.yaml:/etc/powerdns-authadmin/provisioning.yaml:ro

It 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:

bash
# 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.

BlockWhat it createsNotes
version(guard)If set, the applier refuses to run unless the app version matches the prefix.
settingsGlobal settings rowsEvery 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.
rolesCustom rolesEntries 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.
teamsTeamsOwnership boundaries for scoped roles.
zone_templatesNew-zone scaffoldingSOA timers, kind, NS, prelude records, zone settings, metadata.
clustersBackend groupsA multi-primary cluster, or a primary with its secondaries. Peer-selection strategy. See Backends.
pdns_serversThe PowerDNS backendsGroup membership via cluster_slug; optional write_mode: read_only. Encrypted API keys.
oidcOIDC providers (+ group mappings)Same oidc_providers table the UI writes. See OIDC.
samlSAML providers (+ group mappings)Same saml_providers table the UI writes. See SAML.
ldapLDAP providers (+ group mappings)Same ldap_providers table the UI writes. See LDAP.
demo_zonesGenerated demo zonesFor showcasing a fresh stack; omit in production.

Cross-block rules worth knowing#

  • pdns_servers carry 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 same cluster_slug (a cluster declared in clusters: or already in the database). Exactly one row should be is_default.
  • pdns_servers: write_mode: read_only marks a backend that must never receive writes even though its /config looks writable - the database-replicated public nameserver case. It's excluded from peer selection and from every backend picker, and can't be is_default. Defaults to auto.
  • 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 in oidc_providers / saml_providers / ldap_providers). Slugs are unique across all three. The oidc block 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:

yaml
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:


← Docs index