nftables is the successor to iptables and the default packet-filtering framework on Debian 10+, Ubuntu 20.10+, RHEL 8+ and their derivatives. It replaces separate iptables, ip6tables, arptables and ebtables tools with one command, nft, and one syntax — with built-in sets, maps and atomic rule replacement.
Concepts
- Table — a container for chains, with a family:
ip(IPv4),ip6,inet(both),arp,bridge,netdev. Useinetfor server firewalls to cover IPv4 and IPv6 at once. - Chain — a list of rules. Base chains hook into the kernel (
input,forward,output,prerouting,postrouting) with a priority and a default policy. - Rule — match expressions plus a verdict:
accept,drop,reject,jump,log,counter…
Nothing exists by default — no predefined tables or chains like in iptables.
Look at the current ruleset
sudo nft list ruleset
If you use UFW, firewalld or Docker, you’ll see their tables here too.
A complete server firewall
Write the ruleset to /etc/nftables.conf — the file loaded at boot on Debian/Ubuntu (on RHEL-family systems, /etc/sysconfig/nftables.conf):
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set allowed_ssh {
type ipv4_addr
flags interval
elements = { 198.51.100.25, 10.8.0.0/24 }
}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
# ICMP / ICMPv6 (needed for IPv6 to work properly)
ip protocol icmp icmp type echo-request limit rate 10/second accept
meta l4proto ipv6-icmp accept
# SSH only from allowed addresses
tcp dport 22 ip saddr @allowed_ssh accept
# Web
tcp dport { 80, 443 } accept
# WireGuard
udp dport 51820 accept
# Log the rest (rate-limited), then the policy drops it
limit rate 5/minute log prefix "nft-drop: "
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
flush ruleset removes everything, including rules created by Docker or other tools. On a Docker host, flush only your own table instead (delete table inet filter before redefining it), and restart Docker afterwards if you’ve already wiped its rules.
Check syntax, then load:
sudo nft -c -f /etc/nftables.conf # check only
sudo nft -f /etc/nftables.conf # apply atomically
sudo systemctl enable --now nftables # load at boot
Loading is atomic: the whole file applies or nothing does — no half-applied firewall.
Safety net
Keep the old ruleset and schedule a rollback before applying remotely:
sudo nft list ruleset > /root/nft-backup.conf
echo "nft flush ruleset; nft -f /root/nft-backup.conf" | sudo at now + 5 minutes
Cancel the job once you’ve verified you still have access. Your VPS’s VNC console is the last-resort way in.
Working with live rules
sudo nft add rule inet filter input tcp dport 8080 accept
sudo nft insert rule inet filter input tcp dport 9000 drop # at the top of the chain
sudo nft -a list chain inet filter input # show rule handles
sudo nft delete rule inet filter input handle 12
Live changes are lost on reboot unless you also update /etc/nftables.conf. A good habit: edit the file and reload it, rather than adding rules interactively.
Sets: manage IP lists efficiently
Sets are matched in constant time, so a blocklist of thousands of addresses costs almost nothing:
sudo nft add set inet filter blocklist '{ type ipv4_addr; flags interval, timeout; }'
sudo nft insert rule inet filter input ip saddr @blocklist drop
sudo nft add element inet filter blocklist '{ 192.0.2.66, 203.0.113.0/24 }'
sudo nft add element inet filter blocklist '{ 198.51.100.77 timeout 1h }'
sudo nft list set inet filter blocklist
Fail2Ban and CrowdSec can manage nftables sets for you.
Rate limiting new connections
tcp dport 22 ct state new limit rate over 10/minute drop
Per-source limits use a dynamic set (meter):
tcp dport 22 ct state new add @ssh_meter { ip saddr limit rate over 5/minute } drop
with set ssh_meter { type ipv4_addr; flags dynamic, timeout; timeout 5m; } defined in the table.
NAT
Masquerade a VPN subnet out of the public interface:
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.8.0.0/24 oifname "eth0" masquerade
}
}
And allow forwarding in the forward chain (iifname "wg0" accept plus established/related). Enable net.ipv4.ip_forward=1.
Port forwarding:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iifname "eth0" tcp dport 8080 dnat to 10.0.0.20:80
}
}
Counters
Add counter to any rule to see how many packets and bytes matched:
tcp dport { 80, 443 } counter accept
sudo nft list chain inet filter input then shows the counts — handy to confirm a rule is actually hit.
Translating from iptables
iptables-translate -A INPUT -p tcp --dport 22 -s 198.51.100.25 -j ACCEPT
# nft 'add rule ip filter INPUT ip saddr 198.51.100.25 tcp dport 22 counter accept'
iptables-save > rules.txt && iptables-restore-translate -f rules.txt
nftables, UFW and firewalld
UFW and firewalld are friendlier front-ends that (on current systems) produce nftables rules. Use one approach per server: either a front-end, or your own nftables.conf. Mixing them leads to rules that are hard to reason about.
Related: iptables tutorial, UFW, firewalld and open a port on Linux.