Local DNS Stack

Last updated 16 Oct 2025, 04:01

Local DNS Stack

The DNS profile (dns) now ships a split-horizon design:

  • nsd (authoritative) serves zenpower.at from /zones/zenpower.at.zone and is reachable only on the internal compose network.
  • unbound (recursive/caching) fronts the network, validates DNSSEC, and forwards zenpower.at queries to nsd while answering the rest via the public root servers.

Usage

# Lightweight local bring-up (only the dns profile, binds to host port 53)
COMPOSE_PROFILES=dns docker compose -f infra/docker-compose.d/dns.yml up -d

# Full stack bring-up (dns plus the rest of the infra definitions)
COMPOSE_PROFILES=dns docker compose -f infra/docker-compose.pinned.yml \
  -f infra/docker-compose.d/dns.yml up -d unbound nsd
  • Update infra/dns/nsd/zones/zenpower.at.zone with the production IPv4/IPv6 endpoints before the first deploy (serial must increase for every change).
  • DNSSEC validation runs with a baked trust anchor (infra/dns/unbound/root.key) that is refreshed automatically via unbound-anchor on container startup. Override UPDATE_TRUST_ANCHOR=0 if you need the container to stay offline.
  • The compose profile already exposes 53:53/{tcp,udp}; ensure systemd-resolved is disabled beforehand to avoid port conflicts.
  • infra/dns/unbound/unbound.conf controls access lists; tighten them to the production subnets before exposing UDP/TCP 53.
  • The unbound container refreshes the root hints file on boot using curl --resolve against Internic to avoid bootstrap DNS lookups. Set UPDATE_ROOT_HINTS=0 in the service environment to disable the fetch if the host has no egress.

Firewall

Open TCP/UDP 53 only for trusted networks. Example using UFW:

sudo ufw allow from 10.0.0.0/8 to any port 53 proto udp
sudo ufw allow from 10.0.0.0/8 to any port 53 proto tcp

Replace the CIDR blocks with the on-prem ranges that should query this resolver.

Health Checks

  • dig @127.0.0.1 zenpower.at SOA inside the unbound container confirms the stub-zone bridge to nsd.
  • drill @127.0.0.1 zenpower.at SOA inside the nsd container validates the zone loads correctly.
  • tools/dns/check_dns.sh runs end-to-end resolver checks (dig, delv, kdig). Override SERVER, DOMAIN, or EXPECT_NS env vars to point at a remote instance.

Commit both zone and config changes so the authoritative history lives in Git.

Go-live Checklist

  • Disable the host stub resolver (e.g., systemctl disable --now systemd-resolved) and repoint /etc/resolv.conf to the new recursive instance.
  • Update infra/docker-compose.d/dns.yml to expose 53:53/{tcp,udp} once the host is ready, then redeploy: COMPOSE_PROFILES=dns docker compose ... up -d --build.
  • Tighten the firewall to the production CIDRs only (UDP/TCP 53).
  • Verify DNSSEC status with dig +dnssec after startup; the image already bundles unbound-anchor and infra/dns/unbound/root.key for trust-anchor management.
  • After Let’s Encrypt cool-down, repoint public zenpower.at records, rerun GO, and watch Traefik/ACME logs for cert issuance.
  • Add Prometheus/Grafana probes (TODO) to watch latency, SOA serial drift, and resolver health.