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