dns-updater

A small Go daemon that keeps a host's DNS records current on a BIND server via TSIG-signed RFC2136 dynamic updates. It replaces the puppet profiles::dns::updater shell script + dns-update.path/dns-update.service systemd pair with a single long-running service.

What it does

  • Reads a desired-records file (zone|name|type|ttl|value, the same format profiles::dns::record already emits).
  • Pushes the delta to the server — one TSIG-signed UPDATE message per zone, so one bad zone (e.g. a NOTZONE) cannot abort the others.
  • Re-reconciles when:
    • the records file changes (inotify on the directory, so atomic replace is caught),
    • a new interface address appears (DHCP assign/renew) — address removals (interface down) are ignored, so a transient link drop never disturbs records; loopback/link-local are ignored too,
    • a periodic safety-net timer fires,
    • it receives SIGHUP.
  • Exposes a local status API (unix socket) for facter and health checks.

Names are qualified correctly: a record name already ending in . is used verbatim, so there is no .. empty-label bug — and a malformed record is rejected with a clear error instead of being sent as broken wire data.

Why native RFC2136 (not nsupdate)

The daemon talks the update protocol directly (github.com/miekg/dns), so every zone update carries a structured server RCODE and error. That is the observability the shell version lacked — a bad name or a missing zone shows up immediately in the logs and the status API instead of an opaque nsupdate ... failed.

Configuration

Flags or env (see packaging/env.sample); env wins via the systemd EnvironmentFile:

flag env default
-server DNS_UPDATER_SERVER (required)
-key-file DNS_UPDATER_KEY_FILE /etc/dns-updater/key
-records-file DNS_UPDATER_RECORDS_FILE /var/lib/dns-updater/records
-state-file DNS_UPDATER_STATE_FILE /var/lib/dns-updater/applied
-watch-interfaces DNS_UPDATER_WATCH_INTERFACES true
-resync DNS_UPDATER_RESYNC 10m
-api DNS_UPDATER_API /run/dns-updater/api.sock
-log-level DNS_UPDATER_LOG_LEVEL info
-oneshot DNS_UPDATER_ONESHOT false
-deny-ranges DNS_UPDATER_DENY_RANGES (none)
-allow-ranges DNS_UPDATER_ALLOW_RANGES (none)
-deny-domains DNS_UPDATER_DENY_DOMAINS (none)
-allow-domains DNS_UPDATER_ALLOW_DOMAINS (none)

The TSIG key file is BIND format (key "name" { algorithm ...; secret "..."; };).

Filtering

*-ranges are comma-separated CIDRs; *-domains are comma-separated FQDN suffixes. Range rules apply to records that carry an address — A/AAAA by value, PTR by the address encoded in the reverse-DNS owner — so both prodnxsr01-kube-lb0 A 198.18.200.2 and the matching …200.18.198.in-addr.arpa PTR are dropped by -deny-ranges=198.18.200.0/24. Domain rules apply to every record by owner name. Deny wins; a non-empty allow list means "only these". This keeps k8s/LB/internal addresses (pod/service CIDRs, LB VIP ranges) out of the authoritative zones and stops NOTAUTH updates for zones the server does not host.

Status API

  • GET /status → JSON: health, managed-record count, last reconcile/change time, and per-zone {adds, deletes, rcode, error}.
  • GET /healthz → 200 when healthy, 503 otherwise.

The packaged facter fact (/opt/puppetlabs/facter/facts.d/dns_updater.sh) queries this and emits dns_updater_healthy, dns_updater_zones_failed, dns_updater_failed_zones, etc. Puppet already knows the desired records (it writes the file); these facts report what actually landed on the server.

Logging

INFO on real changes (applied) and failures (reconcile partial, zone update failed with zone + rcode); the steady-state "nothing to do" path stays at DEBUG, so periodic resyncs and interface flaps do not spam the journal. slog key=value output parses cleanly in VictoriaLogs.

Build / release

make build      # binary
make test       # unit + in-process TSIG server integration tests
make rpm        # RPM via nfpm (needs the binary)
make patch      # tag vX.Y.(Z+1) and push -> Woodpecker release
S
Description
RFC2136 dynamic-DNS updater daemon: watches a records file and network interfaces and pushes TSIG-signed updates to BIND, with a local status API for facter.
Readme 72 KiB
v0.1.1 Latest
2026-07-18 08:28:28 +10:00
Languages
Go 91.5%
Shell 6%
Makefile 2.5%