Security

nftables Migration: From iptables to Production Firewalls

LinuxProfessionals 5 min read 463 views

iptables is dead. RHEL 9 no longer ships it as default. Debian 12 moved to nftables. Ubuntu followed. Every major distribution now points firewall tooling at nftables — the successor that was merged into the Linux kernel back in 2014 but only recently became the unavoidable standard. If you are still writing iptables rules, you are writing for a compatibility layer that translates them to nftables behind the scenes anyway. Time to cut out the middleman.

Why nftables Replaced iptables

The iptables architecture has fundamental design problems that nftables solves:

Installation and Verification

# RHEL 9 / Rocky / Alma — nftables is default
sudo dnf install -y nftables
sudo systemctl enable --now nftables

# Debian 12 / Ubuntu 24.04
sudo apt install -y nftables
sudo systemctl enable --now nftables

# Verify kernel support
nft list ruleset
# Should output current rules (may be empty)

# Check version
nft --version
# nftables v1.0.9 (Dire Straits)

# IMPORTANT: Stop and disable iptables services if they exist
sudo systemctl stop iptables 2>/dev/null
sudo systemctl disable iptables 2>/dev/null
sudo systemctl stop firewalld 2>/dev/null  # firewalld uses nftables backend now

nftables Concepts: The Mental Model

Before writing rules, understand the hierarchy:

# Table → Chain → Rule
# 
# TABLE: A namespace (any name, any family)
#   Families: ip, ip6, inet (dual-stack), arp, bridge, netdev
#
# CHAIN: Attached to a netfilter hook (or not — base vs regular chains)
#   Base chain hooks: input, output, forward, prerouting, postrouting
#   Regular chains: called from other chains (like functions)
#
# RULE: A match + action (accept, drop, reject, counter, log, jump, etc.)

# The inet family handles both IPv4 and IPv6 in ONE table
# This is the biggest ergonomic win over iptables

Your First nftables Ruleset

# Create the ruleset file
cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f

# Flush existing rules
flush ruleset

# Main firewall table — inet = IPv4 + IPv6
table inet filter {

    # Input chain — traffic destined for this host
    chain input {
        type filter hook input priority filter; policy drop;

        # Connection tracking — accept established/related, drop invalid
        ct state established,related accept
        ct state invalid drop

        # Loopback
        iif "lo" accept

        # ICMP and ICMPv6 — essential for network functionality
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # SSH
        tcp dport 22 accept

        # HTTP/HTTPS
        tcp dport { 80, 443 } accept

        # Log dropped packets (rate-limited to avoid log flooding)
        limit rate 5/minute burst 10 packets log prefix "nft-drop: " level warn
    }

    # Forward chain — traffic passing through this host
    chain forward {
        type filter hook forward priority filter; policy drop;

        ct state established,related accept
        ct state invalid drop
    }

    # Output chain — traffic originating from this host
    chain output {
        type filter hook output priority filter; policy accept;
    }
}
EOF

# Load the ruleset atomically
sudo nft -f /etc/nftables.conf

# Verify
sudo nft list ruleset

Sets: The Feature iptables Engineers Dream About

# Named sets replace ipset entirely
# Define an IP set inline
table inet filter {
    set trusted_ips {
        type ipv4_addr
        flags interval
        elements = {
            10.0.0.0/8,
            172.16.0.0/12,
            192.168.0.0/16
        }
    }

    set blocked_countries {
        type ipv4_addr
        flags interval
        # Auto-expire entries after 24 hours
        flags timeout
        timeout 24h
    }

    set rate_limited {
        type ipv4_addr
        # Dynamically populated by rules
        flags dynamic,timeout
        timeout 5m
        size 65536
    }

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

        ct state established,related accept

        # Allow trusted IPs to everything
        ip saddr @trusted_ips accept

        # Block banned IPs
        ip saddr @blocked_countries drop

        # Rate limiting: add source IP to set if exceeding threshold
        tcp dport 22 ct state new limit rate over 5/minute add @rate_limited { ip saddr } drop
        ip saddr @rate_limited drop

        tcp dport 22 accept
        tcp dport { 80, 443 } accept
    }
}

# Manage sets at runtime (no ruleset reload needed)
sudo nft add element inet filter trusted_ips { 203.0.113.50 }
sudo nft delete element inet filter trusted_ips { 203.0.113.50 }
sudo nft list set inet filter trusted_ips

Verdict Maps: Replace Dozens of Rules with One

Verdict maps are the most powerful nftables feature that iptables cannot replicate. They map a key to an action in a single rule — replacing chains of if/then rules.

table inet filter {
    # Map ports to verdict (accept/drop/jump)
    map port_policy {
        type inet_service : verdict
        elements = {
            22 : accept,
            80 : accept,
            443 : accept,
            8080 : jump rate_limit_chain,
            3306 : drop,
            5432 : drop
        }
    }

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

        ct state established,related accept
        iif "lo" accept

        # One rule replaces six separate rules
        tcp dport vmap @port_policy

        # You can also inline maps
        tcp dport vmap {
            25 : jump smtp_chain,
            53 : accept,
            853 : accept
        }
    }

    chain rate_limit_chain {
        limit rate 100/second burst 50 packets accept
        drop
    }

    chain smtp_chain {
        ip saddr 10.0.0.0/8 accept
        drop
    }
}

Translating iptables Rules to nftables

# The iptables-translate tool converts rules automatically
iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# nft add rule ip filter input tcp dport 22 accept

iptables-translate -A INPUT -s 10.0.0.0/8 -p tcp --dport 3306 -j ACCEPT
# nft add rule ip filter input ip saddr 10.0.0.0/8 tcp dport 3306 accept

# Translate an entire saved ruleset
iptables-save > /tmp/old-rules.txt
iptables-restore-translate -f /tmp/old-rules.txt > /tmp/nft-rules.nft

# WARNING: The translation is mechanical and does not use nftables
# features like sets, maps, or inet family. Always review and refactor.

# Common translations:
# iptables                          → nftables
# -A INPUT                          → add rule inet filter input
# -p tcp --dport 80                 → tcp dport 80
# -p tcp --dport 80:90              → tcp dport 80-90
# -p tcp -m multiport --dports 80,443 → tcp dport { 80, 443 }
# -m state --state ESTABLISHED      → ct state established
# -j ACCEPT                         → accept
# -j DROP                           → drop
# -j REJECT                         → reject
# -j LOG --log-prefix "X"           → log prefix "X"
# -j SNAT --to-source 1.2.3.4      → snat to 1.2.3.4
# -j MASQUERADE                     → masquerade

NAT with nftables

table ip nat {
    chain prerouting {
        type nat hook prerouting priority dstnat; policy accept;

        # Port forwarding: external:8080 → internal:80
        tcp dport 8080 dnat to 10.0.1.50:80

        # Forward based on destination port map
        tcp dport vmap {
            8080 : dnat to 10.0.1.50:80,
            8443 : dnat to 10.0.1.50:443,
            2222 : dnat to 10.0.1.100:22
        }
    }

    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;

        # Masquerade outbound traffic from LAN
        ip saddr 10.0.1.0/24 oifname "eth0" masquerade

        # Or SNAT with a fixed IP (better performance than masquerade)
        ip saddr 10.0.1.0/24 oifname "eth0" snat to 203.0.113.10
    }
}

Production Hardening Ruleset

#!/usr/sbin/nft -f
# /etc/nftables.conf — Production server hardening

flush ruleset

define LAN_NET = 10.0.0.0/8
define MONITOR_IPS = { 10.0.5.10, 10.0.5.11 }

table inet firewall {

    set ssh_allowed {
        type ipv4_addr
        flags interval
        elements = { 10.0.0.0/8, 172.16.0.0/12 }
    }

    set bruteforce {
        type ipv4_addr
        flags dynamic,timeout
        timeout 15m
        size 65536
    }

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

        # Fast-track established connections
        ct state vmap {
            established : accept,
            related     : accept,
            invalid     : drop
        }

        # Loopback
        iif "lo" accept

        # ICMP rate-limited
        ip protocol icmp limit rate 10/second accept
        ip6 nexthdr icmpv6 limit rate 10/second accept

        # SSH with brute-force protection
        tcp dport 22 ct state new {
            ip saddr @bruteforce drop
            ip saddr @ssh_allowed limit rate 3/minute burst 5 packets accept
            ip saddr != @ssh_allowed limit rate over 3/minute add @bruteforce { ip saddr } drop
        }

        # Web traffic
        tcp dport { 80, 443 } ct state new accept

        # Monitoring (Prometheus, node_exporter)
        ip saddr $MONITOR_IPS tcp dport { 9090, 9100 } accept

        # Log and drop everything else
        limit rate 3/minute log prefix "nft-blocked: "
    }

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

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

# Include additional rules from a directory
include "/etc/nftables.d/*.nft"

Testing and Debugging

# Dry-run: check syntax without applying
sudo nft -c -f /etc/nftables.conf
# No output = valid syntax

# List all rules with counters
sudo nft list ruleset -a

# List a specific chain
sudo nft list chain inet firewall input

# Add a counter to see which rules are matching
sudo nft add rule inet firewall input counter

# Trace packets through the ruleset
sudo nft add chain inet firewall trace_chain { type filter hook prerouting priority -300 \; }
sudo nft add rule inet firewall trace_chain meta nftrace set 1
sudo nft monitor trace

# Watch rule counters in real-time
watch -n 1 'nft list chain inet firewall input'

# Export ruleset as JSON (for programmatic management)
sudo nft -j list ruleset | jq '.'

nftables is not just a syntax update over iptables — it is a fundamentally better architecture for packet filtering. Sets eliminate the need for ipset. Verdict maps replace entire chain hierarchies with single rules. Atomic ruleset loading eliminates race conditions. And the inet family finally unifies IPv4 and IPv6 firewall management. The migration is not optional anymore — it is already happening whether you manage it or not.

Share this article
X / Twitter LinkedIn Reddit