--- date: 2026-11-08 branch: fix/wg-home-peer-pihole-sources title: 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.