Files
home-network-k3s/decisions/adr-0008-vpn-egress-model.md
T
jakman85andClaude Opus 4.8 a2cbdf656d Resolve OQ-1/2/3, refine OQ-4: single ExpressVPN gateway, single WAN, external media
Operator answers folded into the specs:
- OQ-3 VPN: ExpressVPN, cluster-controlled SINGLE shared in-cluster tunnel
  (OpenVPN UDP / non-TCP), rules enforced outside the service. New ADR-0012
  supersedes ADR-0008 (Gluetun-sidecar default dropped). Central kill-switch.
- OQ-1 WAN/edge: one commercial WAN, no multi-ISP plans; dumb SOHO edge (no
  VLAN/VPN/services). Multi-WAN out of scope; segmentation + VPN in-cluster;
  edge = NAT + manual port-forward. Gateway VIP resolved to Cilium L2
  announcement. T-3.3 marked won't-do.
- OQ-2 storage: bulk media lives outside the cluster (backing TBD, non-blocking)
  via a 'bulk' external-mount indirection; Plex metadata DB stays on Longhorn.
  Split out a separate OQ-BKP for the off-cluster DR backup target (Phase 4).
- OQ-4 refined: wildcard record covers reachability but NOT ACME DNS-01 (needs
  TXT-write access) and does not match *.home.* depth. Still OPEN: Cloudflare
  API token (Option A) vs acme-dns delegation (Option B). T-2.4/T-2.5 blocked.

Updated: docs 00,01,03,04,08,09; ADR-0008/0012 + index; task board.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-15 23:00:11 -05:00

2.8 KiB

ADR-0008 — VPN egress model: Gluetun sidecar (default) + Cilium Egress Gateway

  • Status: Superseded by ADR-0012 (2026-08-15)
  • Date: 2026-08-15
  • Serves: R-NET-05, R-NET-06, R-NET-07
  • Supersedes: —

Superseded. OQ-3 resolved to ExpressVPN + a single shared in-cluster tunnel with rules enforced outside the service. The Gluetun-sidecar-per-pod default below is no longer the model; see ADR-0012. Retained for history.

Context

A subset of services (sonarr, radarr, jackett, qbittorrent) must egress only through a commercial VPN, with a kill-switch guaranteeing no leak to the plain internet if the tunnel drops. Egress class must be a declarative service property (egress: vpn) that doesn't force a workload rewrite.

Decision

Adopt a two-pattern model behind the single egress: vpn contract, with Gluetun sidecar as the default for the initial catalog:

  • Default — Pattern A: Gluetun sidecar per pod. A Gluetun container holds the WireGuard/OpenVPN tunnel; the app shares its network namespace. Gluetun's built-in firewall kill-switch blocks all non-tunnel egress by default — satisfying R-NET-06 intrinsically and per-service.
  • Available — Pattern B: central VPN egress gateway + Cilium Egress Gateway. One (HA) gateway holds the tunnel; Cilium Egress Gateway steers vpn-labeled pods through it, with policy denying any fallback to the internet path. Use when tunnel count must be minimized.

The service-facing contract is identical for both: declare egress: vpn; the platform wires the chosen pattern. Kill-switch behavior is a hard acceptance gate for both.

Alternatives considered

  • Central gateway only — fewer tunnels but a shared failure domain and more complex kill-switch guarantees across the fabric; kept as Pattern B, not the default.
  • Node-level policy routing / per-node VPN — couples egress to node identity, harder to reason about per-service; rejected as the primary model.
  • App-native VPN config — rejected: not declarative, not uniform, easy to misconfigure and leak.

Consequences

  • Per-service isolation and an intrinsic kill-switch out of the box (Pattern A).
  • More sidecars/tunnels at scale (Pattern A) — Pattern B is the escape hatch if the VPN provider limits concurrent tunnels/egress IPs (OQ-3).
  • Kill-switch must be fault-tested (tunnel forced down ⇒ zero internet egress) as a standing acceptance gate, not a one-time check (03, R-NET-06).
  • VPN credentials are SOPS-encrypted (ADR-0007). WireGuard preferred over OpenVPN where supported.

Open questions

  • OQ-3 — VPN provider/protocol and concurrent-tunnel/egress-IP limits (drives Pattern A vs B and gateway HA).