Skip to content
Go back

nftables in 2026: Stop Pretending iptables Will Live Forever

By SumGuy 9 min read
nftables in 2026: Stop Pretending iptables Will Live Forever

Your iptables Rules Are Running on a Compatibility Shim and You Know It

Here’s the thing: if you’re on Debian 12, Fedora 37+, RHEL 9, or basically any Linux distro from the last three years, the iptables binary you’ve been typing into muscle memory isn’t even real iptables anymore. It’s iptables-nft — a translation layer that converts your ancient rules into nftables kernel calls under the hood.

That’s not wrong, exactly. It works. Your rules land. Your servers don’t burn. But it’s the networking equivalent of writing Java code and running it through a transpiler to C forever — technically fine until the shim maintainers quietly stop caring, or until you try to do something the translation layer doesn’t handle cleanly.

nftables has been the kernel default since Linux 3.13. It shipped in Debian 10, RHEL 8, Fedora 32. We’re in 2026. At some point “I’ll migrate later” becomes “I’m carrying technical debt with a firewall.” That’s a bad place to be.

Let’s actually move.


What You’re Getting With nftables (It’s Not Just iptables with a Facelift)

The iptables-to-nftables migration isn’t just syntax shuffling. The underlying model is genuinely different, and the differences pay off.

One ruleset for IPv4 and IPv6

With iptables you maintain two separate rulesets: iptables for IPv4 and ip6tables for IPv6. Miss a rule in one and you have a policy gap. With nftables, a single table can handle both — just use family inet instead of ip and your rules apply to both stacks. One file, one source of truth.

Named sets and maps (dynamic blocklists that don’t hurt)

This is the killer feature. iptables ipset integration was always a bolt-on. nftables has sets and maps as first-class citizens. You can define a named set of IPs, add to it dynamically, and reference it in rules — all without reloading the whole ruleset. This is how you do fail2ban-style blocking without the performance hit of iterating a massive chain.

set blocklist {
type ipv4_addr
flags dynamic, timeout
timeout 1h
}
chain input {
ip saddr @blocklist drop
}

Adding a ban: nft add element inet filter blocklist { 192.168.1.100 }. Done. No lock, no flush, no reload.

Atomic rule loads

With iptables, there was no safe way to replace your entire ruleset atomically. iptables-restore was close but had race conditions on busy systems. nftables loads rules in a single kernel transaction — either the whole thing applies or none of it does. Your 2 AM self will appreciate this when you’re pushing a firewall config change on a production box.

Verdict maps

Verdict maps let you replace long chains of matching rules with a single lookup. Instead of ten rules checking source IPs one by one, you build a map and jump directly to a verdict. Cleaner rules, faster evaluation.

map service_map {
type inet_service : verdict
elements = { 22 : accept, 80 : accept, 443 : accept }
}
chain input {
tcp dport vmap @service_map
}

The /etc/nftables.conf Skeleton

Here’s a solid baseline config for a single server. This covers the 90% case: allow established/related, allow loopback, allow SSH/HTTP/HTTPS, drop everything else.

/etc/nftables.conf
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Loopback
iif lo accept
# Established/related
ct state established,related accept
# Drop invalid
ct state invalid drop
# ICMPv4
ip protocol icmp accept
# ICMPv6 (required for IPv6 to work)
ip6 nexthdr ipv6-icmp accept
# SSH
tcp dport 22 accept
# HTTP/HTTPS
tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}

Load it: nft -f /etc/nftables.conf. Enable on boot: systemctl enable --now nftables.

Check what’s loaded: nft list ruleset.


Converting Existing Rules: Don’t Do It By Hand

You have iptables rules running somewhere. Maybe it’s a shell script, maybe it’s an iptables-save dump, maybe it’s rules baked into a config management tool. Either way, iptables-translate and iptables-restore-translate exist specifically so you don’t have to mentally map every flag.

Single rule translation:

Terminal window
iptables-translate -A INPUT -p tcp --dport 443 -j ACCEPT

Output:

nft add rule ip filter INPUT tcp dport 443 counter accept

Bulk translation from a saved ruleset:

Terminal window
iptables-save > /tmp/my-rules.v4
iptables-restore-translate -f /tmp/my-rules.v4 > /tmp/my-rules.nft

Same thing for IPv6:

Terminal window
ip6tables-save > /tmp/my-rules.v6
ip6tables-restore-translate -f /tmp/my-rules.v6 >> /tmp/my-rules.nft

Don’t blindly apply the output. Read it. The translation is good but not perfect — it generates per-IP-family tables (ip filter / ip6 filter) instead of the cleaner inet filter. Merge them manually and you’ll end up with a nicer ruleset.


The Awkward Third-Party Problem: Docker, fail2ban, ufw

Here’s where it gets real. You don’t live alone on that server.

Docker

Docker still manages its own firewall rules via the iptables shim. When you start a container with a published port, Docker inserts iptables rules to handle NAT and forwarding. Those rules go through iptables-nft, which translates them into nftables rules — but in the ip nat and ip filter tables, not your clean inet filter table.

This works, but it means Docker rules live separately from your rules, and there can be weird interaction effects. The safest approach on a Docker host: leave the nftables forward chain at policy drop, and let Docker manage its own chains. Don’t try to merge Docker’s iptables rules into your nft config — you’ll chase your own tail.

If Docker’s firewall management is a problem (it bypasses ufw/nftables for published ports), look at --iptables=false in Docker’s config and managing Docker networking yourself — but that’s a whole other article.

fail2ban

fail2ban traditionally uses iptables or ip6tables to insert bans. As of fail2ban 0.11+, it supports nftables natively — set banaction = nftables (or nftables-multiport) in jail.local. This is the right move. Once you’re on direct nftables, fail2ban can manage its own nftables set and you get dynamic bans without the shim.

/etc/fail2ban/jail.local
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports

Restart fail2ban after the config change and verify with nft list ruleset — you should see a f2b-* chain appear when the first ban fires.

ufw

ufw is a wrapper around iptables. It uses iptables-nft on modern systems, so it technically works. But ufw doesn’t know about nftables directly — it’s still generating iptables commands and letting the shim translate them. If you’re running ufw, you’re not really running nftables; you’re running iptables syntax that gets double-translated.

The choice: keep ufw as your interface and accept the shim, or drop ufw and manage nftables directly. For home lab boxes with simple rules, ufw is fine. For anything where you care about the details — sets, maps, atomic reloads — write the nft config yourself.


Kubernetes kube-proxy: nftables Mode Is Default Now

If you’re running a Kubernetes cluster (even a single-node k3s setup), this matters. kube-proxy introduced nftables mode as an alpha feature in Kubernetes 1.29, reaching GA in 1.33. The project is actively deprecating the iptables mode, but iptables remains the default — you must opt in to nftables mode explicitly.

What this means in practice: kube-proxy can manage Service VIPs and NodePort forwarding via native nftables instead of the iptables shim. Better performance, cleaner rule management, and one less reason to keep iptables tooling around.

Check which mode you’re running:

Terminal window
kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode

If you see mode: "" (empty string), you’re on the default iptables mode. If you see mode: nftables, you’ve already opted in. To migrate, set mode: nftables in the kube-proxy configmap and restart kube-proxy.


Migration Tactics: Single Host vs Fleet

Single host

  1. Export current iptables rules: iptables-save > /tmp/before.v4 && ip6tables-save > /tmp/before.v6
  2. Translate: iptables-restore-translate -f /tmp/before.v4 > /etc/nftables.conf
  3. Clean up the output — merge ip filter and ip6 filter into a single inet filter table
  4. Dry-run: nft -c -f /etc/nftables.conf (the -c flag checks without applying)
  5. Apply: nft -f /etc/nftables.conf
  6. Verify connectivity (don’t close your SSH session yet)
  7. Enable: systemctl enable nftables
  8. If using fail2ban: switch to banaction = nftables-multiport, restart

Keep the iptables rules around in /tmp for at least one maintenance window. If something breaks, iptables-restore < /tmp/before.v4 gets you back instantly.

Fleet (Ansible)

For a fleet of servers, the workflow is basically: template your nftables.conf, push with Ansible, validate atomically.

nftables.yml
- name: Deploy nftables config
template:
src: nftables.conf.j2
dest: /etc/nftables.conf
validate: "nft -c -f %s"
notify: reload nftables
handlers:
- name: reload nftables
service:
name: nftables
state: reloaded

The validate: line is key — Ansible runs nft -c -f on the rendered template before deploying it. Syntax errors fail the task, not the firewall. That’s exactly what you want on a remote box you can’t touch physically.

For per-host differences (different open ports, different allowed CIDRs), use Ansible variables in the template. Keep the core structure the same everywhere; only the sets and port lists vary.


Just Do It Already

The iptables shim isn’t going away tomorrow. Nobody’s pulling the rug. But every month you run it is another month of:

The migration isn’t hard. iptables-restore-translate does most of the lifting. An afternoon of testing on a non-critical host and you’ll have a config you can actually read, modify, and reason about.

Honestly, the hardest part is just accepting that the thing you learned in 2014 isn’t the thing anymore. That’s fine. nft list ruleset is way more readable than iptables -L -n -v anyway.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Argo Workflows vs Tekton

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts