Adds a Filter (allow/deny CIDRs + allow/deny domain suffixes) applied to the desired set before reconcile. Range rules match A/AAAA by value and PTR by the address encoded in the reverse-DNS owner, so both the junk A record and its reverse PTR are dropped together. Configured via -deny-ranges/-allow-ranges/ -deny-domains/-allow-domains (comma-separated) or DNS_UPDATER_* env. Purpose: keep k8s pod/service CIDRs, LB VIP ranges and other internal addresses out of the authoritative zones, and stop NOTAUTH updates for reverse zones the server does not host (10.42.x, 198.18.200.x, etc.).
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 formatprofiles::dns::recordalready 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