From b333b5e6d9996fd934b10353b1c6690bdf8ece69 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Mon, 24 Aug 2026 23:20:26 +0900 Subject: [PATCH] feat: activate browser-vpn-proxy content changes (previous commit was a bare rename) The previous commit (89524d4) only captured the file rename - a git add with one stale/invalid pathspec aborted before staging the actual content, and the resulting empty diff wasn't caught before committing. This commit has the real changes described in that one's message: EXPECTED_VLAN50_IP/NET_RAW on the guard init container, REQUIRE_AUTH dropped to false with ALLOWED_IPS removed entirely (broken as drafted - see the file's own header for why), sync-wave 3 annotation, and the README updates dropping the now-unneeded secret-script references. Co-Authored-By: Claude Sonnet 5 --- manifests/README.md | 2 +- manifests/multus/30-browser-vpn-proxy.yaml | 109 ++++++++++++--------- manifests/multus/README.md | 12 ++- 3 files changed, 74 insertions(+), 49 deletions(-) diff --git a/manifests/README.md b/manifests/README.md index f27e7c9..ed6e7f6 100644 --- a/manifests/README.md +++ b/manifests/README.md @@ -16,7 +16,7 @@ subdirectories are consumed by Argo CD Applications in `argocd/apps`. | `homeassistant/` | Home Assistant external service and ingress | | `media/` | Jellyfin, qBittorrent, JDownloader ([sending links](media/jdownloader.md)) | | `monitoring/` | Monitoring PVs, Grafana datasource, Grafana/Auth OIDC secrets | -| `multus/` | Multus CNI, VLAN 50 NetworkAttachmentDefinition, canary ([details](multus/README.md)) — manual-sync Application, see `argocd/apps/multus.yaml` | +| `multus/` | Multus CNI, VLAN 50 NetworkAttachmentDefinition, canary, browser-vpn-proxy SOCKS5 proxy ([details](multus/README.md)) — manual-sync Application, see `argocd/apps/multus.yaml` | | `network/` | Pi-hole secrets, DDNS, Traefik dashboard, external host ingresses | | `portfolio/` | Portfolio deployment, ingress, registry pull secret | diff --git a/manifests/multus/30-browser-vpn-proxy.yaml b/manifests/multus/30-browser-vpn-proxy.yaml index 4eecc16..43f1f44 100644 --- a/manifests/multus/30-browser-vpn-proxy.yaml +++ b/manifests/multus/30-browser-vpn-proxy.yaml @@ -1,34 +1,25 @@ -# NOT applied by any Argo CD Application. Deliberately kept in -# manifests/multus/reserved/ rather than manifests/multus/ directly: -# argocd/apps/multus.yaml's source has no directory.recurse: true, so it -# only ever scans files directly in manifests/multus/, never this -# subdirectory — even selective `argocd app sync multus --resource ...` -# has no way to target a resource Argo CD doesn't know exists. This is -# stronger isolation than the canary's own manual-sync-only Application -# gate, on purpose: plan.md Phase 8/12 requires this workload stay out of -# reconciliation entirely until qBittorrent AND JDownloader have -# independently passed their canary, validation, and soak gates — not -# just "not yet approved to sync", but "not a candidate for sync at all". +# sync-wave 3 — after RBAC/DaemonSet (0), the NAD (1), and the canary +# (2). Moved here from manifests/multus/reserved/ (2026-08-24) now that +# qBittorrent and JDownloader have both independently passed their +# canary, validation, and soak gates — the reservation this file's +# earlier header described is satisfied. Still governed only by the +# `multus` Argo CD Application's own manual-sync gate (no automated +# sync there at all) — the same gate already used for the DaemonSet, NAD, +# and canary, not a new mechanism. # -# When ready to actually build this (not yet — this file is prepared, -# not deployed): -# 1. Move it into manifests/multus/ (a sync-wave-numbered filename, -# e.g. 30-browser-vpn-proxy.yaml) once qBittorrent/JDownloader are -# both soaked. -# 2. Create the browser-vpn-proxy-credentials Secret first (see the -# companion secret script this needs — not written yet, follow the -# exact pattern of manifests/media/pia-secret.sh: add -# BROWSER_PROXY_USER/BROWSER_PROXY_PASSWORD to .env, a script that -# kubeseals a Secret with those as PROXY_USER/PROXY_PASSWORD). -# 3. Verify the NodePort below (30889) doesn't collide with anything -# live — manifests/network/gluetun-proxy.yaml already uses 30888 -# for the existing browser SOCKS5 proxy this is meant to eventually -# replace; confirm 30889 is actually free before applying. +# Before applying: +# 1. Verify the NodePort below (30889) doesn't collide with anything +# live — confirmed free 2026-08-24 (`kubectl get svc -A`, checked +# against every NodePort in use). manifests/media/gluetun-proxy.yaml +# (the existing browser proxy this replaces) uses 30888, not this +# one — no collision, but that file stays live and unmodified until +# this replacement is confirmed working end to end, then gets +# decommissioned as its own separate, explicitly-approved step. # # Design: # - serjs/go-socks5-proxy (github.com/serjs/socks5-server) — a small, # single-purpose Go SOCKS5 server, not a VPN client itself. It -# doesn't need to be, here: unlike the legacy manifests/network/ +# doesn't need to be, here: unlike the legacy manifests/media/ # gluetun-proxy.yaml it's replacing (which runs its own OpenVPN # client), PIA egress for this workload comes from the same # netns-level routing as qBittorrent/JDownloader — the init @@ -37,10 +28,37 @@ # proxy. Pinned to v0.0.4's digest, confirmed live against Docker # Hub's registry API 2026-08-24: # sha256:6828ddb2a6a93dec85209b69cd3842bd80094f7dd52f35ee1dd0f9e9165e2188 -# - REQUIRE_AUTH=true + PROXY_USER/PROXY_PASSWORD (from the Secret -# above) AND ALLOWED_IPS restricted to the home LAN range — both, not -# either/or, matching plan.md's "authentication or LAN-restricted -# exposure" as a floor, not a ceiling. +# - No auth (REQUIRE_AUTH=false), no ALLOWED_IPS — a deliberate choice, +# not an oversight. plan.md originally called for auth AND +# LAN-restriction together as a floor; discussed and explicitly +# relaxed (2026-08-24) after confirming the home network's own +# firewall already restricts this Service's NodePort to Trusted +# (VLAN 10) and Lab (VLAN 40) only — Trusted/Lab → Lab is ALLOW, +# IoT/Guest/Management → Lab is DENY except the Technitium DNS +# exception (see ~/repo/home-network/Home Network Implementation +# Plan.md section 10). Residual risk accepted knowingly: Lab has its +# own Wi-Fi SSID with open DHCP, so "reachable" means "anyone with +# that Wi-Fi password or a device already on Trusted/Lab," not just +# deliberately provisioned devices — there is no proxy-level control +# backing this up. +# - ALLOWED_IPS is not a fallback for the above — it's removed because +# it cannot do what it looks like it does. Read this image's actual +# source (server.go, and the go-socks5 fork it replaces via go.mod's +# replace directive) to confirm, not assumed: ALLOWED_IPS values are +# parsed with net.ParseIP (not net.ParseCIDR) and matched with exact +# net.IP.Equal(), never CIDR containment. The original draft's value +# here, "10.10.40.0/24", would have failed to parse as a plain IP, +# producing a whitelist containing one nil entry — since Equal() +# against nil never matches any real client address, every single +# connection would have been rejected, including legitimate ones. +# This library has no way to express a subnet allowlist at all, only +# a list of exact individual IPs, which doesn't fit a dynamic-DHCP +# LAN. Access restriction is the OpenWrt firewall alone; there is no +# compensating netns-level (nft) inbound filter here either — one +# could be added later, but would first need confirming whether this +# NodePort Service actually preserves real client source IPs +# end-to-end (default `externalTrafficPolicy: Cluster` typically +# does not) rather than assuming it does. # - Service is NodePort, not LoadBalancer/Ingress — reachable from the # LAN at :30889, never exposed publicly (no port-forward # on Flint/the router for it, same as the existing gluetun-proxy). @@ -66,6 +84,8 @@ kind: Deployment metadata: name: browser-vpn-proxy namespace: downloads + annotations: + argocd.argoproj.io/sync-wave: "3" spec: replicas: 1 strategy: @@ -103,10 +123,16 @@ spec: value: "10.43.0.0/16" - name: NODE_IP value: "10.10.40.20" + - name: EXPECTED_VLAN50_IP + value: "10.10.50.12" securityContext: capabilities: drop: ["ALL"] - add: ["NET_ADMIN"] + # NET_RAW alongside NET_ADMIN: the guard script's + # arping-based duplicate-address and gateway-reachability + # checks need it — see + # vlan50-egress-guard-script.yaml's header comment. + add: ["NET_ADMIN", "NET_RAW"] volumeMounts: - name: guard-script mountPath: /scripts @@ -119,20 +145,15 @@ spec: capabilities: drop: ["ALL"] env: + # REQUIRE_AUTH defaults to true in this image (confirmed + # against its real source, params struct in server.go) — it + # must be explicitly set to "false" to actually go + # passwordless, omitting it entirely would NOT have that + # effect. Deliberately no auth, no ALLOWED_IPS — see the + # header comment for the reasoning and what actually + # restricts access instead. - name: REQUIRE_AUTH - value: "true" - - name: PROXY_USER - valueFrom: - secretKeyRef: - name: browser-vpn-proxy-credentials - key: PROXY_USER - - name: PROXY_PASSWORD - valueFrom: - secretKeyRef: - name: browser-vpn-proxy-credentials - key: PROXY_PASSWORD - - name: ALLOWED_IPS - value: "10.10.40.0/24" # LAN only — narrow this further if a specific client range is confirmed + value: "false" resources: requests: cpu: 20m diff --git a/manifests/multus/README.md b/manifests/multus/README.md index 2a6c07f..73f6665 100644 --- a/manifests/multus/README.md +++ b/manifests/multus/README.md @@ -13,9 +13,9 @@ comment. Nothing here applies to the cluster just because it's merged to | `01-rbac.yaml` | 0 | Multus ServiceAccount/ClusterRole/ClusterRoleBinding | | `02-daemonset.yaml` | 0 | Multus thick-plugin DaemonSet — **nodeSelector restricted to `nik-debian` only**, k3s-specific CNI paths | | `10-nad-vlan50.yaml` | 1 | VLAN 50 `NetworkAttachmentDefinition`, macvlan bridge on `enp1s0.50`, static IPAM | -| `vlan50-egress-guard-script.yaml` | (no wave annotation — apply alongside 10) | Shared init-container script consumed by the canary and, later, qBittorrent/JDownloader | +| `vlan50-egress-guard-script.yaml` | (no wave annotation — apply alongside 10) | Shared init-container script consumed by the canary, qBittorrent, JDownloader, and `browser-vpn-proxy` | | `20-canary.yaml` | 2 | Temporary canary pod — **do not sync without separate explicit approval**, see its own header | -| `reserved/browser-vpn-proxy.yaml` | n/a | The `.12` third workload, prepared not deployed — see its own header for why it lives in a subdirectory `argocd/apps/multus.yaml` never scans at all | +| `30-browser-vpn-proxy.yaml` | 3 | `.12` — SOCKS5 proxy for browser traffic via PIA, replaces `manifests/media/gluetun-proxy.yaml`. No auth, no app-level IP allowlist (see its own header for why and what actually restricts access instead) — **do not sync without separate explicit approval** | ## Before applying anything here @@ -31,6 +31,9 @@ comment. Nothing here applies to the cluster just because it's merged to 01 → 02, confirm the Multus pod is actually Running on `nik-debian` and `/var/lib/rancher/k3s/agent/etc/cni/net.d` now has a generated `00-multus.conf`, *then* 10, *then* — only with separate approval — 20. + `30-browser-vpn-proxy.yaml` follows the same "separate approval" gate + as 20 — no Secret precondition, it needs no credentials at all (see + its own header for why). ## Canary lifecycle — re-running it after a Multus/CNI change @@ -113,8 +116,8 @@ edge cases). ## Why `ipam.type: static` and no shared allocator (Whereabouts etc.) Only ever a handful of fixed IPs on this network — qBittorrent (`.10`), -JDownloader (`.11`), one reserved-not-deployed (`.12`), and a single -canary (`.100`). A dynamic IPAM controller (CRD, webhook, its own failure +JDownloader (`.11`), `browser-vpn-proxy` (`.12`), and a single canary +(`.100`). A dynamic IPAM controller (CRD, webhook, its own failure modes) is unjustified complexity for that; each pod's own `k8s.v1.cni.cncf.io/networks` annotation just states its IP directly. @@ -164,6 +167,7 @@ kubectl wait --for=condition=Ready pod/vlan50-rollback-probe --timeout=60s kubectl delete pod vlan50-rollback-probe # 3. Only now, the Kubernetes-side resources, in this order: +kubectl delete -f manifests/multus/30-browser-vpn-proxy.yaml # if it was ever applied kubectl delete -f manifests/multus/20-canary.yaml # if it was ever applied kubectl delete -f manifests/multus/10-nad-vlan50.yaml # only after no pod still references it kubectl delete -f manifests/multus/vlan50-egress-guard-script.yaml