9c81320df8
The operator-generated TSIG Secret previously carried only the managed-by
label, so it could not be mirrored to another namespace by emberstack
reflector (which requires reflection-allowed annotations on the source).
Add spec.secretTemplate.{annotations,labels}, applied both when the Secret
is first generated and reconciled onto the existing Secret when the CR
changes (imported secrets are left untouched so we don't fight their
external manager). This lets the external-dns TSIG key be managed in
bind-internal and reflected into the externaldns namespace.
101 lines
4.4 KiB
Markdown
101 lines
4.4 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
|
||
`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.
|