Compare commits

..
Author SHA1 Message Date
jakman85andClaude Opus 5 f92707f38c feat(vpn-ingress): WireGuard remote access on udp/51820 (hostNetwork, declarative peers)
gate / gate (pull_request) Successful in 1m47s
gate / conformance (pull_request) Has been skipped
Lets the operator's Android / Linux / Windows / macOS devices dial in from
anywhere and behave like LAN machines: full reach to 192.168.2.0/24 plus
internet egress out the house WAN. Opposite direction to platform/vpn-egress,
which sends pods OUT through PIA; the two share nothing but the word "VPN".

WireGuard because it is the only option with a first-party client on all four
named platforms, and because its handshake MAC is keyed with the responder's
own public key: an unauthenticated packet gets NO reply, so udp/51820 is
indistinguishable from a closed port. That, not "it is encrypted", is what
makes an unfiltered WAN port defensible, and why there is no source allowlist.

hostNetwork: true is REQUIRED, not convenient. A pod-networked server must SNAT
clients to its own pod IP, so every remote client would carry that pod's Cilium
identity -- and a pod-sourced request to the shared Gateway VIP returns 403 from
Envoy for ALL 16 hostnames (measured 2026-10-04), because Cilium's Gateway
preserves the original source identity and enforces the destination's ingress
policy. The obvious build would have shipped a healthy-looking tunnel on which
every cluster web UI was 403 and *.home.keithjackson.dev did not even resolve.
In the host netns traffic is SNATed to the node and arrives as host/remote-node,
exactly like a LAN client. It also sidesteps the unsafe ip_forward sysctl, the
L2-announcement lease lottery, and double encapsulation (hence MTU 1420).
Permitted by restrict-host-namespaces' own carve-out for platform namespaces.
The cost is stated in networkpolicy.yaml rather than papered over: no
NetworkPolicy constrains a hostNetwork pod.

A DaemonSet so the hand-made WAN forward cannot be invalidated by a reschedule;
all three nodes answer the same keys, so failover is a one-line router change.

Peer PUBLIC keys are plaintext in peers.yaml, which makes "who can get into my
house" a reviewable PR diff. The only secret -- the server's private key -- is
an out-of-band Secret: an agent must not mint a long-lived private key, and the
two halves are joined only at pod start into a tmpfs that dies with the pod.

Ships INERT-BUT-HEALTHY with zero peers. THREE off-repo manual steps remain
(kernel module, server key, WAN forward) -- runbooks/vpn-remote-access.md.

Touches clusters/** , so ci/approval-policy.sh will block pending the
operator's /approve-override. Deliberately not posted by me (CLAUDE.md §1).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-05 22:29:46 -05:00
2 changed files with 24 additions and 3 deletions

No files matched your search

@@ -113,6 +113,25 @@ Live prod, read-only, 2026-10-05, plus the offline gate.
`spec-immutability`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1` and `approval-policy` all
**pass**. The rc 3 is two environmental skips — `gitea-settings` (no local `SETTINGS_API_TOKEN`)
and `conformance` L2/L3 (`CLUSTER_AVAILABLE` unset). **Not green. Reported as rc 3, not as pass.**
Run from an isolated git worktree with both CLAUDE.md §3 workarounds applied
(`DEV_KUBECONFIG=/dev/null` plus an `ENGINE` shim bind-mounting the main checkout's `.git` and
the sibling spec repo) — which is why `spec-immutability` is **green here and skipped in CI**,
where the runner has no spec checkout. `approval-policy` passing locally is an offline N/A, not a
verdict; the real verdict is the CI run below.
- **The CI gate ran on PR #55 (run 244, head `1a67fc1b`) and `approval-policy` is the ONLY failing
stage** — read from the job log, not from the red tick. Verbatim: "This PR changes the pipeline or
the security posture it rests on, and has NO approving review from anyone other than its author
('gitea-admin')", naming exactly the three files I predicted:
`clusters/base/cluster-vars.yaml`, `clusters/base/kustomization.yaml`,
`clusters/base/vpn-ingress.yaml`. Every other stage that ran reported green —
`gitea-settings`, `decisions-log`, `lint`, `l0`, `render-golden`, `l1`. So the branch is blocked
on the operator's `/approve-override <reason>` (posted as a PR **review** of type "Comment") and
on nothing else. **I have not posted it** — CLAUDE.md §1.
- **Branch protection is live, measured by CI's own `gitea-settings` stage** (it has the token this
workstation lacks): `required_approvals = 0`, `enable_status_check = true`,
`status_check_contexts = ["gate / gate (pull_request)"]`, `block_admin_merge_override = true`,
`enable_push = false`, `dismiss_stale_approvals = true`, and **1 runner online**. That is the §2b
re-measurement of CLAUDE.md §2's table for this branch, not a restatement of it.
- **`flux-postbuild-substitution` (L0) actually exercised the new wiring, and caught a real defect
in it.** On the first run it FAILED, naming `platform/vpn-ingress/daemonset.yaml:118` and
attributing it to "Flux Kustomization 'vpn-ingress' (clusters/base/vpn-ingress.yaml, path
@@ -192,8 +211,6 @@ Live prod, read-only, 2026-10-05, plus the offline gate.
`devices`, so Cilium auto-detects; whether it attaches to `wg0` — and whether that matters — is
unknown. `wg` interfaces are already filtered out by `node_health_collect.py`, so something in
the estate anticipates them, but that is not evidence about Cilium.
- **The CI approval-policy outcome.** The local stage reported green, but that is an offline N/A,
not a verdict: this PR touches `clusters/**`, so CI will block pending an override.
- **Zero peers means the end-to-end authorisation path is untested**, including revocation. The
first enrolment is also the first test of `peers.yaml`'s format being what `wg-quick` accepts.
- **No peer-level observability exists.** An exporter publishing
+5 -1
View File
@@ -267,7 +267,11 @@ spec:
- name: wg-config
emptyDir:
medium: Memory
sizeLimit: 1Mi
# 8Mi, not 1Mi: /config is this image's whole config ROOT, not just our one file — its
# s6 init also writes templates and state there, and a tmpfs that small would ENOSPC
# during startup rather than at a point anyone would connect to the volume size. Still
# counted against the container's 128Mi limit, and still far more than wg0.conf needs.
sizeLimit: 8Mi
# The ONLY secret in this component. Created OUT OF BAND by the operator and referenced by
# name (R-IDP-04) — runbooks/vpn-remote-access.md step 1. It is NOT a *.sops.yaml in this
# tree on purpose: an agent must not mint a long-lived private key (CLAUDE.md §6), and a key