KnowledgebaseLinux VPS › Migrating legacy iptables rules to nftables

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 iptables command is implemented via iptables-nft shim 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 by nftables.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; (or policy 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 ruleset after 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

« « Back

Powered by WHMCompleteSolution