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,RELATEDrule 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:
nftreplaces 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¶
- Linux Ops (Topic Pack, L0)
- Networking Deep Dive (Topic Pack, L1)
Related Content¶
- Mental Models (Core Concepts) (Topic Pack, L0) — Linux Fundamentals, Linux Networking Tools
- Ops Archaeology: The Gateway That Returns 502 (Case Study, L1) — Linux Fundamentals, Linux Networking Tools
- Ops Archaeology: The Replica That Fell Behind (Case Study, L2) — Linux Fundamentals, Linux Networking Tools
- /proc Filesystem (Topic Pack, L2) — Linux Fundamentals
- Advanced Bash for Ops (Topic Pack, L1) — Linux Fundamentals
- Adversarial Interview Gauntlet (30 sequences) (Scenario, L2) — Linux Fundamentals
- Bash Exercises (Quest Ladder) (CLI) (Exercise Set, L0) — Linux Fundamentals
- Case Study: API Latency Spike — BGP Route Leak, Fix Is Network ACL (Case Study, L2) — Linux Networking Tools
- Case Study: ARP Flux Duplicate IP (Case Study, L2) — Linux Networking Tools
- Case Study: Ansible Playbook Hangs — SSH Agent Forwarding Blocked by Firewall (Case Study, L2) — iptables & nftables