nftables is the current packet filtering framework of the Linux kernel and the successor to iptables, ip6tables, arptables and ebtables. A single nft command manages IPv4 and IPv6 together, rules can match against sets of addresses or ports in one lookup, and a whole ruleset is loaded atomically from one file. In this tutorial you will write a dual-stack firewall for a web server in /etc/nftables.conf on Ubuntu 24.04, apply it with an automatic rollback, manage it at runtime with sets, add port forwarding and translate existing iptables rules.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Debian 12 and Rocky Linux 9 ship the same
nftables.service, although Rocky loads its configuration from/etc/sysconfig/nftables.confand uses firewalld by default. - A non-root user with
sudoprivileges. - Access to the server console from your provider's panel, in case a mistake blocks SSH.
Warninguse only one firewall manager. UFW and firewalld create their own nftables tables, and Docker manages its own rules. This guide disables UFW. On a Docker host, do not use
flush ruleset, since it would delete Docker's rules; keep your rules in your own table instead.
Key nftables concepts
| Concept | What it is |
|---|---|
| Family | The traffic a table sees: ip (IPv4), ip6 (IPv6), inet (both), plus arp, bridge and netdev |
| Table | A container for chains, sets and maps. Unlike iptables, there are no predefined tables |
| Base chain | A chain attached to a netfilter hook (input, forward, output, prerouting, postrouting) with a priority and a policy |
| Rule | A list of match expressions followed by a verdict such as accept or drop |
| Set | A named collection of addresses, ports or other values that rules can match in one lookup and that you can change at runtime |
Using the inet family means one table and one set of rules filter both IPv4 and IPv6, instead of maintaining iptables and ip6tables separately.
Step 1 - Installing nftables and disabling UFW
Install the package, which provides the nft command and the nftables.service unit that loads /etc/nftables.conf at boot:
sudo apt update
sudo apt install nftables
Check the version:
sudo nft --version
nftables v1.0.9 (Old Doc Yak #3)
If UFW is active, disable it so it does not add its own rules alongside yours:
sudo ufw disable
Look at what is loaded right now:
sudo nft list ruleset
On a clean server with UFW disabled the output is empty or contains only leftover tables. They will be removed when you load your own configuration.
Step 2 - Writing the firewall configuration
Keep the whole firewall in /etc/nftables.conf, the file nftables.service loads. Open it:
sudo nano /etc/nftables.conf
Replace its contents with this ruleset for a server that offers SSH, HTTP and HTTPS:
#!/usr/sbin/nft -f
flush ruleset
define SSH_PORT = 22
table inet filter {
set web_ports {
type inet_service
elements = { 80, 443 }
}
set blocklist_v4 {
type ipv4_addr
flags interval
}
set blocklist_v6 {
type ipv6_addr
flags interval
}
set ssh_ratelimit_v4 {
type ipv4_addr
flags dynamic
timeout 1m
}
set ssh_ratelimit_v6 {
type ipv6_addr
flags dynamic
timeout 1m
}
chain input {
type filter hook input priority filter; policy drop;
iif "lo" accept
ct state established,related accept
ct state invalid drop
ip saddr @blocklist_v4 drop
ip6 saddr @blocklist_v6 drop
# ICMPv6 is required for neighbour discovery and path MTU discovery
meta l4proto ipv6-icmp accept
icmp type echo-request limit rate 5/second burst 10 packets accept
# SSH: drop sources that open more than 10 new connections per minute
tcp dport $SSH_PORT ct state new add @ssh_ratelimit_v4 { ip saddr limit rate over 10/minute } drop
tcp dport $SSH_PORT ct state new add @ssh_ratelimit_v6 { ip6 saddr limit rate over 10/minute } drop
tcp dport $SSH_PORT ct state new accept
tcp dport @web_ports ct state new accept
udp dport 443 accept comment "HTTP/3 (QUIC)"
limit rate 5/minute burst 10 packets log prefix "nft-drop: " level info
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
How the ruleset works:
flush rulesetclears everything before loading, so running the file twice always gives the same result. The whole file is applied as one transaction: if any line has an error, nothing changes.definecreates a variable. ChangeSSH_PORTonce if SSH listens on another port.- Policies:
inputandforwarddrop by default,outputaccepts, so the server can still reach update mirrors, DNS and APIs. - Connection tracking: packets of established connections are accepted early, which keeps the rest of the chain short.
relatedalso admits ICMP errors that belong to existing connections. - Sets:
web_portsholds the ports to open, and the twoblocklistsets are empty for now. Theintervalflag lets them hold whole networks such as192.0.2.0/24. - SSH rate limiting: each new SSH connection adds the source address to a dynamic set with its own rate counter. Once a source exceeds 10 new connections per minute, further attempts are dropped. Entries expire after one minute of inactivity.
- Logging: the last rule logs packets that are about to hit the drop policy, limited so a flood cannot fill the logs.
Remove the UDP 443 rule if your web server does not use HTTP/3. Check the syntax without applying it:
sudo nft -c -f /etc/nftables.conf
The command prints nothing when the file is valid. Otherwise it points to the line and column of the error.
Step 3 - Applying the ruleset with a rollback timer
nft has no built-in confirmation step, so schedule a rollback before loading the new rules. This transient systemd timer flushes the ruleset in three minutes unless you cancel it, which reopens access if the new rules lock you out:
sudo systemd-run --on-active=3m --unit=nft-rollback /usr/sbin/nft flush ruleset
Running timer as unit: nft-rollback.timer
Will run service as unit: nft-rollback.service
Load the configuration:
sudo nft -f /etc/nftables.conf
Open a new SSH session from another terminal. Your current session is already established and does not prove the rules are correct. If the new session works, cancel the rollback:
sudo systemctl stop nft-rollback.timer
Now enable the service so the file is loaded at every boot:
sudo systemctl enable nftables.service
List the active ruleset to confirm it matches the file:
sudo nft list chain inet filter input
From another machine, check that a port you did not open is now filtered, which shows up as a timeout:
nc -zv -w 3 your_server_ip 3306
nc: connect to your_server_ip port 3306 (tcp) timed out: Operation now in progress
Step 4 - Managing sets and rules at runtime
Sets let you change what the firewall allows without touching the rules. Open an extra port for everyone:
sudo nft add element inet filter web_ports { 8443 }
Block an address or a whole network:
sudo nft add element inet filter blocklist_v4 { 198.51.100.66, 192.0.2.0/24 }
sudo nft add element inet filter blocklist_v6 { 2001:db8:bad::/48 }
Check the contents of a set and remove an entry:
sudo nft list set inet filter blocklist_v4
sudo nft delete element inet filter blocklist_v4 { 198.51.100.66 }
You can also see which addresses the SSH rate limiter is currently tracking:
sudo nft list set inet filter ssh_ratelimit_v4
To delete a single rule you need its handle. List the chain with -a:
sudo nft -a list chain inet filter input
table inet filter {
chain input { # handle 6
type filter hook input priority filter; policy drop;
iif "lo" accept # handle 12
ct state established,related accept # handle 13
...
udp dport 443 accept comment "HTTP/3 (QUIC)" # handle 23
sudo nft delete rule inet filter input handle 23
Importantchanges made with
nft addornft deleteare lost at the next reboot ornft -f. To keep them, add them to/etc/nftables.confas well, for example aselements = { ... }in the set definition, and check the file withsudo nft -c -f /etc/nftables.conf.
Step 5 - Reading logs and counters
Log entries from the last rule go to the kernel log:
sudo journalctl -k --grep 'nft-drop' -n 20
Sep 24 12:40:03 web01 kernel: nft-drop: IN=eth0 OUT= MAC=... SRC=192.0.2.200 DST=198.51.100.20 LEN=44 TOS=0x00 PREC=0x00 TTL=241 ID=50211 PROTO=TCP SPT=51544 DPT=23 WINDOW=1024 RES=0x00 SYN URGP=0
To see how much traffic a rule matches, add the counter keyword to it in the configuration file, for example:
tcp dport @web_ports ct state new counter accept
After reloading, sudo nft list chain inet filter input shows counter packets N bytes M next to that rule. To reset all counters, run sudo nft reset counters.
Step 6 - Forwarding a port to another host (optional)
nftables handles NAT in a table with nat type chains. This example forwards TCP port 8080 arriving on eth0 to a web server at 10.0.0.5:80 on a private network.
Enable IP forwarding in the kernel:
sudo nano /etc/sysctl.d/99-ip-forward.conf
net.ipv4.ip_forward = 1
sudo sysctl --system
In /etc/nftables.conf, replace the empty forward chain so forwarded traffic to the backend is allowed:
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
iif "eth0" ip daddr 10.0.0.5 tcp dport 80 ct state new accept
}
Then append a NAT table at the end of the file:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif "eth0" tcp dport 8080 dnat to 10.0.0.5:80
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip daddr 10.0.0.5 tcp dport 80 masquerade
}
}
The dnat rule rewrites the destination of incoming packets, and masquerade rewrites their source to the gateway's address so the backend's replies return through it. Check and reload, then test from outside with curl -I http://your_server_ip:8080:
sudo nft -c -f /etc/nftables.conf && sudo nft -f /etc/nftables.conf
Step 7 - Translating existing iptables rules
The iptables package includes translators that print the nftables equivalent of iptables syntax. Translate one rule:
iptables-translate -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
nft 'add rule ip filter INPUT tcp dport 22 ct state new counter accept'
Or a complete file saved with iptables-save:
sudo iptables-save > iptables-backup.rules
iptables-restore-translate -f iptables-backup.rules > translated.nft
Use the output as a starting point, not as a finished configuration. The translation keeps the iptables layout (separate ip and ip6 tables, one rule per port), and extensions without an nftables equivalent are left as comments. Rewriting the result with an inet table and sets, as in Step 2, gives you a shorter and faster ruleset.
Troubleshooting
- Locked out after loading rules: wait for the rollback timer, or log in through the provider's console and run
sudo nft flush ruleset, then fix the file. Error: Could not process rule: No such file or directory: the table, chain or set you reference does not exist. Check the names withsudo nft list ruleset.- Rules are gone after reboot:
nftables.serviceis not enabled, or the runtime changes were never written to/etc/nftables.conf. Runsystemctl is-enabled nftables. - IPv6 stops working: ICMPv6 is dropped. Keep
meta l4proto ipv6-icmp acceptbefore any rule that drops traffic. - UFW or Docker rules reappear: another tool manages the firewall too. Run
sudo nft list tablesto see who created which table, and keep one manager.
Conclusion
You wrote a dual-stack nftables firewall with sets, SSH rate limiting and logging, applied it with a rollback timer, managed it at runtime and added optional port forwarding. As next steps, keep /etc/nftables.conf under version control, connect Fail2ban to nftables with its nftables-multiport action, and move any remaining iptables scripts over with iptables-restore-translate.
