eb65ad8f89
Hard switch of the docker push target from the Gitea registry to the artifactapi local docker registry (docker-internal); the Gitea VM and its registry are being retired. Drops the droneci/DRONECI_PASSWORD creds since artifactapi accepts unauthenticated in-cluster pushes. Also updates the README push note (covers bind-operator + bind-tsig-api). Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
102 lines
4.5 KiB
Markdown
102 lines
4.5 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` 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.
|