unkinben ea330bd767
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
Fix authoritative secondary replication (TSIG transfer + stable primary)
Secondaries never replicated any member zone: the master's catalog zone
requires key-authenticated AXFR (allow-transfer { key "transfer-key"; }),
but the rendered secondary config transferred without presenting the key,
so every catalog transfer was REFUSED and no member zones provisioned.
Two further gaps compounded it: member zones had no allow-transfer at all,
and secondaries pointed at the primary's pod IP, which dies on restart.

- Render the catalog transfer key into the secondary catalog-zones
  default-primaries and the secondary catalog zone primaries, so
  key-authenticated AXFR from the primary is accepted.
- Add allow-transfer { key "<transfer-key>"; } to catalog member primary
  zones (when the zone does not set an explicit allow-transfer), so
  secondaries can pull them; applied to existing zones via modzone.
- Point secondaries at the stable primary Service ClusterIP instead of the
  primary pod IP, so replication survives primary pod restarts (falls back
  to the pod IP when no primary Service exists).
2026-07-12 19:42:38 +10:00
2026-07-03 18:32:09 +10:00

bind-operator

A Kubernetes operator that manages fleets of BIND9 servers declaratively: StatefulSet-backed clusters with primary/secondary replication, and zones, views, TSIG keys, ACLs, catalog zones, RPZ policies and DNSSEC policies as custom resources.

Architecture

Each BindCluster is a StatefulSet plus a headless Service (stable per-pod DNS) and a client Service. Ordinal-0 is the primary; the remaining pods are secondaries that replicate via AXFR/IXFR + NOTIFY. A per-pod PVC holds zone databases and journals.

Zone content is delivered dynamically: the operator execs rndc addzone and TSIG nsupdate against the primary pod (the same write path external-dns uses). Cluster-wide config — options, controls, ACLs, views, dnssec-policy blocks and response-policy clauses — is rendered into a ConfigMap-backed named.conf and reloaded with rndc reconfig. New zones land on the secondaries automatically through a catalog zone, so secondaries never need per-zone reconfiguration.

BindCluster ──> StatefulSet (pod-0 = primary, pod-N = secondaries)
                 ├─ headless Service   (pod-0.<cluster>-headless.<ns>.svc…)
                 ├─ client Service     (ClusterIP / LoadBalancer)
                 ├─ ConfigMap          (named.conf.primary / .secondary + entrypoint)
                 ├─ Secret <cluster>-keys   (TSIG key clauses, included by named.conf)
                 └─ Secret <cluster>-rndc   (rndc control key)

BindZone / DNSRecord ──rndc addzone + nsupdate──> primary ──catalog + AXFR──> secondaries

The named.conf is rendered in two variants (primary/secondary); an entrypoint script picks one based on the pod ordinal.

Custom Resources

Kind Purpose
BindCluster A set of BIND9 servers. spec.mode: authoritative or resolver.
BindZone A forward/reverse zone (primary/secondary/forward/stub), records inline, optional dynamic-update + DNSSEC + catalog membership.
DNSRecord A single record set applied via TSIG nsupdate — external-dns as a CRD.
BindView A split-horizon view (match-clients, ordering, per-view recursion).
BindTSIGKey A TSIG key; the operator generates material into a Secret (never stored in the CR).
BindACL A reusable named address_match_list.
BindCatalogZone A BIND catalog zone so secondaries auto-provision member zones.
BindPolicy A Response Policy Zone (RPZ) / DNS firewall.
BindDNSSECPolicy A dnssec-policy for automated signing.

See config/samples/ for worked examples.

Migration mapping

The three Puppet-managed BIND roles map onto three BindClusters:

Puppet role BindCluster Mode
3× authoritative masters auth authoritative (pod-0 primary, 2 secondaries)
3× only-resolvers resolver resolver (3 identical recursive servers)
3× external-dns externaldns authoritative (zones set dynamicUpdate for RFC2136 TSIG updates)

Development

make generate   # regenerate deepcopy, CRDs and RBAC from kubebuilder markers
make build      # build the operator binary
make test       # go test -race
make lint fmt   # go vet / gofmt

Local (kind)

kind create cluster --name bind
docker build -t bind-operator:dev -f Dockerfile.operator .
kind load docker-image bind-operator:dev --name bind

kubectl apply -f config/crd/bases/
kubectl apply -f hack/kind/manifests/
kubectl apply -f config/samples/

CI

Woodpecker runs pre-commit (gofmt + vet), test, and a dry-run image build on pull requests; pushing a v* tag builds and pushes git.unkin.net/unkin/bind-operator to the Gitea registry.

Notes & caveats

  • The BIND container image (spec.image, default internetsystemsconsortium/bind9:9.20) must ship named, rndc and nsupdate. The operator projects its config at /etc/bind-operator (leaving the image's own /etc/bind, including bind.keys, intact) and runs named -g -c /run/named/named.conf.
  • Dynamic updates authenticate with nsupdate -y; the TSIG secret is passed on the argv of an exec'd process inside the pod.
  • RPZ IP-trigger encodings (ip, client-ip, nsip) are emitted verbatim; QNAME and NSDNAME triggers are fully supported.
S
Description
Kubernetes operator for managing BIND9 DNS clusters, zones, views, and TSIG keys
Readme 527 KiB
Languages
Go 99.1%
Makefile 0.9%