platform/vpn-egress — shared VPN egress gateway (Phase 3, ADR-0013)
Single shared Gluetun gateway: PIA over WireGuard, built-in kill-switch, native port
forwarding. vpn-class services (sonarr, radarr, jackett, qbittorrent) egress only through this
tunnel; internet-class services go out the normal WAN. Serves R-NET-05/06/07.
STATUS: authored, NOT deployed. Not wired into
platform/kustomization.yamlyet. Deploy needs (a) thepia-credentialsSOPS secret, and (b) validation of the egress-steering mechanism below, which cannot be tested without a live PIA tunnel.
Pieces
gluetun-gateway.yaml— the Gluetun Deployment (replicas: 1,Recreate) + thegluetun-controlService (control API :8000 for forwarded-port lookup + tunnel-state health). Kill-switch =FIREWALL=on.pia-credentials(SOPS) — PIA username/password only; Gluetun derives the WireGuard config at runtime.
Operator hand-off (when deploying Phase 3)
- Provision
pia-credentials(seebootstrap/sops/secret-templates/pia-credentials.yaml.example, namespacevpn-egress, keysusername/password) →sops -e -i→ uncomment it inkustomization.yaml. - cluster-vars (already defined in
clusters/base/cluster-vars.yaml; a ConfigMap — values are plain strings underdata:, injected into the${...}tokens by FluxpostBuild.substituteFrom). Confirm or override:PIA_REGION— a port-forwarding-capable PIA region (defaultSweden; e.g.Netherlands,CA Toronto,Switzerland; NOT a US region — those lack PF). GluetunSERVER_REGIONS.CLUSTER_POD_CIDR— the Cilium cluster-pool pod CIDR (default10.0.0.0/8; pods get10.x, confirmed live — NOT the k3s10.42.0.0/16). GluetunFIREWALL_OUTBOUND_SUBNETS. Per-env overrides go inclusters/<env>/kustomization.yamlas a cluster-vars JSON patch (- op: replace, path: /data/PIA_REGION, value: "…"). With the self-managing root, agit pushauto-applies (no manualkubectl apply -k).
- Enable
- vpn-egressinplatform/kustomization.yaml(Phase 3).
Egress steering — the DESIGN DECISION (needs live validation)
The hard part: routing vpn-class pods through this ONE shared tunnel. Cilium Egress Gateway
(CiliumEgressGatewayPolicy) SNATs matched pods out of a gateway node's interface — it does not
natively route through a pod's tunnel. Options considered:
- Cilium Egress Gateway → the Gluetun node's tunnel interface (recommended). Pin Gluetun to a
node; a
CiliumEgressGatewayPolicyselects pods labeledhomelab.keithjackson.dev/egress-class=vpnand egresses them via that node's WireGuard interface (wg0). If the tunnel drops,wg0is gone → traffic has no egress path → fail-closed (the kill-switch behavior R-NET-06 needs). This is the most Cilium-native fit but requires Gluetun's tunnel interface to be visible to the node datapath (hostNetwork Gluetun, or a shared netns) — the detail to VALIDATE. - Gateway-pod routing: client pods default-route to the Gluetun pod IP (init-container + NET_ADMIN, or a Multus secondary interface); Gluetun forwards+NATs via the tunnel. Works without Cilium egress gateway but is more moving parts and pod-IP-fragile.
- Per-pod Gluetun sidecar — explicitly REJECTED by ADR-0013 (single shared gateway preferred).
Plan: validate option 1 against a live PIA tunnel (confirm wg0 on the Gluetun node + a
CiliumEgressGatewayPolicy steering egress-class=vpn pods, then run the egress-class (R-NET-05/07)
and vpn-killswitch (R-NET-06) conformance tests). Fall back to option 2 if the interface isn't
addressable. The CiliumEgressGatewayPolicy is intentionally NOT committed until validated, to avoid
shipping an untested datapath policy.
Port forwarding (qBittorrent) — follow-up
PIA assigns a forwarded port that can change on reconnect. A small port-sync helper
(qbittorrent-natmap) reads the current port from the gluetun-control API (:8000
/v1/openvpn/portforwarded) and writes it into qBittorrent, keeping it hands-off (R-UPD). Authored
alongside the qBittorrent app (Phase 6 apps / or here when qBittorrent lands).