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. Use inet for 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.

  • nftables
  • Firewall
  • Linux