fix(firewall): clean up legacy DOCKER-USER rules; test the script with a simulated iptables #230

Merged
jfraeys merged 1 commit from jfraeys/fix/firewall-legacy-rule-cleanup into main 2026-10-16 15:16:15 -04:00
Owner

After the OVH rehearsal the old and new nodes' DOCKER-USER chains differed: the old one carried tailnet-to-443 ACCEPT rules (IPv4 and IPv6) and 2222 DROP/ACCEPT rules (IPv6). Packet counters on the old node were 0 after 8 weeks for all of them, and the script's own comment says 2222 matches never match (DNAT rewrites the port first). The script removed other known leftovers but not these, so a host could differ from what the script defines. Adds the removal for both families.

New test scripts/tests/test-docker-user-firewall.sh (in lint.yml): renders the template and runs it against a simulated iptables/ip6tables seeded with those leftovers. Checks: leftovers removed; for ports 22/25/17170 on docker0 and br+ the tailnet ACCEPT sits above the DROP; Cloudflare 443 allowed and the web ports dropped for everyone else; conntrack first and RETURN last; a second run changes nothing; restrict_web_to_cloudflare=false adds no 80/443 DROP. Run against the template without the cleanup, the test fails (2 checks), so it detects the problem.

Not covered: real iptables behavior (the simulation matches rules by parsed options, not by the kernel); the molecule test in roles/firewall/molecule still applies. Effect on production: removes rules with 0 packets; the next firewall deploy to the old node does it. A declarative rebuild of the whole chain (iptables-restore --noflush) would remove the whole class of drift; not done here because it needs a Linux test host.

roles/ is a staged path: staging, promotion; deploy with ./infra deploy --tags firewall --limit services.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ

After the OVH rehearsal the old and new nodes' DOCKER-USER chains differed: the old one carried tailnet-to-443 ACCEPT rules (IPv4 and IPv6) and 2222 DROP/ACCEPT rules (IPv6). Packet counters on the old node were 0 after 8 weeks for all of them, and the script's own comment says 2222 matches never match (DNAT rewrites the port first). The script removed other known leftovers but not these, so a host could differ from what the script defines. Adds the removal for both families. New test scripts/tests/test-docker-user-firewall.sh (in lint.yml): renders the template and runs it against a simulated iptables/ip6tables seeded with those leftovers. Checks: leftovers removed; for ports 22/25/17170 on docker0 and br+ the tailnet ACCEPT sits above the DROP; Cloudflare 443 allowed and the web ports dropped for everyone else; conntrack first and RETURN last; a second run changes nothing; restrict_web_to_cloudflare=false adds no 80/443 DROP. Run against the template without the cleanup, the test fails (2 checks), so it detects the problem. Not covered: real iptables behavior (the simulation matches rules by parsed options, not by the kernel); the molecule test in roles/firewall/molecule still applies. Effect on production: removes rules with 0 packets; the next firewall deploy to the old node does it. A declarative rebuild of the whole chain (iptables-restore --noflush) would remove the whole class of drift; not done here because it needs a Linux test host. roles/ is a staged path: staging, promotion; deploy with ./infra deploy --tags firewall --limit services. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
fix(firewall): remove the legacy tailnet-443 and 2222 DOCKER-USER rules; test with a simulated iptables
All checks were successful
Lint / lint (pull_request) Successful in 3m16s
Secret scan / scan (pull_request) Successful in 16s
ba8e410a46
The old production node carried tailnet-to-443 ACCEPT rules and 2222 DROP/ACCEPT rules from earlier versions of the script
(0 packets after 8 weeks). The script only removed other known leftovers, so a host's chain could differ from what the
script defines. Adds the removal for IPv4 and IPv6.
test-docker-user-firewall.sh renders the template and runs it against a simulated iptables/ip6tables: leftovers removed,
every tailnet-only port has its ACCEPTs above the DROP, conntrack first and RETURN last, idempotent, and the
restrict_web_to_cloudflare=false case. Checked that the test fails against the template without the cleanup.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ge5tovcwdEUBtPwUktsZZZ
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!230
No description provided.