Why you still need to know iptables in 2026
RHEL 8/9 and Debian 11/12 have formally moved to nftables, but the iptables
command on those systems is often just a frontend (iptables-nft), and old
scripts keep working. On Alpine, Ubuntu LTS before 22.04, embedded systems,
and a lot of legacy infrastructure, it is still plain iptables. Docker still
writes rules through iptables. Knowing it means being able to read production.
For new code, use [[cmd-nft|nft]]: one syntax instead of iptables/ip6tables/arptables/ebtables, with support for sets, namespaces, and atomic updates.
Architecture: tables and chains
A packet travels through netfilter hooks in the kernel. iptables attaches chains to those hooks, grouped into tables by purpose:
| Table | What it does |
|---|---|
| filter | allow or block traffic (default) |
| nat | address translation (SNAT, DNAT, [[nat |
| mangle | header modification (TOS, TTL, MARK) |
| raw | before conntrack (NOTRACK to exclude traffic) |
| security | SELinux MAC labels |
Standard chains and where they fire:
┌─PREROUTING─→─routing─┬─FORWARD──→─POSTROUTING─┐
in iface ────┤ │ ├──→ out iface
└────────────────────────→ INPUT → local proc OUTPUT
- PREROUTING (raw, mangle, nat): fires immediately on ingress
- INPUT (filter, mangle): packets destined for the local host
- OUTPUT (raw, mangle, nat, filter): locally generated packets
- FORWARD (filter, mangle): transit packets (requires ip-forwarding)
- POSTROUTING (mangle, nat): fires just before egress
Basic syntax
iptables [-t TABLE] -A CHAIN [-s SRC] [-d DST] [-p PROTO] [--dport N] [-i IFACE] -j TARGET
| Option | What it does |
|---|---|
-A CHAIN | append: add to the end of the chain |
-I CHAIN [N] | insert at position N (1 means the top) |
-D CHAIN N | delete by number; or -D CHAIN <rule> for an exact match |
-L [CHAIN] | list (usually with -n -v --line-numbers) |
-F [CHAIN] | flush: remove all rules |
-P CHAIN POLICY | default policy (ACCEPT/DROP) |
-N CHAIN | create a custom chain |
-X CHAIN | delete an empty chain |
Targets (jump)
| Target | What it does |
|---|---|
ACCEPT | let the packet through |
DROP | silently discard |
REJECT | discard and send ICMP unreachable |
LOG | log to dmesg/syslog (--log-prefix) |
MASQUERADE | SNAT with automatic source-IP selection |
SNAT --to-source IP | source NAT |
DNAT --to-destination IP[:PORT] | destination NAT |
MARK --set-mark N | mark for policy routing |
<custom-chain> | jump to a custom chain |
RETURN | return from the current chain |
Common recipes
Basic host firewall
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT
iptables -A INPUT -i lo -j ACCEPT # localhost
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT # ssh
iptables -A INPUT -p tcp --dport 443 -j ACCEPT # https
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT # ping
Without the conntrack rule, outgoing connections will not receive replies. This is the first thing people forget.
NAT for a container network
echo 1 > /proc/sys/net/ipv4/ip_forward # see [[ip-forwarding]]
iptables -t nat -A POSTROUTING -s 172.17.0.0/16 -o eth0 -j MASQUERADE
Port forwarding
iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.5:80
iptables -A FORWARD -p tcp -d 10.0.0.5 --dport 80 -j ACCEPT
Rate-limiting brute-force attempts
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 5 --name SSH -j DROP
No more than 5 new SSH connections from a single IP per minute.
Saving and restoring rules
Changes made with iptables -A survive only until reboot. You must save them
explicitly:
iptables-save > /etc/iptables/rules.v4 # export current state
iptables-restore < /etc/iptables/rules.v4 # import (atomic)
ip6tables-save > /etc/iptables/rules.v6 # IPv6: separate command!
Distribution-specific notes:
- Debian/Ubuntu:
apt install iptables-persistentsaves to/etc/iptables/rules.v[46]and loads on boot. - RHEL:
iptables-services,systemctl enable iptables ip6tables.
iptables-legacy vs iptables-nft
On modern distributions, the iptables command is a shim over nftables:
iptables --version
# iptables v1.8.7 (nf_tables) ← nft backend
# iptables v1.8.7 (legacy) ← old xt_ backend
You cannot mix them. Rules in legacy and in nft live in separate namespaces.
If Docker and kube-proxy write rules through different backends, you can end up
with a hole in your firewall. The fix: update-alternatives --config iptables,
or use iptables-legacy / iptables-nft explicitly.
ip6tables and ebtables
IPv6 has a separate rule set and a separate command:
ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
Ethernet bridge filtering uses ebtables (at L2). nftables unifies all of
this under a single nft command, which is a real advantage.
When something goes wrong
- Locked yourself out of SSH: the first
INPUT DROPrule with no explicit permit for port 22. Before any rules-up script, runat +5 'iptables -F', or work through a console or serial connection. Permission denied: iptables requires root and the NET_ADMIN capability.- Rule not matching: check the order with
iptables -L INPUT -n -v --line-numbers. The first match wins; byte counters show what passed through. - NAT not working: missing
ip_forward=1, missingMASQUERADE, or missing a FORWARD permit. - Conntrack overflow:
dmesg | grep nf_conntrackshowstable full. Raisenf_conntrack_maxvia [[cmd-sysctl|sysctl]], or remove stateful traffic from conntrack with-t raw -j NOTRACK. - iptables-restore silently skipped part of the file: a syntax error in the middle. It reads line by line; everything before the error is applied atomically, everything after is not. Check the exit code.
Alternatives
- [[cmd-nft|nftables]]: the modern successor
- firewalld: a high-level daemon, abstraction over nft/iptables (RHEL default)
- ufw: simplified frontend over iptables (Ubuntu)
- shorewall: an older rules compiler