From 28d062801a73eeb9aef669b229be06bab70e2158 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Mon, 24 Aug 2026 22:15:12 +0900 Subject: [PATCH] fix: use nft instead of ip6tables/sysctl for IPv6 blocking in vlan50 guard 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 --- .../multus/vlan50-egress-guard-script.yaml | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/manifests/multus/vlan50-egress-guard-script.yaml b/manifests/multus/vlan50-egress-guard-script.yaml index 8b192b1..d371dc6 100644 --- a/manifests/multus/vlan50-egress-guard-script.yaml +++ b/manifests/multus/vlan50-egress-guard-script.yaml @@ -190,40 +190,48 @@ data: iptables -A VLAN50-GUARD -o eth0 -j DROP echo "[vlan50-egress-guard] blocking IPv6 entirely (both interfaces) — IPv4-only design" - # sysctl, not an ip6tables DROP policy: nicolaka/netshoot:v0.11 (the - # pinned tag actually in use) is built on Alpine 3.18.0 per its real, - # tag-pinned Dockerfile — confirmed live, 2026-08-24, "ip6tables: not - # found". Alpine 3.18 packages ip6tables SEPARATELY from iptables - # (confirmed against Alpine's own v3.18 package index), and this - # image's Dockerfile only installs the latter, so ip6tables genuinely - # does not exist in this container at all — not a PATH issue (the - # iptables calls just above this ran fine from the same image). - # Disabling IPv6 at the netns level is also strictly stronger than a - # DROP policy would have been anyway: no IPv6 address, neighbor - # discovery, or routing activity happens on any interface here at - # all, not just filtered OUTPUT/FORWARD traffic. A single write to - # conf/all/disable_ipv6 is sufficient by itself — the kernel's own - # ip-sysctl documentation defines it as equivalent to writing - # conf/default/disable_ipv6 (for any interface created afterward) - # *and* every existing per-interface disable_ipv6 (lo/eth0/net1) all - # at once, not merely an aggregate read. Namespaced net.ipv6.* - # sysctls are writable directly by a process with CAP_NET_ADMIN in - # its own netns — no pod-spec sysctls: field is needed for this. - DISABLE_IPV6=/proc/sys/net/ipv6/conf/all/disable_ipv6 - if [ ! -w "${DISABLE_IPV6}" ]; then - echo "[vlan50-egress-guard] FATAL: ${DISABLE_IPV6} is not writable — refusing to continue without confirmed IPv6 disablement" >&2 - print_diagnostics - exit 1 - fi - echo 1 > "${DISABLE_IPV6}" - if [ "$(cat "${DISABLE_IPV6}")" != "1" ]; then - echo "[vlan50-egress-guard] FATAL: wrote 1 to ${DISABLE_IPV6} but it did not stick — refusing to continue without confirmed IPv6 disablement" >&2 - print_diagnostics - exit 1 - fi - echo "[vlan50-egress-guard] IPv6 disabled (conf/all/disable_ipv6=1)" + # nft (nftables), not ip6tables and not a /proc/sys sysctl write — + # both tried first and both failed live, 2026-08-24: + # - ip6tables: genuinely absent from this image. Alpine 3.18.0 + # (nicolaka/netshoot:v0.11's real, tag-pinned base — confirmed + # against that exact Dockerfile, not master's) packages ip6tables + # SEPARATELY from iptables, and this image's Dockerfile only + # installs the latter (confirmed against Alpine's own v3.18 + # package index) — "ip6tables: not found", not a PATH issue (the + # iptables calls just above this ran fine from the same image). + # - `echo 1 > /proc/sys/net/ipv6/conf/all/disable_ipv6`: failed + # with "Read-only file system" despite the [ -w ] check on that + # path reporting it writable and NET_ADMIN already being granted + # — permission bits and the container runtime's own read-only + # mount of /proc/sys are two independent gates, and [ -w ] only + # tests the first. Kubernetes' two supported ways to actually get + # a writable /proc/sys/net/* here are pod-spec + # securityContext.sysctls (kubelet-mediated, pre-container-start + # — but net.ipv6.conf.*.disable_ipv6 isn't on kubelet's own + # default safe-sysctls allowlist, so this would need a + # node-level --allowed-unsafe-sysctls kubelet flag) or + # securityContext.procMount: Unmasked (which itself requires + # pod-level user namespaces, spec.hostUsers: false) — both far + # more cluster-wide blast radius than an IPv6 kill switch on one + # pod justifies. + # - nft is genuinely present: the same Dockerfile that omits + # ip6tables installs the separate "nftables" apk package + # (confirmed against Alpine's own v3.18 package index, same way + # ip6tables' absence was). nftables handles the ip6 address + # family as an ordinary table — no separate ip6-specific binary + # needed — so this needs no image or capability change beyond + # what's already granted (CAP_NET_ADMIN, already present for the + # iptables/ip rules above). Syntax verified against nftables' + # own wiki (base chain + policy, and the meta oifname interface + # matcher), not guessed. + nft add table ip6 vlan50guard6 + nft 'add chain ip6 vlan50guard6 output { type filter hook output priority 0; policy drop; }' + nft 'add chain ip6 vlan50guard6 forward { type filter hook forward priority 0; policy drop; }' + nft add rule ip6 vlan50guard6 output meta oifname lo accept + echo "[vlan50-egress-guard] IPv6 blocked (nft table ip6 vlan50guard6, output+forward policy drop, lo excepted)" echo "[vlan50-egress-guard] final state:" ip route show iptables -S OUTPUT iptables -S VLAN50-GUARD + nft list table ip6 vlan50guard6