# 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.-headless..svc…) ├─ client Service (ClusterIP / LoadBalancer) ├─ ConfigMap (named.conf.primary / .secondary + entrypoint) ├─ Secret -keys (TSIG key clauses, included by named.conf) └─ Secret -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). `spec.secretTemplate` stamps extra labels/annotations onto that Secret (e.g. reflection hints to mirror it into another namespace). | | `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 `BindCluster`s: | 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 ```sh 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) ```sh 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 `artifactapi.k8s.syd1.au.unkin.net/docker-internal/bind-operator` (and `bind-tsig-api`) to the artifactapi local docker 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.