Skip to content

EAI-7893 Stop persisting kube-proxy's live iptables rules - #294

Open
pre wants to merge 2 commits into
mainfrom
fix-iptables-snapshot
Open

EAI-7893 Stop persisting kube-proxy's live iptables rules#294
pre wants to merge 2 commits into
mainfrom
fix-iptables-snapshot

Conversation

@pre

@pre pre commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Related:

Jira: https://amd.atlassian.net/browse/EAI-7893

Problem

Bloom's Save iptables handler runs iptables-save > /etc/iptables/rules.v4.
Handlers fire at the end of the play, by which point RKE2 is up and kube-proxy
and Cilium have programmed the live ruleset. iptables-save is a whole-machine
dump, so bloom writes another controller's runtime state into the file
netfilter-persistent replays at boot.

Two failures follow:

  • The boot replay runs roughly 23s before RKE2 starts. kube-proxy's
    KUBE-FORWARD rule needs an nfacct accounting object that only exists once
    kube-proxy has created it, so the restore fails with
    RULE_APPEND failed (No such file or directory) and netfilter-persistent
    is left permanently failed. This is what the fleet smoke test reports.
  • When the replay does succeed it is worse, because it reapplies a stale
    snapshot of cluster state. On int-test-003 the file had grown to 110 KB of
    KUBE-SVC-* / KUBE-SEP-* chains naming pod IPs from two months earlier.

Re-running bloom does not repair an affected node. The handler only fires when
a port rule actually changes, which on an already-prepared node it does not.

Fix

Two parts:

  • Flush handlers immediately after prepare_node, so the snapshot is taken
    while the ruleset is still only bloom's own. Every notifier has run by that
    point: the port rules in prepare_node and the REJECT removals in
    validate_node.
  • In validate_node, detect a rules.v4 that already holds another
    controller's chains and replace it in place with a clean iptables-save.
    This repairs nodes bloomed by an earlier version.

Detection keys on KUBE/CILIUM/ts- chains rather than on file size or age,
so a hand-written rules.v4 predating RKE2 survives untouched. Files
containing ufw- chains are skipped entirely and only logged: ufw does not
read rules.v4, it programs its own chains from /etc/ufw/*.rules, so such a
file is one where two tools already disagree about ownership, and bloom is not
the right place to settle that.

Safety for non-OCI users

The replacement is written in place rather than deleted and left to the
handler, because a targeted run such as --tags iptables would otherwise
delete the file and never reach flush_handlers to write the replacement. The
file is never absent, on any code path.

No option here can lock an operator out of their own machine.
iptables-restore applies policy lines before rules, and the capture records
whatever policies the node already has, so this can only preserve the current
firewall posture, never tighten it. It runs in validate_node, before RKE2
starts, so the live ruleset it captures is still the operator's own.

Verification

All four detection arms exercised on a fresh Kaytoo VM (Ubuntu 24.04, OCI
image with its own 4489-byte rules.v4, ufw not installed), running the
branch binary via bloom cli --export.

pre added 2 commits August 6, 2026 16:01
The "Save iptables" handler runs iptables-save, a whole-machine dump,
and nothing flushed it, so it fired at the very end of the play with
RKE2 already up. The file netfilter-persistent replays at boot was
therefore a snapshot of kube-proxy's and Cilium's live state.

That breaks in two directions. The replay runs about 23 seconds before
RKE2, and kube-proxy's KUBE-FORWARD rule needs an nfacct object that
only exists once kube-proxy has created it, so the restore fails with
"RULE_APPEND failed (No such file or directory)" and the unit stays
failed. Where it does not fail it is worse: it replays a stale copy of
another controller's state before that controller starts. One int-test
node had 110 KB of KUBE-SVC-*/KUBE-SEP-* chains naming pod IPs from two
months earlier.

Flushing handlers after prepare_node fixes both. Every notifier has run
by then, the port rules and the REJECT removals, and the ruleset is
still only ours.

New nodes are enough for the timing, but already-bloomed ones keep the
bad file: the handler only fires when a port rule changes, which on a
prepared node it does not. So validate_node now discards a rules.v4
that contains KUBE, CILIUM or ts- chains and lets the flushed handler
write a correct one. Detection is on those chain names rather than size
or age, so a hand-written file that predates RKE2 is left alone.

Verified against six nodes: the one file never overwritten by the
handler is kept, and all five snapshots taken from running clusters are
matched, including one whose unit has not failed yet because its
snapshot predates kube-proxy's KUBE-FORWARD chain.
Deleting the file and leaving the handler to write the replacement has
a gap: the handler only runs where flush_handlers is reached, so a
targeted run such as --tags iptables would delete the file and never
write anything back, leaving the node with no persisted rules at all.
Writing in place means the file is never absent on any code path.

Also leave the file alone when it holds ufw chains. ufw does not read
rules.v4, it programs its own chains from /etc/ufw/*.rules, so a file
containing both is one where two tools already disagree about who owns
the ruleset, and bloom is not the place to settle that. The check now
returns 0 rewrite, 1 leave alone, 2 ufw owns it, 3 no file yet, with
the no-file arm explicit because grep on a missing path also exits 2
and would otherwise report a fresh node as a ufw node.

Neither path can tighten a firewall. iptables-save records whatever
policy the node already has, and it runs in validate_node before RKE2
starts, so what it captures is still the operator's own ruleset.

Verified against six nodes and five synthetic files: a hand-written
rules.v4 with :INPUT DROP is left alone, a ufw file is left alone, a
missing file reports as missing, and the five snapshots taken from
running clusters are rewritten.
@pre
pre requested a review from a team as a code owner August 6, 2026 13:41
@pre

pre commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Kaytoo VM verification

Fresh Kaytoo VM useocpm2m-silogen-petrus-t9jwt6, Ubuntu 24.04.3 (Noble),
OCI image with its own rules.v4 (4489 bytes, InstanceServices chain, all
policies ACCEPT), netfilter-persistent enabled and active, ufw not
installed. Deliberately a non-OCI-agnostic baseline: the case that must not
break is an operator-owned firewall file that bloom has never touched.

Branch binary built from fix-iptables-snapshot and confirmed on the VM
(bloom --version reports fix-iptables-snapshot). Exported with
bloom cli --export and both changes verified present in the exported tree,
then the validate_node iptables tasks were driven through each detection arm.

Detection matrix

Case rules.v4 contents rc Behaviour Result
1 OCI pristine, no cluster chains 1 both new tasks skipped file left alone
2 contains KUBE-FORWARD + nfacct rule 0 replaced in place kube chains gone (0 remaining)
3 contains ufw-before-input and KUBE-FORWARD 2 note logged, replacement skipped md5 unchanged
4 file absent 3 both tasks skipped, no error nothing written

Case 3 is the arm that needs the explicit no-file check to exist: grep on a
missing path also exits 2, so without case 4's test -f ... || exit 3 a node
that has never had a rules.v4 would have been reported as a ufw node.

Case 1 in detail

The file's md5 does change across a case-1 run, but not from anything in this
PR. Both new tasks skip (rc: 1), and the diff is bloom's own long-standing
remediation: this OCI image ships the -A INPUT -j REJECT --reject-with icmp-host-prohibited and matching FORWARD rule, bloom deletes
them as it always has, and the handler re-saves. Policies after the run are
still :INPUT ACCEPT / :FORWARD ACCEPT / :OUTPUT ACCEPT, zero
KUBE/CILIUM/ts- chains, netfilter-persistent active and not failed, and
the SSH session stayed up throughout.

Post-fix file restores

iptables-restore --test /etc/iptables/rules.v4 passes after the case-2
replacement, and netfilter-persistent reload completes cleanly.

One negative I could not reproduce on this VM, stated plainly: the polluted
file also passed iptables-restore --test. That is expected rather than
contradictory, since --test does not resolve the nfacct object and the real
failure is an ordering one, the boot replay running before kube-proxy has
created it. The evidence for that failure remains the int-test observation
described in the PR body, not this VM.

Not covered

A full bloom cli install was not run. The changes are in validate_node and
prepare_node, both of which complete long before anything pulls from Docker
Hub, so the export path exercises them directly and end to end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant