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.
On this page
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
#!/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:
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 bootIf 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:
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 clienttable ip nat
port 22: open
port 443: open
port 8080: openCheck, load, and probe again from the client:
nft -c -f /etc/nftables.conf && echo "nft -c: OK"
nft -f /etc/nftables.confnft -c: OK
port 22: open
port 443: open
port 8080: no answerRemove the client's network from admin_v4, and SSH stops answering too:
nft flush set inet filter admin_v4
nft add element inet filter admin_v4 '{ 192.0.2.0/24 }'port 22: no answerThe counter shows what the policy dropped:
nft list chain inet filter inputtable 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.confhas an input chain withpolicy drop;.- The forward chain has
policy drop;unless the host routes traffic. ct state established,related acceptis 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.confpasses before every load.- The
nftablesservice 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 freeThe 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