Local DNS Stack
Local DNS Stack
The DNS profile (dns) now ships a split-horizon design:
- nsd (authoritative) serves
zenpower.atfrom/zones/zenpower.at.zoneand is reachable only on the internal compose network. - unbound (recursive/caching) fronts the network, validates DNSSEC, and forwards
zenpower.atqueries 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.zonewith 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 viaunbound-anchoron container startup. OverrideUPDATE_TRUST_ANCHOR=0if you need the container to stay offline. - The compose profile already exposes
53:53/{tcp,udp}; ensuresystemd-resolvedis disabled beforehand to avoid port conflicts. infra/dns/unbound/unbound.confcontrols 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 --resolveagainst Internic to avoid bootstrap DNS lookups. SetUPDATE_ROOT_HINTS=0in 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 SOAinside the unbound container confirms the stub-zone bridge to nsd.drill @127.0.0.1 zenpower.at SOAinside the nsd container validates the zone loads correctly.tools/dns/check_dns.shruns end-to-end resolver checks (dig,delv,kdig). OverrideSERVER,DOMAIN, orEXPECT_NSenv 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.confto the new recursive instance. - Update
infra/docker-compose.d/dns.ymlto expose53: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 +dnssecafter startup; the image already bundlesunbound-anchorandinfra/dns/unbound/root.keyfor 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.