Ubuntu 22.04 uses nftables by default — but most tutorials still show iptables. The iptables package on modern systems is just a wrapper over nftables via iptables-nft. Rules written in iptables are automatically translated to nftables syntax. Working with nftables directly is better: cleaner syntax, higher performance, and one table replaces iptables, ip6tables, arptables, and ebtables.
Check What the System Is Using
Confirm that nftables is active, not legacy iptables:
sudo iptables --version
If the output shows (nf_tables) — the system is already on nftables. If (legacy) — an explicit migration is needed.
View current nftables rules:
sudo nft list ruleset
On a fresh Ubuntu 22.04 the output will be empty or contain only UFW's base table if it is installed.
Architecture: Tables, Chains, Rules
In iptables there were several fixed tables: filter, nat, mangle. In nftables you create your own tables with any names and choose their address family.
Families:
ip— IPv4 onlyip6— IPv6 onlyinet— IPv4 and IPv6 simultaneously (most convenient for most tasks)arp,bridge,netdev— specialized
Chain types:
type filter hook input— incoming traffictype filter hook forward— transittype filter hook output— outgoingtype nat hook prerouting— DNAT before routingtype nat hook postrouting— SNAT after routing
Installation and First Run
nftables is already installed on Ubuntu 22.04. Check and enable:
sudo apt install nftables
sudo systemctl enable --now nftables
Configuration file:
sudo nano /etc/nftables.conf
Apply after changes:
sudo nft -f /etc/nftables.conf
Check syntax without applying:
sudo nft -c -f /etc/nftables.conf
Basic Stateful Firewall for VPS
A minimal working configuration that allows SSH, HTTP, HTTPS and blocks everything else:
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
ct state invalid drop
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
Key decisions explained:
iif lo accept — allow loopback. Without this, DNS resolution and inter-process communication break.
ct state established,related accept — allow replies to already-established connections. This is stateful: no need to write rules in both directions.
ct state invalid drop — explicitly drop packets with invalid connection state. Protection against certain attacks.
policy drop on the input chain — deny everything not explicitly allowed. Safer than policy accept.
Sets: Working With Groups of Addresses and Ports
Sets are one of nftables' biggest advantages over iptables. Instead of dozens of individual rules — one rule and a list.
Allow multiple ports:
tcp dport { 22, 80, 443, 8080 } accept
Create a named set for blocked IPs:
table inet filter {
set blocklist {
type ipv4_addr
flags interval
elements = { 192.168.1.0/24, 10.0.0.1, 203.0.113.0/28 }
}
chain input {
type filter hook input priority 0; policy drop;
ip saddr @blocklist drop
ct state established,related accept
tcp dport { 22, 80, 443 } accept
}
}
Add an IP to the set on the fly without reloading rules:
sudo nft add element inet filter blocklist { 198.51.100.1 }
Remove:
sudo nft delete element inet filter blocklist { 198.51.100.1 }
Set with timeout — automatically remove IPs after a set time (similar to a temporary ban):
set blocklist {
type ipv4_addr
flags dynamic, timeout
timeout 1h
}
NAT: Port Forwarding and Masquerade
Forward incoming port 8080 to an internal service on 3000:
table ip nat {
chain prerouting {
type nat hook prerouting priority -100;
tcp dport 8080 dnat to :3000
}
chain postrouting {
type nat hook postrouting priority 100;
oif "eth0" masquerade
}
}
masquerade automatically uses the outgoing interface IP as the source NAT address. More convenient than snat to when the interface IP may change.
Enable IP forwarding (required for NAT):
sudo sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.d/99-forward.conf
Rate Limiting: Brute Force Protection
Limit new SSH connections — brute force protection without fail2ban:
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
tcp dport 22 ct state new limit rate 5/minute burst 10 packets accept
tcp dport 22 ct state new drop
tcp dport { 80, 443 } accept
}
limit rate 5/minute burst 10 packets — maximum 5 new connections per minute with a short-term burst allowance of 10. Everything above is dropped by the next rule.
Logging Dropped Packets
Log what is being dropped (useful during debugging):
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
tcp dport { 22, 80, 443 } accept
limit rate 5/minute log prefix "nft drop: " flags all
drop
}
View logs:
sudo journalctl -k | grep "nft drop"
Migrating From iptables
Export current iptables rules to nftables format:
sudo iptables-save | sudo iptables-restore-translate -f /dev/stdin
The output shows equivalent nftables rules. Copy them into /etc/nftables.conf.
Check that UFW rules do not conflict:
sudo nft list ruleset | grep -A5 "ufw"
If using UFW — either keep it, or disable and switch to pure nftables:
sudo ufw disable
sudo systemctl stop ufw
Persisting Rules Across Reboots
Save the current ruleset to file:
sudo nft list ruleset > /etc/nftables.conf
The nftables systemd service applies /etc/nftables.conf at startup automatically. Confirm the service is enabled:
sudo systemctl enable nftables
Quick Reference
| Task | Command |
|---|---|
| View all rules | sudo nft list ruleset |
| Apply config | sudo nft -f /etc/nftables.conf |
| Check syntax | sudo nft -c -f /etc/nftables.conf |
| Flush all rules | sudo nft flush ruleset |
| Add IP to blocklist | sudo nft add element inet filter blocklist { IP } |
| Remove IP from blocklist | sudo nft delete element inet filter blocklist { IP } |
| List tables | sudo nft list tables |
| List chains | sudo nft list chains |
| Translate iptables rules | sudo iptables-save | iptables-restore-translate -f /dev/stdin |
| Save ruleset | sudo nft list ruleset > /etc/nftables.conf |