feat(dns): fleet .internal naming — git-sourced, agent-managed, three resolvers
Names for fleet hosts so addresses stop needing to be memorised. Built because IPv6 makes that hopeless — and, more to the point, because v6 addresses are derived rather than assigned, so they cannot reliably be written down once and trusted either. 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 Naming is <host>.<site>.internal with sites ana/esh/nh3 (operator's call). .internal is ICANN-reserved for this; .local is reserved for mDNS, which is why searxng.pfi.local was a collision that merely happened to work. Same posture as deploy-stack.sh: file is intent, resolvers are derived state, you see a diff before anything changes. Every name is published to every resolver, so the site label says where a host IS, not who knows about it. Two properties that matter: - Authority is scoped to the ZONE, not the resolver. ESH carries hand-made esteban.net rewrites predating this; they are read, ignored and preserved. Resolver-wide authority would have silently deleted them. - Within .internal it IS authoritative, so UI-added names get removed. That is the point — one place to look. Colo gap closed: ana-docker had no resolver at all (hosts went straight to 1.1.1.1). Its AdGuard runs API on 8053 because 8080/3000 were taken, so the port is carried per-site in the yaml rather than assumed by the script. It ships with no blocklists — a false positive on a server network breaks service-to-service calls for no upside. Auth is a dedicated infra-ops AdGuard user, not the operator's account, password vaulted at nh3-dev/adguard-infra-ops-password. Pre-change configs backed up on each resolver. Both resolvers stayed answering across the restart. searxng.pfi.local -> searxng.ana.internal, with the old Host() kept alongside so nothing breaks mid-migration. matrix.pfi.local deliberately NOT migrated: a Matrix server_name is baked into every user id, room id and signing key, so renaming it rebuilds the homeserver's identity rather than changing a DNS name. The v6 column is empty and correct — no fleet host has a global v6 address yet. The file documents why addresses must be pinned statically before they go in, since a record that silently stops matching is worse than no record.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
# adguard-ana — the Anaheim colo's resolver.
|
||||
#
|
||||
# Stood up 2026-08-19 to close the gap found while designing the .internal
|
||||
# naming scheme: ESH and NH3 each had an AdGuard, but colo hosts were
|
||||
# resolving straight against 1.1.1.1, so the colo had no way to answer for
|
||||
# internal names at all.
|
||||
#
|
||||
# ⚠️ PORTS DIFFER FROM THE OTHER TWO SITES. ana-docker is a busy host: :8080
|
||||
# and :3000 are already taken (langfuse and friends), so AdGuard's web UI and
|
||||
# API live on :8053 here, not :8080. Anything that talks to this API — notably
|
||||
# scripts/dns-sync.py — reads the port from dns/internal.yaml rather than
|
||||
# assuming the ESH/NH3 value.
|
||||
#
|
||||
# ⚠️ THIS IS NOT YET THE COLO'S RESOLVER-OF-RECORD. Standing the service up and
|
||||
# repointing a whole site's DNS are two different changes with two different
|
||||
# blast radii. Colo hosts still use 1.1.1.1; the cutover is a separate,
|
||||
# operator-approved step. Until then this answers only what asks it directly.
|
||||
#
|
||||
# ESH and NH3 run their own, older, unmanaged compose files. They were left
|
||||
# alone deliberately — adopting three live resolvers into this repo while also
|
||||
# introducing a new naming system is two risky changes at once. Worth doing
|
||||
# later, as its own piece of work.
|
||||
|
||||
name: adguard-ana
|
||||
|
||||
services:
|
||||
adguardhome:
|
||||
image: adguard/adguardhome:latest
|
||||
container_name: ${AG_CONTAINER:-adguardhome}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${AG_DNS_PORT:-53}:53/tcp"
|
||||
- "${AG_DNS_PORT:-53}:53/udp"
|
||||
# Web UI + REST API. 8053, not 8080 — see the note above.
|
||||
- "${AG_WEB_PORT:-8053}:80/tcp"
|
||||
volumes:
|
||||
- adguard_work:/opt/adguardhome/work
|
||||
- adguard_conf:/opt/adguardhome/conf
|
||||
networks:
|
||||
- tnet
|
||||
labels:
|
||||
- homepage.group=Service Networking
|
||||
- homepage.name=AdGuard (ANA)
|
||||
- homepage.icon=mdi-dns
|
||||
- homepage.description=DNS resolver + .internal zone (colo)
|
||||
- homepage.href=http://10.250.50.70:${AG_WEB_PORT:-8053}
|
||||
- homepage.siteMonitor=http://10.250.50.70:${AG_WEB_PORT:-8053}
|
||||
|
||||
volumes:
|
||||
adguard_work: {}
|
||||
adguard_conf: {}
|
||||
|
||||
networks:
|
||||
tnet:
|
||||
name: traefik-net
|
||||
external: true
|
||||
@@ -0,0 +1,42 @@
|
||||
# FIRST-BOOT SEED ONLY — AdGuard rewrites this file at runtime.
|
||||
#
|
||||
# It exists so the container comes up already configured instead of sitting in
|
||||
# the setup wizard waiting for a human. After first boot the live copy is in
|
||||
# the adguard-ana_adguard_conf volume and diverges from this file; treat this
|
||||
# as the bootstrap, not as ongoing config. Notably the `rewrites:` list stays
|
||||
# EMPTY here — scripts/dns-sync.py owns it from dns/internal.yaml.
|
||||
#
|
||||
# ⚠️ NO BLOCKLISTS, DELIBERATELY. The other two AdGuards filter ads for human
|
||||
# browsing; this one resolves for a rack of servers. A blocklist false-positive
|
||||
# here breaks service-to-service calls at 3am for no upside, so filtering is
|
||||
# off and this instance is a plain recursive resolver plus the .internal zone.
|
||||
http:
|
||||
address: 0.0.0.0:80
|
||||
users:
|
||||
- name: infra-ops
|
||||
password: $2a$05$v7d1ASBg9qprZNx3Y50YqempdiL.Lo8.jMYxyVr6MroaGF0w5tQje
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
language: en
|
||||
theme: auto
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 53
|
||||
upstream_dns:
|
||||
- 1.1.1.1
|
||||
- 1.0.0.1
|
||||
bootstrap_dns:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
protection_enabled: true
|
||||
filtering_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
parental_enabled: false
|
||||
rewrites: []
|
||||
filters: []
|
||||
whitelist_filters: []
|
||||
user_rules: []
|
||||
log:
|
||||
verbose: false
|
||||
schema_version: 29
|
||||
@@ -23,7 +23,12 @@ services:
|
||||
# ------------------------------------------------------------------
|
||||
environment:
|
||||
- SEARXNG_SECRET=${SEARXNG_SECRET}
|
||||
- BASE_URL=https://searxng.pfi.local/
|
||||
# searxng.ana.internal, not the old searxng.pfi.local (migrated
|
||||
# 2026-08-19). `.local` is reserved for mDNS, so the old name was a
|
||||
# standards collision that happened to work; `.internal` is ICANN-
|
||||
# reserved for exactly this. The name is served by the fleet's AdGuard
|
||||
# resolvers from dns/internal.yaml — see scripts/dns-sync.py.
|
||||
- BASE_URL=https://searxng.ana.internal/
|
||||
- INSTANCE_NAME=SearXNG
|
||||
# ------------------------------------------------------------------
|
||||
# Resource limits — tune for VM 102's available RAM/CPU
|
||||
@@ -72,7 +77,12 @@ services:
|
||||
labels:
|
||||
# Traefik configuration — auto-discovery via Docker provider
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.searxng.rule=Host(`searxng.pfi.local`)
|
||||
# Both names during the migration: `.internal` is the real one now, and
|
||||
# the old `.pfi.local` is kept as a fallback so anything still pointing
|
||||
# at it (a bookmark, a hardcoded config elsewhere) does not break the
|
||||
# day the name changes. Drop the second Host() once nothing uses it —
|
||||
# the Traefik access log will tell you when that is.
|
||||
- traefik.http.routers.searxng.rule=Host(`searxng.ana.internal`) || Host(`searxng.pfi.local`)
|
||||
- traefik.http.routers.searxng.entrypoints=websecure
|
||||
- traefik.http.routers.searxng.tls=true
|
||||
- traefik.http.routers.searxng.service=searxng
|
||||
|
||||
Reference in New Issue
Block a user