TL;DR — Quick Summary

nftables replaces iptables on Linux with cleaner syntax and atomic rule loading. Full migration guide with sets, NAT, rate limiting, and web server firewall.

nftables is the Linux kernel’s modern packet classification framework and the official replacement for the legacy iptables, ip6tables, arptables, and ebtables toolchain. Available since kernel 3.13 and now the default backend on Debian 10+, Ubuntu 20.10+, RHEL 8+, and Fedora 18+, nftables delivers a unified command interface, native set-based matching, and atomic ruleset loading that eliminates the security gaps inherent in per-rule iptables updates. This guide walks through the complete migration path — from installing nftables and understanding its object hierarchy to building a production-ready web server firewall with sets, rate limiting, NAT, and persistent systemd integration.

Prerequisites

Before you begin, make sure you have:

  • A Linux server running kernel 3.13 or later (Ubuntu 20.04+, Debian 10+, or RHEL 8+ recommended)
  • Terminal access with sudo privileges
  • SSH access to the server if configuring remotely — keep a second session open as a safety net
  • Familiarity with TCP/UDP ports and basic IP networking
  • Existing iptables rules exported if you plan to migrate them (sudo iptables-save > ~/iptables-backup.txt)

Why nftables Replaces iptables

The legacy Netfilter toolchain evolved organically over two decades, resulting in four separate utilities (iptables, ip6tables, arptables, ebtables) each with its own syntax, kernel interface, and extension mechanism. nftables was designed from the ground up to resolve these structural problems.

Unified Framework

One nft command replaces all four legacy tools. A single inet family table handles both IPv4 and IPv6 simultaneously, eliminating the need to duplicate every rule for both address families.

Better Syntax

nftables uses a structured grammar that reads more naturally than iptables flags:

# iptables
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT

# nftables equivalent
nft add rule inet filter input tcp dport 443 ct state new accept

Atomic Rule Loading

With iptables, adding and removing rules happens one at a time — a race condition exists between each individual operation. nftables loads an entire ruleset as a single atomic transaction using nft -f, ensuring the firewall is never in a partial state.

Native Sets and Maps

iptables requires the separate ipset extension to match against large lists of IPs or ports efficiently. nftables includes sets and maps natively, supporting timeouts, concatenated keys, and verdict maps with no additional packages.

The nft Command Basics

nftables organizes everything into a three-level hierarchy: tables contain chains, chains contain rules. Every table belongs to an address family.

Address Families

FamilyTraffic handled
ipIPv4 only
ip6IPv6 only
inetBoth IPv4 and IPv6 (recommended for servers)
arpARP protocol
bridgeBridge/switch level
netdevIngress on a specific interface

Managing Tables

sudo nft add table inet filter          # create a table
sudo nft list tables                    # list all tables
sudo nft delete table inet filter       # delete table and contents
sudo nft flush table inet filter        # remove all rules, keep structure

Managing Chains

Base chains are attached to a Netfilter hook. They require a type, hook, and priority:

sudo nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
sudo nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
sudo nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }

Managing Rules

sudo nft add rule inet filter input tcp dport 22 accept      # append
sudo nft insert rule inet filter input tcp dport 22 accept   # prepend
sudo nft list chain inet filter input                         # view with handles
sudo nft delete rule inet filter input handle 5               # delete by handle

Building a Basic Firewall

The following sequence builds a complete baseline firewall step by step.

Step 1 — Create the Table and Chains

sudo nft add table inet filter
sudo nft add chain inet filter input   { type filter hook input priority 0 \; policy drop \; }
sudo nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
sudo nft add chain inet filter output  { type filter hook output priority 0 \; policy accept \; }

Step 2 — Accept Established Connections and Loopback

These rules must come first so existing sessions are not broken when you drop all new traffic:

sudo nft add rule inet filter input ct state established,related accept
sudo nft add rule inet filter input ct state invalid drop
sudo nft add rule inet filter input iif lo accept

Step 3 — Allow ICMP

sudo nft add rule inet filter input ip protocol icmp limit rate 10/second burst 20 packets accept
sudo nft add rule inet filter input ip6 nexthdr icmpv6 limit rate 10/second burst 20 packets accept

Step 4 — Allow Service Ports

sudo nft add rule inet filter input tcp dport 22 accept
sudo nft add rule inet filter input tcp dport { 80, 443 } accept

Step 5 — Final Reject

Send an ICMP error to unmatched traffic rather than silently dropping it:

sudo nft add rule inet filter input reject with icmpx type port-unreachable

Verify the Ruleset

sudo nft list ruleset

Expected output:

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
        ip protocol icmp limit rate 10/second burst 20 packets accept
        ip6 nexthdr ipv6-icmp limit rate 10/second burst 20 packets accept
        tcp dport 22 accept
        tcp dport { 80, 443 } accept
        reject with icmpx type port-unreachable
    }
    chain forward {
        type filter hook forward priority 0; policy drop;
    }
    chain output {
        type filter hook output priority 0; policy accept;
    }
}

Sets and Maps for Efficient Rule Management

Sets eliminate the need to write one rule per port or IP address. They can be updated dynamically without touching the rules that reference them.

Anonymous Sets (Inline)

sudo nft add rule inet filter input tcp dport { 22, 80, 443, 8443 } accept

Named Sets

# Create the set
sudo nft add set inet filter tcp_accepted { type inet_service \; }

# Populate it
sudo nft add element inet filter tcp_accepted { 22, 80, 443 }

# Reference it in a rule (no rule change needed when updating the set)
sudo nft add rule inet filter input tcp dport @tcp_accepted accept

# Add or remove ports without touching the rule
sudo nft add element inet filter tcp_accepted { 8080 }
sudo nft delete element inet filter tcp_accepted { 8080 }

IP Blocklist with Auto-Expiry

sudo nft add set inet filter blocklist { type ipv4_addr \; timeout 24h \; }
sudo nft add element inet filter blocklist { 203.0.113.10, 198.51.100.5 }
sudo nft add rule inet filter input ip saddr @blocklist drop

Entries expire automatically after 24 hours unless refreshed.

Concatenated Sets (Multi-Field Matching)

sudo nft add set inet filter db_access { type ipv4_addr . inet_service \; }
sudo nft add element inet filter db_access { 10.0.1.20 . 3306, 10.0.1.21 . 5432 }
sudo nft add rule inet filter input ip saddr . tcp dport @db_access accept

Verdict Maps

sudo nft add map inet filter port_verdict { type inet_service : verdict \; }
sudo nft add element inet filter port_verdict { 22 : accept, 80 : accept, 443 : accept, 23 : drop }
sudo nft add rule inet filter input tcp dport vmap @port_verdict

Rate Limiting and Connection Tracking

Global SSH Rate Limit

Limit new SSH connections across all source addresses:

sudo nft add rule inet filter input tcp dport 22 ct state new limit rate 5/minute burst 10 packets accept
sudo nft add rule inet filter input tcp dport 22 ct state new drop

Per-Source-IP Rate Limiting with Meters

Meters enforce limits independently for each source address — far more effective against distributed brute force:

sudo nft add rule inet filter input tcp dport 22 ct state new \
  meter ssh_limit { ip saddr limit rate 3/minute burst 5 packets } accept

Connection Tracking States

StateMeaning
newFirst packet of a new connection
establishedReturn traffic for an accepted connection
relatedAssociated with an existing connection (e.g., FTP data)
invalidCannot be identified — always drop
untrackedExplicitly bypassed conntrack

NAT and Port Forwarding

Create the NAT Table

sudo nft add table inet nat
sudo nft add chain inet nat prerouting  { type nat hook prerouting priority -100 \; }
sudo nft add chain inet nat postrouting { type nat hook postrouting priority 100 \; }

Masquerade (Source NAT for Routers/VPNs)

sudo nft add rule inet nat postrouting oif "eth0" masquerade

For a specific subnet:

sudo nft add rule inet nat postrouting ip saddr 10.0.0.0/24 oif "eth0" masquerade

Port Forwarding (Destination NAT)

Forward external port 8080 to an internal web server:

sudo nft add rule inet nat prerouting iif "eth0" tcp dport 8080 dnat to 10.0.0.50:80
sudo nft add rule inet filter forward ip daddr 10.0.0.50 tcp dport 80 ct state new accept
sudo nft add rule inet filter forward ct state established,related accept

Enable IP forwarding:

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-nftables.conf
sudo sysctl -p /etc/sysctl.d/99-nftables.conf

Logging and Monitoring

Log Before Drop

sudo nft add rule inet filter input ct state invalid log prefix "nft_invalid: " level warn drop

Rate-Limited Logging

Prevent log flooding during attacks:

sudo nft add rule inet filter input tcp dport 22 ct state new \
  limit rate 1/minute log prefix "nft_ssh_new: " level info accept

View Kernel Logs

sudo journalctl -k --grep="nft_" -f
sudo dmesg | grep "nft_"

Real-Time Rule Monitoring

sudo nft monitor

Add Counters to Rules

sudo nft add rule inet filter input tcp dport 443 counter accept
sudo nft list chain inet filter input    # shows packets and bytes per rule

Saving and Restoring Rulesets

Export the Current Ruleset

sudo nft list ruleset | sudo tee /etc/nftables.conf

Managing rulesets in a file enables version control and atomic application. A complete /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

define WAN = eth0
define SSH_RATE = 5/minute

table inet filter {
    set tcp_accepted {
        type inet_service
        elements = { 22, 80, 443 }
    }

    set blocklist {
        type ipv4_addr
        timeout 24h
    }

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

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

        ip saddr @blocklist drop

        ip protocol icmp limit rate 10/second burst 20 packets accept
        ip6 nexthdr ipv6-icmp limit rate 10/second burst 20 packets accept

        tcp dport 22 ct state new meter ssh_limit { ip saddr limit rate $SSH_RATE burst 10 packets } accept
        tcp dport @tcp_accepted accept

        reject with icmpx type port-unreachable
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
    }

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

Load and validate:

sudo nft -c -f /etc/nftables.conf      # dry-run syntax check
sudo nft -f /etc/nftables.conf          # apply atomically

Migrating from iptables

Translate Individual Rules

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

Translate a Complete Ruleset

sudo iptables-save > /tmp/ipt-backup.txt
iptables-restore-translate -f /tmp/ipt-backup.txt > /tmp/nft-migrated.nft
cat /tmp/nft-migrated.nft              # review carefully before applying
sudo nft -f /tmp/nft-migrated.nft

Check the Compatibility Layer

Modern Ubuntu uses iptables-nft by default. Verify:

iptables --version
# Legacy: iptables v1.8.7 (legacy)
# nftables backend: iptables v1.8.7 (nf_tables)

If you see nf_tables, existing iptables commands already translate to nftables internally.

Migration Best Practices

  1. Back up firstsudo iptables-save > ~/iptables-backup.txt
  2. Translate and review — never apply auto-translated rules blindly
  3. Test on non-production — apply translated rules on a staging server first
  4. Keep a console session — cloud console or KVM access as a fallback if SSH is lost
  5. Flush iptables after confirmingsudo iptables -F && sudo iptables -X
  6. Disable legacy servicesudo systemctl disable iptables or ip6tables if present

systemd Integration

Enable nftables to load /etc/nftables.conf at boot:

sudo systemctl enable nftables
sudo systemctl start nftables
sudo systemctl status nftables

Reload the ruleset without rebooting:

sudo systemctl reload nftables

The reload triggers nft -f /etc/nftables.conf which includes flush ruleset — the swap is atomic.

Comparison: nftables vs iptables vs UFW vs firewalld

FeaturenftablesiptablesUFWfirewalld
Single tool for all trafficYesNo (4 tools)Yes (frontend)Yes (frontend)
IPv4 + IPv6 in one tableYes (inet)NoYesYes
Native sets/mapsYesNo (needs ipset)NoNo
Atomic rule replacementYes (nft -f)NoNoPartial
GUI / simplified syntaxNoNoYesYes
Dynamic rule updatesYesLimitedLimitedYes (zones)
Scripting flexibilityHighMediumLowMedium
Performance at scaleExcellentGoodGoodGood
Distribution defaultDebian 10+, RHEL 8+Legacy distrosUbuntu defaultRHEL/Fedora default

Choose nftables when you need full control, high performance, or complex multi-table configurations. Choose UFW when you want a simpler interface on Ubuntu with fewer rules to manage. Choose firewalld when working with RHEL/CentOS and zone-based policies.

Real-World Scenario: Web Server and Reverse Proxy Firewall

You have a production server running Nginx as a reverse proxy in front of two backend application servers at 10.0.0.10 and 10.0.0.11. The server has one public interface (eth0) and one internal interface (eth1). Requirements: SSH access from your management CIDR only, public HTTP/HTTPS, backend traffic only from the internal network, and SSH brute-force protection.

#!/usr/sbin/nft -f

flush ruleset

define WAN = eth0
define LAN = eth1
define MGMT_CIDR = 10.10.10.0/24
define BACKEND_1 = 10.0.0.10
define BACKEND_2 = 10.0.0.11

table inet filter {
    set mgmt_hosts {
        type ipv4_addr
        flags interval
        elements = { $MGMT_CIDR }
    }

    set backend_servers {
        type ipv4_addr
        elements = { $BACKEND_1, $BACKEND_2 }
    }

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

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

        # ICMP with rate limiting
        ip protocol icmp limit rate 10/second burst 20 packets accept
        ip6 nexthdr ipv6-icmp limit rate 10/second burst 20 packets accept

        # SSH restricted to management CIDR with per-IP rate limiting
        ip saddr @mgmt_hosts tcp dport 22 ct state new \
          meter ssh_mgmt { ip saddr limit rate 5/minute burst 10 packets } accept

        # Public web traffic
        tcp dport { 80, 443 } accept

        # Backend health check access from LAN
        iif $LAN ip saddr @backend_servers tcp dport { 8080, 8081 } accept

        reject with icmpx type port-unreachable
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept

        # Allow Nginx to forward to backends
        iif $WAN oif $LAN ip daddr @backend_servers tcp dport { 8080, 8081 } ct state new accept
    }

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

table inet nat {
    chain prerouting {
        type nat hook prerouting priority -100;

        # Distribute incoming traffic across backend servers
        iif $WAN tcp dport 80 dnat to numgen inc mod 2 map { 0 : $BACKEND_1:8080, 1 : $BACKEND_2:8080 }
        iif $WAN tcp dport 443 dnat to numgen inc mod 2 map { 0 : $BACKEND_1:8081, 1 : $BACKEND_2:8081 }
    }

    chain postrouting {
        type nat hook postrouting priority 100;
        oif $LAN masquerade
    }
}

Save to /etc/nftables.conf, validate with sudo nft -c -f /etc/nftables.conf, and apply.

Gotchas and Edge Cases

  • Rule order matters — nftables evaluates rules top to bottom within a chain. Place ct state established,related accept before any drop rules.
  • No default tables or chains — unlike iptables, nftables starts empty. If you forget to create a chain, traffic is not filtered at all.
  • flush ruleset is destructive — always include it at the top of config files to ensure a clean slate, but never run it interactively on a live server without a backup plan.
  • Variables require define — nftables uses define VAR = value in config files; you cannot use shell variables inside nft commands directly.
  • inet family does not cover bridge — if you filter bridged traffic (e.g., in a VM hypervisor), add a separate bridge family table.
  • iptables and nftables can conflict — if both are running, rules from both frameworks apply independently. Flush iptables rules after confirming nftables works correctly.
  • Meters are not sets — meters created inline in rules are not listed in nft list sets; use nft list meters to inspect them.

Troubleshooting

Rules not taking effect after reboot: Verify the service is enabled with sudo systemctl is-enabled nftables. Check that /etc/nftables.conf contains flush ruleset at the top and valid nft syntax.

SSH locked out after applying rules: Access via cloud console, then run sudo nft flush ruleset to clear all rules. Rebuild the configuration with SSH allowed before setting a drop policy.

Rules applied but traffic still blocked: Run sudo nft list ruleset to confirm rules are loaded. Add temporary counter rules (counter accept) to diagnose which chain is dropping the traffic.

iptables and nftables conflict: Run sudo iptables -L -n to check for legacy rules. Flush them with sudo iptables -F and sudo iptables -X, then stop any legacy iptables service.

Kernel module missing: Run lsmod | grep nf_tables. If absent, load with sudo modprobe nf_tables nft_chain_nat nft_ct.

Syntax error in config file: Use sudo nft -c -f /etc/nftables.conf for a dry-run that reports the exact line and error without applying any changes.

Summary

nftables is the production-ready replacement for iptables on any Linux system running kernel 3.13 or later. Its unified nft command, native sets and maps, atomic rule loading, and cleaner syntax address every major structural weakness of the legacy toolchain.

Key takeaways:

  • Use the inet address family to manage both IPv4 and IPv6 from a single table
  • Always open with ct state established,related accept and iif lo accept to preserve existing sessions
  • Use named sets to manage lists of ports and IPs without rewriting rules
  • Protect authentication services with per-IP meters rather than global rate limits
  • Store the ruleset in /etc/nftables.conf and load atomically with nft -f
  • Enable the nftables systemd service so the ruleset persists across reboots
  • Use iptables-restore-translate to convert existing rulesets, then review carefully before applying