Skip to content

Portal | Level: L1: Foundations | Topics: iptables & nftables, Linux Fundamentals, Linux Networking Tools | Domain: Linux

iptables & nftables - Primer

Why This Matters

Under the hood: Both iptables and nftables are userspace tools that talk to the Netfilter framework inside the Linux kernel. Netfilter was written by Rusty Russell and Paul "Rusty" Russell in 1998 for the Linux 2.4 kernel. The name "nftables" comes from "Netfilter tables" — it is not a new kernel framework, but a more efficient interface to the same Netfilter hooks.

Every Linux server has a firewall built into the kernel. iptables has been the standard interface to the Netfilter framework since Linux 2.4. nftables is its modern replacement, shipping as default on Debian 11+, Ubuntu 22.04+, RHEL 9+, and Fedora 33+. Whether you're running a bare-metal web server, a Docker host, or a Kubernetes node, understanding packet filtering at the kernel level is non-negotiable. Misconfigured firewalls are behind lockouts, mysterious connectivity failures, and security breaches.

The Tables / Chains / Rules Model

iptables organizes packet filtering into three layers:

Tables define what kind of processing happens. Each table contains chains.

Table Purpose Common Chains
filter Accept/drop/reject packets (default table) INPUT, FORWARD, OUTPUT
nat Network address translation PREROUTING, POSTROUTING, OUTPUT
mangle Modify packet headers (TTL, TOS, etc.) All five chains
raw Bypass connection tracking PREROUTING, OUTPUT

Chains are ordered lists of rules evaluated sequentially. Each chain has a policy (default action if no rule matches: ACCEPT or DROP).

Rules match packets and specify a target action.

# Anatomy of a rule
$ iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -j ACCEPT
#          │       │       │           │              │
#          │       │       │           │              └─ target: ACCEPT the packet
#          │       │       │           └─ match: from 10.0.0.0/8
#          │       │       └─ match: destination port 22
#          │       └─ match: TCP protocol
#          └─ append to INPUT chain

Packet Flow Through Netfilter

Understanding which chain processes a packet depends on where the packet is going:

                           INCOMING PACKET
                          ┌─────▼─────┐
                          │ PREROUTING │ (nat, mangle, raw)
                          └─────┬─────┘
                         Routing Decision
                          ┌─────┴─────┐
                     For this host?    For another host?
                          │                    │
                    ┌─────▼─────┐        ┌─────▼─────┐
                    │   INPUT   │        │  FORWARD   │
                    │ (filter)  │        │  (filter)  │
                    └─────┬─────┘        └─────┬─────┘
                          │                    │
                    Local Process         ┌────▼──────┐
                          │               │POSTROUTING│ (nat)
                    ┌─────▼─────┐        └────┬──────┘
                    │  OUTPUT   │              │
                    │ (filter)  │         OUTGOING PACKET
                    └─────┬─────┘
                    ┌─────▼──────┐
                    │POSTROUTING │ (nat)
                    └─────┬──────┘
                    OUTGOING PACKET
  • INPUT: Packets destined for the local machine (SSH connections to this host, etc.)
  • OUTPUT: Packets generated by the local machine (DNS queries, outbound HTTP, etc.)
  • FORWARD: Packets passing through this machine to another destination (router/gateway scenarios, Docker bridge traffic)
  • PREROUTING: Processes packets before the routing decision (DNAT, port forwarding)
  • POSTROUTING: Processes packets after the routing decision (SNAT, masquerade)

Connection Tracking (conntrack)

Netfilter tracks the state of every connection passing through the system. This is the foundation of stateful filtering — matching packets based on whether they belong to an existing connection.

Connection States

State Meaning
NEW First packet of a connection (SYN for TCP, first UDP datagram)
ESTABLISHED Part of an already-established connection (server responded)
RELATED Related to an existing connection (ICMP error, FTP data channel)
INVALID Doesn't belong to any known connection (malformed, out of sequence)

Stateful Filtering Pattern

The most important rule in any firewall configuration:

# Allow all traffic belonging to established connections
# This MUST be near the top of the INPUT chain
$ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop invalid packets
$ iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# Then allow specific NEW connections
$ iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
$ iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT

Without the ESTABLISHED,RELATED rule, response packets from connections your server initiates (DNS lookups, package downloads, API calls) are dropped by the default policy.

Debug clue: If a server can connect out but responses never arrive, check whether the ESTABLISHED,RELATED rule is present and positioned early in the INPUT chain. This is the most common firewall misconfiguration — the rule exists but is placed after a DROP rule that catches the traffic first. Rules are evaluated top-to-bottom; order matters.

Viewing the Connection Tracking Table

# Show all tracked connections
$ conntrack -L
tcp      6 431999 ESTABLISHED src=10.0.0.50 dst=93.184.216.34 sport=54321 dport=443 ...

# Count connections by state
$ conntrack -L 2>/dev/null | awk '{print $4}' | sort | uniq -c | sort -rn

# Watch for new connections in real time
$ conntrack -E

# Connection tracking table size
$ cat /proc/sys/net/netfilter/nf_conntrack_count     # current entries
$ cat /proc/sys/net/netfilter/nf_conntrack_max       # max entries

Targets (Actions)

Target Effect
ACCEPT Allow the packet through
DROP Silently discard the packet (sender gets no response)
REJECT Discard and send an ICMP error back (sender knows it was blocked)
LOG Log the packet to syslog, then continue to next rule
SNAT Change the source IP (nat table, POSTROUTING)
DNAT Change the destination IP (nat table, PREROUTING)
MASQUERADE SNAT using the outgoing interface's current IP (for dynamic IPs)
REDIRECT Redirect to a local port (transparent proxy)

DROP vs REJECT: DROP is stealthier (attacker can't distinguish filtered from nonexistent). REJECT is more helpful for debugging internal networks (clients get an immediate error instead of waiting for timeout).

Remember: Mnemonic: "DROP is for the Door (external), REJECT is for the Room (internal)." On public-facing interfaces, DROP makes port scans slow and uninformative. On internal networks, REJECT gives instant feedback so engineers do not waste time waiting for connection timeouts.

Common Rule Patterns

Minimal Server Firewall

# Flush existing rules and set default policies
$ iptables -F
$ iptables -X

# Default policies
$ iptables -P INPUT DROP
$ iptables -P FORWARD DROP
$ iptables -P OUTPUT ACCEPT

# Allow loopback
$ iptables -A INPUT -i lo -j ACCEPT

# Allow established and related connections
$ iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Drop invalid
$ iptables -A INPUT -m conntrack --ctstate INVALID -j DROP

# Allow SSH
$ iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT

# Allow HTTP/HTTPS
$ iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW -j ACCEPT
$ iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT

# Allow ICMP (ping, MTU discovery)
$ iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT
$ iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT
$ iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s -j ACCEPT

# Log anything that falls through
$ iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "IPT-INPUT-DROP: "

Allow SSH from Specific Networks Only

$ iptables -A INPUT -p tcp --dport 22 -s 10.0.0.0/8 -m conntrack --ctstate NEW -j ACCEPT
$ iptables -A INPUT -p tcp --dport 22 -s 172.16.0.0/12 -m conntrack --ctstate NEW -j ACCEPT
$ iptables -A INPUT -p tcp --dport 22 -j DROP

Rate Limit SSH Connections (Anti-Brute-Force)

$ iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
    -m recent --set --name SSH
$ iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW \
    -m recent --update --seconds 60 --hitcount 4 --name SSH -j DROP

This drops SSH connections from any IP that makes more than 3 connection attempts in 60 seconds.

NAT (Network Address Translation)

SNAT / MASQUERADE (Outbound)

Allow a private network to access the internet through a gateway:

# Enable IP forwarding
$ echo 1 > /proc/sys/net/ipv4/ip_forward

# SNAT with a static public IP (more efficient than MASQUERADE)
$ iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j SNAT --to-source 203.0.113.1

# MASQUERADE with a dynamic IP (DHCP, PPPoE)
$ iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE

# Allow forwarding for the private network
$ iptables -A FORWARD -s 10.0.0.0/24 -o eth0 -j ACCEPT
$ iptables -A FORWARD -d 10.0.0.0/24 -i eth0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

DNAT / Port Forwarding (Inbound)

Forward incoming traffic to an internal server:

# Forward port 8080 on the gateway to 10.0.0.50:80
$ iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.50:80

# Must also allow the forwarded traffic
$ iptables -A FORWARD -p tcp -d 10.0.0.50 --dport 80 -j ACCEPT

Saving and Restoring Rules

iptables rules live in kernel memory. They are lost on reboot unless explicitly saved.

# Save current rules to a file
$ iptables-save > /etc/iptables/rules.v4
$ ip6tables-save > /etc/iptables/rules.v6

# Restore rules from a file
$ iptables-restore < /etc/iptables/rules.v4
$ ip6tables-restore < /etc/iptables/rules.v6

# Atomic restore (replaces ALL rules at once — no window of no-rules)
$ iptables-restore < /etc/iptables/rules.v4

# On Debian/Ubuntu: install iptables-persistent for automatic save/restore
$ apt install iptables-persistent
# Rules are auto-loaded from /etc/iptables/rules.v4 on boot

# On RHEL/CentOS:
$ service iptables save
$ systemctl enable iptables

The iptables-save format is also useful for version control — it's a plain text format that diffs cleanly:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
COMMIT

nftables: The Modern Replacement

nftables replaces iptables, ip6tables, arptables, and ebtables with a single framework. It uses a different syntax but maps to the same Netfilter kernel subsystem.

Why nftables?

  • Single tool: nft replaces iptables, ip6tables, arptables, ebtables
  • Better syntax: human-readable, less repetitive
  • Sets and maps: native support for IP sets, port sets, verdict maps
  • Atomic rule replacement: load entire rulesets atomically
  • Better performance: rules compile to a more efficient bytecode
  • Concatenations: match on multiple fields in a single rule

nftables vs iptables Comparison

iptables nftables
iptables -A INPUT -p tcp --dport 22 -j ACCEPT nft add rule inet filter input tcp dport 22 accept
iptables -L -n nft list ruleset
iptables -F nft flush ruleset
iptables-save nft list ruleset > rules.nft
iptables-restore nft -f rules.nft

Basic nftables Configuration

# Create a table (inet = both IPv4 and IPv6)
$ nft add table inet filter

# Create chains with policies
$ nft add chain inet filter input { type filter hook input priority 0 \; policy drop \; }
$ nft add chain inet filter forward { type filter hook forward priority 0 \; policy drop \; }
$ nft add chain inet filter output { type filter hook output priority 0 \; policy accept \; }

# Add rules
$ nft add rule inet filter input iif lo accept
$ nft add rule inet filter input ct state established,related accept
$ nft add rule inet filter input ct state invalid drop
$ nft add rule inet filter input tcp dport 22 ct state new accept
$ nft add rule inet filter input tcp dport { 80, 443 } ct state new accept

nftables Sets

Sets let you group IPs, ports, or other values and match against them in a single rule:

# Create a named set of allowed SSH sources
$ nft add set inet filter ssh_allowed { type ipv4_addr \; }
$ nft add element inet filter ssh_allowed { 10.0.0.0/8, 172.16.0.0/12 }

# Use the set in a rule
$ nft add rule inet filter input tcp dport 22 ip saddr @ssh_allowed accept

# Port sets — allow multiple ports in one rule
$ nft add rule inet filter input tcp dport { 80, 443, 8080, 8443 } accept

nftables Maps and Verdict Maps

Maps let you make decisions based on key-value lookups:

# Verdict map: different action per source IP
$ nft add map inet filter action_map { type ipv4_addr : verdict \; }
$ nft add element inet filter action_map { 10.0.0.5 : accept, 10.0.0.99 : drop }
$ nft add rule inet filter input ip saddr vmap @action_map

# Regular map: redirect ports based on destination port
$ nft add map inet filter port_redirect { type inet_service : inet_service \; }
$ nft add element inet filter port_redirect { 8080 : 80, 8443 : 443 }

nftables Concatenations

Match on multiple fields in a single rule for powerful and efficient filtering:

# Block specific source+port combinations
$ nft add rule inet filter input ip saddr . tcp dport { 10.0.0.5 . 80, 10.0.0.6 . 443 } drop

Complete nftables Ruleset File

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    set ssh_allowed {
        type ipv4_addr
        flags interval
        elements = { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }
    }

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

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

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

        icmp type { destination-unreachable, time-exceeded, echo-request } accept

        log prefix "NFT-DROP: " limit rate 5/minute
    }

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

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

Save as /etc/nftables.conf and load with nft -f /etc/nftables.conf.

Migration Path: iptables to nftables

Step 1: Check Your Current Backend

# What does iptables point to?
$ update-alternatives --display iptables 2>/dev/null
$ iptables --version
# "nf_tables" = already using nft backend
# "legacy" = old kernel module backend

Step 2: Translate Existing Rules

# Export current iptables rules in nftables format
$ iptables-translate -A INPUT -p tcp --dport 22 -j ACCEPT
# Output: nft add rule ip filter INPUT tcp dport 22 counter accept

# Translate an entire saved ruleset
$ iptables-restore-translate -f /etc/iptables/rules.v4 > /etc/nftables.conf

Step 3: Switch to nftables

# On Debian/Ubuntu
$ apt install nftables
$ systemctl enable nftables
$ systemctl start nftables

# Disable old iptables persistence
$ systemctl disable netfilter-persistent

firewalld as a Frontend

firewalld provides a zone-based abstraction over iptables/nftables. It's the default firewall management tool on RHEL, CentOS, Fedora, and some SUSE systems.

# Check status
$ firewall-cmd --state
$ firewall-cmd --get-active-zones

# Allow a service
$ firewall-cmd --zone=public --add-service=https --permanent
$ firewall-cmd --reload

# Allow a specific port
$ firewall-cmd --zone=public --add-port=8080/tcp --permanent
$ firewall-cmd --reload

# List all rules in a zone
$ firewall-cmd --zone=public --list-all

# Direct rules (bypass zones, add raw iptables/nftables rules)
$ firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -s 10.0.0.0/8 -j ACCEPT

firewalld uses nftables as its backend on modern systems. You can see the generated rules with nft list ruleset. Avoid mixing firewall-cmd and direct nft commands — let firewalld manage the ruleset.

Key Concepts Summary

Concept iptables nftables
List rules iptables -L -n -v nft list ruleset
Save rules iptables-save nft list ruleset > file
Restore rules iptables-restore < file nft -f file
Flush all iptables -F nft flush ruleset
Insert rule at top iptables -I INPUT 1 ... nft insert rule inet filter input ...
Delete rule iptables -D INPUT 3 nft delete rule inet filter input handle N
IP sets ipset (separate tool) Built-in sets
IPv4 + IPv6 Separate commands Single inet family

Wiki Navigation

Prerequisites