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>
sync-wave 3 (after the canary's wave 2) was a documentation convention,
not a real dependency, and it broke syncing entirely: this cluster's
argocd-cm ignores /status updates for reconciliation cluster-wide
(resource.customizations.ignoreResourceUpdates.all), so Argo's wave
orchestrator can never observe a freshly recreated Pod's Pending ->
Running transition happening during an active sync - exactly what the
canary does every test round, since it's deleted and recreated each
time. The sync hung forever on "waiting for healthy state of
/Pod/vlan50-canary" even once the pod was actually Running live.
Move browser-vpn-proxy to wave 1, alongside the NAD - the only thing
it actually needs to exist first. Document the root cause and the
kubectl patch to clear a stuck operation in the README, since this
risk applies to anything synced after the canary in this Application,
not just this one file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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>
The previous ip6tables->sysctl fix (26894d1) was itself wrong: writing
/proc/sys/net/ipv6/conf/all/disable_ipv6 failed live with "Read-only
file system" despite NET_ADMIN and a passing [ -w ] check - the
container runtime mounts /proc/sys read-only by default regardless of
capabilities, independent of file permission bits. Making it writable
needs either kubelet's securityContext.sysctls (and
net.ipv6.conf.*.disable_ipv6 isn't on its default safe-sysctls
allowlist, so that means a node-level --allowed-unsafe-sysctls flag)
or securityContext.procMount: Unmasked (which needs pod-level user
namespaces) - too much blast radius for one pod's IPv6 kill switch.
nft (nftables) is genuinely present in the same image (the "nftables"
apk package, installed alongside "iptables" but not "ip6tables") and
handles the ip6 address family without a separate binary, so it needs
no image or capability change. Verified by actually running the exact
commands against the real pinned nicolaka/netshoot:v0.11 image
(digest-matched to what's on nik-debian), not just against docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
nicolaka/netshoot:v0.11 (the pinned tag actually in use) is built on
Alpine 3.18.0, where ip6tables is packaged separately from iptables;
this image's Dockerfile only installs the latter, so ip6tables does
not exist in the container at all (confirmed live: "ip6tables: not
found", and against Alpine's own v3.18 package index, not master's).
Write 1 to /proc/sys/net/ipv6/conf/all/disable_ipv6 instead - per the
kernel's own ip-sysctl docs this is equivalent to also setting
conf/default/disable_ipv6 and every existing per-interface
disable_ipv6 (lo/eth0/net1) at once, and is strictly stronger than an
iptables DROP policy would have been: no IPv6 activity happens on any
interface at all, not just filtered output. Needs no capability beyond
the NET_ADMIN the guard init container already has.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diagnosed the vlan50-canary FailedCreatePodSandBox->net1-timeout failure
as a stale Pod artifact of the Multus DaemonSet rollout race (sandbox
created via a transient daemon/shim state mid-rollout; the current,
fully-settled daemon's own logs show no ADD for that UID, only a DEL).
No defect found in 02-daemonset.yaml or 10-nad-vlan50.yaml; both are
unchanged.
Independent of that diagnosis, harden the shared guard script per
review: print safe interface/address/route diagnostics before every
FATAL exit; validate net1 actually carries the workload's expected
static /24 address rather than just existing; add arping-based
duplicate-address and gateway-reachability checks before installing
the net1 default route (exit-code semantics verified against arping's
own source). Requires EXPECTED_VLAN50_IP and NET_RAW (for arping's raw
ARP sockets) on every consumer - wired into 20-canary.yaml now,
qbittorrent.yaml/jdownloader.yaml need the same when they're migrated.
Document the canary's Pod lifecycle: restartPolicy: Never means a
Failed canary never reruns on its own, and Argo "Synced" only reflects
manifest match, not runtime success - recreate it (new UID) after any
Multus/CNI change before trusting its result. Recommend keeping it a
manually recreated, controller-less Pod rather than a Job/Deployment,
since unattended auto-retry risks a duplicate-address race on its
static .100 IP - the exact class of bug this diagnosis just walked
through.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
k3s deliberately ships only the plugins its own flannel networking
needs (bandwidth, bridge, firewall, flannel, host-local, loopback,
portmap — confirmed against k3s's own build script; macvlan's absence
is an intentional upstream scope decision, k3s-io/k3s#9224). Multus's
macvlan delegate for the vlan50 NAD therefore had nothing to exec even
after the binDir/mount fixes.
Add a cni-plugins init container mirroring Rancher's own official
rke2-multus Helm chart (the fix k3s's own docs recommend for this
exact gap): rancher/hardened-cni-plugins, digest-pinned, writing into
our existing cnibin mount instead of the image's hardcoded
/opt/cni/bin, with SKIP_CNI_BINARIES=flannel so it doesn't overwrite
k3s's already-working flannel plugin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
binDir alone (previous commit) wasn't sufficient: k3s's plugin bin dir
is a directory of symlinks into a content-hash-named sibling directory,
and the narrow cnibin mount didn't cover that sibling. FindInPath's
os.Stat follows symlinks, so it failed inside the container even though
the same path resolves fine on the real host. Mount the parent
(k3s-cni-data) into the main container instead so the symlinks resolve;
the init container keeps the narrower cnibin mount since it only writes
files and never follows them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause: Multus's ChrootExec resolves a delegate plugin's absolute
path from the daemon container's own filesystem view, then execs that
exact path string after chrooting into the real host root. binDir
defaulted to /opt/cni/bin, which is valid inside the container (bind-
mounted to k3s's real bin dir) but doesn't exist on the actual host, so
every pod attaching to the VLAN 50 NAD failed FailedCreatePodSandBox
trying to delegate to flannel. Confirmed against multus-cni's source at
the exact pinned v4.3.0 tag, not assumed. Sets binDir explicitly to
k3s's real path and matches the cnibin mount path to it on both sides of
the chroot boundary (main container and the install_multus init
container).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CRD, RBAC, thick-plugin DaemonSet (nik-debian only, k3s CNI paths,
digest-pinned), the VLAN 50 NAD (macvlan bridge on enp1s0.50), the
shared pod egress-guard script, a temporary canary pod, and the
not-yet-deployed browser-vpn-proxy workload (kept in reserved/, outside
this Application's non-recursive source path).
Referenced by argocd/apps/multus.yaml (already pushed) but not deployed
by it — that Application has no syncPolicy.automated, so this still
needs an explicit selective sync per resource.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>