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]>
This commit is contained in:
1 parent
020e736541
commit
f13d336630
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.
|
||||
Reference in new issue
Block a user