Merge pull request 'fix(vpn): enrol keith-phone-home and admit the WireGuard subnet to PiHole DNS' (#158) from fix/wg-home-peer-pihole-sources into main
gate / gate (push) Successful in 55s
gate / conformance (push) Skipped

Reviewed-on: #158
This commit was merged in pull request #158.
This commit is contained in:
jakman85 committed 2026-11-09 02:50:04 +00:00
commit da0515c8c8
7 files changed
+363 -17

No files matched your search

@@ -0,0 +1,103 @@
---
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.