BlogCybersecurity
Cybersecurity

nftables in 2026: The Complete Guide to Replacing iptables on Modern Linux Servers

iptables is deprecated. nftables is the default firewall framework in every major Linux distribution since 2022. This guide covers the nftables syntax, migrating from iptables, building production rulesets for web servers, rate limiting with meters, and integrating with Docker and fail2ban.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

February 9, 2026
20 min read

If you are still writing iptables rules in 2026, you are using a deprecated tool. The Linux kernel has included nftables as the successor to iptables since kernel 4.x (2014), and every major distribution β€” Ubuntu 22.04+, Debian 11+, RHEL 9+, AlmaLinux 9+, Fedora 35+ β€” uses nftables as the default packet filtering framework. The iptables command you run on modern systems is actually iptables-nft, a compatibility layer that translates iptables syntax to nftables rules behind the scenes. This translation layer adds overhead and prevents you from using nftables' most powerful features: sets, maps, meters, and concatenated matches.

This guide covers nftables from first principles β€” not as a translation from iptables, but as its own system with its own logic. We build a complete production ruleset for a web server, implement rate limiting, handle Docker's network quirks, and integrate with fail2ban.

nftables Architecture: Families, Tables, Chains, and Rules

nftables uses a hierarchical structure that is more logical than iptables' flat table/chain model:

# Hierarchy:
# Family β†’ Table β†’ Chain β†’ Rule

# Families (address families):
# inet  β€” IPv4 + IPv6 combined (most common, recommended)
# ip    β€” IPv4 only
# ip6   β€” IPv6 only
# arp   β€” ARP
# bridge β€” Bridge filtering
# netdev β€” Ingress/egress on a specific interface

# The "inet" family handles both IPv4 and IPv6 in a single ruleset.
# This is the biggest advantage over iptables, where you needed
# separate iptables and ip6tables rules.

Your First nftables Ruleset

Unlike iptables, nftables does not have predefined tables or chains. You create everything from scratch, which gives you complete control over the packet flow:

#!/usr/sbin/nft -f
# /etc/nftables.conf β€” Production web server ruleset

# Flush all existing rules:
flush ruleset

# Define variables for readability:
define SSH_PORT = 2222
define WEB_PORTS = { 80, 443 }
define TRUSTED_IPS = { 10.0.0.0/8, 192.168.0.0/16 }
define DNS_SERVERS = { 1.1.1.1, 1.0.0.1, 8.8.8.8, 8.8.4.4 }

table inet firewall {

    # === Sets (dynamic, can be updated at runtime) ===
    set blocklist {
        type ipv4_addr
        flags timeout
        comment "Dynamically blocked IPs"
    }

    set rate_limited {
        type ipv4_addr
        flags dynamic, timeout
        timeout 5m
        comment "IPs exceeding rate limits"
    }

    # === Input Chain (traffic TO this server) ===
    chain input {
        type filter hook input priority 0; policy drop;

        # Connection tracking β€” allow established/related, drop invalid:
        ct state established,related accept
        ct state invalid drop

        # Allow loopback:
        iifname "lo" accept

        # Drop blocked IPs immediately:
        ip saddr @blocklist drop

        # Drop rate-limited IPs:
        ip saddr @rate_limited drop

        # Allow ICMP (ping) with rate limit:
        ip protocol icmp icmp type echo-request limit rate 5/second accept
        ip6 nexthdr icmpv6 icmpv6 type echo-request limit rate 5/second accept

        # Allow essential ICMPv6 (neighbor discovery, etc.):
        ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } accept

        # Allow SSH from anywhere (with rate limiting in a separate chain):
        tcp dport $SSH_PORT jump ssh_filter

        # Allow HTTP/HTTPS:
        tcp dport $WEB_PORTS accept

        # Allow monitoring from trusted IPs only:
        ip saddr $TRUSTED_IPS tcp dport { 9090, 9100, 3000 } accept

        # Log dropped packets (with rate limit to prevent log flooding):
        limit rate 10/minute log prefix "[nft-drop] " level info
    }

    # === SSH Filter Chain ===
    chain ssh_filter {
        # Rate limit: max 3 new SSH connections per minute per IP:
        ct state new meter ssh_meter { ip saddr limit rate 3/minute burst 5 packets } accept

        # If rate exceeded, add to rate_limited set and drop:
        add @rate_limited { ip saddr timeout 15m }
        log prefix "[nft-ssh-blocked] " drop
    }

    # === Forward Chain (for Docker containers) ===
    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        ct state invalid drop

        # Docker manages its own forwarding rules.
        # Allow forwarding on Docker bridge interfaces:
        iifname "docker0" accept
        oifname "docker0" accept
        iifname "br-*" accept
        oifname "br-*" accept
    }

    # === Output Chain (traffic FROM this server) ===
    chain output {
        type filter hook output priority 0; policy accept;
        # Most servers need unrestricted outbound.
        # For high-security environments, restrict outbound:
        # tcp dport { 80, 443 } accept
        # udp dport 53 ip daddr $DNS_SERVERS accept
        # drop
    }
}

table inet nat {
    chain prerouting {
        type nat hook prerouting priority -100;
    }
    chain postrouting {
        type nat hook postrouting priority 100;
        # Masquerade for Docker containers:
        oifname != "lo" ip saddr 172.16.0.0/12 masquerade
    }
}

Managing Rules at Runtime

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

# List all rules:
sudo nft list ruleset

# List a specific table:
sudo nft list table inet firewall

# Add an IP to the blocklist dynamically:
sudo nft add element inet firewall blocklist { 1.2.3.4 timeout 24h }

# Remove an IP from the blocklist:
sudo nft delete element inet firewall blocklist { 1.2.3.4 }

# List all IPs in the blocklist:
sudo nft list set inet firewall blocklist

# Add a rule interactively (useful for debugging):
sudo nft add rule inet firewall input tcp dport 8080 accept comment "Temporary: testing port 8080"

# Delete a rule by handle number:
sudo nft -a list chain inet firewall input  # Show handles
sudo nft delete rule inet firewall input handle 42

# Monitor nftables events in real-time:
sudo nft monitor

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

# Enable nftables to persist across reboots:
sudo systemctl enable nftables

Migrating from iptables

If you have existing iptables rules, you can translate them to nftables syntax automatically:

# Save current iptables rules:
sudo iptables-save > /tmp/iptables-rules.txt

# Translate to nftables:
sudo iptables-restore-translate -f /tmp/iptables-rules.txt > /tmp/nftables-translated.nft

# Review the translation:
cat /tmp/nftables-translated.nft

# Common translation patterns:
# iptables: -A INPUT -p tcp --dport 22 -j ACCEPT
# nftables: tcp dport 22 accept

# iptables: -A INPUT -s 10.0.0.0/8 -p tcp --dport 3306 -j ACCEPT
# nftables: ip saddr 10.0.0.0/8 tcp dport 3306 accept

# iptables: -A INPUT -p tcp --dport 22 -m connlimit --connlimit-above 3 -j DROP
# nftables: tcp dport 22 meter ssh_limit { ip saddr ct count over 3 } drop

nftables + Docker: Solving the Bypass Problem

Docker manipulates iptables/nftables rules directly to expose container ports, and these rules bypass your custom firewall rules. This is the same problem as the UFW + Docker bypass β€” Docker's NAT rules are processed before your filter rules. The solution is to use the DOCKER-USER chain (which Docker creates for user-defined rules) or to configure Docker to use a custom nftables table:

# Option 1: Use Docker's DOCKER-USER chain equivalent in nftables:
# Add rules to the forward chain BEFORE Docker's rules:
table inet docker-filter {
    chain forward {
        type filter hook forward priority -1; policy accept;
        # This chain runs BEFORE Docker's forwarding rules (priority -1 vs 0)

        # Block external access to containers except web:
        iifname != "docker0" oifname "docker0" tcp dport != { 80, 443 } drop
        iifname != "br-*" oifname "br-*" tcp dport != { 80, 443 } drop
    }
}

# Option 2: Disable Docker's iptables management entirely:
# /etc/docker/daemon.json:
# { "iptables": false }
# Then manually manage all container networking in nftables.
# WARNING: This breaks Docker networking unless you set up NAT rules manually.

Integrating with Fail2Ban

Fail2Ban 0.11.2+ supports nftables natively. Configure it to use nftables actions instead of the legacy iptables actions:

# /etc/fail2ban/jail.local
[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
chain = input

[sshd]
enabled = true
port = 2222
filter = sshd
backend = systemd
maxretry = 3
findtime = 600
bantime = 86400

# Fail2Ban will automatically create and manage an nftables set
# called "f2b-sshd" and add/remove IPs from it.

# Verify Fail2Ban's nftables rules:
sudo nft list table inet f2b-table

nftables is the present and future of Linux firewalling. Its syntax is cleaner, its performance is better (especially with large rulesets), and its feature set β€” sets, maps, meters, concatenated matches β€” is far more powerful than iptables. Every new server deployment should use nftables directly, not the iptables compatibility layer. ZeonEdge configures production firewalls with nftables as part of our server hardening service. Learn about our infrastructure security services.

S

Sarah Chen

Senior Cybersecurity Engineer with 12+ years of experience in penetration testing and security architecture.

Ready to Transform Your Infrastructure?

Let's discuss how we can help you achieve similar results.