The vault-plugin-secrets-bind-tsig plugin needs an HTTP endpoint that
creates, reads, rotates and deletes TSIG keys on its behalf, decoupling
Vault from direct Kubernetes API access. This adds that companion API and
lets the operator deploy it declaratively.
- Add BindTSIGAPI CRD: creating one makes the operator reconcile a
Deployment, Service, ConfigMap (env vars), token Secret and namespaced
RBAC for the companion API. Spec covers image, replicas, port,
targetNamespace, tokenSecretName, extra env, service exposure and
resources.
- Generate the master access token Secret only when absent, so a
VaultStaticSecret may pre-seed/overwrite it; the operator does not own it.
- Add the companion API server (internal/tsigapi): bearer-auth HTTP
contract POST /v1/keys, GET/DELETE /v1/keys/{name}, POST
/v1/keys/{name}/rotate, backed by BindTSIGKey custom resources the
operator reconciles into key material.
- Add cmd/tsigapi entrypoint and Dockerfile.tsigapi (distroless).
- Wire the reconciler into setup, regenerate CRDs/RBAC/deepcopy, and add
Woodpecker build (PR dry-run) and release (tag push) steps for the
bind-tsig-api image.
- Cover the API server with auth and key-lifecycle unit tests.
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). |
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 BindClusters:
| 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
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)
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, defaultinternetsystemsconsortium/bind9:9.20) must shipnamed,rndcandnsupdate. The operator projects its config at/etc/bind-operator(leaving the image's own/etc/bind, includingbind.keys, intact) and runsnamed -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.