Linux hosts

nftables default-drop firewall, the secure way

Somebody started a debug server on port 8080 "just for an hour" in March. It is still there, and the whole internet has been welcome to it. A firewall that drops by default would have turned that mistake into a non-event.

The short answer

Use one inet table with an input chain whose policy is drop. Accept established and related traffic, loopback, the ICMP types the network needs, SSH only from an admin address set, and the public service ports. Drop the forward chain too. Check with nft -c, load with nft -f, and probe the ports from another machine.

Updated Houssam Hammoudi, CTOTested with nftables 1.0.6 (Debian 12)

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

What goes wrong

A Linux host with no firewall accepts a connection on every port where some program listens. That includes the database bound to 0.0.0.0 by mistake, the metrics port, and the debug server someone forgot.

An "allow list" of drop rules does not fix it. You block the ports you know about, and the next new listener is open by default. The safe shape is the reverse: drop everything inbound, then accept the few things this host exists to serve.

The second common problem is a ruleset that works until reboot. Rules typed at the prompt live in the kernel only. After a restart, the host is open again.

What the docs say

NOTE: If no policy is explicitly selected, the default policy accept will be used.

Source: nftables wiki, Configuring chains

Hence, an accept verdict - be it by way of a rule or the default chain policy - isn't necessarily final.

Source: nftables wiki, Configuring chains

drops take immediate effect, with no further rules or chains being evaluated.

Source: nftables wiki, Configuring chains

So a chain you forget to give a policy accepts everything, and an accept in your table can still be dropped by another table on the same hook (Docker, firewalld, Kubernetes add their own). The wiki explains the verdicts well. It does not warn that flush ruleset at the top of a file also deletes those other tools' tables.

The secure configuration

conf
#!/usr/sbin/nft -f
# /etc/nftables.conf: default-drop inbound for a server that offers SSH and HTTPS.
flush ruleset

table inet filter {
	set admin_v4 {
		type ipv4_addr
		flags interval
		# Networks allowed to reach SSH. Replace with your own.
		# 172.30.99.0/24 is the client network of the test.
		elements = { 192.0.2.0/24, 172.30.99.0/24 }
	}

	chain input {
		type filter hook input priority filter; policy drop;

		# Replies to connections this host opened, and their related ICMP errors.
		ct state established,related accept
		# Packets that match no known connection state are never valid.
		ct state invalid drop
		# Loopback is local-only traffic.
		iif "lo" accept
		# ICMP and ICMPv6 carry path MTU and neighbor discovery; IPv6 breaks without them.
		meta l4proto icmp icmp type { echo-request, destination-unreachable, time-exceeded } limit rate 10/second accept
		meta l4proto ipv6-icmp accept
		# SSH only from the admin networks, with a rate limit on new connections.
		tcp dport 22 ip saddr @admin_v4 ct state new limit rate 15/minute accept
		# The public service.
		tcp dport 443 accept
		# Everything else: count it, log a sample, and fall through to the drop policy.
		limit rate 5/minute log prefix "nft-drop: " level info
		counter comment "dropped by policy"
	}

	chain forward {
		# This host is not a router.
		type filter hook forward priority filter; policy drop;
	}

	chain output {
		type filter hook output priority filter; policy accept;
	}
}

Load it in a way that cannot lock you out, then make it survive reboots:

bash
nft -c -f /etc/nftables.conf          # check only; loads nothing
# Safety net for a remote host: flush the rules in 5 minutes unless you cancel.
echo "nft flush ruleset" | at now + 5 minutes
nft -f /etc/nftables.conf             # atomic: the whole file applies, or none of it
# Open a NEW ssh session. If it works, cancel the safety net (atq, atrm <job>).
systemctl enable --now nftables       # Debian, Ubuntu: loads /etc/nftables.conf at boot

If you need SSH over IPv6, add a matching admin_v6 set with type ipv6_addr and a rule with ip6 saddr @admin_v6. On Fedora, RHEL and Rocky, firewalld also writes nftables rules; use one tool, not both.

Prove it

Before the ruleset, every listener answers, including the forgotten one on 8080:

bash
nft list tables
nc -z -w 2 172.30.99.10 22; nc -z -w 2 172.30.99.10 443; nc -z -w 2 172.30.99.10 8080   # from the client
text
table ip nat
port 22: open
port 443: open
port 8080: open

Check, load, and probe again from the client:

bash
nft -c -f /etc/nftables.conf && echo "nft -c: OK"
nft -f /etc/nftables.conf
text
nft -c: OK
port 22: open
port 443: open
port 8080: no answer

Remove the client's network from admin_v4, and SSH stops answering too:

bash
nft flush set inet filter admin_v4
nft add element inet filter admin_v4 '{ 192.0.2.0/24 }'
text
port 22: no answer

The counter shows what the policy dropped:

bash
nft list chain inet filter input
text
table inet filter {
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept
		ct state invalid drop
		iif "lo" accept
		icmp type { destination-unreachable, echo-request, time-exceeded } limit rate 10/second accept
		meta l4proto ipv6-icmp accept
		tcp dport 22 ip saddr @admin_v4 ct state new limit rate 15/minute accept
		tcp dport 443 accept
		limit rate 5/minute log prefix "nft-drop: " level info
		counter packets 4 bytes 240 comment "dropped by policy"
	}
}

The table ip nat in the first output belongs to Docker inside the test container. The flush ruleset line deleted it, which is the next section's first mistake. The test is secure-tests/nftables-default-drop-firewall/run.sh.

Mistakes people make

flush ruleset on a container host

flush ruleset removes every table, including the ones Docker, Podman, Kubernetes or firewalld created. Containers lose their NAT until those tools restart. On such hosts, replace the first line with table inet filter followed by delete table inet filter, so the file only replaces its own table.

Forgetting the policy

A base chain without policy drop; accepts everything that no rule dropped. Read the first line of each chain in nft list ruleset, not your file.

Dropping all ICMP

Path MTU discovery uses ICMP "fragmentation needed" messages, and IPv6 neighbor discovery is ICMPv6. Drop them and connections hang in ways that look like application bugs. Accept the types above; rate-limit echo requests if you like.

Opening SSH to the world "for now"

Put admin networks in a set and allow SSH from the set only. Adding an address is one nft add element command, so there is no reason for 0.0.0.0/0.

Rules that vanish at reboot

Typing nft add rule changes the running kernel only. Keep the ruleset in /etc/nftables.conf and enable the service, then reboot a test host once to prove it.

Checklist

  • /etc/nftables.conf has an input chain with policy drop;.
  • The forward chain has policy drop; unless the host routes traffic.
  • ct state established,related accept is the first rule.
  • SSH is accepted only from an admin address set.
  • Only the ports this host serves are accepted.
  • ICMP types for errors and ICMPv6 are accepted.
  • nft -c -f /etc/nftables.conf passes before every load.
  • The nftables service is enabled, and the ruleset survived a test reboot.
  • A probe from another machine shows only the expected ports open.
  • On container hosts, the file does not use flush ruleset.

A default-drop firewall is the colleague who says "no" first and asks what you meant second. On a server, that is exactly the colleague you want.

FND

Learn it on a live range

Linux 3: securing Linux, in Foundation: a real host in your browser, and every objective checked on the machine.

Start free

The Secure Way

More on linux hosts

SSH, sudo, firewalls, mount options, updates and the host basics every engineer should get right.

All linux hosts guides