fix(dns): resilient node resolvers — mechanism-detecting, per-environment (R-DNS-01/03)
Re-authors the fix stranded on origin/fix/node-dns-resilient-upstream (7a34590). That
commit was NOT landed: it wrote a netplan drop-in and ran `netplan apply`, but netplan
does not exist on these nodes any more (Debian 13 trixie generic cloud image; not in
node_baseline_packages). Landing it would have failed the handler with command-not-found
and BROKEN node-baseline.yml on every node — worse than not landing it. Its dev-LAN
literals also sat in role defaults, which prod inherits, pointing prod nodes at a PiHole
on a network they cannot reach.
The underlying problem is real and verified: a SINGLE upstream resolver is a hard SPOF.
PiHole answers jackson.lan instantly but its external recursion flaps — 0/25 for public
names (registry-1.docker.io, quay.io, ghcr.io) while gitea.jackson.lan resolved 5/5 —
breaking containerd image pulls and CoreDNS `forward . /etc/resolv.conf`.
MECHANISM DETECTION (new `dns` tag in node_baseline). Five read-only probes feed one
set_fact yielding three outcomes, because the two environments no longer share a resolver
stack (dev-1 is behind resolved's 127.0.0.53 stub; prod-1 has a direct resolv.conf and no
cloud-init at all):
* systemd-resolved — resolved running AND owning /etc/resolv.conf (proven by the
symlink pointing into /run/systemd/resolve/, or by every nameserver being a resolved
loopback). Writes resolved.conf.d/99-k3s-dns.conf with an ordered DNS=, Domains=, and
FallbackDNS= EXPLICITLY EMPTIED so systemd's compiled-in public resolvers can never
silently bypass the local zone and ad-blocking.
* resolv-conf-file — plain directly-managed file (prod's shape). Ordered nameserver
lines + options timeout:2 attempts:2, backup: true. Deliberately NO `rotate`, which
would destroy PiHole-first ordering.
* contended — resolvconf/openresolv present or NetworkManager active. Writes NOTHING
and fails loudly: a change that silently gets reverted is worse than no change.
Resolved-running-but-not-owning-resolv.conf correctly falls through to the file branch.
Ordering is the policy that keeps the local zone working — the second server is only
consulted after the first fails, so a gateway NXDOMAIN can never beat PiHole to a
jackson.lan answer.
SEAMS. Dev values in role defaults ([192.168.1.18, 192.168.1.1], search jackson.lan);
prod values in inventory/group_vars/k3s_prod/vars.yml. NOT cluster-vars — that ConfigMap
is consumed by Flux postBuild for platform/ and apps/, and no Ansible code reads it.
PROD'S SECOND UPSTREAM IS AN OPEN OPERATOR DECISION and is left as a loud
BLOCKED-pending-second-upstream placeholder, the same precedent as the node_baseline disk
devices, with a dns-tagged assert that stops the play before any file is written. A
public resolver was NOT invented as a default: it would bypass the local zone, which is a
decision, not a default. PROD-PROVISIONING.md §4 was rewritten (the old bullet pointed at
the now-dropped branch) to state the choice, including that prod routing to the dev
PiHole is unverified across the pfSense boundary.
tofu: servers = [var.dns_server, var.network_gateway] — the one hunk of the old branch
worth taking as-is. Dev-only by nature.
ALSO FIXES A FALSE WARN THIS WOULD OTHERWISE CREATE: node_health_collect.py parsed
/etc/resolv.conf only, so on dev it read the 127.0.0.53 stub and would report a permanent
single-upstream WARN even after a correct fix. It now asks resolvectl for the real
upstreams when it sees the stub — and if that fails, nameservers is NULL with the reason
recorded, never the stub and never a guess. assess.yml turns null into SKIP, not WARN and
not PASS. The honesty discipline is preserved exactly.
Verified with NO play run against any node: syntax-check + yamllint + tofu fmt clean;
--tags packages still selects ONLY the apt task (the prod package-floor escape hatch
bypasses DNS entirely); --list-hosts unchanged; -c local simulation driving the REAL
detection expression and REAL file bodies extracted from the role by script (so the test
cannot drift) passes all 5 scenarios — dev stub -> systemd-resolved, prod shape ->
resolv-conf-file, NetworkManager and resolvconf -> contended; dev defaults pass the
assert byte-identically while prod overrides fail it by name; collector unit-tested
against 8 resolver shapes.
Serves: R-DNS-01, R-DNS-03, R-ENV-02, R-ENV-03.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
14be9b68f6
commit
c1f24e4a6d
10 files changed
+505
-23
No files matched your search
@@ -125,10 +125,37 @@ Consequences to settle before imaging nodes 2 and 3:
|
||||
(`GATEWAY_VIP` `.53` = first of `.53-.60`).
|
||||
- [ ] **Reserve all of `192.168.2.9` and `.20-.30` outside any DHCP range** on the prod LAN before
|
||||
the build, and confirm nothing else on the network already answers on them.
|
||||
- [ ] **Resolver: configure at least two upstreams.** Node 1 currently has a single IPv4 nameserver
|
||||
(`192.168.2.1`). A single upstream is a known SPOF that has already bitten this project —
|
||||
it broke containerd image pulls and CoreDNS forwarding on dev. A fix exists but is **stranded
|
||||
unmerged** on `fix/node-dns-resilient-upstream`; land it, then apply the same pattern here.
|
||||
- [ ] **Resolver: SECOND UPSTREAM IS AN OPEN DECISION — blocks `node-baseline.yml --tags dns`.**
|
||||
Node 1 has a single IPv4 nameserver, `192.168.2.1`, which is also the LAN gateway. A single
|
||||
upstream is a known SPOF that has already bitten this project — it broke containerd image
|
||||
pulls and CoreDNS forwarding on dev.
|
||||
|
||||
The MECHANISM is now in the repo and needs no further work: the `dns`-tagged block in
|
||||
`roles/node_baseline` detects whether a node uses systemd-resolved (dev: writes
|
||||
`/etc/systemd/resolved.conf.d/99-k3s-dns.conf`, restarts resolved) or a directly-managed
|
||||
`/etc/resolv.conf` (prod's shape: writes the file, refusing if resolvconf/NetworkManager
|
||||
owns it), and configures the ordered upstream list either way. The earlier
|
||||
`fix/node-dns-resilient-upstream` branch was **dropped, not merged** — it wrote a netplan
|
||||
drop-in and ran `netplan apply`, and netplan is not installed on these Debian 13 trixie
|
||||
nodes, so it would have broken `node-baseline.yml` on every host.
|
||||
|
||||
What is still MISSING is the VALUE. `group_vars/k3s_prod/vars.yml` sets
|
||||
`node_baseline_dns_servers` to `192.168.2.1` followed by the deliberately invalid
|
||||
`BLOCKED-pending-second-upstream-decision-see-PROD-PROVISIONING-md-4`, and the role's assert
|
||||
stops the play on it before writing anything. Choosing the second upstream is a **network
|
||||
decision, not a default the role may take**: a public resolver (`8.8.8.8`, `1.1.1.1`) would
|
||||
resolve reliably but would also bypass any local zone and any filtering the gateway applies.
|
||||
Decide between — and record the choice HERE, in the same change that edits the group_vars:
|
||||
- a second on-LAN resolver on `192.168.2.x`;
|
||||
- the dev-side PiHole `192.168.1.18`, **if** prod can actually route to it (unverified —
|
||||
prod sits on the workstation side of the pfSense boundary);
|
||||
- an explicit, written-down acceptance of a public resolver as fallback-only.
|
||||
|
||||
Also decide whether node 1's current IPv6 nameservers (`2001:4860:4860::8888` / `::8844`)
|
||||
are kept: the direct-file write path replaces `/etc/resolv.conf` with exactly the listed
|
||||
servers, so keeping them means listing them explicitly.
|
||||
`node_baseline_dns_search` is `[]` (no search domain, node 1's behaviour today) until prod's
|
||||
local domain is decided.
|
||||
- [ ] Confirm the prod LAN's relationship to the dev LAN (`192.168.1.x`) and to the pfSense NAT —
|
||||
prod on `192.168.2.x` sits on the *workstation* side of the boundary dev lives behind.
|
||||
|
||||
|
||||
Reference in new issue
Block a user