feat: an opt-in firewall derived from the manifests, cloud advanced only (0041 step 3) #59

Merged
marcos-mendez merged 8 commits from feat/manifest-firewall into main 2026-09-30 15:54:33 -03:00
marcos-mendez commented 2026-09-30 15:06:20 -03:00 (Migrated from github.com)

The firewall part of step 3 of handbook decision 0041, split from #58 and stacked on it: until #58 merges, this diff also shows its three commits; the firewall is the last one.

Scoped by the maintainer on 2026-09-30: optional and for cloud advanced only. firewall.enabled: true is refused unless installation.mode: cloud_advanced (and an appliance is named); absent is off, and in the simple and cloud simple modes keel installs and changes no firewall rule.

Why nftables

trixie's iptables is the nftables backend by default, and TurnKey 19 switches the alternative to iptables-legacy only for Webmin's firewall module (its conf/turnkey.d/webmin-fw says "TODO: drop use of iptables-legacy and use nftables directly"); 0041 replaces those WEBMIN_FW_TCP_INCOMING rules. nftables gives keel one table of its own, inet keel, replaced in one transaction, touching no iptables rule, no CrowdSec bouncer table and nothing of the operator's, and nft -c checks a file before it is loaded. Legacy iptables and nftables are both evaluated at the input hook, so while an image still loads TurnKey's rules.v4 keel's table can only narrow what gets in.

The ruleset and its guards

  • Public ports of the appliance's processes and of enabled overlays on every interface; mesh ports on the WireGuard interface only (none, with a note, without network.overlay.wireguard); the overlay's UDP port. Always: loopback, established and related, ICMPv6, IPv4 echo, DHCPv6 and DHCPv4 client replies. Policy drop on input only.
  • Refused, nothing written or loaded, unless every port sshd is configured on (Port, ListenAddress, sshd_config.d; 22 by default) is opened.
  • Left exactly as it is while a network change waits for keel network confirm (0018).
  • nft -c -f, then nft -f; a file keel did not write is never replaced.
  • keel-firewall.service (shipped enabled, After=nftables.service, Before=network-pre.target) reloads it at boot only while the file exists. enabled: false removes keel's file and table.
  • diff: derived.firewall (the file whole) and derived.firewall.loaded (the digest in the kernel table's comment, live only).

Real test

In the s3-test container (TurnKey B4, legacy iptables policy ACCEPT), reached from the VM host over SSH:

  • firewall.enabled: true in simple: spec validate exit 3 with the reason.
  • cloud_advanced: applied over an SSH session that kept running ("the session that ran apply is alive"). From the host after the change: new SSH ok, HTTP 80 (Apache, not in Core's manifest) dropped, was 200 before. Second apply 0 change(s); diff derived.firewall and .loaded same.
  • Port 2222 added to sshd_config.d: refused, "the ruleset would close SSH", nothing written.
  • A pending network marker: firewall left as it was (table stayed absent); after the marker was gone, loaded again.
  • Container restarted: keel-firewall.service active, table loaded, diff clean, SSH ok, HTTP dropped.
  • enabled: false over SSH: file and table removed, second apply unchanged (off), diff clean, HTTP 200 again.

Test plan

  • set -o pipefail; coverage run -m pytest tests: 2209 passed, coverage 99 percent, new modules 100
  • every rendered ruleset given to nft -c (1.1.3) in unshare -rn
  • the real test above
  • CI green
The firewall part of step 3 of handbook decision 0041, split from #58 and stacked on it: until #58 merges, this diff also shows its three commits; the firewall is the last one. Scoped by the maintainer on 2026-09-30: optional and for cloud advanced only. `firewall.enabled: true` is refused unless `installation.mode: cloud_advanced` (and an appliance is named); absent is off, and in the simple and cloud simple modes keel installs and changes no firewall rule. ## Why nftables trixie's `iptables` is the nftables backend by default, and TurnKey 19 switches the alternative to `iptables-legacy` only for Webmin's firewall module (its `conf/turnkey.d/webmin-fw` says "TODO: drop use of iptables-legacy and use nftables directly"); 0041 replaces those `WEBMIN_FW_TCP_INCOMING` rules. nftables gives keel one table of its own, `inet keel`, replaced in one transaction, touching no iptables rule, no CrowdSec bouncer table and nothing of the operator's, and `nft -c` checks a file before it is loaded. Legacy iptables and nftables are both evaluated at the input hook, so while an image still loads TurnKey's `rules.v4` keel's table can only narrow what gets in. ## The ruleset and its guards - Public ports of the appliance's processes and of enabled overlays on every interface; mesh ports on the WireGuard interface only (none, with a note, without `network.overlay.wireguard`); the overlay's UDP port. Always: loopback, established and related, ICMPv6, IPv4 echo, DHCPv6 and DHCPv4 client replies. Policy drop on input only. - Refused, nothing written or loaded, unless every port sshd is configured on (`Port`, `ListenAddress`, sshd_config.d; 22 by default) is opened. - Left exactly as it is while a network change waits for `keel network confirm` (0018). - `nft -c -f`, then `nft -f`; a file keel did not write is never replaced. - `keel-firewall.service` (shipped enabled, `After=nftables.service`, `Before=network-pre.target`) reloads it at boot only while the file exists. `enabled: false` removes keel's file and table. - `diff`: `derived.firewall` (the file whole) and `derived.firewall.loaded` (the digest in the kernel table's comment, live only). ## Real test In the s3-test container (TurnKey B4, legacy iptables policy ACCEPT), reached from the VM host over SSH: - `firewall.enabled: true` in `simple`: `spec validate` exit 3 with the reason. - `cloud_advanced`: applied over an SSH session that kept running ("the session that ran apply is alive"). From the host after the change: new SSH ok, HTTP 80 (Apache, not in Core's manifest) dropped, was 200 before. Second apply `0 change(s)`; diff `derived.firewall` and `.loaded` same. - `Port 2222` added to sshd_config.d: refused, "the ruleset would close SSH", nothing written. - A pending network marker: firewall left as it was (table stayed absent); after the marker was gone, loaded again. - Container restarted: `keel-firewall.service` active, table loaded, diff clean, SSH ok, HTTP dropped. - `enabled: false` over SSH: file and table removed, second apply `unchanged (off)`, diff clean, HTTP 200 again. ## Test plan - [x] `set -o pipefail; coverage run -m pytest tests`: 2209 passed, coverage 99 percent, new modules 100 - [x] every rendered ruleset given to `nft -c` (1.1.3) in `unshare -rn` - [x] the real test above - [ ] CI green
Sign in to join this conversation.
No description provided.