Commit Graph
100 Commits
Author SHA1 Message Date
jakman85andClaude Opus 4.8 83ea050e82 docs: record ADR-0015 impl (merged, not deployed) + escalation ruling + operator prereq
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 07:32:58 -05:00
jakman85 33d048d379 Merge feature/vpn-egress-gateway-role into main (ADR-0015 host tunnel role, not yet deployed) 2026-08-24 07:31:27 -05:00
jakman85 af75a6ddc9 Merge feature/vpn-egress-cilium-steering into main (ADR-0015 steering policy, not yet wired) 2026-08-24 07:31:27 -05:00
jakman85andClaude Opus 4.8 2a2ead8b28 chore: drop egress-steering.yaml from this branch (owned by feature/vpn-egress-cilium-steering; working-tree race)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 07:31:26 -05:00
jakman85andClaude Opus 4.8 2c109e2a31 feat(vpn): node-level split-tunnel egress gateway Ansible role (ADR-0015)
Author the vpn_egress_gateway role + playbook that stands up the NODE-level
PIA OpenVPN tunnel on egress-gateway node(s) — the host half of ADR-0015. The
CiliumEgressGatewayPolicy steering vpn-class pods is separate GitOps against the
same contract (group vpn_egress_gateway, node label
homelab.keithjackson.dev/egress-gateway=true, host iface tun0).

Honors all four ADR-0015 invariants:
- Split-tunnel (inv 1&2): OpenVPN route-noexec + pull-filter ignore
  redirect-gateway => installs no routes; policy routing in a dedicated table
  (51820) with `ip rule from <tun0-addr>` steers ONLY Cilium-SNAT'd vpn-class
  traffic. Node default route stays on the WAN, tunnel up or down.
- Scoped nftables kill-switch (inv 3&4): own inet table, policy accept, drops
  only tunnel-sourced (@vpn_egress_gateway_src set) packets not leaving tun0 =>
  vpn-class fails closed (blackhole), node/internet-class fail open (WAN),
  never matched.

PIA creds + the proven sweden-aes-128-cbc-udp-ip .ovpn come from Ansible vault
(host-side per ADR-0015/docs-07); vault keys documented, no secret values
committed. ansible-lint (production) + yamllint clean. Not deployed: green
conformance is gated on the live tunnel (operator host creds) — see role README.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 01:09:19 -05:00
jakman85andClaude Opus 4.8 38fcfadbf0 phase3(vpn): author Cilium Egress Gateway steering policy (ADR-0015)
Add platform/vpn-egress/egress-steering.yaml — a CiliumEgressGatewayPolicy
that steers homelab.keithjackson.dev/egress-class=vpn pods out a gateway
node's host tun0 split-tunnel interface, per ADR-0015 (node-level tunnel +
Cilium Egress Gateway). Honors all four ADR-0015 invariants: only vpn-class
pods are selected (default/internet + node stay on the local/WAN path),
RFC1918 + cluster ranges are excludedCIDRs so only public internet is
tunneled and tun0 is never a default route, and the host nftables kill-switch
(Ansible) fails vpn-class CLOSED while internet-class fails OPEN.

Interface contract shared verbatim with the parallel Ansible host-tunnel role:
  node label homelab.keithjackson.dev/egress-gateway=true, interface tun0,
  client label homelab.keithjackson.dev/egress-class=vpn.

Cilium Egress Gateway prerequisites confirmed enabled (egressGateway.enabled,
bpf.masquerade, kubeProxyReplacement all true, [parity]) — no HelmRelease change.

Intentionally NOT added to platform/vpn-egress/kustomization.yaml: the datapath
stays out of the runtime until the live tunnel exists and the egress-class /
vpn-killswitch conformance tests pass (README rule; DoD). Gluetun is retained
as the validated fallback until then. Serves R-NET-05/06/07.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 00:59:36 -05:00
jakman85 272a7d7d2f Merge feature/phase3-egress-steering-groundwork into main 2026-08-24 00:26:27 -05:00
jakman85andClaude Opus 4.8 34f41a0fba phase3(vpn): egress-class conformance fixtures + steering design finding
Author the internet/vpn egress-class fixture pods for the egress-class + vpn-killswitch
conformance tests. Record the datapath finding: ADR-0013's preferred steering (Cilium
egress gateway -> gluetun tun0) is NOT viable because gluetun must run hostNetwork to
put tun0 on the node, and gluetun then redirects the NODE's default route through the
VPN (breaks k3s/etcd). Escalated: remaining viable path is option 2 (gateway-pod
routing). No steering datapath committed until the mechanism is chosen + validated.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 00:26:26 -05:00
jakman85 f650a4dc8e Merge fix/prometheus-oom into main 2026-08-24 00:04:18 -05:00
jakman85andClaude Opus 4.8 7b619f8bb0 fix(monitoring): Prometheus memory limit 1Gi -> 2Gi (OOMKilled on compaction)
prometheus-...-0 was OOMKilled (exit 137) every ~4min during TSDB compaction/checkpoint
(261 restarts/37h). Raise limit to 2Gi + request to 512Mi; nodes have headroom.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 00:04:17 -05:00
jakman85andClaude Opus 4.8 03768ff6b0 docs: Phase 3 VPN resolved (working + stable, PIA Stockholm egress)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-24 00:02:47 -05:00
jakman85 db39879e1c Merge feature/vpn-liveness-exec into main 2026-08-23 23:57:32 -05:00
jakman85andClaude Opus 4.8 4a781ace49 fix(vpn): exec (localhost) liveness probe — VPN routing breaks kubelet HTTP probe
Once the tunnel redirects the pod default route via tun0, the pod's response to the
kubelet's httpGet liveness probe egresses the tunnel and never returns → probe times
out → pod restarts ~every 2min despite the tunnel being up (PIA egress IP confirmed).
Switch to a local wget exec probe (stays on lo).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 23:57:31 -05:00
jakman85 7e41cc0282 Merge feature/vpn-custom-ovpn-ip into main 2026-08-23 01:01:20 -05:00
jakman85andClaude Opus 4.8 63f6e15d0e feat(vpn): new IP-based .ovpn secret + map its key to /config/pia.ovpn
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 01:01:19 -05:00
jakman85andClaude Opus 4.8 c8b9653e33 fix(vpn): custom mode with IP-based .ovpn; gluetun v3.41.0 -> v3.41.3
Operator provides a new .ovpn with an IP remote (gluetun's custom parser rejects
hostnames), so drop the resolve-remote initContainer/VPN_ENDPOINT_IP attempts and
mount the .ovpn directly (key pia.ovpn). Bump image to latest v3.41.3.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:57:01 -05:00
jakman85 3787644c48 Merge feature/vpn-endpoint-ip into main 2026-08-23 00:50:54 -05:00
jakman85andClaude Opus 4.8 66a76e6e33 fix(vpn): VPN_ENDPOINT_IP for custom mode (.ovpn hostname not parseable)
gluetun's custom parser rejects the .ovpn's 'remote sweden.privacy.network' (needs
an IP). sweden.privacy.network LB pool = 158.173.241.{22,34,55,112,239}; pin .22
(verified to handshake) via VPN_ENDPOINT_IP/PORT while the .ovpn supplies cipher/CA.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:50:54 -05:00
jakman85 3c359e2937 Merge feature/vpn-ovpn-keyname into main 2026-08-23 00:46:31 -05:00
jakman85andClaude Opus 4.8 998b46df3d fix(vpn): map .ovpn secret key (filename) to /config/pia.ovpn
Operator's kubectl create secret keyed the file by its name
(sweden-aes-128-cbc-udp-dns.ovpn); mount item now references that key -> pia.ovpn.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:46:31 -05:00
jakman85 6c34fb636d Merge feature/vpn-custom-ovpn into main 2026-08-23 00:45:39 -05:00
jakman85andClaude Opus 4.8 47b6cb6afd feat(vpn): add operator-provisioned pia-custom-ovpn Secret (SOPS)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:45:39 -05:00
jakman85andClaude Opus 4.8 91aa7acab8 wip(vpn): gluetun custom-provider mode with operator's PIA .ovpn
PIA-provider server selection is flaky here (random dead IPs); the operator's .ovpn
uses sweden.privacy.network (load-balanced, reliable). Switch to custom mode
(VPN_SERVICE_PROVIDER=custom, OPENVPN_CUSTOM_CONFIG=/config/pia.ovpn), mount the
pia-custom-ovpn Secret, keep creds + kill-switch + tun-mtu 1300. Drops PIA-provider
server list/updater and native PF (Phase-6 concern). Pending the operator-provisioned
pia-custom-ovpn Secret (not merged until it exists).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:40:08 -05:00
jakman85 ee349c6d94 Merge feature/vpn-preset-envname into main 2026-08-23 00:18:03 -05:00
jakman85andClaude Opus 4.8 be1e317456 fix(vpn): correct env name PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET
gluetun silently ignored PIA_ENCRYPTION_PRESET (renamed) and defaulted to strong
(RSA-4096) the whole time. The correct v3.x var is
PRIVATE_INTERNET_ACCESS_OPENVPN_ENCRYPTION_PRESET=normal → RSA-2048/AES-128-CBC/1198,
the smaller handshake that fits the 1450 pod MTU.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:18:03 -05:00
jakman85 93010a5287 Merge feature/vpn-normal-preset into main 2026-08-23 00:09:48 -05:00
jakman85andClaude Opus 4.8 6a44d973b4 fix(vpn): PIA normal preset (RSA-2048/AES-128-CBC/SHA1, 1198) for MTU reliability
RSA-4096 strong handshake wouldn't hold over the pod's 1450 MTU (Cilium VXLAN);
MTU tuning is exhausted (no --tls-mtu in OpenVPN 2.6, PIA rejects --fragment).
Operator downloaded the lower-encryption PIA config (port 1198, RSA-2048,
AES-128-CBC, SHA1) = gluetun's 'normal' preset. Smaller handshake fits 1450.
Dropped the strong-handshake band-aid flags; kept OPENVPN_MTU 1300 for the data channel.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-23 00:09:47 -05:00
jakman85 8bcccf518e Merge feature/vpn-mtu-disc into main 2026-08-22 23:56:28 -05:00
jakman85andClaude Opus 4.8 772d032d4c tune(vpn): --mtu-disc no so oversized handshake packets fragment (not drop)
RSA-4096 strong-preset handshake is marginal over the 1450 pod MTU (Cilium VXLAN).
--mtu-disc no clears DF so oversized control packets fragment through instead of
being silently dropped. Last MTU-path lever (OpenVPN 2.6 has no --tls-mtu).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:56:28 -05:00
jakman85 605b99409d Merge feature/vpn-mtu-tune2 into main 2026-08-22 23:50:01 -05:00
jakman85andClaude Opus 4.8 0e2289a2a8 fix(vpn): valid OpenVPN flags only (--hand-window 120; --tls-mtu invalid in 2.6)
OpenVPN 2.6.16 rejects --tls-mtu (not a real option) and PIA doesn't support
--fragment, so control-channel packets can't be shrunk. Keep --hand-window 120 to
give the RSA-4096 handshake more time over a lossy/marginal path, plus tun-mtu 1300.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:50:00 -05:00
jakman85 bc42c4bee9 Merge feature/vpn-mtu-tune into main 2026-08-22 23:46:37 -05:00
jakman85andClaude Opus 4.8 b859e52697 tune(vpn): shrink OpenVPN control-channel MTU + widen handshake window
RSA-4096 strong-preset handshake is marginal over the pod's 1450 MTU (Cilium VXLAN).
Lower tun-mtu to 1300 and add --tls-mtu 1200 (smaller control packets) + --hand-window
120 (more time to complete the handshake over a lossy path). Empirical test of the
MTU-tuning path (operator's choice over Cilium native routing).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:46:36 -05:00
jakman85 49dca0e8bb Merge feature/vpn-pf-cidr-narrow into main 2026-08-22 23:36:51 -05:00
jakman85andClaude Opus 4.8 7d2cea8402 fix(vpn): narrow FIREWALL_OUTBOUND_SUBNETS to 10.0.0.0/16 (PF gateway overlap)
PIA's PF gateway 10.31.128.1 sits inside the /8 cluster-pool, so
FIREWALL_OUTBOUND_SUBNETS=10.0.0.0/8 made gluetun route it around the tunnel and
port forwarding timed out (getSignature to 10.31.128.1:19999). /16 covers actual
pod allocations and excludes PIA's 10.31.x.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:36:51 -05:00
jakman85 1899a54c07 Merge feature/vpn-pia-strong-preset into main 2026-08-22 23:30:47 -05:00
jakman85andClaude Opus 4.8 41234b85f5 fix(vpn): PIA strong encryption preset (RSA-4096/AES-256-CBC/SHA256, port 1197)
Operator's working config is sweden.privacy.network:1197 with RSA-4096, AES-256-CBC,
SHA256 = PIA's 'strong' preset. gluetun defaults PIA to 'normal' (AES-128-GCM), a
cipher mismatch against the 1197 (strong) endpoint, so the handshake never completed
(silent after UDP link). PIA_ENCRYPTION_PRESET=strong matches it and selects 1197.
Reverts the 8080 port red herring.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:30:46 -05:00
jakman85 7bdcd6edda Merge feature/vpn-port-8080-live into main 2026-08-22 23:26:17 -05:00
jakman85andClaude Opus 4.8 b76de52b7f test(vpn): use port 8080 (avoid home 1197 return-path conflict), live servers
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:26:17 -05:00
jakman85 b4bd18d716 Merge feature/vpn-updater-enduser into main 2026-08-22 23:17:16 -05:00
jakman85andClaude Opus 4.8 2e5fdd29d7 fix(vpn): gluetun update needs -enduser flag in the init container
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:17:16 -05:00
jakman85andClaude Opus 4.8 a735784f94 fix(vpn): initContainer fetches PIA's current server list (bundled list stale)
UPDATER_PERIOD only refreshes AFTER the tunnel is ready (chicken-and-egg: can't
connect with stale servers), and gluetun's bundled PIA Sweden list is retired
46.246.x (current is 158.173.x). Add an `update-servers` initContainer running
`gluetun update -providers "private internet access"` into a shared /gluetun
emptyDir the main container reads, so it dials current servers. UPDATER_PERIOD
raised to 720h (docs-compliant) for ongoing in-pod refresh.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:14:42 -05:00
jakman85 16bf3e3218 Merge feature/vpn-updater-stale-servers into main 2026-08-22 23:08:20 -05:00
jakman85andClaude Opus 4.8 457edb7a38 fix(vpn): enable gluetun server-list updater (bundled list was stale) + re-enable
CORRECTION: the connection failures were gluetun dialing RETIRED PIA IPs (46.246.x
= dead → timeouts, which I mis-read as a network block). A fresh PIA .ovpn connects
from the same host, and PIA's current SE Stockholm servers (158.173.x) are reachable.
Cilium was independently cleared (default-allow, host-fw off, Hubble FORWARDED).

Fix: UPDATER_PERIOD=24h so gluetun fetches PIA's current servers; keep
OPENVPN_MTU/MSSFIX=1350 for the real Cilium-VXLAN 1450 pod MTU (data channel);
PIA default UDP port. Re-enable vpn-egress. Full correction in DECISIONS-LOG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 23:08:18 -05:00
jakman85 50a416077c Merge feature/vpn-network-blocks-pia into main 2026-08-22 18:15:14 -05:00
jakman85andClaude Opus 4.8 95a55952cc fix(vpn): disable vpn-egress — network blackholes PIA IPs (not ports/MTU/cluster)
Decisive diagnosis: OpenVPN's outbound init packets are 14 bytes with zero
replies (so MTU was never the blocker), pod UDP egress is healthy (NTP replies),
but TCP 501/443 to PIA IPs time out while Google/Cloudflare are OPEN — across many
PIA servers. This 192.168.2.x WiFi segment blackholes PIA's IP ranges (and
intercepts DNS). Not code-fixable. Reverted the port/MTU red-herring env to PIA
defaults; disabled vpn-egress pending an operator network fix (move to the
unfiltered 192.168.1.x LAN, or unblock PIA on the router). Full trace in
DECISIONS-LOG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 18:15:11 -05:00
jakman85 9fc117e06d Merge feature/vpn-openvpn-mtu into main 2026-08-22 18:03:47 -05:00
jakman85andClaude Opus 4.8 f17c46b767 fix(vpn): shrink OpenVPN tun-mtu/mssfix to 1350 (Cilium VXLAN pod MTU 1450)
Actual root cause (operator-directed layered MTU check): Cilium runs VXLAN
(routing-mode tunnel) so pods get MTU 1450 (1500-50 encap) while node, hypervisor,
and the Google Fiber WAN are all 1500. OpenVPN's ~1500-byte handshake packets
exceed 1450 and are dropped (PMTUD broken). The ports were a red herring.

Fix: OPENVPN_MTU=1350 + OPENVPN_MSSFIX=1350 so encrypted packets (~1402) fit under
1450. Keeps OpenVPN + VXLAN unchanged. Re-enable vpn-egress. Full layered trace
(pod/node/host) in DECISIONS-LOG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 18:03:46 -05:00
jakman85 5a8fd4f20a Merge feature/vpn-escalate-wireguard into main 2026-08-22 16:47:18 -05:00
jakman85andClaude Opus 4.8 0aab1702e0 chore(vpn): disable vpn-egress, escalate OpenVPN→WireGuard decision
OpenVPN can't connect on this network (each port fails differently, so it's
OpenVPN-specific not a UDP block — NTP/123 works): 1197 = home OpenVPN-server
conflict; 53 = DNS interception; 8080 = TLS handshake fails across servers (MTU
on WiFi+triple-NAT). Recommend pivoting to WireGuard via thrnz/docker-wireguard-pia
(UDP 1337, MTU-robust, ADR-0013 fallback, operator's original preference) — a
spec-level (ADR-0013) change, so escalated. Disabled meanwhile. Full trace in
DECISIONS-LOG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 16:47:18 -05:00
jakman85 5674458754 Merge feature/vpn-port-8080 into main 2026-08-22 16:39:30 -05:00
jakman85andClaude Opus 4.8 4cde690008 fix(vpn): PIA OpenVPN port 53 -> 8080 (53 is DNS-intercepted on this network)
Port 53 also got no PIA replies: this network transparently intercepts outbound
:53, so dig works but OpenVPN-over-53 is swallowed. An NTP/123 probe from the host
returned fine, proving arbitrary non-53 UDP works bidirectionally (so it's
port-specific: 1197 conflict + 53 DNS-intercept, not a UDP block). Move to 8080
(community-verified for PIA, non-DNS, non-1197).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 16:39:30 -05:00
jakman85 7a88863f71 Merge feature/vpn-port-string-fix into main 2026-08-22 16:33:38 -05:00
jakman85andClaude Opus 4.8 734093cf92 fix(vpn): hardcode OPENVPN_ENDPOINT_PORT "53" as string (Flux var int-coercion)
Wiring the port via "${PIA_OPENVPN_PORT}" made Flux/kustomize emit an unquoted
53 (int) after substitution, which k8s rejects for an env value ("expected
string, got 53"). Same gotcha as the Longhorn numberOfReplicas fix. Hardcode the
quoted string; cluster-var removed with a NOTE explaining why.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 16:33:38 -05:00
jakman85 f73719a474 Merge feature/vpn-openvpn-port-53 into main 2026-08-22 16:28:38 -05:00
jakman85andClaude Opus 4.8 ec5ceb328f fix(vpn): PIA OpenVPN on UDP/53 (avoid home WAN:1197 conflict); re-enable vpn-egress
Operator identified the block: the home network NATs its own OpenVPN server on
WAN:1197, colliding with the cluster's outbound PIA-on-1197 flow (port-specific,
not a general UDP block — UDP/53 worked in tcpdump). PIA accepts OpenVPN on
53/1194/1197/1198/8080/9201; chose 53 (empirically proven to traverse this net).

- cluster-vars: add PIA_OPENVPN_PORT="53"
- gluetun: OPENVPN_ENDPOINT_PORT=${PIA_OPENVPN_PORT}
- re-enable vpn-egress; DECISIONS-LOG resolution noted

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 16:28:37 -05:00
jakman85 347f7a54b6 Merge feature/vpn-blocked-network into main 2026-08-22 15:05:21 -05:00
jakman85andClaude Opus 4.8 9d761872ea fix(vpn): disable vpn-egress — tunnel blocked by home network (env), config kept
Phase-3 VPN config is complete + correct (OpenVPN-UDP, SE Stockholm), but the
OpenVPN UDP/1197 handshake to PIA never gets a reply. Traced with tcpdump: packets
leave pod→node→host→WiFi (SNAT 192.168.2.103) fine, but 0 replies return to the
host, while host UDP/53 works. The 192.168.2.x home router/ISP drops OpenVPN UDP —
upstream of everything in this repo. Reverted to disabled to avoid a crashloop
firing false KubePodCrashLooping alerts; one-line re-enable once the network is
fixed. Full trace in DECISIONS-LOG.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 15:05:20 -05:00
jakman85 e4d856fd83 Merge feature/pia-region-fix into main 2026-08-22 14:51:29 -05:00
jakman85andClaude Opus 4.8 9a108443e8 fix(vpn): PIA_REGION Sweden -> "SE Stockholm" (Gluetun OpenVPN region name)
Gluetun's PIA OpenVPN provider rejects "Sweden"; the valid PF-capable Sweden
region identifier is "SE Stockholm" ("SE Streaming Optimized" lacks port
forwarding). Preserves the operator's Sweden choice.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 14:51:29 -05:00
jakman85 b50433d705 Merge feature/vpn-openvpn-enable into main 2026-08-22 14:49:17 -05:00
jakman85andClaude Opus 4.8 015b423353 feat(vpn): enable vpn-egress on OpenVPN-UDP; align docs to ADR-0013 amendment
Gluetun cannot do PIA over WireGuard (upstream unsupported; live "provider name
is not valid for Wireguard"). Operator approved OpenVPN-UDP and ADR-0013 was
amended in the spec. This aligns the impl:

- platform/kustomization.yaml: re-enable `- vpn-egress` (Phase 3, OpenVPN-UDP)
- gluetun-gateway.yaml: clean stale WireGuard comments (header, image note,
  VPN_TYPE/creds comments); config already VPN_TYPE=openvpn
- README.md: PIA-over-OpenVPN, status=enabled, steering iface wg0 -> tun0
- DECISIONS-LOG.md: disk/drive incident (k3s -> NVMe; failing Seagate) + Phase-3
  OpenVPN resolution

Stale WireGuard comments inside the operator's encrypted pia-credentials.sops.yaml
header left untouched (editing breaks the SOPS MAC; cosmetic).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-22 14:49:16 -05:00
jakman85 41da3d96e5 fix(vpn): Attempting to try OpenVPN protocol with PIA 2026-08-21 23:49:49 -05:00
jakman85 6a2c7c3020 Merge Gluetun v3.41.0 (PIA WireGuard support) 2026-08-21 23:10:48 -05:00
jakman85andClaude Opus 4.8 13a8df4e4a fix(vpn): bump Gluetun to v3.41.0 (v3.40.0 lacks PIA WireGuard support)
The deployed v3.40.0 rejected PIA+WireGuard ('private internet access is not valid for
Wireguard'). Verified v3.41.0 accepts the PIA+WireGuard provider/protocol (passes config
validation; ADR-0013's model holds — v3.40.0 was just too old). Bump the pin.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 23:10:47 -05:00
jakman85andClaude Opus 4.8 cbcfae01e1 feat(phase3): enable vpn-egress (deploy the Gluetun PIA/WireGuard gateway)
PIA creds provisioned (SOPS) + wired. Enable vpn-egress in platform/kustomization.yaml
to deploy the shared Gluetun gateway (PIA over WireGuard, Sweden region, kill-switch).
Egress-class steering (vpn-class pods -> tunnel) still to be validated against the live
tunnel next. Gate PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 23:06:27 -05:00
jakman85 643c3225ac Merge PIA_REGION=Sweden default 2026-08-21 23:00:51 -05:00
jakman85andClaude Opus 4.8 0d55c793f3 fix(vpn): default PIA_REGION to Sweden (operator choice, PF-capable)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 23:00:51 -05:00
jakman85 9667dce419 Merge VPN cluster-vars definition + pod-CIDR fix 2026-08-21 22:56:15 -05:00
jakman85andClaude Opus 4.8 6591e11ad1 fix(vpn): define PIA_REGION + CLUSTER_POD_CIDR cluster-vars; correct pod CIDR + doc where
gluetun-gateway.yaml referenced ${PIA_REGION}/${CLUSTER_POD_CIDR} but they were never defined
in cluster-vars, and the README didn't say where to set them. Add both to
clusters/base/cluster-vars.yaml (ConfigMap data:, string values, Flux postBuild substitution).
CLUSTER_POD_CIDR is 10.0.0.0/8 (Cilium cluster-pool — pods get 10.x, confirmed live), NOT the
k3s 10.42.0.0/16 I'd assumed. README now names the file + format + per-env override + PF-region note.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 22:56:14 -05:00
jakman85 140cb651d2 Merge Phase-3/4 + infra-recovery log 2026-08-21 22:40:18 -05:00
jakman85 2223e8403d docs: Phase-4 monitoring deployed + Phase-3 VPN authored + infra recovery + F-MON1 2026-08-21 22:40:17 -05:00
jakman85 4f17c5334e Merge monitoring PrometheusRule ordering fix 2026-08-21 22:30:02 -05:00
jakman85andClaude Opus 4.8 eeb97208fa fix(monitoring): move PrometheusRule alerts to platform-config (CRD ordering)
Enabling monitoring deadlocked the platform Kustomization: alerts.yaml ships
PrometheusRule CRs whose monitoring.coreos.com CRDs are installed by the
kube-prometheus-stack HelmRelease in the SAME pass → 'no matches for kind
PrometheusRule' → atomic apply failure → NO monitoring HelmReleases created.
Same controllers/configs fix as cert-manager/kyverno: drop alerts.yaml from
platform/monitoring and apply it via platform-config (dependsOn platform), so the
monitoring CRDs exist first. Verified: platform has 0 PrometheusRule (keeps the 3
monitoring HRs); platform-config has the 8 alert rules; gate PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 22:30:01 -05:00
jakman85 3bfd055df2 Merge Phase-3 VPN egress gateway (author-first) 2026-08-21 22:19:24 -05:00
jakman85andClaude Opus 4.8 1a98429434 feat(phase3): author shared VPN egress gateway (Gluetun/PIA/WireGuard) — author-first
Per ADR-0013: platform/vpn-egress/ = a single shared Gluetun gateway (PIA over WireGuard,
built-in kill-switch FIREWALL=on, native port forwarding), namespace (non-app, NET_ADMIN +
/dev/net/tun), control-API Service, and a pre-wired (commented) pia-credentials SOPS line.
Defined the PIA secret location (platform/vpn-egress, keys username/password → OPENVPN_USER/
OPENVPN_PASSWORD) and updated the SOPS template accordingly.

NOT wired into platform/kustomization.yaml (Phase 3, author-first) — deploy needs PIA creds +
validation of the egress-steering mechanism (Cilium Egress Gateway → the Gluetun node's wg0;
documented in README with options + a fail-closed rationale, deliberately NOT committed as an
untested CiliumEgressGatewayPolicy). WireGuard chosen (UDP, protocol != TCP); PIA supports it on
all regions and the secret stays username/password.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 22:19:22 -05:00
jakman85 e6c61c97b3 Merge Phase-4 monitoring deploy 2026-08-21 22:06:24 -05:00
jakman85andClaude Opus 4.8 dfde5292d7 feat(phase4): deploy monitoring (kube-prometheus-stack + Loki + Alloy + Alertmanager)
Enable monitoring in platform/kustomization.yaml (Phase 4). Grafana admin +
Alertmanager SMTP secrets are provisioned (SOPS) and wired; Longhorn SC is up for
Prometheus/Loki PVCs. Set the Alertmanager receiver 'to:' to a valid inbox
([email protected]) so the config loads (REPLACE-ME is not a valid address);
operator can change it. NOTE: smtp_from ([email protected]) differs from
the Gmail auth user — Gmail may reject/rewrite unless a send-as alias is configured;
that only affects email delivery at alert time, not stack health.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 22:06:23 -05:00
jakman85 75764580d8 Merge fail-closed green log 2026-08-17 17:43:27 -05:00
jakman85 e8b5cae34d docs: R-NET fail-closed (L3) green; conformance status 2026-08-17 17:43:27 -05:00
jakman85 cde3335086 Merge L3 default-deny-failclosed (R-NET fail-closed green) 2026-08-17 17:42:35 -05:00
jakman85andClaude Opus 4.8 1bf8e3bfc4 feat(conformance): implement L3 default-deny-failclosed (R-NET-01/02, fault injection)
Was a scaffold (fixture TODO, fault-injection a no-op echo). Implemented against the
live datapath: fixtures/allowed-pair.yaml = default-deny baseline + source + dest(:8080)
+ the sole explicit allow. The test verifies source->dest works (ALLOWED_OK), then
REVOKES both halves of the allow (real fault: kubectl delete networkpolicy), and asserts
connectivity fails CLOSED (FAIL_CLOSED_OK) while DNS still resolves (DNS_OK). Proves
default-deny is datapath-enforced and fails closed on revocation. DNS check asserts the
resolved answer, since busybox nslookup exits non-zero on a successful forward lookup.
PASSES (74.8s).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 17:42:34 -05:00
jakman85 1631a7bde8 Merge R-STATE-01 + harness fix log 2026-08-17 17:02:40 -05:00
jakman85andClaude Opus 4.8 ce55332028 docs: R-STATE-01 green + chainsaw =~ harness fix (F-TEST1)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 17:02:39 -05:00
jakman85 76478f2651 Merge chainsaw check block-style fix 2026-08-17 17:02:05 -05:00
jakman85andClaude Opus 4.8 70c0701d6f fix(conformance): block-style chainsaw checks (flow-map broke on contains comma)
My previous commit converted =~ to contains() but kept flow style
'check: { (contains($stdout, 'X')): true }' — the comma inside contains() is a
YAML flow-map separator, so 9 tests failed yamllint ('expected , or }'). Convert
those checks to block style (matching data-survival), where a comma in the key
scalar is fine. Gate PASS; data-survival still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 17:02:05 -05:00
jakman85 3440bbad92 Merge chainsaw check contains() fix (data-survival R-STATE-01 green) 2026-08-17 16:58:38 -05:00
jakman85andClaude Opus 4.8 e0c5c58710 fix(conformance): chainsaw checks use contains(), not unsupported =~
Every chainsaw `check` used the regex operator `=~` (`($stdout =~ 'X'): true`),
which chainsaw's expression engine rejects ('SyntaxError: Unknown char: ~') — so
EVERY such test failed on the assertion even when the behavior passed. Proven with
data-survival (R-STATE-01): the Longhorn canary genuinely SURVIVED pod destruction
(DATA_SURVIVED_OK) but the test FAILED on the `=~` syntax. Converted all 10 tests to
JMESPath `contains($stdout, 'X')` (data-survival's alternation → contains||contains).
data-survival now PASSES; the other 9 (later-phase components) are syntactically fixed
for when their phases deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 16:58:38 -05:00
jakman85 54fd963cc4 Merge F-OPS1 resolved log 2026-08-17 16:51:30 -05:00
jakman85andClaude Opus 4.8 429708f0a7 docs: F-OPS1 resolved (self-managing root)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 16:51:30 -05:00
jakman85 4381a71964 Merge self-managing root Kustomization 2026-08-17 16:48:58 -05:00
jakman85andClaude Opus 4.8 d06947cbfd feat(gitops): self-managing root Kustomization (fixes F-OPS1)
The on-cluster cluster-vars ConfigMap wasn't reconciled by Flux — this bootstrap
model had no Kustomization watching clusters/dev, so cluster-vars/overlay changes
needed a manual 'kubectl apply -k clusters/dev' (e.g. DNS_ZONE substituted empty
until re-applied). Add clusters/dev/sync.yaml: a self-managing root Kustomization
(flux2 gotk-sync pattern, name flux-system) that reconciles ./clusters/dev from git
— cluster-vars, the GitRepository, platform/platform-config/apps, and itself. A git
push now suffices. Bootstrap unchanged ('kubectl apply -k clusters/dev' creates it
once). Verified: dev build contains all 6 objects (root + existing), prod unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 16:48:57 -05:00
jakman85 3c8b339463 Merge kyverno image fix log 2026-08-17 08:43:08 -05:00
jakman85andClaude Opus 4.8 892fdc6a15 docs: log kyverno bitnami/kubectl cleanup-image fix (F-KY1)
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 08:43:08 -05:00
jakman85 928cd5e122 Merge kyverno cleanup image fix (bitnamilegacy) 2026-08-17 08:38:53 -05:00
jakman85andClaude Opus 4.8 5521059c04 fix(kyverno): replace purged bitnami/kubectl cleanup image with bitnamilegacy
The kyverno chart's cleanup jobs (policyReportsCleanup, webhooksCleanup) default to
bitnami/kubectl:1.30.2, which Bitnami removed from Docker Hub (2024 catalog purge) ->
ImagePullBackOff. policyReportsCleanup is a Helm post-upgrade HOOK, so its failure
stalls every helm upgrade (5m timeout -> HelmRelease not-Ready -> platform Kustomization
blocked -> platform-config/apps cascade). Override both to Bitnami's relocated
bitnamilegacy/kubectl (identical image: /bin/bash + awk/wc/xargs + kubectl). Verified
via helm template that no bitnami/kubectl reference remains.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 08:38:53 -05:00
jakman85 de81bafdc1 Merge Phase-2 conformance exit decision log 2026-08-17 08:33:24 -05:00
jakman85andClaude Opus 4.8 f78ce4ebe7 docs: Phase-2 conformance exit achieved (R-NET/R-TLS/R-DNS green) + findings
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 08:33:24 -05:00
jakman85 17622069fc Merge Phase-2 TLS/DNS conformance tests + ExternalDNS zone fix 2026-08-17 08:24:53 -05:00
jakman85andClaude Opus 4.8 20de78f5eb feat(conformance): author R-TLS + R-DNS L2 tests; fix ExternalDNS zone filter
Phase-2 exit tests (were Phase-0 scaffolds), authored against the live platform:
- trusted-tls (R-TLS-01/03): in-cluster probe dials the gateway Service (env-agnostic
  via --connect-to) with wildcard SNI and verifies the served cert CHAINS TO THE LE
  STAGING ROOT (operator decision A; dev uses LE-staging per ADR-0006). Fixture embeds
  the '(STAGING) Pretend Pear X1' root. Probes public + internal-only hostnames (R-TLS-03).
  PASSES.
- dns-exposure (R-DNS-02): a LoadBalancer Service with an external-dns hostname; polls
  public DNS until ExternalDNS auto-publishes the record; teardown + policy=sync removes it.

ExternalDNS fix (root cause of the DNS test): domain-filter was the platform SUBDOMAIN
home.keithjackson.dev, which filters OUT the actual Cloudflare zone keithjackson.dev
('no hosted zone matching record DNS Name' — record generated but skipped). Add a
DNS_ZONE cluster-var (keithjackson.dev) and set domainFilters to it. policy=sync +
txt-registry still only manage ExternalDNS-owned records (the pre-existing wildcard is
untouched). Resolves the file's WS-NET R-DNS-02 TODO.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-17 08:24:51 -05:00