Migrating legacy iptables rules to nftables
Every modern Linux distro now ships with nftables as the native firewall backend; iptables is kept around as a compatibility shim. Existing iptables rules still work via the shim, but new firewall work is easier in native nftables syntax, and at some point you'll want to migrate your rules over. This article covers the translation patterns and what to watch for.
The state of things
- Debian 11+, Ubuntu 22.04+, AlmaLinux 9+: nftables is the kernel backend; the
iptablescommand is implemented viaiptables-nftshim that converts rules into nftables under the hood. - You can run mixed setups, but the rules don't see each other — an iptables drop rule and an nftables accept rule are evaluated in two different tables. Recipe for confusion. Pick one and stick with it.
- nftables.conf is the persistent config file (
/etc/nftables.conf). Loaded at boot bynftables.service.
Translating common iptables patterns
Allow SSH, drop everything else
iptables:
iptables -P INPUT DROP
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
nftables equivalent in /etc/nftables.conf:
#!/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
tcp dport 22 accept
}
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
}
Apply: nft -f /etc/nftables.conf then systemctl enable --now nftables.
Allow HTTP + HTTPS to a web server
tcp dport { 22, 80, 443 } accept
nftables's set syntax ({ ... }) is one of its real wins over iptables; what was 3 separate -A lines becomes one line.
Rate-limiting (slowing brute force)
iptables:
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 22 -m state --state NEW \
-m recent --update --seconds 60 --hitcount 5 -j DROP
nftables:
tcp dport 22 ct state new limit rate 5/minute accept
tcp dport 22 ct state new drop
NAT (port forward)
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat;
iif eth0 tcp dport 8080 dnat to 10.0.0.5:80
}
chain postrouting {
type nat hook postrouting priority srcnat;
oif eth0 masquerade
}
}
IPv4 + IPv6 in one ruleset
iptables required separate iptables + ip6tables commands. nftables's inet family covers both:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
tcp dport { 22, 80, 443 } accept
icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
ip6 nexthdr ipv6-icmp icmpv6 type echo-request accept
}
}
The auto-conversion tool
nftables ships with iptables-translate for direct rule translation:
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# nft add rule ip filter INPUT tcp dport 22 counter accept
For a whole rule set:
# Save current iptables rules
iptables-save > /tmp/old-rules.txt
# Convert
iptables-restore-translate -f /tmp/old-rules.txt > /tmp/new-rules.nft
# Inspect, edit, apply
less /tmp/new-rules.nft
nft -f /tmp/new-rules.nft
The output is usable but verbose; treat it as a starting point, then refactor by hand into idiomatic nftables with sets and named chains.
Common surprises during migration
- Default policies don't carry over the same way. nftables chains default to no policy if you don't specify — meaning the chain accepts whatever doesn't match. Always include
policy drop;(orpolicy accept;) explicitly. - nftables doesn't have an INPUT/OUTPUT/FORWARD "default" table. You define your tables and chains. Pick names that reflect intent.
- Order matters within a chain, but nftables doesn't have iptables's "first match wins" mental model as cleanly because counters and sets change the dynamics. Test with
nft list rulesetafter applying to make sure the rules are in the order you intended. - If you use fail2ban, it generates rules in its own table when nftables is the backend. Don't manually clean up fail2ban's chains; it manages them.
Verifying the migration didn't lock you out
Standard advice — set up a delayed-revert before applying restrictive rules:
# Schedule auto-revert in 5 minutes (give yourself time to test the new rules)
at now + 5 minutes <<EOF
nft flush ruleset
nft -f /etc/nftables.conf.backup
EOF
# Apply new rules
nft -f /etc/nftables.conf.new
# Test (SSH in from another terminal, verify everything works)
# If good: atrm <jobid> to cancel the revert
If you're locked out, the LYLIX browser console (in the portal) bypasses the network entirely — log in there and fix the rules.
Also Read
Powered by WHMCompleteSolution