# iptables vs nftables: A Practical Linux Firewall Guide

URL: https://www.devopsness.com/blog/iptables-vs-nftables-linux-firewall-guide
Author: Kiril Urbonas
Published: 2026-08-20
Updated: 2026-08-20
Categories: Linux
Tags: Security, Linux, Networking, Sysadmin, Firewall

Summary: 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.

---
# 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:

```bash
# iptables
iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
```

```bash
# 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:

```nft
# /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;
    }
}
```

```bash
$ 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:

```bash
$ 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 `-m` match 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

```bash
$ 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](/blog/zero-trust-service-auth-spiffe-spire) for identity-based access control instead of IP-based rules, and the broader [Linux troubleshooting guide](/blog/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.

