iptables vs nftables: A Practical Linux Firewall Guide
iptables still works, but nftables is what every modern distro ships by default now. Here is how the two actually differ and how to migrate a real ruleset.
Key takeaways
- iptables still works, but nftables is what every modern distro ships by default now.
- Here is how the two actually differ and how to migrate a real ruleset.
On this page
iptables vs nftables: A Practical Linux Firewall Guide
If you apt install iptables on a current Debian, Ubuntu, or RHEL system, you get a wrapper around nft. The actual iptables kernel modules are gone from the default install path. nftables replaced iptables as the kernel's packet-filtering framework years ago, and the compatibility layer (iptables-nft) exists specifically so the old commands and scripts keep working while everything underneath has already changed. Understanding the real framework matters the moment you need something the compatibility shim doesn't cover, or you're debugging why a rule behaves differently than it did on an older box.
Why the kernel moved on#
Classic iptables evaluates four separate tables (filter, nat, mangle, raw), each with its own set of chains, and every table walks the packet through its own traversal. Rules are matched top-to-bottom with linear search, so a ruleset with thousands of entries gets measurably slower as it grows. nftables replaces this with a single in-kernel virtual machine: rules compile to bytecode, tables and chains are user-defined rather than fixed, and lookups can use actual data structures (sets, maps) instead of linear scans.
The practical differences that matter day to day:
| iptables | nftables | |
|---|---|---|
| Rule evaluation | Linear scan per table | Compiled bytecode, can use sets/maps |
| Tables/chains | Fixed tables (filter, nat, mangle, raw) | User-defined, only what you declare |
| IPv4 + IPv6 | Separate tools (iptables, ip6tables) | One syntax, one ruleset |
| Rule syntax | One rule per -A command | Multiple rules in one statement block |
| Atomic reloads | Not atomic by default (iptables-restore helps) | Atomic by design (nft -f) |
| Default on current distros | Compatibility layer only (iptables-nft) | Native |
Reading the same rule in both syntaxes#
A rule allowing established SSH traffic and dropping new connections from anywhere else:
# iptables
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
# nftables: same intent, one rule, no separate conntrack module needed
nft add rule inet filter input tcp dport 22 ct state established,related accept
nft add rule inet filter input tcp dport 22 drop
The inet family in nftables handles IPv4 and IPv6 in the same table, so there's no more maintaining parallel iptables and ip6tables rulesets that drift apart because someone forgot to update one of them.
A minimal nftables ruleset#
nftables rulesets are usually written as a file and loaded atomically, not built up rule-by-rule on the command line:
# /etc/nftables.conf
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
tcp dport 22 accept
tcp dport { 80, 443 } accept
ip protocol icmp accept
# log then drop everything else, rate-limited so it can't fill the disk
limit rate 5/minute log prefix "nft-drop: "
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
$ sudo nft -c -f /etc/nftables.conf # -c: check syntax only, don't load
$ sudo nft -f /etc/nftables.conf # load atomically, no partial-rule window
$ sudo systemctl enable --now nftables
The -c flag is worth building into any deploy pipeline that touches firewall rules: it validates the whole file before anything is applied, so a typo can't leave the box in a half-configured state mid-load.
Migrating an existing iptables ruleset#
Don't hand-translate a large ruleset. Use the conversion tool and review the output:
$ sudo iptables-save > /tmp/current-rules.iptables
$ sudo iptables-restore-translate -f /tmp/current-rules.iptables > /tmp/current-rules.nft
$ cat /tmp/current-rules.nft
iptables-restore-translate produces a working nftables ruleset from an iptables-save dump, but always read the output before loading it. Two things it won't fix for you:
- Rule order semantics. iptables' first-match-wins per table can produce a different effective policy than a naively translated nftables chain if your original ruleset relied on jump targets across tables in a specific order. Walk through the translated file logically, not just syntactically.
- Custom kernel modules. If your old rules used a niche
-mmatch module with no nftables equivalent, the translator will flag it rather than silently drop it; check the tool's stderr output, not just the generated file.
Verifying and debugging live#
$ sudo nft list ruleset # the full active config, in nft syntax
$ sudo nft list chain inet filter input # just one chain
$ sudo nft monitor # live rule matches as they happen: the nftables equivalent of `iptables -L -v` counters, but real-time
nft monitor is the fastest way to confirm a rule is actually matching traffic instead of staring at packet counters that only update after the fact.
When to still reach for iptables syntax#
If you're running the iptables-nft compatibility layer (the default on any current distro), your existing iptables/iptables-save/iptables-restore scripts, Ansible playbooks, and CI checks keep working unchanged; they're translated to nftables rules under the hood automatically. There's no requirement to rewrite everything immediately. The case for migrating the syntax itself is when you're touching a ruleset anyway (new box, new deploy pipeline) and want atomic reloads, IPv4/IPv6 in one file, or a ruleset large enough that set-based lookups meaningfully beat linear scans. A few hundred rules and up is where that starts to show.
If you manage firewall rules across many hosts rather than one box's local iptables/nftables config, that's a different layer entirely; see zero-trust service-to-service auth with SPIFFE and SPIRE for identity-based access control instead of IP-based rules, and the broader Linux troubleshooting guide for the rest of the diagnostic toolkit this pairs with.
The call we'd make#
New box, new ruleset: write it in native nftables syntax from the start, since atomic reloads and one file for IPv4/IPv6 are worth it. Existing box with a working iptables ruleset and no active pain: leave the compatibility layer alone rather than migrating for its own sake. Either way, validate with nft -c before loading and confirm with nft monitor, not just packet counters, that a rule is doing what you think it's doing.
Get the DevOps Troubleshooting Cheat Sheet
Subscribe and get our free one-page reference for the errors that eat an afternoon — CrashLoopBackOff, OOMKilled, Terraform state locks, and more — plus new guides as we publish them.
Go Concurrency Explained: Goroutines and Channels
A practitioner's tour of goroutines, channels, select, the sync package, context, and the pitfalls that leak or deadlock real Go services.
Go vs Python Performance: What the Difference Really Is
A practical look at why Go usually outruns Python at runtime, where Python holds its own, and how to pick per workload.
More from Linux
Explore more articles in this category
ext4 vs XFS vs Btrfs: Choosing a Filesystem for a Server
The default filesystem your distro picks is not always the right one for your workload. Here is what actually differs and when each one wins.
journald Log Management: Retention, Filtering, and Forwarding
journald is the default log sink on every systemd distro, and most of it runs on defaults nobody chose. Here is how to actually control it.
DNS Troubleshooting on Linux: A Systematic Approach
\"It's always DNS\" is a joke because the failure modes are so scattered: resolver config, caching, search domains, split DNS. Here is where to actually look.
You might have missed
Evergreen posts worth revisiting.