Repository navigation
Block the CVE-2026-98432 trigger at the host firewall - #19
Conversation
Until the boxes run a fixed kernel, an nft table of its own at prerouting/raw drops IPv4 TCP carrying IP options (the bug needs a SYN with them) and strips the MPTCP option from SYNs. Raw priority is ahead of conntrack and kube-proxy, so pods are covered as well as the host. It is loaded by its own oneshot unit, not nftables.service: Ubuntu's /etc/nftables.conf starts with `flush ruleset`, which would wipe the rules k3s, ufw and fail2ban keep in nf_tables. Loading it touches no other table and restarts nothing, so there is no needle warm-up. cve_2026_98432_mode starts at `count`, which only counts, so what `enforce` would drop can be seen before it drops anything. Enforce one box first through host_vars, then the rest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of c8d91b2I recommend changes before enforcement. I reviewed the actual diff and surrounding hardening, k3s, networking, and CI configuration. Two actionable issues stood out. [P2] Recover live rules when a previous reload was interruptedLocation: If a deployment writes the new ruleset but stops before the reload handler completes, the next deployment cannot converge:
An interrupted Fix: Read and compare live state before deciding whether to reload. Reload when the table is missing or differs from the desired configuration, then assert the result. Add an interrupted-transition recovery test. [P2] Multiple MPTCP options bypass the stripping ruleLocation:
Reproduced: With enforcement enabled, a downstream input-hook counter saw zero MPTCP-bearing packets after a single-option SYN, then one after a two-option SYN. This defeats the advertised MPTCP-disabling layer. It does not bypass the separate IPv4 IP-options drop rule, and I did not establish an MPTCP handshake in this test. Fix: Ensure no MPTCP option remains—for example, drop a packet if another matching option remains after stripping—and add a duplicate-option regression test. Security assumptions and validation
Validation performed: Rollout assessment: Count-first and single-host enforcement are sensible, but the current tests do not establish reboot persistence, systemd failure recovery, rollback convergence, or complete CNI coverage. The external SSH probe establishes one packet path only. Before enforcement, add tests for the two findings above and exercise reboot/rollback plus representative pod paths on the canary. Production hosts were not modified. |
Review of #19 found two problems, both reproduced here. The reload was a handler notified by the template tasks. A run that wrote the ruleset and died before loading it left the old mode live, and every later run found the file already right and never reloaded: an interrupted count -> enforce stayed counting, an interrupted rollback kept dropping. The reload is now decided from the kernel: the file is loaded into a throwaway network namespace and listed, and the table is reloaded when the live listing (counters blanked) differs or is missing. The handler is gone. The assert compares the same listings. `reset tcp option mptcp` blanks only the first matching option (nft_exthdr_tcp_strip_eval returns after one), so a SYN with two kept the second. Reproduced on sing-1's kernel in a throwaway namespace; in enforce mode a SYN still carrying an MPTCP option after the strip is now dropped. Same test after the fix: plain SYN arrives, one option arrives stripped, two options and IP options are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review. Both findings were real; I reproduced each and fixed them in a657246. 1. Multiple MPTCP options bypass the stripping ruleI confirmed it in the fleet's kernel: Fix: in enforce mode only, a third rule runs after the strip, 2. Live rules don't recover from an interrupted reloadFix: the handler is gone, and the reload is decided from what the kernel holds:
An interrupted count → enforce, or enforce → count, is now put right by the next run. A run that finds the table already correct doesn't reload, so the counters survive. Tests (sing-1, kernel 6.17.0-23, nft 1.0.9; throwaway namespaces only, host ruleset untouched)Packet test: a raw socket sends crafted IPv4 SYNs over loopback through the ruleset, and a counter table at the input hook records what arrives.
Reload decision: run on real listings with the role's exact expression. A live count table with non-zero counters, compared with the count file, gives no reload. Compared with the enforce file, it gives a reload. Also:
Your other points
The next steps are unchanged: merge in count mode, then enforce on sing-1 through |
Until the boxes run a kernel with the fix, a dedicated nft table,
inet cve_2026_98432, drops IPv4 TCP that carries IP options, which is the bug's trigger. It also strips the MPTCP option from SYNs. It hooks prerouting at raw priority, ahead of conntrack and kube-proxy, so pods are covered as well as the host.This PR only counts.
cve_2026_98432_mode: countmatches and counts but drops nothing, so merging it changes no traffic.enforcecomes in follow-up PRs: one box first throughhost_vars, then the group default.Why it can't disrupt anything
nfttransaction that touches no other table. k3s, ufw, fail2ban and the workloads keep running, so there's no needle warm-up.nftables.service. Ubuntu's/etc/nftables.confstarts withflush ruleset, which would wipe the rules k3s, ufw and fail2ban keep in nf_tables. A oneshot unit of its own,cve-2026-98432-filter.service, loads only this table, early in boot.nft -c, which hands the transaction to the kernel and aborts it.reset tcp optionstrips only the first match, so in enforce mode a SYN that still carries an MPTCP option after the strip is dropped.sudo systemctl stop cve-2026-98432-filterdeletes the table;offremoves the unit and files.Tested
counttable with non-zero counters compared with thecountfile gives no reload; compared with theenforcefile it gives a reload.countandenforce, loaded twice and listed on sing-1 inside a throwaway network namespace (unshare -n). The host's own ruleset was untouched.site.yml --check --diff, limited to the new tag, on all three boxes. Each box shows 3 changes and no failures.site.yml --checkon sing-1. On the box, the new files are the only change.--syntax-checkpasses for every playbook, and yamllint is clean on the changed files.Rollout
site.ymlrun. Leave it counting for about an hour and read the counters:sudo nft list table inet cve_2026_98432.cve_2026_98432_mode: enforceinhost_vars/sing-1. Prove it from outside withsudo nping --tcp -p 22 --ip-options R 5.199.165.77: a SYN-ACK before, none after, and the drop counter goes up. Don't use 443; only Cloudflare can reach it.enforceas the group default, for lith-1 and chic-1.🤖 Generated with Claude Code