# ============================================================================= # PowerDNS-AuthAdmin - exhaustive first-boot provisioning template # ============================================================================= # # This file is the canonical "every setting at a glance" reference for the # YAML-driven IaC layer. Copy it, comment out what you don't need, and point # `PROVISIONING_FILE=/path/to/your.yaml` at it before first boot. # # How the applier runs: # # 1. On startup the app calls `applyProvisioning()` if `PROVISIONING_FILE` # is set AND the `settings.provisioned_at` row is absent. # 2. Each top-level block is processed in order: settings → roles → teams # → zone_templates → clusters → pdns_servers → oidc → saml → ldap # → demo_zones. Inter-block references resolve by slug (e.g. an oidc # group_mapping pointing to a role defined a few blocks up just works). # 3. On success the applier writes `settings.provisioned_at = `. # Subsequent boots skip the file - the admin UI is the source of truth # from that point on. To force a re-apply on a running database: # # 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';" # 4. Parse errors abort startup. The app refuses to boot rather than # half-apply a malformed file - fail loud, fix the YAML, retry. # # Secrets policy: # # API keys (PDNS) and client secrets (OIDC) live in this file in plaintext # and are encrypted at rest with `APP_ENCRYPTION_KEY` before they hit the # database. Treat this file as sensitive: chmod 600, owner = the app's # runtime user, mounted read-only in containers. Prefer mounting from a # secret store (Docker secrets, K8s Secret, HashiCorp Vault). # # Every top-level section is optional. Drop the blocks you don't need. # Unknown keys at any level are rejected at parse time - typos fail fast # instead of silently becoming no-ops. # ============================================================================= # ----------------------------------------------------------------------------- # version (optional) # ----------------------------------------------------------------------------- # When set, the applier refuses to run unless the app's reported version # starts with this prefix. Belt-and-braces for "we changed the schema, your # IaC is now stale." Leave unset to opt out. # ----------------------------------------------------------------------------- # version: "0.1" # ----------------------------------------------------------------------------- # settings - global key/value writes to the `settings` table # ----------------------------------------------------------------------------- # Every key here mirrors the admin Settings page (see # lib/validators/settings.ts → KNOWN_SETTING_KEYS). Unknown keys reject at # parse time. Each key is independently optional. # ----------------------------------------------------------------------------- settings: # Plain-text site name. Shown in the nav and the login page . site_name: "My Org DNS" # Optional brand logo. Accepts an absolute https:// URL OR an inline # `data:image/...` URI. Inline data: is convenient but rows show up in # audit snapshots - keep it under ~1.5 MB or use an external URL. # brand_logo_url: "https://example.com/static/logo.png" # Support contact line shown on the login page footer + 5xx error pages. support_contact: "ops@example.com" # Optional intro paragraph shown above the sign-in form. Use it to # tell the operator which SSO provider to choose, or to call out # an outage. Max 2000 chars. login_intro: "Sign in with your company SSO. Reach out in #dns-ops for access." # Failed-login lockout policy. Defaults: 5 attempts in 900 seconds → 15-min # lockout. Both fields are independently tunable. login_lockout_threshold: 5 # int 1..100 - attempts before lockout login_lockout_seconds: 900 # int 60..86400 - lockout duration # Self-service password reset (the login page "Forgot password?" link). # Default true. Set false to hide the link and make the endpoint a no-op - # local accounts only; SSO users always reset through their IdP. allow_password_reset: true # TTL pre-filled when adding a record or a zone-template row. Default 3600. # A zone's X-AUTHADMIN-DEFAULT-TTL metadata overrides it for that zone. default_record_ttl: 300 # int 1..2147483647 # Keep PTR records in hosted reverse zones in sync when A/AAAA records are # saved or deleted. Default false. A zone's X-AUTHADMIN-AUTO-PTR metadata # (on / off) overrides it; the record editor offers a per-save checkbox. # Reverse zones are never created and a PTR pointing at another name is # never replaced without an explicit choice. auto_ptr: false # Default sign-in method. Decides which provider /login auto-redirects to # on a fresh visit. Three accepted forms: # # - "local" - show the form (the default; recovery via /login?force-local=1). # - bare slug, e.g. "company-sso" - resolved against the slugs reserved # in this file or already in the DB. Slugs are globally unique across # OIDC / SAML / LDAP, so a bare slug is unambiguous. # - typed-prefix, e.g. "oidc:company-sso" - equivalent; useful when # scripting against the DB directly. # # The applier resolves whatever form you write into the canonical # typed-prefix form before persisting. Edited at runtime from # /admin/authentication. auth_default_provider: "local" # Example bare-slug form (uncomment to make `company-sso` defined below # the default - operators hitting /login auto-redirect through its initiate): # auth_default_provider: "company-sso" # ----------------------------------------------------------------------------- # roles - custom (org-specific) roles # ----------------------------------------------------------------------------- # The 5 system roles (super-admin, team-owner, operator, zone-editor, # read-only) are seeded by `db:seed` and CANNOT be redefined here - the # applier will refuse a slug collision. Use this section only for the # custom roles your org needs on top. # # `permissions` is a list of strings drawn from lib/rbac/permissions.ts. # Unknown permission names are validated at sign-in (the CASL builder # logs + skips them). The full vocabulary, grouped: # # Zones: zone.read, zone.create, zone.update, zone.delete, # zone.export, zone.import, # zone.settings.read (see the Zone settings tab; writing it # is zone.update) # Records: record.read, record.create, record.update, record.delete, # record.update.apex-ns (EXTRA, on top of the matching # record.* one, to write the apex NS - # the zone's delegation. NS below the # apex is an ordinary record.) # SOA: soa.read (see the SOA tab), soa.update (edit it - this # REPLACES the record.* requirement, it is not additive) # DNSSEC: dnssec.read, dnssec.configure # Metadata: metadata.read, metadata.write # TSIG keys: tsig.read, tsig.manage (tsig.read = list-only; manage # = create/regenerate/reveal) # Autoprimary: autoprimary.manage # Templates: template.use, template.manage # Users: user.read, user.create, user.update, user.delete, # user.disable, user.reset-password # Teams: team.read, team.create, team.update, team.delete, # team.manage-members # Roles: role.read, role.create, role.update, role.delete, # role.assign # PDNS servers: server.read, server.create, server.update, server.delete # API tokens: token.read.own, token.create.own, token.delete.own, # token.read.all, token.delete.all # Audit/settings: audit.read, settings.read, settings.write # Auth IdPs: auth.read, auth.manage (OIDC, SAML, LDAP) # System: system.backup # # Leaving soa.read / zone.settings.read off a role HIDES those tabs entirely - # which is how you give a hosting customer their records without giving them # the zone's authority. See docs/07-RBAC.md. # ----------------------------------------------------------------------------- roles: - slug: zone-noc # lowercase kebab-case name: NOC Zone Operator description: "Read everything; write only records inside zones the NOC owns." requires_mfa: true # forces second-factor enrolment (TOTP or passkey) before any action permissions: - zone.read - record.create - record.update - record.delete - audit.read - slug: billing-readonly name: Billing (read-only) description: "Auditor access for the finance team." requires_mfa: false permissions: - zone.read - audit.read - server.read # ----------------------------------------------------------------------------- # teams - multi-team RBAC unit # ----------------------------------------------------------------------------- # A team is a named ownership boundary inside the app. Zones can be team- # scoped (Phase 5); role assignments can target a team (`scope: team:<slug>` # in oidc/group_mappings or in admin-issued assignments). # # Adding/removing members happens via the admin UI OR indirectly via OIDC # group → role mappings (a `scope: team:<slug>` assignment counts as # membership for visibility purposes). # ----------------------------------------------------------------------------- teams: - slug: noc # lowercase kebab-case name: Network Operations description: "On-call DNS team." contact: "NOC Lead <noc-lead@example.com>" mail: "noc@example.com" - slug: corp-it name: Corporate IT description: "Internal IT services." contact: "corp-it-lead@example.com" # `mail` is optional; omitted here on purpose to show it's not required. # ----------------------------------------------------------------------------- # zone_templates - scaffolding for new zones # ----------------------------------------------------------------------------- # Applied at zone-creation time. The create-zone form preselects a template # when the operator picks a backend listed in `default_for_primary_slugs`. # Templates carry three layers of state, applied in this order: # # 1. PDNS createZone() - wire-format kind (Master/Slave/Native), seed # NS records, seed SOA via the soa_* timers. # 2. PUT /zones/<id> - zone-object settings (soa_edit, soa_edit_api, # api_rectify). See the PDNS docs for what each accepts. # 3. PUT /zones/<id>/metadata/<kind> - per-kind metadata bag. # # Failures in steps 2 + 3 are logged but don't abort the create - the zone # exists; the operator can fix metadata later from the Zone settings tab. # ----------------------------------------------------------------------------- zone_templates: - slug: internal # lowercase kebab-case name: Internal corp zones description: "Default scaffolding for internal-facing zones (corp.example, etc.)" # ------- SOA timers (seconds) ------- # Defaults shown match the schema's. Cap is min 60 / max int32 except # soa_expire which has a min of 3600. soa_ttl: 3600 # TTL on the SOA record itself soa_refresh: 3600 # secondary refresh interval soa_retry: 900 # secondary retry on refresh failure soa_expire: 604800 # secondary expiry if primary unreachable soa_minimum: 3600 # negative-cache TTL (RFC 2308) # ------- Wire-format zone kind ------- # PDNS still uses Master/Slave on the wire even though the modern docs # call them Primary/Secondary. The applier accepts either spelling. # Native means "no AXFR/IXFR to secondaries; stored locally only." # Master/Primary = source of truth; Slave/Secondary = read mirror. kind: Native # Native | Master | Slave | Primary | Secondary # ------- Seeded NS records ------- # Two is the RFC 2182 recommendation. List MUST be fully-qualified # (trailing dot). For Primary+Secondaries topologies, every Secondary's # supermaster-registered hostname (SELF_NS) MUST appear here so PDNS' # auto-secondary verification accepts the NOTIFY. nameservers: - ns1.internal.example. - ns2.internal.example. # ------- Seeded prelude records ------- # Applied alongside NS + SOA at create. `name` is relative to the apex # ("@" = apex, "www" = www.<zone>). `type` is the record type; `ttl` # in seconds; `content` is the wire-format RDATA. `disabled: true` # creates the record in disabled state. records: - { name: "@", type: TXT, ttl: 3600, content: '"v=spf1 -all"' } - { name: _dmarc, type: TXT, ttl: 3600, content: '"v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com"', } - { name: www, type: CNAME, ttl: 3600, content: "@" } - { name: legacy, type: A, ttl: 300, content: "192.0.2.10", disabled: true } # ------- Zone-object settings (PDNS PUT /zones/<id>) ------- # All optional. Omit a field to leave PDNS's default in place. # # soa_edit / soa_edit_api: SOA serial mutation policies. The most # common combo is `INCEPTION-INCREMENT` + `DEFAULT` which keeps the # serial monotonic across API edits. See: # https://doc.powerdns.com/authoritative/dnssec/operational.html#soa-edit-api soa_edit: INCEPTION-INCREMENT soa_edit_api: DEFAULT # api_rectify: when true, PDNS rebuilds NSEC/NSEC3 + ordername after # every API edit. Required for DNSSEC zones; harmless for non-DNSSEC. api_rectify: true # ------- Per-kind metadata bag (PDNS PUT /zones/<id>/metadata/<kind>) ------- # Free-form map: each key is a metadata kind; each value is a list of # strings. Allowed kinds vary across PDNS versions - individual PUT # failures are logged but never abort the apply. metadata: ALLOW-AXFR-FROM: - "10.0.0.0/8" ALSO-NOTIFY: - "192.0.2.53:53" # Examples of other commonly-used kinds. PDNS accepts most as # multi-valued lists; some (TSIG-ALLOW-AXFR) accept multiple keys # by listing each on its own line. # NSEC3PARAM: ["1 0 5 abcdef"] # PRESIGNED: ["1"] # API-RECTIFY: ["1"] # equivalent to api_rectify above # TSIG-ALLOW-AXFR: ["zone-transfer-key"] # ------- Auto-select on backend pick ------- # When the create-zone form's backend picker resolves to one of these # primary slugs, this template is preselected. List the primaries # whose default zone-shape this template represents. default_for_primary_slugs: - primary-internal - slug: external name: External / customer-facing zones description: "Public DNS - longer SOA timers, 3 NS, no metadata bag." soa_ttl: 86400 soa_refresh: 86400 soa_retry: 7200 soa_expire: 1209600 soa_minimum: 3600 kind: Master # legacy spelling accepted; "Primary" is equivalent nameservers: - ns1.example.com. - ns2.example.com. - ns3.example.com. records: [] # empty list ⇒ no prelude records metadata: {} # empty map ⇒ no metadata writes default_for_primary_slugs: - primary-public # ----------------------------------------------------------------------------- # clusters - multi-primary peer groups # ----------------------------------------------------------------------------- # A cluster is N writable PDNS peers backed by a replicated store (Galera, # Postgres logical replication, etc.). The cluster appears as ONE logical # backend in every operator-facing picker; the peer-selection strategy # (UI label) picks which peer to read from / write to per request. # # A "group" (cluster row) relates backends that replicate together - the # writable peers of a multi-primary cluster, OR a primary together with its # secondaries (ADR-0014). Any role may join a group via `cluster_slug`. # ----------------------------------------------------------------------------- clusters: - slug: prod-cluster # lowercase kebab-case name: "Production cluster (3 peers)" description: "Three writable peers sharing one Galera-replicated MariaDB." # Strategy options (column name is `write_strategy`; the UI calls it # "Peer selection strategy"): # # round_robin Spread requests across peers in order. The default. # random Uniform random choice per request. # lowest_latency Use the peer with the lowest p50 from the sampler. # Falls back to round_robin until the sampler has # data. # least_load Use the peer with the fewest zones from the sampler. # Useful when peers are unevenly sized. write_strategy: round_robin # A primary + its secondaries also form a group. The single writable Primary # is the group's peer; the Secondaries ride along as read-only members. - slug: public-group name: "Public group (primary + 2 secondaries)" description: "One writable Primary mirrored by two read-only Secondaries." # ----------------------------------------------------------------------------- # pdns_servers - the PDNS Authoritative backends # ----------------------------------------------------------------------------- # Primaries are full read/write backends. Secondaries are read-only mirrors # we poll for sync state + stats. Cluster peers are primaries that share a # replicated store (see clusters: above). # # Cross-row rules enforced at parse: # • cluster_slug → optional on ANY role; must resolve to a cluster defined # in this file or already in the DB. Group a primary with # its secondaries, or the peers of a multi-primary cluster. # • is_default → only a primary may be the default; set on EXACTLY ONE # row (the applier doesn't enforce uniqueness; the admin # UI does). A `write_mode: read_only` row can't be it. # • write_mode → "auto" (default) trusts the daemon's observed /config; # "read_only" vetoes ALL writes to this backend. Use it for # a node fed by database replication whose DB user has no # write access - PowerDNS reports such a daemon as a plain # standalone primary, so nothing else can detect it. # • monitor_accept_single_ns → optional; true accepts single-nameserver zones # on a standalone backend (zone monitoring then reports # them as accepted information, not a warning). Ignored # with a cluster_slug. Omit to leave the UI's choice alone. # # `api_key` is the PDNS X-API-Key in plaintext; encrypted with # APP_ENCRYPTION_KEY before persisting. `base_url` must point at the PDNS # HTTP API path (`/api/v1`). The SSRF guard (APP_PDNS_ALLOW_PRIVATE_NETWORKS, # APP_PDNS_ALLOW_INSECURE_HTTP) gates which URLs are accepted at runtime. # ----------------------------------------------------------------------------- pdns_servers: # --- Standalone primary (no cluster, no secondaries) --- - slug: primary-internal # lowercase kebab-case name: "Internal primary (Sydney)" description: "Authoritative for internal.example. and corp.example." base_url: "http://pdns-internal.svc:8081/api/v1" # https:// required in prod unless APP_PDNS_ALLOW_INSECURE_HTTP=true server_id: localhost # PDNS' internal server-id (almost always "localhost") api_key: "REPLACE_ME_internal_pdns_api_key" is_default: true # exactly ONE row in the file should be default # --- Primary with three Secondaries --- # The Secondaries auto-bootstrap via PDNS supermaster: on a NOTIFY from # the primary they create the zone and AXFR it, provided each zone's NS # list contains the receiving Secondary's `supermasters.nameserver` value. - slug: primary-public name: "Public primary (Sydney)" description: "Authoritative for example.com and customer zones." base_url: "https://pdns-public.example.com/api/v1" server_id: localhost api_key: "REPLACE_ME_public_pdns_api_key" is_default: false cluster_slug: public-group # group this primary with its secondaries below - slug: secondary-public-eu name: "Public secondary (Frankfurt)" description: "Read-only mirror of primary-public." base_url: "https://pdns-fra.example.com/api/v1" server_id: localhost api_key: "REPLACE_ME_fra_secondary_api_key" cluster_slug: public-group # join the same group as the primary it mirrors - slug: secondary-public-us name: "Public secondary (Ashburn)" base_url: "https://pdns-iad.example.com/api/v1" server_id: localhost api_key: "REPLACE_ME_iad_secondary_api_key" cluster_slug: public-group # --- Cluster peers (members of prod-cluster) --- # Every peer in a cluster shares the same backend store, so they all see # the same zones + records. The app reads/writes through one peer per # request (chosen by the cluster's peer-selection strategy). All cluster # members are role=primary; cluster_slug binds them to the cluster row. - slug: peer-1 name: "Cluster peer 1 (Sydney AZ-A)" base_url: "http://pdns-peer-1:8081/api/v1" server_id: localhost api_key: "REPLACE_ME_peer_1_api_key" cluster_slug: prod-cluster # must match a cluster defined above - slug: peer-2 name: "Cluster peer 2 (Sydney AZ-B)" base_url: "http://pdns-peer-2:8081/api/v1" server_id: localhost api_key: "REPLACE_ME_peer_2_api_key" cluster_slug: prod-cluster - slug: peer-3 name: "Cluster peer 3 (Sydney AZ-C)" base_url: "http://pdns-peer-3:8081/api/v1" server_id: localhost api_key: "REPLACE_ME_peer_3_api_key" cluster_slug: prod-cluster # ----------------------------------------------------------------------------- # demo_zones - generated demo zones (optional) # ----------------------------------------------------------------------------- # Each entry produces N synthetic zones with realistic record content via # the PDNS HTTP API. Useful for showcasing the UI on a fresh stack; # disable in production by removing this whole block. # # Exactly one of `target_server` / `target_cluster` must be set per entry. # Cluster targets route through the cluster's peer-selection strategy # (round_robin spreads the N zones across the peers). # ----------------------------------------------------------------------------- # demo_zones: # - target_server: primary-internal # OR target_cluster: prod-cluster (not both) # count: 10 # number of zones to create (1..500) # records_per_zone: 10 # records per zone, excluding NS+SOA (1..100) # name_prefix: corp # zones are <prefix>-N.<base_domain>. # base_domain: example # apex of the generated zones (default "demo") # kind: Master # Native | Master | Primary # nameservers: # optional; defaults to ns1.<base>./ns2.<base>. # - ns1.internal.example. # - ns2.internal.example. # ----------------------------------------------------------------------------- # oidc - identity providers # ----------------------------------------------------------------------------- # Multi-provider supported. Each row is created from scratch on first # apply; the applier never deletes providers (only creates + updates by # slug). `client_secret` is plaintext here; encrypted at rest. # # Group → role materialisation: # On every successful sign-in, the user's `claim_groups` claim is # matched against `group_mappings`. Each match yields a role assignment # tagged `provider_id = <this provider>`, so the NEXT sign-in revokes # it when the user is no longer in the group. Admin-issued assignments # (provider_id IS NULL) are never touched. # # `scope` syntax in group_mappings: # # global applies everywhere # team:<slug> applies to the named team itself (no zone access) # zone:<zone-name> the role's zone-bound permissions on every zone with that # name, on any backend (canonical fqdn); nothing else # server:<slug> applies to the backend object itself; does NOT grant the # zones on it (zones carry no server binding the authorizer # can match) - use zone: scopes or zone grants for that # # Mappings whose role / team / server can't be resolved at sign-in are # logged + audited under auth.oidc.group_sync.mapping_unresolved and # skipped - the rest of the sign-in proceeds. # ----------------------------------------------------------------------------- oidc: - slug: company-sso # lowercase kebab-case name: "Company SSO" issuer_url: "https://auth.example.com/realms/example" client_id: "powerdns-authadmin" client_secret: "REPLACE_ME_company_sso_client_secret" scopes: "openid profile email groups" # space-separated OAuth scopes; default "openid profile email" # Claim names - change only if your IdP uses non-standard claim keys. claim_email: email claim_name: name claim_groups: groups enabled: true # false hides the provider from the login page # DEPRECATED - kept for one minor for backward compat. Set the global # `auth_default_provider` setting from /admin/authentication instead; # the applier translates a legacy `force_default: true` here into that # setting at apply time and logs a deprecation warning. force_default: false # Optional inline logo for the login button. https:// only. # icon_url: "https://auth.example.com/static/logo.png" # Restrict auto-provisioning of NEW users to these email domains. # Existing users keep signing in regardless. Empty/unset ⇒ no restriction. allowed_email_domains: - example.com - contractors.example.com group_mappings: # global → everyone in this IdP group is super-admin everywhere - group: pdns-superadmins role: super-admin scope: global - group: pdns-operators role: operator scope: global # team-scoped → role applies to the team itself (view/update/members); # it does not open the team's zones - give the team zone grants for that. # The team slug must exist (defined above or pre-existing). - group: noc-zone-editors role: zone-editor scope: team:noc # zone-scoped → the role's zone-bound permissions on ONE zone name # (canonical fqdn - trailing dot), matched on every backend. - group: corp-zone-readers role: read-only scope: zone:corp.example. # server-scoped → role applies to the backend object only; it does NOT # grant the zones on it (see the scope syntax note above). - group: public-dns-only role: zone-editor scope: server:primary-public # Custom roles defined in `roles:` above are usable here by slug - # the materialiser doesn't care whether the role is system-seeded # or org-custom. - group: dns-noc-team role: zone-noc scope: global # A second provider - Google personal sign-in, defined but disabled. # Flip `enabled: true` from the admin UI when you're ready. - slug: google name: "Google" issuer_url: "https://accounts.google.com" client_id: "REPLACE_ME_google_client_id.apps.googleusercontent.com" client_secret: "REPLACE_ME_google_client_secret" scopes: "openid profile email" claim_email: email claim_name: name claim_groups: groups enabled: false # DEPRECATED - see the note on the first provider above. force_default: false group_mappings: [] # explicit empty list - no group sync # ----------------------------------------------------------------------------- # saml: SAML 2.0 identity providers (ADR-0021) # ----------------------------------------------------------------------------- # One entry per IdP. Identifiers + group sync work the same way as OIDC; the # transport differences are: SP signing keypair is REQUIRED (we sign every # AuthnRequest), assertions MUST be signed by default, and the optional SP # encryption keypair adds support for EncryptedAssertion. # # Public certs (IdP signing cert, SP signing cert, SP encryption cert) sit as # PEM blocks in plain text. Private keys are encrypted at rest by the applier; # YAML still holds them plaintext, so this file MUST live in a secret store / # git-crypt setup (same posture as OIDC client secrets, PDNS API keys, …). # ----------------------------------------------------------------------------- saml: - slug: corp-sso # lowercase kebab-case; reserved across every provider type name: "Corporate SSO" idp_entity_id: "https://idp.example.com/saml" idp_sso_url: "https://idp.example.com/saml/sso" # Optional. When unset, /api/auth/saml/<slug>/slo clears the local session # only - the IdP keeps its own session until it expires. idp_slo_url: "https://idp.example.com/saml/slo" # Public X.509 cert the IdP signs Responses with. idp_signing_cert: | -----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgIUREPLACE_ME_with_your_IdPs_signing_certificate -----END CERTIFICATE----- # SP signing keypair. Generate locally with: # openssl req -x509 -newkey rsa:2048 -keyout sp.key -out sp.crt \ # -nodes -days 1825 -subj "/CN=corp-sso" sp_signing_key: | -----BEGIN PRIVATE KEY----- REPLACE_ME_with_the_SP_private_key_PEM -----END PRIVATE KEY----- sp_signing_cert: | -----BEGIN CERTIFICATE----- REPLACE_ME_with_the_SP_signing_certificate_PEM -----END CERTIFICATE----- # Optional: enable EncryptedAssertion. Both halves required together. # sp_encryption_key: | # -----BEGIN PRIVATE KEY----- # ... # -----END PRIVATE KEY----- # sp_encryption_cert: | # -----BEGIN CERTIFICATE----- # ... # -----END CERTIFICATE----- # require_encrypted_assertion: false require_signed_response: true signature_algorithm: sha256 # sha1 | sha256 | sha512 name_id_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" claim_email: email claim_name: name claim_groups: groups enabled: true # allowed_email_domains: # - example.com group_mappings: - group: DomainAdmins role: super-admin scope: global - group: NetOps role: operator scope: global - group: NocZoneEditors role: zone-editor scope: team:noc # ----------------------------------------------------------------------------- # LDAP providers (ADR-0020) # # Direct-bind sign-in against Active Directory or OpenLDAP. The applier # encrypts `bind_password` (and `tls_ca_cert`, if you pin a CA) with the # same AES-256-GCM envelope used everywhere else - they NEVER round-trip # back through the API in plaintext. # # Slugs are reserved in the cross-type `auth_provider_slugs` table the # same way as OIDC + SAML; a slug collision across types fails the # provisioning step with a clean error. # ----------------------------------------------------------------------------- ldap: # ----- Worked AD example ----------------------------------------------- - slug: corp-ad name: "Corporate AD" server_url: "ldaps://ad.corp.example.com:636" # StartTLS upgrades a plain ldap:// connection after connect. # MUST be false on ldaps:// URLs (the implicit-TLS port doesn't # speak StartTLS) - the validator refuses the redundant pairing. start_tls: false bind_dn: "CN=svc-authadmin,OU=ServiceAccounts,DC=corp,DC=example,DC=com" bind_password: "REPLACE_ME_ad_service_account_password" user_search_base: "OU=Employees,DC=corp,DC=example,DC=com" # AD's typical OR filter - matches `sAMAccountName` (the # Active Directory login name), `userPrincipalName`, or `mail`. user_search_filter: "(|(sAMAccountName={{username}})(userPrincipalName={{username}})(mail={{username}}))" # AD fully resolves `memberOf` on the user record at query time, # so the optional second-search pair is unnecessary. Leave # group_search_base / group_search_filter out for AD. group_attr: memberOf claim_email: mail claim_name: displayName # Optional CA pin for an internal certificate authority. Concatenated # PEM is accepted (cert + intermediates). Use this OR # LDAP_TLS_INSECURE_SKIP_VERIFY=true (NEVER both - the pin is the # secure equivalent). # tls_ca_cert: | # -----BEGIN CERTIFICATE----- # ... # -----END CERTIFICATE----- enabled: true allowed_email_domains: - corp.example.com group_mappings: # Map AD groups (by their full DN - AD's memberOf is DN-valued) # to roles. Group strings are case-sensitive matches against the # values in the user's memberOf attribute. - group: "CN=PDNS Super Admins,OU=Groups,DC=corp,DC=example,DC=com" role: super-admin scope: global - group: "CN=NOC,OU=Groups,DC=corp,DC=example,DC=com" role: zone-editor scope: team:noc - group: "CN=DNS Public Zone Readers,OU=Groups,DC=corp,DC=example,DC=com" role: read-only scope: zone:corp.example. # ----- Worked OpenLDAP 2.6 example ----------------------------------- # OpenLDAP with the memberof overlay enabled in cn=config behaves # like AD (group memberships resolved on the user record). Without # the overlay, configure the optional second search below - we # match groupOfNames / groupOfUniqueNames by `member={{userDn}}`. - slug: dev-openldap name: "Dev OpenLDAP" # Plain ldap:// is refused by the validator unless start_tls=true # is set here (RFC 4511 §4.14 upgrade) or # LDAP_ALLOW_INSECURE_PORT_389=true is set in the environment. server_url: "ldap://openldap.dev.example.com:389" start_tls: true bind_dn: "cn=svc-authadmin,ou=ServiceAccounts,dc=example,dc=com" bind_password: "REPLACE_ME_openldap_service_account_password" user_search_base: "ou=Users,dc=example,dc=com" user_search_filter: "(|(uid={{username}})(mail={{username}}))" # OpenLDAP without the memberof overlay: configure the second # search. The filter substitutes {{userDn}} with the LDAP-escaped # DN of the just-authenticated user. group_search_base: "ou=Groups,dc=example,dc=com" group_search_filter: "(&(objectClass=groupOfNames)(member={{userDn}}))" # Empty attr is the conventional "no in-entry attribute, always # consult the second search" path; keep memberOf as the default # so installs that later flip the overlay on don't need a config # change here. group_attr: memberOf claim_email: mail claim_name: cn enabled: true group_mappings: - group: "cn=admins,ou=Groups,dc=example,dc=com" role: super-admin scope: global - group: "cn=devs,ou=Groups,dc=example,dc=com" role: read-only scope: global