Feature Specification: Component bind

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/bind/default.nix, core/components/hosting-provider/bind.nix, core/components/hosting-provider/apex-update-policy.nix, core/components/hosting-provider/dnssec-policy.nix, core/components/hosting-provider/operator-zones.sh, core/resources/dns/self-hosted.nix, core/setups/groups/hosting-provider.nix, core/checks/default.nix, core/checks/ci.nix, core/checks/nix-unit-tests.nix), issues #200 and #1374.

User Scenarios & Testing (mandatory)

User Story 1 - Serve the group apex from its own nameservers (Priority: P1)

A hosting provider holds its apex itself. The bind component runs a BIND named on each DNS node of the hosting-provider group. One node holds the apex as primary. The other nodes take the zone from it as secondaries. The octodns-zone effect pushes the per-node records into the primary at deploy time. The self-hosted provider of the dns resource points at this node.

Why this priority: without an authoritative server, no group name resolves, and no node of the group finds its peers.

Independent Test: integrations-octodns-bind. It pushes the group-derived zone into the primary over RFC2136. It checks the transfer to the second nameserver, a refused transfer to anyone else, and a peer that resolves group nodes through the primary.

Acceptance Scenarios:

  1. Given a node with serveApex = true and apexUpdates set, When the publisher key sends an RFC2136 update, Then named accepts any record type under the apex.
  2. Given a node with masters set, When the primary sends NOTIFY, Then the node transfers the zone with the master TSIG key and refuses all transfers itself.
  3. Given a primary with secondaries, When a host that is not a listed secondary asks for a transfer, Then named refuses it.
  4. Given an in-bailiwick name in apexNameservers without an apexGlue entry, When the setup evaluates, Then an assertion fails.

User Story 2 - Let operator deploys publish app names (Priority: P2)

An operator deploy publishes the address of its app under the provider apex. The provider must not lose its own names to the operator. The operatorUpdates option grants a second TSIG key only A and AAAA records below the apex. It denies the apex and every name in reservedNames. The operatorZones option adds an RFC 9432 catalog zone. In this catalog the operator key adds domains of its own.

Why this priority: operator deploys need names, but a wrong grant lets an operator take over provider names.

Independent Test: integrations-publish-records-named and nix-unit-setups-operator-publish. The NixOS test publishes, repoints and releases an app name. It also checks that the server refuses the apex, the provider names, and every type but the address types to the operator key. It then adds an operator domain to the catalog.

Acceptance Scenarios:

  1. Given operatorUpdates with reservedNames = [ "auth" ], When the operator key updates auth.<apex>, Then named refuses the update.
  2. Given operatorUpdates, When the operator key adds a TXT record at app.<apex>, Then named refuses the update.
  3. Given operatorZones on a primary, When the operator key adds a catalog member, Then named-operator-zones seeds the zone once with SOA and NS, and the primary and the secondaries serve it.
  4. Given a catalog member above the apex, above an admitted member, or below the catalog, When named-operator-zones runs, Then it refuses the member and leaves it in the catalog.

User Story 3 - Sign the apex with a key from the KV store (Priority: P3)

A provider wants a signed apex so that the registry can publish a DS record. With signApex = true, the primary signs the apex inline with one CSK. The key comes from the KV store through signingKey. Pooled secondaries hold no key and serve the signed zone as they receive it (#1374).

Why this priority: DNSSEC is optional, but a broken key makes the apex fail validation for every resolver.

Independent Test: integrations-dns-key-render, integrations-apex-key-mint and nix-unit-setups-apex-signing.

Acceptance Scenarios:

  1. Given signApex = true and a signingKey, When named-signing-key runs, Then it renames the key to K<domain>.+<alg>+<tag> and writes a .state file with the KSK: and ZSK: roles before bind.service starts.
  2. Given signApex = true and no key in the KV store, When named-signing-key runs, Then the unit fails closed and writes no key file.
  3. Given a signed apex, When named-apex-ds runs, Then it writes <domain>.ds and <domain>.dnskey.json in /var/lib/named.
  4. Given signApex = true and no signingKey, When the setup evaluates, Then an assertion fails.

Edge Cases

  • A secret file does not arrive in 180 seconds: the key render unit (named-apex-key, named-operator-key, named-challenge-key, named-secondary-key-<name>, named-master-key) fails. It does not write an empty secret.
  • Two DNS nodes of the group roster have the same label: the evaluation fails in core/setups/wiring/apex-group.nix. Without the group wiring, two nodes that keep the default nameserverLabel (ns1) both seed themselves as ns1, and the zone answers wrong.
  • A secondary has a higher serial than the seed: named-apex-seed raises the seed serial above the secondaries before bind.service starts.
  • The zone is already published: named-apex-seed does not refresh the seed. It refreshes only while the zone is provably unpublished.
  • A catalog member drops out: its zone file stays on disk.
  • Two catalog members overlap: the first to arrive wins, and the other is refused.
  • An incumbent secondary has no TSIG surface: transferByAddress = true allows the transfer by address.
  • The backup reads the apex zone over AXFR under the key of the publisher, and does not keep the SOA, the NS records of the apex and the DNSSEC records. If the transfer has no SOA, the backup fails.
  • The restore replays each record set of the backup as a dynamic update that replaces the set with the same name and type, thus named increases the serial and the secondaries transfer the zone. The restore does not remove a record set that is not in the backup.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare components.bind with name = "BIND", package = pkgs.bind, and an implementation that imports bind.nix and requests the spiffe resource.
  • FR-002: bind.nix MUST put its options under fediversity.dns-master and serve the apex that the fediversity.apex option (core/apex-option.nix) names.
  • FR-003: named MUST act as primary when masters is null and as secondary when masters is set.
  • FR-004: A secondary MUST set allow-transfer { none; }, and a primary MUST send also-notify to its secondaries.
  • FR-005: The update-policy MUST come from apex-update-policy.nix: zonesub ANY for the publisher key, then a deny for the apex and each reserved name, then wildcard *.<apex>. A AAAA for the operator key.
  • FR-006: The catalog zone MUST grant the operator key only wildcard *.zones.<catalog>. PTR.
  • FR-007: named-operator-zones MUST refuse malformed names, names that overlap configured zones, and names that overlap admitted members.
  • FR-008: named-operator-zones MUST run after bind.service, from a timer with OnBootSec=2min and OnUnitInactiveSec=1min, and from a path unit on the catalog.
  • FR-009: challengeUpdates MUST grant its key only TXT at each listed challenge zone.
  • FR-010: The dnssec-policy MUST come from dnssec-policy.nix: one CSK, inline-signing, the signingAlgorithm (default ecdsap256sha256) and signatureValidityDays (default 7).
  • FR-011: named-signing-key MUST run before bind.service, fail closed, and start bind.service with --no-block after it succeeds.
  • FR-012: Each key render unit MUST run after generate-vars.service and fail after 180 seconds without the secret.
  • FR-013: systemd.paths units MUST re-run the key renders when the secret files, the signing key or the catalog change.
  • FR-014: The firewall MUST open TCP and UDP port 53. named MUST NOT act as an open resolver.
  • FR-015: Evaluation MUST fail when masters is set without serveApex, or together with apexUpdates or signingKey.
  • FR-016: Evaluation MUST fail when operatorUpdates is set without apexUpdates, or operatorZones on a primary lacks either update key.
  • FR-017: Evaluation MUST fail when two lanes name the same zone or two capabilities name the same TSIG key.
  • FR-018: The build MUST run named-checkconf -z on named.conf. The check replays the systemd.tmpfiles rules of /var/lib/named into a scratch directory and writes a key block with a dummy secret to each key file. The installed named.conf keeps the /var/lib/named paths.
  • FR-019: The bind component MUST request garage through mkBackupsGarageRequest with the bucket bind-backups. On a primary with apexUpdates, bind/backup.nix MUST add a streamingBackup of the records of the apex zone to that bucket with the restic provider, as the user named. A secondary MUST NOT have a backup.

Key Entities

  • Apex zone: the zone file <domain>.zone in /var/lib/named, seeded with serial 1970010100 by named-apex-seed.
  • Publisher key (apexUpdates): the TSIG key that the octodns-zone effect uses for whole-zone writes.
  • Operator key (operatorUpdates): the TSIG key that operator deploys use for address records and catalog members.
  • Catalog zone (operatorZones): an RFC 9432 catalog of operator domains, with nameservers for each member zone.
  • Challenge zones (challengeUpdates): single-name zones for ACME TXT challenges.
  • Signing key (signingKey): the CSK at keyPath, privatePath and statePath, read from the KV store (see openbao).
  • Seed serial: .apex-seed-serial in /var/lib/named.
  • Offsite backup: the restic repository bind_apex in the bind-backups bucket, with the records of the apex zone, also the records that the operator deployments add. It does not hold the operator zones, the challenge zones and the DNSSEC keys.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: integrations-octodns-bind passes: the primary takes the zone, the secondary transfers it, and an unlisted host gets no transfer.
  • SC-002: integrations-publish-records-named passes: the operator key writes only A and AAAA below the apex and catalog members, and the publisher key writes anything.
  • SC-003: integrations-dns-key-render and integrations-apex-key-mint pass.
  • SC-004: nix-unit-setups-delegated-zones, nix-unit-setups-apex-signing, nix-unit-setups-group-zone, nix-unit-setups-dns-routing, nix-unit-setups-operator-publish and nix-unit-setups-apex-group pass.

Assumptions

  • The group roster in core/setups/groups/hosting-provider.nix sets component = "bind" on each DNS node and gives each a distinct label and a real internal address. core/setups/wiring/apex-group.nix fails the evaluation when two DNS nodes have the same label.
  • The TSIG secrets and the signing key reach the node as files, through the vars plumbing and the KV store (see spire and openbao).
  • The setups decide which nodes point their resolver at named, through the nameservers of the network resource (see setups and wiring).
  • The deployer reaches the primary on port 53 to push records.

Known Deviations

None.