Blog
4 min read

nftables vs iptables: The Linux Firewall, Then and Now

nftables is the successor to iptables in the Linux kernel: one tool for IPv4, IPv6 and more, sets and maps, atomic rule updates, and a cleaner syntax. How the two relate (including iptables-nft), a complete server ruleset, translating rules, and the Docker, UFW and firewalld interactions that trip people up.

For two decades, Linux firewalls meant iptables. Its successor, nftables, has been in the kernel since 3.13 (2014) and is now the default backend on Debian, Ubuntu, RHEL and Fedora. Even when you type iptables today, you're very likely talking to nftables underneath.

How they relate

Both are front ends to netfilter, the kernel's packet-filtering framework.

  • Legacy iptables used separate tools and kernel tables per protocol: iptables, ip6tables, arptables, ebtables.
  • nftables replaces all of them with one kernel subsystem and one tool, nft, plus a small virtual machine in the kernel that evaluates rules.
  • iptables-nft is a compatibility layer: the familiar iptables command syntax, but rules are stored in nftables. Check which you have:
iptables -V
# iptables v1.8.x (nf_tables)   ← compatibility layer over nftables
# iptables v1.8.x (legacy)      ← old backend

Mixing legacy iptables and nftables rules on one host is a recipe for confusion — both get evaluated, and neither tool shows the other's rules.

What nftables improves

iptables nftables
Protocols One tool per family inet family covers IPv4 + IPv6 in one table
Tables/chains Fixed built-ins You create only what you need
Updates Rule by rule, non-atomic Whole ruleset replaced atomically
Matching many addresses One rule per address (or ipset) Native sets and maps
Multiple actions per rule No Yes (e.g. count + log + drop)
Performance with large rule lists Linear Sets are hash/tree lookups

A complete server ruleset

This example is for a standalone server whose firewall you manage entirely with nftables. flush ruleset removes every nftables table, including rules installed by Docker or firewalld. Don't apply it as-is on those hosts. Replace the documentation-only admin IPs with your own before loading it, and keep console or rescue access available while testing.

#!/usr/sbin/nft -f
flush ruleset

table inet filter {
  set admin_ips {
    type ipv4_addr
    elements = { 203.0.113.7, 198.51.100.20 }
  }

  chain input {
    type filter hook input priority filter; policy drop;

    ct state established,related accept
    ct state invalid drop
    iif "lo" accept

    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept

    tcp dport 22 ip saddr @admin_ips accept
    tcp dport { 80, 443 } accept

    limit rate 5/minute log prefix "nft-drop: "
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
  }

  chain output {
    type filter hook output priority filter; policy accept;
  }
}

Load it atomically and check:

sudo nft -c -f /etc/nftables.conf   # syntax check only
sudo nft -f /etc/nftables.conf
sudo nft list ruleset
sudo systemctl enable nftables      # load at boot

Always keep an existing SSH session open while changing firewall rules on a remote server, so a mistake doesn't lock you out. (SSH Hardening, CIDR Notation Explained)

Sets can be updated live without reloading the ruleset — handy for blocklists:

sudo nft add element inet filter admin_ips '{ 192.0.2.44 }'

Translating iptables rules

iptables-translate -A INPUT -p tcp --dport 443 -j ACCEPT
# nft 'add rule ip filter INPUT tcp dport 443 counter accept'

iptables-save > rules.v4
iptables-restore-translate -f rules.v4 > rules.nft

Review the output — translations are literal, and you'll usually want to restructure into the inet family and sets.

The interactions that trip people up

  • Docker programs its own NAT and forwarding rules. Published ports (-p 5432:5432) can bypass host INPUT rules because traffic is DNAT'd and goes through FORWARD. Bind published ports to 127.0.0.1 when only local access is needed. With Docker's iptables backend, custom filtering belongs in DOCKER-USER. With its native nftables backend, there is no DOCKER-USER chain: use a separate table with a forward base chain and appropriate priority, following Docker's nftables documentation. Don't edit Docker-owned tables. (Docker Compose Networking, Container Networking Internals)
  • UFW is a front end that writes iptables rules (through iptables-nft on modern systems). Pick one way of managing the firewall per host. (UFW Firewall Basics)
  • firewalld uses nftables as its backend on current distributions; don't hand-edit rules underneath it.
  • fail2ban can use either; configure the nftables actions on nftables hosts. (fail2ban)
  • Rule priority: multiple base chains on the same hook all run, ordered by priority; an accept in one chain doesn't stop another chain on the same hook from dropping the packet.

Should you migrate?

On a new server, write nftables directly — it's cleaner and atomic. On an existing iptables-nft setup that works, there's no urgency; you're already on nftables under the hood. If anything still uses legacy iptables, plan to move off it.

The summary

  • nftables replaces iptables/ip6tables/ebtables with one tool, one syntax and atomic updates.
  • Modern iptables commands usually write nftables rules via iptables-nft.
  • Use inet tables and sets, check with nft -c, and watch Docker's port publishing.

EasySpawn gives each server its own isolated VM with the network firewall managed for you, so only your app's web ports face the internet. See how it works or join the waitlist.

Related: Linux Namespaces Explained · What Is eBPF? · Secure a New VPS · Agent Egress Control With a Proxy

Keep reading