fe5fbdaf6d
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
99 lines
4.1 KiB
Markdown
99 lines
4.1 KiB
Markdown
# 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.
|