ExternalDNS on Kubernetes
Automatic Sync with BIND via RFC2136
How to fully automate DNS management in a bare-metal Kubernetes homelab using Cilium, BIND, and HashiCorp Vault.
When you run a Kubernetes homelab with multiple exposed services — Grafana, Keycloak, ArgoCD, PgAdmin — you quickly find yourself maintaining DNS entries in BIND manually. It's repetitive, prone to errors, and breaks the GitOps flow. The solution is ExternalDNS. This controller monitors your Services, Ingresses, and HTTPRoutes in real-time, automatically pushing DNS updates to BIND as soon as a route is created. No more forgotten records.
Lab Architecture
| Node | IP | Role |
|---|---|---|
node-4 | 192.168.2.75 | Control Plane + NFS |
node-2 | 192.168.2.46 | Worker — Monitoring |
node-3 | 192.168.2.74 | Worker — BIND DNS |
On node-3, BIND must be configured to accept dynamic updates signed with a TSIG key.
Generate the TSIG key
tsig-keygen -a hmac-sha256 externaldns-key
Result:
key "externaldns-key" {
algorithm hmac-sha256;
secret "YourVeryLongBase64KeyHere==";
};
Configure named.conf
// TSIG Key Declaration
key "externaldns-key" {
algorithm hmac-sha256;
secret "YourVeryLongBase64KeyHere==";
};
zone "mirecloud.com" {
type master;
file "/var/lib/bind/db.mirecloud.com";
// Allow updates only with the proper TSIG key
allow-update { key "externaldns-key"; };
};
systemctl reload named
vault kv put secret/dns/rfc2136 \
tsig-secret="YourVeryLongBase64KeyHere=="
ESO automatically creates the Kubernetes Secret by pulling the value from Vault. Zero secrets in Git, and it refreshes every minute.
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: external-dns-bind-secret
namespace: external-dns
spec:
refreshInterval: 1m
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: rfc2136-tsig-secret # K8s Secret created automatically
creationPolicy: Owner
data:
- secretKey: tsig-secret
remoteRef:
key: secret/dns/rfc2136
property: tsig-secret
apiVersion: v2
name: external-dns
version: 1.0.0
dependencies:
- name: external-dns
version: 1.20.0
repository: https://kubernetes-sigs.github.io/external-dns/
external-dns:
logLevel: info
interval: 1m
sources:
- service
- ingress
- gateway-httproute # Cilium Gateway API support
policy: sync # Creates AND deletes DNS records
registry: txt # Tracking via TXT records
domainFilters:
- mirecloud.com
provider: rfc2136
extraArgs:
- --rfc2136-host=192.168.2.74
- --rfc2136-port=53
- --rfc2136-zone=mirecloud.com
- --rfc2136-tsig-keyname=externaldns-key
- --rfc2136-tsig-axfr
- --rfc2136-tsig-secret-alg=hmac-sha256
env:
- name: EXTERNAL_DNS_RFC2136_TSIG_SECRET
valueFrom:
secretKeyRef:
name: rfc2136-tsig-secret # Created by ESO from Vault
key: tsig-secret
securityContext:
runAsNonRoot: true
runAsUser: 65532
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
A simple git push and ArgoCD deploys ExternalDNS automatically. No manual helm install required.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-dns
namespace: argocd
spec:
project: default
source:
repoURL: "git@github.com:mirecloud/home_lab.git"
targetRevision: HEAD
path: infrastructure/external-dns
destination:
server: https://kubernetes.default.svc
namespace: external-dns
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Here is a complete test manifest to validate the whole chain: cert-manager generates the TLS, Cilium exposes the service, and ExternalDNS updates BIND.
# 1. TLS Certificate via cert-manager
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: testexternal-tls-cert
namespace: default
spec:
secretName: externaldns-tls-secret
issuerRef:
name: mirecloud-ca-issuer
kind: ClusterIssuer
commonName: testexternal.mirecloud.com
dnsNames:
- testexternal.mirecloud.com
---
# 2. Cilium Gateway (L4 entry point)
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: externaldns-gateway
namespace: default
spec:
gatewayClassName: cilium
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: Same
- name: https
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- kind: Secret
name: externaldns-tls-secret
allowedRoutes:
namespaces:
from: Same
---
# 3. HTTPRoute (L7 routing)
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: externaldns-route
namespace: default
spec:
parentRefs:
- name: externaldns-gateway
hostnames:
- "testexternal.mirecloud.com"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: kubernetes
port: 443
kubectl apply -f test-route-external-dns.yaml
Result — BIND zone automatically updated
In less than a minute, checking /var/lib/bind/db.mirecloud.com on node-3:
$ORIGIN mirecloud.com.
argocd A 192.168.2.201
grafana A 192.168.2.205
keycloak A 192.168.2.204
pgadmin A 192.168.2.202
testexternal A 192.168.2.206 ← added automatically in < 1 min
; TXT tracking record (ExternalDNS ownership)
a-testexternal TXT "heritage=external-dns,external-dns/owner=default,
external-dns/resource=httproute/default/externaldns-route"
ExternalDNS Logs
time="2026-02-22T18:53:32Z" level=info msg="All records are already up to date"
time="2026-02-22T18:54:34Z" level=info msg="All records are already up to date"
The complete flow — from git push to DNS
source: gateway-httproute)If you delete the HTTPRoute, ExternalDNS also deletes the DNS record. The sync policy ensures that the DNS always reflects the actual state of the cluster.
Security
| Aspect | Implementation |
|---|---|
| TSIG Key | Stored in HashiCorp Vault · never in Git |
| Injection | External Secrets Operator → K8s Secret · 1m refresh |
| Algorithm | HMAC-SHA256 |
| Container | runAsNonRoot · readOnlyRootFilesystem · no capabilities |
| DNS Scope | Filtered to mirecloud.com only (--domain-filter) |
| BIND Updates | Allowed only with the TSIG key |
Conclusion
No more forgotten DNS records. No more drift between what's running in the cluster and what's declared in BIND.
The DNS automatically follows the cluster state, in both directions — creation and deletion. This is exactly the kind of automation that makes the difference between a hacked-together homelab and a true production-like infrastructure.
Comments
Post a Comment