Files
home-network-k3s-impl/decisions/2026-11-08-wg-home-peer-pihole-sources.md
T
jakman85andClaude Opus 5.5 f13d336630
gate / gate (pull_request) Successful in 1m12s
gate / conformance (pull_request) Skipped
fix(vpn): enrol keith-phone-home and admit the WireGuard subnet to PiHole DNS
Add the at-home peer (10.90.0.3) and list ${WG_CLIENT_CIDR} in the PiHole
DNS frontends' loadBalancerSourceRanges. wg0 is a Cilium device, so tunnel
clients reach the source-range check untranslated; a LAN-only list is
predicted to drop their DNS. Adds an L0 guard that both ranges are present
and every range is RFC1918, and corrects the README's SNAT and IPv6 claims.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 20:45:48 -06:00

6.6 KiB

date, branch, title
date branch title
2026-11-08 fix/wg-home-peer-pihole-sources Enrol keith-phone-home and admit the WireGuard subnet to PiHole's DNS frontends

What changed

  • platform/vpn-ingress/peers.conf: new peer keith-phone-home, 10.90.0.3/32, key generated on the phone. It is the home half of a two-tunnel profile (Endpoint 192.168.2.10:51820), because the router has no hairpin NAT and wan. is unusable from inside the house.
  • apps/wave-0/pihole-dns.yaml: ${WG_CLIENT_CIDR} added after ${LAN_CIDR} on both pihole-dns-udp and pihole-dns-tcp. This file is hand-written, not rendered (catalog/render.py only lists it in EXTRA_UNITS), so it is the source of truth and --emit-waves was not run. catalog/pihole.yaml changes in one comment only, so nothing renders differently.
  • New L0 guard conformance/tests/l0-static/pihole-dns-source-ranges (whole-repo scope, R-DNS-03): every PiHole LoadBalancer Service lists both tokens, and every range resolves through cluster-vars (base and prod overlay) to RFC1918.
  • platform/vpn-ingress/README.md: corrected "clients arrive SNATed" (false for LoadBalancer VIPs) and the IPv6 claim (Android's GoBackend blocks IPv6 for a single-peer default route); documented the home/away pattern, why wan. fails from inside, and the prod-1 dependency.

Rollout on merge: the wg-peers ConfigMap hash changes (m9kcckcb5t -> 29mf6k867t), so the DaemonSet rolls, maxUnavailable: 1, maxSurge: 0, one node at a time in an order the controller chooses. Each roll deletes and recreates that node's wg0, so an established session on that node drops and must re-handshake. keith-phone has no session today (see below), so nothing is interrupted in practice.

The one risk worth watching: wg0 is in Cilium's auto-detected device list and devices is still unpinned. On 2026-10-06 a crash-looping wg0 churned that list and all three nodes lost their main-table default route. A rollout is one delete/create per node, not a loop, so it should not repeat that, but after merge check ip route get 1.1.1.1 on each node.

The Service change is an in-place update: same VIP, same Service objects, no pod restart.

Checked on the rest of the path and left unchanged:

  • Gateway VIP .20 (gateway/cilium-gateway-shared): no source ranges, live. Nothing needed.
  • pihole-allow-lan-dns-ingress admits world on :53, which covers an untranslated 10.90.0.x source. platform/vpn-ingress/networkpolicy.yaml does not apply to a hostNetwork pod.
  • No L0 check pinned these ranges to the LAN; none needed updating.

Flagged, not changed (out of scope):

  • runbooks/vpn-remote-access.md "Known limitations" repeats both corrected claims, and step 3 of its verification table says the Gateway sees a host-identity source.
  • platform/vpn-ingress/daemonset.yaml's header repeats the SNAT claim.
  • clusters/base/cluster-vars.yaml's substitution-coverage comment does not list WG_CLIENT_CIDR under apps-wave-0. The variable is defined and substituted there regardless.
  • peers.conf still says "No peers enrolled yet" above the enrolled peers.
  • PiHole's NodePorts are not covered by source ranges (already acknowledged in pihole-dns.yaml). Tunnel clients can reach node addresses, so they can reach those too; they are enrolled devices.
  • Pinning Cilium's devices remains the open hardening for the churn risk above.

What I verified

Live, read-only, 2026-11-08:

  • cilium-dbg status on one agent: KubeProxyReplacement: True [eno1 ..., wg0 10.90.0.1, tun0 ...] and Masquerading: BPF [eno1, wg0, tun0]. So wg0 really is a load-balancing device.
  • pihole-dns-tcp and -udp carry ranges=[192.168.2.0/24]; no other LoadBalancer Service has source ranges. Live cluster-vars: WG_CLIENT_CIDR=10.90.0.0/24, LAN_CIDR=192.168.2.0/24.
  • wg show wg0 latest-handshakes on all three pods: keith-phone = 0.
  • wan.home.keithjackson.dev resolves publicly to a single A record (via 1.1.1.1).

Offline, in the pinned toolbox:

  • flux envsubst --strict renders both Services to [192.168.2.0/24, 10.90.0.0/24].
  • kubectl kustomize platform/vpn-ingress: wg-peers-m9kcckcb5t on the old peers.conf, wg-peers-29mf6k867t on the new one.
  • Guard mutation test, each restored afterwards. Baseline passes (22 assertions, 2 Services). All seven fail with rc 1 and a named message: WG range dropped from one Service; 0.0.0.0/0 added; 8.8.8.0/24 added; WG_CLIENT_CIDR made public in cluster-vars; prod overlay sets LAN_CIDR to 0.0.0.0/0; ranges block removed; undefined token added.
  • The Android claim was read from upstream GoBackend.java (wireguard-android, master): the allowFamily calls are skipped when sawDefaultRoute && config.getPeers().size() == 1.
  • Gate (pinned toolbox, from this worktree, on the committed tree): rc 3, UNDETERMINED, not green. gitea-settings is the only stage counted as not run (no token locally). conformance (L2/L3) also skipped as the declared no-cluster gap. spec-immutability, decisions-log, lint, l0 (including the new guard), render-golden and l1 passed. approval-policy reported N/A offline; it will be red in CI until the operator overrides, because conformance/** is sensitive.
  • git merge-tree against feat/pihole-explicit-lan-names (#157): no conflicts.

What I did NOT verify

  • DROP_NOT_IN_SRC_RANGE was NOT observed live. It is a prediction from Cilium's source. Only the wg0 device membership was measured; the drop itself has never been seen.
  • Full-tunnel behaviour was NOT observed live, on either profile. No handshake has ever been recorded for any peer: keith-phone reads 0 on all three pods (a counter that resets when a pod restarts, last 22 days ago, so it covers only the time since; earlier entries here also record none).
  • That tunnel DNS works after this change, including the reply path from a backend on another node and from one on the ingress node. That needs the phone: dig over the tunnel, while watching cilium-dbg monitor --type drop.
  • That the Gateway serves tunnel clients. Its source is predicted to be world, as for a LAN browser, which is what the apps admit; not tested.
  • That the new peer's key is the one on the phone. I copied the public key from the brief.
  • That the rollout leaves node default routes intact. Reasoned from the 2026-10-06 incident being a loop, not measured.
  • That a LAN client is unaffected while the Service's source ranges are updated. Expected, not measured.
  • IPv6 behaviour on any non-Android client.
  • That the WAN forward targets prod-1: it is off-repo.