No unattended security updates and no firewall ruleset #15

Closed
opened 2026-08-27 23:22:26 +00:00 by tiagoagueda · 1 comment
Owner

unattended-upgrades is not installed, and nft is present with an empty ruleset.
Only port 22 listens today, but nothing prevents that changing as services are added.

⚠️ If unattended-upgrades is enabled, keep Automatic-Reboot off until the SPL hang issue
is resolved — an automatic reboot on a board that sometimes does not come back is a worse
failure than a late patch.

Done when

  • unattended-upgrades installed, security-only, no automatic reboot
  • a minimal nftables ruleset: SSH from the LAN, drop the rest
`unattended-upgrades` is not installed, and `nft` is present with an **empty ruleset**. Only port 22 listens today, but nothing prevents that changing as services are added. ⚠️ If unattended-upgrades is enabled, keep `Automatic-Reboot` **off** until the SPL hang issue is resolved — an automatic reboot on a board that sometimes does not come back is a worse failure than a late patch. **Done when** - [x] `unattended-upgrades` installed, security-only, no automatic reboot - [x] a minimal nftables ruleset: SSH from the LAN, drop the rest
Author
Owner

Done — and the issue was understating it

Both boxes met, but the premise needed correcting first.

nft is present with an empty ruleset — it was worse than that

nftables was installed and nft ran, so an empty ruleset looked like the whole problem. It wasn't. This kernel was built with CONFIG_NETFILTER switched off entirely:

# systemctl start nftables
src/mnl.c:64: Unable to initialize Netlink socket: Protocol not supported

No firewall of any kind was possible. The package being installed made it look one command away; it was a kernel rebuild away. That is the third time today a capability looked present in userspace and was absent in the kernel, after zram (#13) and the module tree (#54).

IPv6 was enabled at the same time, which changes the stakes

The board now holds two global IPv6 addresses. That matters more than it sounds: with IPv4 only it sat behind NAT and was unreachable from outside by construction. A routed prefix has no such property — every address is globally routable, and the ruleset becomes the only thing between the internet and sshd, which listens on ::.

So ssh over v6 is allowed from link-local and ULA only, never global. If the ISP routes a prefix here, a global rule would silently publish this board's sshd to the internet and look identical from inside. Local access is unaffected; ouranos reaches the board over IPv4.

NF_TABLES_INET depends on IPV6, so the first pass had to use an IPv4-only table ip. With IPv6 on, the ruleset is table inet and covers both stacks.

The ruleset

Default-drop input, drop forward, accept output. Beyond ssh and mDNS, two rules exist because their absence is expensive and looks like something else entirely:

  • ICMPv6 accepted by type. Neighbour discovery replaces ARP and router advertisements are how the board gets an address, so dropping ICMPv6 does not degrade IPv6 — it stops it. This is the usual way a hand-written v6 ruleset kills the host it protects.
  • DHCP replies (udp 67→68, and 546). They arrive as connectionless broadcast, so conntrack does not see them as related to the request. Without an explicit rule the lease fails to renew and the board falls off the LAN hours later, looking nothing like a firewall problem. Both eth0 and wlan0 hold leases.

The subnet is written as RFC1918 generally rather than this lab's /24 — behind NAT that blocks nothing that could otherwise reach the board, and it keeps the addressing out of a public repo (#6).

unattended-upgrades

Security origin only, Automatic-Reboot "false", and Remove-Unused-Kernel-Packages "false" since the running kernel comes from deploy-kernel.sh rather than apt. The reboot restriction is written into the config file with its reasoning, so nobody enables it later without meeting the argument — #3 records this board failing to return from an ordinary reboot, and #5 records that there is no remote power control.

Allowed origins are: origin=Debian,codename=trixie-security,label=Debian-Security

One mistake worth recording

Applying provision.sh afterwards silently replaced the IPv6 ruleset with the earlier IPv4-only one, and reported failures: 0. The staged copy was stale, and every check I had written passed on the old file too — it also has hook input, tcp dport 22 accept and udp dport 68 accept. Nothing reloads nftables on install, so it would have surfaced only at the next reboot, as a firewall quietly missing every IPv6 rule.

Checks are now specific enough to fail on the previous version. A check that passes on the thing it is meant to reject is worse than no check, because it converts "I did not look" into "I looked and it was fine".

Verified after a reboot, on both media

nftables active, policy drop, 2 ip6 rules, icmpv6 by type, 2 global IPv6 addresses, ssh/ping/DNS/apt all working, 0 failed units, drop counter showing only stray broadcast. The ruleset was applied behind a 180-second auto-rollback each time, because a wrong rule here costs the only access path.

Done when

  • unattended-upgrades installed, security-only, no automatic reboot
  • a minimal nftables ruleset: SSH from the LAN, drop the rest
## Done — and the issue was understating it Both boxes met, but the premise needed correcting first. ### `nft is present with an empty ruleset` — it was worse than that nftables was installed and `nft` ran, so an empty ruleset looked like the whole problem. It wasn't. This kernel was built with **`CONFIG_NETFILTER` switched off entirely**: ``` # systemctl start nftables src/mnl.c:64: Unable to initialize Netlink socket: Protocol not supported ``` No firewall of any kind was possible. The package being installed made it look one command away; it was a kernel rebuild away. That is the third time today a capability looked present in userspace and was absent in the kernel, after zram (#13) and the module tree (#54). ### IPv6 was enabled at the same time, which changes the stakes The board now holds **two global IPv6 addresses**. That matters more than it sounds: with IPv4 only it sat behind NAT and was unreachable from outside *by construction*. A routed prefix has no such property — every address is globally routable, and the ruleset becomes the only thing between the internet and sshd, which listens on `::`. So **ssh over v6 is allowed from link-local and ULA only, never global**. If the ISP routes a prefix here, a global rule would silently publish this board's sshd to the internet and look identical from inside. Local access is unaffected; ouranos reaches the board over IPv4. `NF_TABLES_INET` depends on `IPV6`, so the first pass had to use an IPv4-only `table ip`. With IPv6 on, the ruleset is `table inet` and covers both stacks. ### The ruleset Default-drop input, drop forward, accept output. Beyond ssh and mDNS, two rules exist because their absence is expensive and looks like something else entirely: - **ICMPv6 accepted by type.** Neighbour discovery replaces ARP and router advertisements are how the board gets an address, so dropping ICMPv6 does not degrade IPv6 — it stops it. This is the usual way a hand-written v6 ruleset kills the host it protects. - **DHCP replies (udp 67→68, and 546).** They arrive as connectionless broadcast, so conntrack does not see them as related to the request. Without an explicit rule the lease fails to renew and the board falls off the LAN hours later, looking nothing like a firewall problem. Both eth0 and wlan0 hold leases. The subnet is written as RFC1918 generally rather than this lab's /24 — behind NAT that blocks nothing that could otherwise reach the board, and it keeps the addressing out of a public repo (#6). ### unattended-upgrades Security origin only, `Automatic-Reboot "false"`, and `Remove-Unused-Kernel-Packages "false"` since the running kernel comes from `deploy-kernel.sh` rather than apt. The reboot restriction is written into the config file with its reasoning, so nobody enables it later without meeting the argument — #3 records this board failing to return from an ordinary reboot, and #5 records that there is no remote power control. ``` Allowed origins are: origin=Debian,codename=trixie-security,label=Debian-Security ``` ### One mistake worth recording Applying `provision.sh` afterwards **silently replaced the IPv6 ruleset with the earlier IPv4-only one, and reported `failures: 0`**. The staged copy was stale, and every check I had written passed on the old file too — it also has `hook input`, `tcp dport 22 accept` and `udp dport 68 accept`. Nothing reloads nftables on install, so it would have surfaced only at the next reboot, as a firewall quietly missing every IPv6 rule. Checks are now specific enough to fail on the previous version. **A check that passes on the thing it is meant to reject is worse than no check**, because it converts "I did not look" into "I looked and it was fine". ### Verified after a reboot, on both media `nftables active`, `policy drop`, 2 ip6 rules, icmpv6 by type, 2 global IPv6 addresses, ssh/ping/DNS/apt all working, 0 failed units, drop counter showing only stray broadcast. The ruleset was applied behind a 180-second auto-rollback each time, because a wrong rule here costs the only access path. **Done when** - [x] `unattended-upgrades` installed, security-only, no automatic reboot - [x] a minimal nftables ruleset: SSH from the LAN, drop the rest
Sign in to join this conversation.
No description provided.