homelab/manifests/multus/30-browser-vpn-proxy.yaml
Nik Afiq 6f6c3cf0eb
Some checks failed
validate / lint (push) Failing after 0s
feat: decommission legacy gluetun-proxy in favor of browser-vpn-proxy
manifests/media/gluetun-proxy.yaml (the standalone Gluetun/OpenVPN
HTTP proxy, NodePort 30888) is now fully replaced by
manifests/multus/30-browser-vpn-proxy.yaml, live and validated
end-to-end (connectivity, DNS-through-proxy, PIA egress, fail-closed
kill switch). Removing the file from git - the live Deployment/Service
still need a separate kubectl delete, since media has prune: false.

Note gluetun.home.arpa (the dashy VPN-IP-banner hostname, and the
Certificate/IngressRoute in qbittorrent.yaml routing it to that pod's
ip-reporter sidecar) is unrelated and unaffected - it never pointed at
this standalone proxy.

Update the root README's VLAN 50 status table with browser-vpn-proxy
and this removal, including the exact commit to restore the old file
from if ever needed.

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

200 lines
9.3 KiB
YAML

# sync-wave 1 — same wave as the NAD (10-nad-vlan50.yaml), the only
# thing this actually needs to exist first. NOT wave 3/after-the-canary
# as originally drafted: that was a documentation convention, not a real
# dependency, and it turned out to actively break syncing. Root-caused
# live (2026-08-24): this cluster's argocd-cm sets
# resource.customizations.ignoreResourceUpdates.all to ignore /status
# changes cluster-wide (a deliberate, existing setting, not something to
# change just for this file) — Argo's wave orchestrator waits for each
# wave's resources to report Healthy before starting the next wave, but
# a Pod that's deleted and recreated (exactly what the canary is, every
# test round) needs Argo to observe a fresh Pending -> Running /status
# transition *during that sync*, which the ignore-updates config
# prevents it from ever seeing — so the operation hangs forever on
# "waiting for healthy state of /Pod/vlan50-canary" even once the Pod is
# actually Running. Already-stable resources (the DaemonSet, RBAC, NAD)
# never hit this because their cached health from a prior sync is still
# valid when nothing about them changed. 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.
#
# 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/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
# 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
# - 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 <any node IP>:30889, never exposed publicly (no port-forward
# on Flint/the router for it, same as the now-decommissioned
# gluetun-proxy was).
#
# 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
annotations:
argocd.argoproj.io/sync-wave: "1"
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"
- name: EXPECTED_VLAN50_IP
value: "10.10.50.12"
securityContext:
capabilities:
drop: ["ALL"]
# 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
containers:
- name: socks5
image: "serjs/go-socks5-proxy@sha256:6828ddb2a6a93dec85209b69cd3842bd80094f7dd52f35ee1dd0f9e9165e2188" # v0.0.4
ports:
- containerPort: 1080
securityContext:
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: "false"
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