Skip to content

Bug: init-iptables.sh picks wrong backend on nft-only nodes (readability probe ≠ liveness) #502

Description

@huang195

Summary

init-iptables.sh's detect_iptables_cmd() selects the wrong iptables backend on nft-only nodes (ROSA, recent RHEL/OpenShift, EKS, recent Ubuntu). It decides which backend to use by checking whether the legacy nat table can be listed, treats that read as proof the legacy backend is in use, and prefers legacy whenever the read succeeds. On nft-only hosts the legacy nat table is still readable (just empty), so the script picks iptables-legacy and programs our chains into a table the live datapath never consults. The result is a silent no-op: enforce-redirect (and redirect) rules insert successfully but never take effect.

File: authbridge/authproxy/init-iptables.sh (detect_iptables_cmd).

Current logic

detect_iptables_cmd() {
  if [ -n "${IPTABLES_CMD:-}" ]; then
    echo "${IPTABLES_CMD}"; return
  fi
  if command -v iptables-legacy >/dev/null 2>&1 && \
     iptables-legacy -t nat -L -n >/dev/null 2>&1; then   # readability probe
    echo "iptables-legacy"                                # legacy-first bias
  else
    echo "iptables"                                       # nft backend
  fi
}

Root cause

Linux exposes two independent packet-filtering subsystems behind the same CLI syntax: legacy x_tables (iptables-legacy) and nf_tables (iptables, the nft backend on Alpine). Rules written to one are not evaluated against the other. Our rules only work if they go into the backend the host (kube-proxy / CNI / Istio) actually uses.

Two problems with the current detection:

  1. Readability ≠ liveness. iptables-legacy -t nat -L -n only answers "can I enumerate the legacy nat table?" — a read. It does not answer "is the legacy backend where the live rules live?" On an nft-only node the legacy nat table typically lists fine (empty), so the probe passes and legacy is chosen even though every real NAT rule is in nf_tables.
  2. Legacy-first bias. The "prefer legacy for compatibility" default reflects an era when K8s/Istio used legacy. Most current clusters are nft-only, so the default is backwards.

Failure mode is silent

Because the rules insert successfully into a real (but dead) table, every command returns success: the init container exits 0, the pod is healthy, and iptables-legacy -t nat -L shows the chains present — yet egress that should be captured/redirected leaks straight out. There is no error to alert on; you only notice that traffic isn't being intercepted.

This is distinct from the capability/SCC failure seen on ROSA (a hard EPERM initializing the nat table that CrashLoops the init container). That one is loud and is about CAP_NET_ADMIN/SCC admission, not backend selection. The detection bug doesn't crash — it leaks. Both can be present on the same platform.

Proposed fix

  1. Probe liveness, not readability. Pick the backend that already holds the host's rules — e.g. compare iptables-legacy-save -t nat vs iptables-nft-save -t nat and prefer the backend that contains the existing kube-proxy / Istio chains.
  2. Write-probe the chosen backend (insert a throwaway rule, then delete it) so failures cleanly distinguish "wrong backend" from "no CAP_NET_ADMIN", and so a silent no-op can't happen.
  3. Default to nft when ambiguous, not legacy.
  4. Keep the existing IPTABLES_CMD / IP6TABLES_CMD override as the escape hatch, and have the operator set it explicitly on known platforms.

Impact

  • enforce-redirect (proxy-sidecar / lite) silently fails open on nft-only nodes — egress enforcement is bypassed without any signal.
  • redirect (envoy-sidecar) interception silently does nothing on the same nodes.

cf. upstream caution that spawn/list success ≠ a functioning backend.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions