homelab/manifests/multus/30-browser-vpn-proxy.yaml
Nik Afiq 89524d45e3 feat: activate browser-vpn-proxy (VLAN 50 SOCKS5 proxy via PIA)
Move manifests/multus/reserved/browser-vpn-proxy.yaml into the active
multus tree (sync-wave 3) now that qBittorrent and JDownloader have
both independently passed their canary, validation, and soak gates -
the precondition this file's own header already called for.

Add EXPECTED_VLAN50_IP/NET_RAW to its guard init container, matching
the fix qBittorrent/JDownloader/canary all needed for the arping-based
checks to run.

Deliberately no auth (REQUIRE_AUTH=false) and no ALLOWED_IPS, after
discussion: access restriction relies on the home network's own
firewall (Trusted/Lab -> Lab NodePort ALLOW, other VLANs DENY except
Technitium DNS), not an app-level control. Along the way, found and
avoided shipping a real bug in the original draft - go-socks5-proxy's
ALLOWED_IPS parses values with net.ParseIP (not CIDR-aware) and
matches with exact net.IP.Equal(), so the drafted "10.10.40.0/24"
value would have produced a nil whitelist entry that matches no real
client, rejecting every connection. This library has no way to express
a subnet allowlist at all - removed rather than left silently broken.

Also drops the now-unneeded browser-vpn-proxy-credentials Secret and
its companion script/README references.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:19:48 +09:00

163 lines
6.7 KiB
YAML

# 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".
#
# 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.
#
# 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/
# 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
# container below sends this pod's default route out net1/PIA, the
# SOCKS5 server itself just needs to be a plain, correctly-behaving
# 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.
# - Service is NodePort, not LoadBalancer/Ingress — reachable from the
# LAN at <any node IP>:30889, never exposed publicly (no port-forward
# on Flint/the router for it, same as the existing gluetun-proxy).
#
# Browser configuration — this is the part that actually matters for
# "DNS must be resolved through the proxy": configuring a SOCKS5 proxy
# address alone is NOT enough. By default most browsers resolve
# hostnames locally (via the LAN's normal DNS) and only send the already-
# resolved IP through the proxy — that leaks every site you visit to
# your normal LAN resolver and bypasses Technitium/PIA for DNS entirely,
# defeating the point. "socks5h" (vs plain "socks5") in a proxy URL is
# the conventional way tools signal "resolve hostnames on the proxy side,
# not locally" (e.g. curl --socks5-hostname, or a proxy URL scheme of
# socks5h://). In Firefox specifically: Settings -> Network Settings ->
# Manual proxy configuration -> SOCKS Host set to this Service -> SOCKS
# v5 selected -> the checkbox "Proxy DNS when using SOCKS v5" MUST be
# checked, or Firefox resolves locally despite the proxy being SOCKS5.
# Verify this actually holds (a DNS leak test site, or packet capture
# showing no DNS queries leaving the browser's own host) before trusting
# it — do not assume the checkbox alone is sufficient without checking.
apiVersion: apps/v1
kind: Deployment
metadata:
name: browser-vpn-proxy
namespace: downloads
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: browser-vpn-proxy
template:
metadata:
labels:
app: browser-vpn-proxy
annotations:
k8s.v1.cni.cncf.io/networks: |
[{"name": "vlan50", "namespace": "downloads", "interface": "net1", "ips": ["10.10.50.12/24"]}]
spec:
nodeSelector:
node-role: storage
kubernetes.io/hostname: nik-debian
dnsPolicy: None
dnsConfig:
nameservers:
- "10.10.40.53"
initContainers:
- name: vlan50-egress-guard
image: nicolaka/netshoot:v0.11
command: ["/bin/sh", "/scripts/guard.sh"]
env:
- name: VLAN50_GATEWAY
value: "10.10.50.1"
- name: TECHNITIUM_IP
value: "10.10.40.53"
- name: POD_CIDR
value: "10.42.0.0/16"
- name: SERVICE_CIDR
value: "10.43.0.0/16"
- name: NODE_IP
value: "10.10.40.20"
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_ADMIN"]
volumeMounts:
- name: guard-script
mountPath: /scripts
containers:
- name: socks5
image: "serjs/go-socks5-proxy@sha256:6828ddb2a6a93dec85209b69cd3842bd80094f7dd52f35ee1dd0f9e9165e2188" # v0.0.4
ports:
- containerPort: 1080
securityContext:
capabilities:
drop: ["ALL"]
env:
- 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
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
volumes:
- name: guard-script
configMap:
name: vlan50-egress-guard-script
defaultMode: 365 # octal 0555, r-xr-xr-x
---
apiVersion: v1
kind: Service
metadata:
name: browser-vpn-proxy
namespace: downloads
spec:
selector:
app: browser-vpn-proxy
type: NodePort
ports:
- name: socks5
port: 1080
targetPort: 1080
nodePort: 30889 # verify unused before applying — see header