nftables
nftables
The Replacement You Did Not Notice
Run iptables -L on a current Debian or RHEL box and it answers,
exactly as it always did. That command is lying to you by omission. On
anything recent it is not the old iptables at all. It is
iptables-nft, a compatibility shim that speaks the old iptables
syntax at the front and drives the nftables kernel subsystem at the
back. The legacy xtables backend that iptables used to sit on is gone
from the default install. The kernel-side migration happened years ago,
quietly, during a routine upgrade, and most people never noticed
because the command kept working.
So the question is not whether to migrate to nftables. If you run a
modern distribution, the engine under your firewall is already
nftables. The question is whether you are going to keep writing iptables
rules that get translated on the fly into a model they do not fit, or
learn the native tool and get the things the native tool was built to
give you. The quiet half of the migration is done for you. The half
that actually pays off is the one you have to do on purpose, and it is
the one almost everyone skipped.
What iptables Actually Was
To see why nftables exists, look at the mess it replaced, which was not
one tool but four. iptables handled IPv4. ip6tables handled IPv6, as a
separate command with a separate ruleset you had to keep in sync by
hand. arptables did ARP. ebtables did Ethernet bridging. Four tools,
four rule stores, four slightly different syntaxes, all solving the same
shape of problem against the same packets, because each had grown
independently inside the kernel's netfilter machinery.
Inside each one, rules lived in a fixed set of built-in chains and were
evaluated top to bottom, linearly. If you wanted to block a thousand IP
addresses, you wrote a thousand rules, and every packet walked that list
until it matched or fell off the end. The data structure for "a set of
addresses" was "a very long list of rules," which is as inefficient to
evaluate as it sounds and as miserable to manage as you remember.
And you applied rules one command at a time. Each iptables invocation
mutated the live ruleset immediately. There was no transaction. If you
were partway through reconfiguring a firewall over SSH and a rule
failed, or you fat-fingered the ordering, you could lock yourself out of
the box with a half-applied ruleset, which is a rite of passage every
sysadmin goes through exactly once before they start scheduling a
rollback job before touching a remote firewall.
The nftables Model
nftables throws out the four-tools split and the fixed chains. There is
one command, nft, and one framework that covers IPv4, IPv6, ARP, and
bridging through address families. You do not get a fixed set of
built-in chains handed to you. You create your own.
The hierarchy is small and regular. A table is a container, scoped to an
address family such as inet (which covers IPv4 and IPv6 at once, so
the dual-stack syncing problem simply disappears). Inside a table you
define chains. A base chain is attached to a netfilter hook with a type,
a hook point, and a priority, and that attachment is where it plugs into
packet processing. A compact filter table looks like this:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif "lo" accept
tcp dport { 22, 443 } accept
ip protocol icmp accept
}
}
One table, one chain hooked at input with a default-drop policy, and a
handful of rules. The inet family means this covers v4 and v6 together.
The { 22, 443 } is an anonymous set, already more compact than two
rules. This is the whole model: you declare the structure explicitly
instead of inheriting a fixed one, which is more to type the first time
and far clearer to reason about forever after.
The Two Features Worth the Switch
The model is tidier, but tidiness alone does not justify relearning a
tool. Two concrete capabilities do.
The first is atomic ruleset reloads. You write your entire ruleset to a
file and load it with nft -f ruleset.nft, and nftables applies the
whole thing in a single transaction. It all takes effect or none of it
does. There is no window where half your rules are live and the other
half failed. The lock-yourself-out-over-SSH failure mode that defined
iptables administration is designed out: a broken ruleset never
partially applies, so a syntax error leaves the old working ruleset
exactly in place rather than stranding you in a half-state. For anyone
who manages firewalls on machines they cannot walk over to, this one
property is worth the entire migration on its own.
The second is native sets and maps. nftables has real data structures,
not simulated ones. A named set is a typed collection, say a set of IPv4
addresses, that you reference from a rule and populate separately. Your
thousand blocked addresses become one set and one rule that matches
against it, evaluated with a proper lookup instead of a linear thousand
-rule walk. Sets can carry timeouts, so you can build a dynamic blocklist
whose entries expire by themselves, which is how you implement a
"ban this scanner for an hour" without a cron job sweeping stale rules.
Maps go one step further and associate a key with a verdict or a value,
so a single rule can map a destination port to an action, or an address
to a rate limit, instead of a rule per case. This is the difference
between a firewall you edit and a firewall you program.
The Migration Footguns
This is the honest part, because the switch is not free and the
quickstarts gloss over where it bites.
The syntax is genuinely different, not a reskin. Your iptables muscle
memory does not transfer, and neither do your scripts. There is an
iptables-translate helper that converts an iptables rule to its nft
equivalent, and it is useful for learning, but it produces a literal,
ugly translation that mirrors the old linear structure rather than
idiomatic nftables using sets and maps. Translating your old ruleset
line by line gives you nftables that is written like iptables, which
throws away most of the reason you switched. The rewrite that is worth
doing is a rethink, not a transliteration.
Mixing the two on one box is the sharper footgun. Because iptables-nft
and native nft both drive the same kernel subsystem, you can end up
with rules added through the iptables shim and rules added through nft
coexisting as two different representations over one engine. It works,
technically, but reasoning about the combined behavior is a headache,
and nft list ruleset will show you shim-generated rules in a form that
does not match how you wrote them. Pick one path per machine. If you are
going native, go native, and do not leave half your automation poking
the iptables shim on the side.
And the ecosystem still has lag in corners. Some older guides, some
vendor scripts, and some tools assume iptables and shell out to it. The
shim keeps them working, which is exactly why the shim exists, but it
means "we moved to nftables" is rarely as total as the kernel migration
suggests, and you will meet iptables-shaped assumptions in other
people's tooling for a while yet.
How the Egress-Deny Idea Reads Here
In the tank-os post I built an agent sandbox whose outbound traffic was
default-deny: nothing leaves the box except to an explicit allowlist, so
a compromised agent cannot phone home or exfiltrate. That idea was
written in iptables. In nftables it is cleaner, because the allowlist of
permitted destinations is exactly what a named set is for. An output
chain with policy drop, one set holding the handful of addresses the
workload is allowed to reach, and one rule accepting traffic whose
destination is in that set. Adding or removing a permitted destination
becomes a one-line set update rather than a rule insert at the right
position, and the whole egress policy loads atomically so there is never
a moment where the deny is live but the allow is not. Same security
property, expressed in a structure that was built for it instead of
one that tolerated it.
The Takeaway
iptables is not your firewall anymore, whatever the command prompt
tells you. The kernel moved to nftables years ago and dragged your
rules along through a shim. The work left to you is the deliberate half:
stop writing translated iptables rules over an engine that would rather
you did not, and use the native tool for the two things it does that
the old one could not. Atomic reloads mean a bad ruleset never locks you
out halfway. Sets and maps mean a thousand addresses are one lookup, not
a thousand rules. Rewrite the ruleset as a rethink rather than a
transliteration, keep one box on one path, and you end up with a
firewall you program instead of one you nurse. The migration you did not
notice is done. The one that pays off is the one you choose.