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]>
57 lines
2.9 KiB
YAML
57 lines
2.9 KiB
YAML
---
|
|
# Flux Kustomization: WireGuard remote-access VPN — reconciles ./platform/vpn-ingress.
|
|
#
|
|
# POST-PLATFORM TIER (dependsOn platform-config), NON-GATING (wait:false). Not a line in
|
|
# platform/kustomization.yaml, for the standard reason plus one specific to this component:
|
|
#
|
|
# * STANDARD: a leaf must never sit in the gating `platform` tier. While `platform` is not-Ready
|
|
# NOTHING downstream reconciles — the incident behind the headlamp/oauth2-proxy/homepage moves
|
|
# (DECISIONS-LOG: "Headlamp gating stall").
|
|
# * SPECIFIC, AND THE REASON THIS IS NOT A JUDGEMENT CALL: this DaemonSet cannot start until the
|
|
# operator creates the out-of-band `wg-server-key` Secret (runbooks/vpn-remote-access.md
|
|
# step 1). That is the correct fail-closed state — an unenrolled VPN must not come up — but in
|
|
# the gating tier it would have been a self-inflicted platform-wide outage ON MERGE, with the
|
|
# cause (a missing manual step) invisible from any Flux condition.
|
|
#
|
|
# WHY dependsOn platform-config AND NOT cilium. This component needs no Gateway, no TLS cert, no
|
|
# Keycloak and no LB-IPAM pool — it has no HTTPRoute and no LoadBalancer Service, and hostNetwork
|
|
# means it does not even need a Cilium endpoint. It could legally reconcile much earlier. It is
|
|
# pinned behind platform-config anyway so that the ONE thing remote clients actually consume
|
|
# through it — split-horizon DNS and the shared Gateway — exists by the time the tunnel does.
|
|
# A tunnel that comes up before the things on the far side of it is a support call, not a feature.
|
|
#
|
|
# wait:false: nothing in the graph dependsOn this, and its pods are legitimately not-Ready whenever
|
|
# the server key is absent or the `wireguard` kernel module has not been loaded on a node. Neither
|
|
# condition should be able to gate anything else, even if a future resource is mistakenly pointed
|
|
# at it.
|
|
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
|
kind: Kustomization
|
|
metadata:
|
|
name: vpn-ingress
|
|
namespace: flux-system
|
|
spec:
|
|
interval: 10m
|
|
retryInterval: 2m
|
|
timeout: 5m
|
|
prune: true
|
|
wait: false
|
|
dependsOn:
|
|
- name: platform-config
|
|
sourceRef:
|
|
kind: GitRepository
|
|
name: flux-system
|
|
path: ./platform/vpn-ingress
|
|
# REQUIRED, not boilerplate: platform/vpn-ingress/daemonset.yaml renders ${WG_CLIENT_CIDR} into
|
|
# the config-assembly script. Without this block that token becomes the EMPTY STRING and the
|
|
# Kustomization still reports Ready (Flux's substitution fails OPEN — ci:
|
|
# flux-postbuild-substitution), leaving an initContainer that derives a server address of ".1/"
|
|
# and a MASQUERADE rule with no source. Both fail loudly at pod start, but the Kustomization
|
|
# would look healthy.
|
|
postBuild:
|
|
substituteFrom:
|
|
- kind: ConfigMap
|
|
name: cluster-vars
|
|
# NO `decryption:` stanza is needed in the overlays: this path carries no *.sops.yaml. The server
|
|
# key is an out-of-band Secret (platform/vpn-ingress/kustomization.yaml says what to do if that
|
|
# ever changes).
|