Files
bind-operator/README.md
T
unkinben fe5fbdaf6d Initial bind-operator: 9 CRDs + controllers
Implements a Kubernetes operator that manages fleets of BIND9 servers
declaratively, using controller-runtime (matching forgebot conventions).

- add BindCluster reconciler: StatefulSet (pod-0 primary, secondaries),
  headless + client Services, rendered named.conf ConfigMap, TSIG keys
  Secret and rndc control Secret; watches dependent CRs to re-render
- add BindTSIGKey reconciler that generates key material into a Secret
- add BindZone/DNSRecord reconcilers using fully-dynamic delivery
  (rndc addzone + TSIG nsupdate against the primary pod)
- add BindCatalogZone reconciler so secondaries auto-provision zones
- add BindPolicy (RPZ), BindDNSSECPolicy, BindView, BindACL reconcilers
- render primary/secondary named.conf variants selected by pod ordinal
- generate CRDs, deepcopy and RBAC; add samples mapping the three Puppet
  roles (authoritative/resolver/external-dns) to three BindClusters
- add Makefile, Dockerfile.operator, Woodpecker CI and kind manifests
2026-07-03 15:48:13 +10:00

99 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`, `resolver`, or `dynamic`. |
| `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 `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` | `dynamic` (RFC2136 TSIG updates on primary) |
## 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
`git.unkin.net/unkin/bind-operator` to the Gitea registry.
## Notes & caveats
- The BIND container image (`spec.image`, default
`git.unkin.net/unkin/bind9:latest`) must ship `named`, `rndc` and `nsupdate`,
read `/run/named/named.conf`, and honour the operator's `/etc/bind` layout.
- 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.