Skip to content

iptables and nftables

Firewall and UFW covered UFW as a readable front end for firewall rules. UFW itself doesn't implement packet filtering — it generates rules for a lower-level kernel subsystem. That subsystem used to be exclusively iptables; on a modern Linux system it's nftables, with iptables itself now typically implemented as a compatibility layer translating old-style commands into nftables rules underneath. This article covers both: enough iptables to read rules on an older system or one still using it directly, and nftables as the tool actually worth learning for anything new.

What You Will Learn

By the end of this article you will be able to:

  • explain the relationship between iptables, nftables, and UFW;
  • read and write basic iptables rules across the filter table's built-in chains;
  • write equivalent rules in nftables syntax, including its more expressive matching;
  • make either tool's rules persist across a reboot;
  • decide when to reach for raw nftables/iptables instead of UFW.

Why Two Tools, and How They Relate

iptables dates to the 2.4 kernel series and organizes rules into tables (categories of behavior — filter for allow/deny decisions, nat for address translation) each containing chains (points in a packet's journey through the kernel — INPUT, OUTPUT, FORWARD). nftables, introduced later, replaces the same underlying netfilter hooks with a single, more expressive syntax and its own rule engine — it isn't a wrapper around iptables, it's the newer of the two talking to the kernel directly, with iptables-nft (the version shipped by default on current Debian, Ubuntu, and RHEL) instead translating old iptables syntax into nftables rules for compatibility.

sudo iptables --version
iptables v1.8.9 (nf_tables)

(nf_tables) in the version string confirms this system's iptables command is the compatibility shim, not the legacy engine — meaning rules written with either iptables or nft end up in the same underlying ruleset and are both visible through nft list ruleset.

iptables: Tables and Chains

The filter table is the one relevant to basic firewalling, and it has three built-in chains corresponding to where in a packet's path the decision is made:

Chain Applies to
INPUT Packets destined for this host
OUTPUT Packets originating from this host
FORWARD Packets passing through this host to somewhere else (routing)
sudo iptables -L -v -n
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 ACCEPT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:22

-L lists rules, -v shows packet/byte counters, -n avoids DNS lookups on addresses (both for speed and to avoid the output changing based on reverse-DNS availability).

sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -P INPUT DROP
  • -A INPUT appends a rule to the end of the INPUT chain;
  • -p tcp --dport 22 matches TCP traffic to port 22;
  • -j ACCEPT is the target — what to do with a matching packet;
  • -P INPUT DROP sets the chain's default policy — what happens to a packet that matches no rule at all.

Setting the default policy to DROP before the allow rules exist locks out the current session

Just as with UFW, the order matters critically here: -P INPUT DROP takes effect immediately, and if the SSH allow rule for the current session hasn't been added yet, the very next command over that same connection has no way to reach the host. Always add every needed ACCEPT rule — SSH first — before setting any default policy to DROP, and verify from a second, independent connection before closing the first.

iptables rules live only in kernel memory and vanish on reboot unless explicitly saved:

sudo apt install iptables-persistent
sudo netfilter-persistent save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/15-ip4tables save
run-parts: executing /usr/share/netfilter-persistent/plugins.d/25-ip6tables save

nftables: One Syntax, More Expressive Matching

nftables organizes rules the same conceptual way — tables containing chains containing rules — but the tables and chains themselves are created explicitly rather than being fixed built-ins, and a single rule can express conditions that would need several separate iptables rules.

sudo nft list ruleset
table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        tcp dport 22 accept
        tcp dport { 80, 443 } accept
    }
}

table inet filterinet means this table applies to both IPv4 and IPv6 simultaneously, eliminating the need for iptables and a separate ip6tables running in parallel with duplicated rules. tcp dport { 80, 443 } accept — matching a set of ports in one line is native syntax, not a special extension.

Building the same ruleset from scratch:

sudo nft add table inet filter
sudo nft add chain inet filter input '{ type filter hook input priority 0; policy drop; }'
sudo nft add rule inet filter input tcp dport 22 accept
sudo nft add rule inet filter input tcp dport { 80, 443 } accept

Reading this back confirms the chain's policy and every accepted port in one readable block, unlike iptables -L's flatter listing.

nftables rules are equally transient by default; the standard way to persist them is a plain configuration file, loaded explicitly or via a systemd unit:

sudo nft list ruleset > /etc/nftables.conf
sudo systemctl enable --now nftables
sudo nano /etc/nftables.conf
#!/usr/sbin/nft -f

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept
        tcp dport 22 accept
        tcp dport { 80, 443 } accept
    }
}

ct state established,related accept deserves particular attention: without it, only the very first packet of a connection matches an explicit rule, and the response traffic for an already-permitted connection gets dropped by the default policy, because a stateless ruleset without this line has no memory of which connections were previously allowed. iif lo accept permits loopback traffic, needed for many applications that talk to themselves or to a local database over 127.0.0.1.

Which One to Actually Use

Situation Recommendation
Ordinary server, need to open a handful of ports UFW — see Firewall and UFW
Need to read or maintain an existing, older iptables-based configuration Learn enough iptables syntax to read it, as covered above
Writing new, from-scratch firewall rules directly (not through UFW) nftables — it's the modern, actively developed tool, and UFW itself increasingly targets it under the hood
Complex NAT, port forwarding, or traffic shaping beyond simple allow/deny nftables, or a purpose-built router/firewall distribution — beyond the scope of basic host firewalling

On most single-server setups, UFW's generated rules are entirely sufficient, and reaching for raw nftables only makes sense once a requirement genuinely exceeds what UFW's syntax can express — a rule matching on a specific combination of source, destination, and connection state that UFW has no shorthand for, for instance.

Practical Scenario: Reading an Inherited Firewall Configuration

Problem. A server was configured by someone who has since left, and its firewall behavior needs to be understood before making any change to it.

Check which tool actually owns the rules:

sudo iptables --version
iptables v1.8.9 (nf_tables)

List the full ruleset through nft, regardless of which command originally created it:

sudo nft list ruleset
table ip filter {
    chain INPUT {
        type filter hook input priority 0; policy drop;
        ct state established,related counter packets 48213 bytes 6291842 accept
        iif "lo" accept
        tcp dport 22 accept
        tcp dport 443 accept
    }
}

Conclusion. Even though this ruleset was almost certainly built with iptables commands originally (the chain names INPUT/FORWARD/OUTPUT in uppercase are the iptables convention, versus lowercase custom names in native nftables configs), nft list ruleset shows the actual, current state regardless of which tool wrote it — the single most reliable way to audit a firewall's real behavior on a modern system is to read it through nft, not to guess based on which command line tool is installed.

Common Mistakes

  • Setting a DROP policy before adding the SSH allow rule. As covered above, this can cut off the very session used to make the change, with no way back in except console access.
  • Forgetting the ct state established,related rule in a hand-written nftables config. Without it, only the first packet of any connection is evaluated against the rules — response traffic for an already-permitted connection is silently dropped by the default policy.
  • Assuming iptables -L shows the complete picture on a modern system. As shown above, the same ruleset is visible (often more completely) through nft list ruleset, and relying on legacy tooling to audit a system that may have nftables-native rules added directly can miss part of the configuration.
  • Editing a persistent rules file without reloading it. A change to /etc/nftables.conf has no effect until the service is reloaded (systemctl reload nftables) or the file is applied directly (nft -f /etc/nftables.conf) — editing the file alone changes nothing about the running ruleset.

Exercises

  1. On a lab VM, write an nftables ruleset from scratch that accepts loopback traffic, established/related connections, and SSH, with a default drop policy — then verify a second SSH session still connects before closing the first.
  2. Run iptables --version on a system you have access to and explain what the presence or absence of (nf_tables) in the output tells you about how that command is actually implemented.
  3. Explain, using the example ruleset in this article, why tcp dport { 80, 443 } accept in nftables is more concise than the iptables equivalent, and what the iptables equivalent would look like.
  4. For a server currently managed with UFW, describe a scenario specific enough that UFW's syntax couldn't express it directly, requiring a raw nftables rule instead.

Summary

iptables and nftables both configure the kernel's netfilter packet filter, organizing rules into tables and chains; on a modern system, iptables itself is usually a compatibility layer feeding the same nftables engine underneath, so nft list ruleset is the most reliable way to audit a firewall's actual state regardless of which command built it. UFW, covered in the previous article, remains the right default choice for most single-server firewalling; raw nftables is worth learning for cases that genuinely need more expressive matching than UFW provides. Next, fail2ban adds a layer that reacts to log patterns rather than just static rules — automatically writing and removing firewall bans based on observed attack behavior.

References