Files
vh bc3aada73a memory: snapshot — .internal DNS live, waterland containerised, homepage themed
Captures a long infra session: fleet *.internal DNS (git-sourced, 42 names,
three resolvers including a new colo one), waterland studio containerised on
irv-ml1, Homepage cleaned up and themed with Australis Skyfall over an
Arbo-generated background, and four unmanaged stacks adopted into stacks/.

Four detail files added. Auto-archived 4 entries to archival-memory.md
(Recent decisions 2, Tried and abandoned 2); 5 held back by the open-deferred
guard rather than moved.

Also records three operator-owned open items: the colo DNS repoint, the
static-v6 convention, and the deliberately belayed AI-tab Dormant regrouping.
2026-08-19 01:20:53 -07:00

5.7 KiB

[2026-08-19] Fleet .internal DNS — git-sourced, agent-managed, three resolvers

Operator: "with ipv6 i can't memorize the IP addresses anymore. need a way to keep track of local .internal dns names that can be agent managed and is lightweight." Built and live in one session; commit b8003c7.

Shape

dns/internal.yaml     source of truth — 38 hosts + 4 service aliases
scripts/dns-sync.py   reconciles AdGuard resolvers against it
stacks/adguard-ana/   the colo's resolver, which did not exist
dns/README.md         workflow, naming, the IPv6 caveat

Deliberately the same posture as deploy-stack.sh: the file is intent, the resolvers are derived state, you see a diff before anything changes. --dry-run / --yes / --site <s>. Verified idempotent — a second run prints nothing to do.

Naming is <host>.<site>.internal with sites ana / esh / nh3 (operator's call). .internal is ICANN-reserved for private use since 2024; .local is reserved for mDNS, which is why the pre-existing searxng.pfi.local was a standards collision that merely happened to work.

Every name is published to every resolver — the site label says where a host is, not which resolver knows about it.

The framing correction that mattered most

The ask reads as "I can't memorise v6 addresses", but the deeper problem is that v6 addresses are derived, not assigned, so they cannot reliably be written down once either. SLAAC gives EUI-64 (MAC-coupled) or privacy-extension (rotating) addresses, and UniFi has no v6 equivalent of a DHCP reservation — so a hand-maintained v6 table rots on its own.

⇒ The fix has two halves and only the second is DNS: (1) pin static v6 on server-class hosts, (2) then the name table is just a file. Surfaced to the operator before building.

Verified 2026-08-19: no fleet host has a global v6 address at all yet — ESH's /56 is live only on esh-cameras, NH3's LANs are back to ipv6_interface_type: none, the colo has none. So the v6: column ships EMPTY and correct, and the naming layer was built first rather than blocking on v6. Names established now need no renaming when addresses land.

Suggested convention when they do (awaiting operator): each server static at its site's /64 with low-order bits echoing the v4 host octet — esh-docker-vm at …::45 — so addresses are declarable and semi-memorable.

Two properties not to break

Authority is scoped to the ZONE, not the resolver. Only rewrites ending in .internal are managed. ESH's resolver turned out to carry three hand-made esteban.net rewrites (eshnas, brotherprinter, eshhome) — my first read of the config missed them, because an awk range on rewrites: matched an empty-looking block. A resolver-wide authoritative sync would have silently deleted all three on first run. Verified intact after sync.

Within .internal it IS authoritative — names added by hand in the AdGuard UI get deleted by the next sync. That is the point: one place to look.

The colo had no resolver at all

ESH and NH3 each ran AdGuard; ana-docker resolved straight against 1.1.1.1, so the colo had no way to answer for internal names. Closed with stacks/adguard-ana/.

⚠️ Its API is on 8053, not 8080 — :8080 and :3000 were already taken on that busy host. The port is therefore carried per-site in the yaml, not assumed by the script, so the odd one out cannot be forgotten.

⚠️ It ships with no blocklists, deliberately. The other two filter ads for human browsing; this one resolves for a rack of servers, where a blocklist false-positive breaks service-to-service calls at 3am for no upside.

First boot uses a seed config (conf/AdGuardHome.seed.yaml) copied into the conf volume before first start, so the container comes up configured instead of sitting in the setup wizard.

Credential — service account, not the operator's

Added a dedicated infra-ops AdGuard user to all three resolvers rather than asking for the lkraven password (per the standing migrate-off-operator- creds directive). Password vaulted at nh3-dev/adguard-infra-ops-password; lkraven untouched; pre-change configs backed up on each host as AdGuardHome.yaml.bak-preinfraops-*. Both existing resolvers kept answering across the restart.

Two landmines worth keeping:

  • Go's bcrypt rejects htpasswd's $2y$ prefix. Same algorithm, different marker; golang.org/x/crypto/bcrypt accepts only $2a$/$2b$. Normalise the prefix, and self-verify the hash with htpasswd -vb BEFORE installing it on a live resolver.
  • The vault appends a trailing newline on get. A password carrying a stray \n fails auth in a way that looks exactly like a wrong password. dns-sync.py strips it.

pfi.local migration — and the one that must NOT move

searxng.pfi.localsearxng.ana.internal, with the old Host() kept alongside in the Traefik rule so nothing breaks mid-migration; both return 200. Drop the fallback once the access log shows the old name unused.

matrix.pfi.local deliberately NOT migrated. A Matrix server_name is baked into every user ID, room ID and signing key, and federation identity derives from it — renaming it is not a DNS change, it is rebuilding the homeserver's identity and invalidating its history. The operator approved "migrate pfi.local" generally; this was surfaced as a deliberate exclusion rather than executed blindly.

Still open

Colo hosts still point at 1.1.1.1, so they do not yet use the new resolver — it only answers what asks it directly. Repointing a whole site's DNS is a bigger change than standing the service up, and is the operator's to schedule.

See also 2026-08-17-fleet-ipv6-mesh.