Skip to main content

Introducing my Homelab Part-4

MireCloud Home Lab · DevOps

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.

📅 February 22, 2026 ⏱ ~10 min read 🔧 ExternalDNS v0.20.0 ☸ Kubernetes v1.34
Kubernetes v1.34 ExternalDNS v0.20.0 Cilium Gateway API BIND (RFC2136) TSIG / HMAC-SHA256 HashiCorp Vault External Secrets Operator ArgoCD (GitOps) cert-manager

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

┌─────────────────────────────────────────────────────────┐ │ Kubernetes Cluster │ │ │ Cilium Gateway API (grafana, keycloak, pgadmin, argocd...) │ │ │ │ ▼ │ ExternalDNS v0.20.0 (namespace: external-dns) sources: service · ingress · gateway-httproute │ │ │ │ │ RFC2136 Dynamic DNS Update │ │ TSIG / HMAC-SHA256 └───────────┼─────────────────────────────────────────────┘ ┌─────────────────────┐ │ BIND (node-3) │ │ 192.168.2.74:53 │ │ zone: mirecloud.com│ └─────────────────────┘
NodeIPRole
node-4192.168.2.75Control Plane + NFS
node-2192.168.2.46Worker — Monitoring
node-3192.168.2.74Worker — BIND DNS
1
Configure BIND for Dynamic DNS Updates

On node-3, BIND must be configured to accept dynamic updates signed with a TSIG key.

Generate the TSIG key

bash
tsig-keygen -a hmac-sha256 externaldns-key

Result:

output
key "externaldns-key" {
    algorithm hmac-sha256;
    secret "YourVeryLongBase64KeyHere==";
};

Configure named.conf

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"; };
};
bash
systemctl reload named
2
Store the TSIG key in HashiCorp Vault
⚠ The TSIG key must never be committed to Git. We store it securely in Vault.
bash
vault kv put secret/dns/rfc2136 \
  tsig-secret="YourVeryLongBase64KeyHere=="
3
Inject the key via External Secrets Operator

ESO automatically creates the Kubernetes Secret by pulling the value from Vault. Zero secrets in Git, and it refreshes every minute.

infrastructure/external-dns/templates/external-secret.yaml
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
4
Deploy ExternalDNS via Helm
infrastructure/external-dns/Chart.yaml
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/
infrastructure/external-dns/values.yaml
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"]
5
ArgoCD Application — GitOps

A simple git push and ArgoCD deploys ExternalDNS automatically. No manual helm install required.

clusters/home-lab/external-dns-app.yaml
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
6
Real-world Testing — Certificate + Gateway + HTTPRoute

Here is a complete test manifest to validate the whole chain: cert-manager generates the TLS, Cilium exposes the service, and ExternalDNS updates BIND.

test-route-external-dns.yaml
# 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
bash
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:

✓ mirecloud.com zone — automatic update
$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

kubectl logs -n external-dns deployment/external-dns -f
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

1.git push → ArgoCD detects the change in the repo
2.ArgoCD applies Gateway + HTTPRoute in the cluster
3.Cilium Gateway API assigns an external IP (192.168.2.206)
4.ExternalDNS detects the HTTPRoute (source: gateway-httproute)
5.ExternalDNS sends a RFC2136 Dynamic Update signed with TSIG → BIND
6.BIND creates the A record in the mirecloud.com zone
7.BIND creates a TXT tracking record for ExternalDNS
8.testexternal.mirecloud.com → 192.168.2.206 ✓
⚡ Total time: < 60 seconds

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

AspectImplementation
TSIG KeyStored in HashiCorp Vault · never in Git
InjectionExternal Secrets Operator → K8s Secret · 1m refresh
AlgorithmHMAC-SHA256
ContainerrunAsNonRoot · readOnlyRootFilesystem · no capabilities
DNS ScopeFiltered to mirecloud.com only (--domain-filter)
BIND UpdatesAllowed only with the TSIG key

Conclusion

ExternalDNS + BIND + RFC2136 + Vault + ArgoCD = DNS as Code

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

Popular posts from this blog

FastAPI Instrumentalisation with prometheus and grafana Part1 [Counter]

welcome to this hands-on lab on API instrumentation using Prometheus and FastAPI! In the world of modern software development, real-time API monitoring is essential for understanding usage patterns, debugging issues, and ensuring optimal performance. In this lab, we’ll demonstrate how to enhance a FastAPI-based application with Prometheus metrics to monitor its behavior effectively. We’ve already set up the lab environment for you, complete with Grafana, Prometheus, and a PostgreSQL database. While FastAPI’s integration with databases is outside the scope of this lab, our focus will be entirely on instrumentation and monitoring. For those interested in exploring the database integration or testing , you can review the code in our repository: FastAPI Monitoring Repository . What You’ll Learn In this lab, we’ll walk you through: Setting up Prometheus metrics in a FastAPI application. Instrumenting API endpoints to track: Number of requests HTTP methods Request paths Using Grafana to vi...

Join Ubuntu 20.04 to Active Directory with SSSD and SSH Access

Join Ubuntu 20.04 to Active Directory with SSSD and SSH Access  Overview This guide walks you through joining an Ubuntu 20.04 machine to an Active Directory domain using SSSD, configuring PAM for AD user logins over SSH, and enabling automatic creation of home directories upon first login. We’ll also cover troubleshooting steps and verification commands. Environment Used Component Value Ubuntu Client       ubuntu-client.bazboutey.local Active Directory FQDN   bazboutey.local Realm (Kerberos)   BAZBOUTEY.LOCAL AD Admin Account   Administrator Step 1: Prerequisites and Package Installation 1.1 Update system and install required packages bash sudo apt update sudo apt install realmd sssd libnss-sss libpam-sss adcli \ samba-common-bin oddjob oddjob-mkhomedir packagekit \ libpam-modules openssh-server Step 2: Test DNS and Kerberos Configuration Ensure that the client can resolve the AD domain and discover services. 2.1 Test domain name resol...

Observability with grafana and prometheus (SSO configutation with active directory)

How to Set Up Grafana Single Sign-On (SSO) with Active Directory (AD) Grafana is a powerful tool for monitoring and visualizing data. Integrating it with Active Directory (AD) for Single Sign-On (SSO) can streamline access and enhance security. This tutorial will guide you through the process of configuring Grafana with AD for SSO. Prerequisites Active Directory Domain : Ensure you have an AD domain set up. Domain: bazboutey.local AD Server IP: 192.168.170.212 Users: grafana (for binding AD) user1 (to demonstrate SSO) we will end up with a pattern like this below Grafana Installed : Install Grafana on your server. Grafana Server IP: 192.168.179.185 Administrator Privileges : Access to modify AD settings and Grafana configurations. Step 1: Configure AD for LDAP Integration Create a Service Account in AD: Open Active Directory Users and Computers. Create a user (e.g., grafana ). Assign this user a strong password (e.g., Grafana 123$ ) and ensure it doesn’t expire. Gather Required AD D...