--- # ============================================================================ # Cilium Egress Gateway steering for `egress-class=vpn` pods — ADR-0015. # Serves: R-NET-05, R-NET-06, R-NET-07. # # ########################################################################## # ## LIVE (2026-08-24): wired into kustomization.yaml, reconciled by Flux ## # ## and conformance-green (egress-class + vpn-killswitch pass on dev). ## # ########################################################################## # # This policy steers vpn-class pods out the gateway node's host `tun0` interface. It REQUIRES the # Ansible-managed host OpenVPN split-tunnel service on a node labeled # `homelab.keithjackson.dev/egress-gateway=true` — deploy that role BEFORE this policy reconciles, or # vpn-class pods are SNAT'd to a non-existent interface. The tunnel is live and validated end-to-end; # if tun0 is ever down, the host nftables kill-switch fails vpn-class CLOSED (see below). ADR-0015 # chose this mechanism (node tunnel + Cilium Egress Gateway); this file IS that datapath. # # --------------------------------------------------------------------------- # PREREQUISITES CONFIRMED (2026-08-24): # * Cilium Egress Gateway is ENABLED per ADR-0002 — platform/cilium/helmrelease.yaml and # platform/cilium/values.yaml both set (all [parity] with the Ansible bootstrap render): # egressGateway.enabled: true # bpf.masquerade: true (Egress Gateway REQUIRES eBPF masquerade) # kubeProxyReplacement: true # No Cilium HelmRelease change is needed for this policy. (If any of the three were false this # would be an ESCALATION, not a silent HelmRelease edit — they are all true, so we proceed.) # # INTERFACE CONTRACT (shared verbatim with the parallel Ansible host-tunnel role — ADR-0015): # * Gateway node label ...... homelab.keithjackson.dev/egress-gateway=true (applied by Ansible) # * Host tunnel interface .... tun0 (split-tunnel OpenVPN) # * Client selection ......... homelab.keithjackson.dev/egress-class=vpn (Kyverno-enforced) # --------------------------------------------------------------------------- apiVersion: cilium.io/v2 kind: CiliumEgressGatewayPolicy metadata: name: vpn-egress-steering annotations: homelab.keithjackson.dev/requirements: R-NET-05,R-NET-06,R-NET-07 homelab.keithjackson.dev/adr: ADR-0015 spec: # --- Client selection (INVARIANTS 1 & 2) -------------------------------- # ONLY pods explicitly opted into the vpn class are matched. Default/internet-class pods and node # traffic are never selected, so they stay on the local/private-WAN path (invariant 1: default is # the local path). Because ONLY these flows are steered, tun0 never becomes a default route for the # node or for any other pod (invariant 2). The selector is intentionally cluster-wide (no namespace # scope) so the class label alone is the contract — every namespace's vpn-class pods route here. selectors: - podSelector: matchLabels: homelab.keithjackson.dev/egress-class: vpn # --- What gets tunneled (INVARIANT 1: only PUBLIC internet leaves via the VPN) ------------------ # destinationCIDRs is the full internet; excludedCIDRs carves out everything that must stay LOCAL: # all RFC1918 private ranges (which also cover the Cilium cluster-pool pod CIDR 10.0.0.0/8, the k3s # service/pod ranges, and the LAN 192.168.x/10.73.x node network). So intra-cluster east-west, # pod->service, and LAN traffic are NOT SNAT'd out tun0 — only genuinely public destinations are. # Excluded flows fall through to Cilium's normal local datapath (invariant 1). destinationCIDRs: - "0.0.0.0/0" excludedCIDRs: - "10.0.0.0/8" # RFC1918 + Cilium cluster-pool pod CIDR (CLUSTER_POD_CIDR) + k3s service net - "172.16.0.0/12" # RFC1918 - "192.168.0.0/16" # RFC1918 (home LAN / node network family) # --- Where it egresses -------------------------------------------------- # The gateway node (Ansible-labeled) SNATs matched traffic out its host split-tunnel `tun0`. When # tun0 is absent/down, Cilium has no egress interface here; the host nftables kill-switch (Ansible, # ADR-0015) is the authoritative fail-closed backstop that DROPS this vpn-class traffic — scoped to # vpn-class only, so default/internet-class is unaffected (invariants 3 & 4: vpn fails CLOSED, # internet fails OPEN). egressGateway: nodeSelector: matchLabels: homelab.keithjackson.dev/egress-gateway: "true" interface: tun0 # --------------------------------------------------------------------------- # DONE (2026-08-24): enabled + validated + Gluetun retired. # * Host tun0 split-tunnel is up on the egress-gateway node; host nftables kill-switch in place. # * This file is wired into platform/vpn-egress/kustomization.yaml and reconciled by Flux. # * `egress-class` (R-NET-05/07) and `vpn-killswitch` (R-NET-06) conformance tests PASS live. # * The Gluetun Deployment was retired per ADR-0015 ("Gluetun is dropped as the gateway"). # ---------------------------------------------------------------------------