nftables on Debian: a working configuration
Finally moved my servers over from iptables to nftables. Debian 12 made nftables the default, so iptables is now a compatibility shim anyway. The syntax is different enough that there was a learning curve, but the underlying model is cleaner once it clicks.
These are the concepts I needed to understand and a config that actually works.
How it differs from iptables
The big conceptual shift: iptables had separate commands for IPv4 (iptables)
and IPv6 (ip6tables), and rules were spread across built-in tables with
fixed chains. nftables lets you define your own tables and chains, and the
inet address family covers both IPv4 and IPv6 in a single ruleset.
Also: nft rule changes are atomic. No more partial updates if something fails halfway.
Key concepts
Tables
A table is a container for chains. It has an address family: ip (IPv4 only),
ip6 (IPv6 only), inet (both), arp, bridge,
or netdev. For a server firewall, inet is almost always what
you want.
Chains
Chains are where rules live. There are two kinds:
- Base chains — attached to a netfilter hook (input, output, forward, prerouting, postrouting). Packets flow through these automatically.
- Regular chains — not attached to any hook. Only reached via a
jumporgotofrom another chain.
Each base chain has a type, a hook, a priority,
and a default policy. Priority determines order when multiple chains attach
to the same hook — lower number runs first.
Rules
Rules are evaluated top to bottom within a chain. A rule that matches and has a
terminal verdict (accept, drop, reject) ends
processing for that packet. If no rule matches, the chain's default policy applies.
A working server configuration
This is what I run on servers that need to accept SSH, HTTP, and HTTPS, with everything
else dropped inbound. Using the inet family so one ruleset handles both
IPv4 and IPv6.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# loopback
iif lo accept
# established and related connections
ct state established,related accept
# drop invalid packets early
ct state invalid drop
# ICMP
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# services
tcp dport 22 accept # SSH
tcp dport 80 accept # HTTP
tcp dport 443 accept # HTTPS
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
The flush ruleset at the top wipes all existing rules before loading
the new ones. Important to know if you have other services (like fail2ban) that manage
their own nftables rules — they'll need to re-add them after a reload.
at can schedule a rule flush as a safety net.
Common nft commands
| Command | What it does |
|---|---|
nft list ruleset | Print all tables, chains, and rules |
nft list chains | List chains only (no rules) |
nft flush ruleset | Remove everything — blank slate |
nft -f /etc/nftables.conf | Load rules from file |
nft -c -f /etc/nftables.conf | Check file for errors (dry run) |
nft list table inet filter | Print one specific table |
nft add rule inet filter input tcp dport 8080 accept | Add a rule live |
Connection tracking states
The ct state matcher is one of the most useful things in nftables.
States I actually use:
new— first packet of a new connectionestablished— part of a connection already seen in both directionsrelated— related to an existing connection (e.g. FTP data, ICMP errors)invalid— doesn't match any known connection; usually safe to drop
Enabling on boot
Debian's nftables service reads /etc/nftables.conf on start:
systemctl enable nftables
systemctl start nftables
After editing the config file, reload with systemctl reload nftables
or nft -f /etc/nftables.conf directly.