Most major distributions have defaulted to nftables as the kernel-level packet filtering framework for several years now, with iptables surviving mainly as a compatibility shim. Migrating existing rulesets that predate the switch is still a manual exercise, and one where getting the cutover wrong means a remote server that's suddenly unreachable.
Why bother switching at all
iptables still works, via the nf_tables backend on modern kernels, so this isn't an urgent migration. The reasons to do it anyway: nftables rulesets are more readable, support native sets and maps that previously needed ipset as a separate tool, and apply rule changes atomically — a ruleset reload either fully succeeds or fully fails, rather than applying line-by-line and potentially leaving a host half-configured mid-update.
The mental model difference
iptables organizes rules into a fixed set of tables and chains (INPUT, OUTPUT, FORWARD) that you fill in. nftables instead has you define your own tables and chains, which feels like more setup upfront but maps more directly onto what your firewall is actually meant to do.
A minimal nftables base structure
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
}
}
The default policy being drop at the chain level, rather than appended as a final rule, is a small but meaningful difference — there's no risk of a rule ordering mistake leaving an implicit allow-all at the end.
Translating common iptables rules
Most existing rulesets translate fairly mechanically. A handful of common patterns:
| iptables | nftables equivalent |
|---|---|
-p tcp --dport 22 -j ACCEPT | tcp dport 22 accept |
-s 10.0.0.0/8 -j ACCEPT | ip saddr 10.0.0.0/8 accept |
-m state --state ESTABLISHED -j ACCEPT | ct state established accept |
For a ruleset of any real size, the iptables-translate and iptables-restore-translate tools (shipped alongside nftables on Debian and Ubuntu) convert existing rules automatically, which is a faster starting point than translating by hand line by line:
iptables-restore-translate -f /etc/iptables/rules.v4 -o /etc/nftables.conf
The output is a reasonable starting draft, not a finished ruleset — it's worth reading through rather than trusting blindly, since the translation is mechanical and won't restructure rules to take advantage of nftables' sets and maps.
A cutover plan that avoids locking yourself out
The single biggest risk in this migration is applying a new ruleset over SSH and discovering it blocks the very connection you're using to apply it. The approach that's worked reliably for me:
- Write and test the new nftables ruleset in a separate table name, so it coexists with the existing iptables rules rather than replacing them immediately.
- Schedule a one-time
atjob to flush nftables and re-enable iptables a few minutes in the future, as a safety net in case the new rules lock out access. - Apply the new ruleset, verify connectivity from a second, independent connection — not the one you used to apply it.
- Once confirmed, cancel the safety-net job, disable iptables persistence, and enable nftables on boot.
The safety-net job
echo "systemctl stop nftables; systemctl start iptables" | at now + 5 minutes
Test this exact safety-net pattern on a non-production host first. The point of the at job is to be the thing that saves you when something goes wrong — it needs to be something you've already confirmed actually works.
Across a dozen or so servers migrated this way, the rollback job has actually fired exactly once — a typo in a port range — and the five minutes it took to notice and fix the issue was a far better outcome than discovering a typo by being locked out entirely.