rdmsm4x-changelog-20260903-1735-fleet-dynamic-dns-cloudflare
2026-09-03 17:12–17:35 EDT · claude@rdmsm4x · ticket TASK-20260903-31
Replaced hand-maintained fleet DNS with a per-host Cloudflare API
updater on all six Macs. Every Mac now publishes its own LAN and
Tailscale addresses to dataroo.net and keeps them current,
instead of relying on records a human typed once and on UDM DHCP
reservations.
Why
Rich asked how to configure macOS's "Use dynamic global hostname" against Cloudflare. That control is Apple's RFC 2136 / TSIG DNS UPDATE client; Cloudflare exposes no DNS UPDATE endpoint, only HTTPS, so the toggle can never work against it and should stay off. The equivalent capability is a small updater calling the Cloudflare API.
Scope
All six fleet Macs: rdmsm4x, rdmbair15m5, rdmbair13m5, jdmbair13m5,
rdmpw3265m, rdmpw3275m. Cloudflare zone dataroo.net. No
other zone touched; eastcoastscience.com deliberately
untouched (it is the live mail/MDM zone).
What changed
New Cloudflare API token. Scoped to Zone:DNS Read +
Write on dataroo.net only, token id
26224061762cf405c14847d115e7aa76. Verified to see exactly
one zone. Stored as CLOUDFLARE_DDNS_TOKEN in
~/.secrets/global.env on each host, mode 600, with a dated
comment. The pre-existing Global API Key was not reused for this. No
secret value appears in any file, log, or record.
New script, canonical at
rdmsm4x:~/dev/net/cloudflare/ddns/fleet_ddns.sh (v1.0.1),
runtime copy at ~/scripts/fleet_ddns.sh on all six hosts,
sha256 prefix bde065701909 on every one. bash 3.2, pure
ASCII, Blocks theme, exit codes 0/10/20/30.
New LaunchAgent
com.eastcoastscience.ddns on all six hosts:
~/Library/LaunchAgents/com.eastcoastscience.ddns.plist,
300s StartInterval, RunAtLoad, WatchPaths on
/private/var/run/resolv.conf, ThrottleInterval 30, Nice
10.
DNS records now maintained automatically (TTL 60,
DNS-only): <host>.dataroo.net A,
<host>.ts.dataroo.net A and AAAA. Six LAN A records
were created — two of them (rdmbair15m5,
jdmbair13m5) had never existed anywhere. The ts records
already existed from the 2026-08-21 normalization and were left as-is
where correct.
Files modified: ~/.secrets/global.env
(all six hosts, one comment + one key appended; backup
~/.secrets/global.env.bak-20260903-ddns mode 600 on each).
Files added: ~/scripts/fleet_ddns.sh (six
hosts),
~/Library/LaunchAgents/com.eastcoastscience.ddns.plist (six
hosts),
~/dev/net/cloudflare/ddns/{fleet_ddns.sh,README.md,SESSION-STATE.md},
appended a section to ~/dev/net/cloudflare/PROJECT.md.
Verification evidence
- 18 records read back from the Cloudflare API with the new token, all correct.
- Update path proven, not just create:
rdmsm4x.dataroo.netwas deliberately set to10.99.99.99, then a run corrected it, loggedupdated(10.99.99.99->192.168.0.29). fleet_ddns.sh statusexits 0 on all six hosts (it checks over DoH, notdig).launchctl print gui/$UID/com.eastcoastscience.ddnssucceeds on all six.- Runs correctly under a launchd-like minimal environment.
Traps found, and the fixes
- The UDM intercepts outbound port 53.
dig +short @8.8.8.8 rdmbair13m5.dataroo.netreturns192.168.0.131; a public resolver cannot know an RFC1918 address. An earlier report in this session listed<host>.dataroo.netrecords as existing in Cloudflare when Cloudflare held none — the answers came from the router. Verify this zone over DNS-over-HTTPS only.statusdoes. - Concurrent runs raced. launchd
RunAtLoadfired while the rollout ran a manual pass; four hosts loggedAn identical record already exists.Fixed in v1.0.1 with amkdirlock (5-minute stale reclaim) and by treating that error as success only after re-reading and confirming content. Records were correct throughout. - A piped exit code lied.
... run | sed 's/^/ /'reportedexit=0fromsedwhile two hosts had failed. Capture, then filter. command -v jqfails under launchd (minimal PATH omits/opt/homebrew/bin). Resolved by absolute path, astailscalealready was.- Negative caching hid newly created records from the local resolver for minutes.
How to undo
Per host: bash ~/scripts/fleet_ddns.sh uninstall
(removes the agent; DNS records are left untouched), then optionally
rm ~/scripts/fleet_ddns.sh and restore
~/.secrets/global.env.bak-20260903-ddns. To revoke access
entirely, delete the API token in the Cloudflare dashboard (id above) —
that alone stops every host from writing. The six created LAN A records
can be deleted in the dashboard if the whole idea is abandoned.
Outstanding owner actions
- UniFi credential needed. Rich asked to remove the
UDM's local DNS entries via the UniFi API so Cloudflare is the single
source of truth.
~/.secrets/udm.envholds only a root SSH password (itself flagged for rotation), andsshpassis not installed. The Network API needs a local-admin login or an API key. Endpoint confirmed present, returns 401:https://192.168.1.1/proxy/network/api/s/default/rest/dnsrecord. Entries appear to exist for four Macs only: rdmsm4x, rdmbair13m5, rdmpw3265m, rdmpw3275m. - Private addresses are now in public DNS.
<host>.dataroo.netpublishes RFC1918 addresses worldwide. That is normal for this pattern and is what makes it work off the UDM, but it does disclose internal addressing. Say the word and the LAN names can be dropped, keeping only the Tailscale names. UDM_ROOT_PWin~/.secrets/udm.envis still flagged for rotation from 2026-08-24.