iptables is the classic Linux packet filtering and NAT framework.
It controls what network traffic is allowed, dropped, or forwarded by the kernel's netfilter subsystem.
Despite being superseded by nftables in newer kernels, iptables remains
ubiquitous โ you will encounter it on virtually every Linux server in production today.
filter |
Other tables: nat mangle raw
iptables commands require root or sudo.
Rules are not persistent by default โ they live in memory and are lost on reboot.
Use iptables-save / iptables-restore or your distro's persistence
mechanism (iptables-persistent on Debian/Ubuntu, iptables.service on RHEL).
iptables organizes rules into tables (by function) and chains (by packet lifecycle stage). Understanding this is the key to everything else.
Packets destined for this host. Controls what arrives at local processes.
Packets originating from this host. Controls outbound traffic.
Packets passing through this host (routing/NAT gateways).
Before routing decision. Used for DNAT โ redirecting incoming packets.
After routing decision. Used for SNAT/MASQUERADE โ outbound NAT.
ACCEPT ยท DROP ยท REJECT ยท LOG ยท DNAT ยท SNAT
Always start by seeing what is already in place:
# List all rules in the filter table (default) sudo iptables -L # List with line numbers and numeric addresses (faster, no DNS) sudo iptables -L -n --line-numbers # List with verbose output (shows packet/byte counts) sudo iptables -L -v -n # List a specific chain sudo iptables -L INPUT -n --line-numbers # List the NAT table sudo iptables -t nat -L -n
Typical output:
Chain INPUT (policy ACCEPT) num target prot opt source destination 1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 2 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22 3 DROP all -- 0.0.0.0/0 0.0.0.0/0
-n, iptables does reverse DNS on every
address which can be extremely slow when you have hundreds of rules.
The most essential rule โ allowing your own access before locking anything down:
# Allow incoming SSH from anywhere sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT # Better โ allow SSH only from a specific management network sudo iptables -A INPUT -p tcp -s 192.168.1.0/24 --dport 22 -j ACCEPT # Allow established/related connections back in (stateful โ essential) sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
Flags explained:
-A INPUT โ Append to the INPUT chain-p tcp โ Match TCP protocol--dport 22 โ Match destination port 22-s 192.168.1.0/24 โ Match source network (optional but recommended)-j ACCEPT โ Jump to ACCEPT target (allow the packet)# Allow HTTP sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT # Allow HTTPS sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT # Both in one rule using multiport sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT # Allow outbound HTTP/HTTPS from this server (for yum/apt updates) sudo iptables -A OUTPUT -p tcp -m multiport --dports 80,443 -j ACCEPT
-m multiport match extension lets you
specify up to 15 ports in a single rule, which is far more efficient than writing
one rule per port.
# Drop all traffic from a specific IP (silently) sudo iptables -A INPUT -s 203.0.113.42 -j DROP # Reject with ICMP error (tells sender they are blocked) sudo iptables -A INPUT -s 203.0.113.42 -j REJECT # Block a /24 subnet sudo iptables -A INPUT -s 203.0.113.0/24 -j DROP # INSERT at position 1 (before other rules) for immediate effect sudo iptables -I INPUT 1 -s 203.0.113.42 -j DROP
DROP silently discards โ the sender gets no
response and must wait for a timeout. REJECT sends an ICMP "port unreachable"
back immediately. DROP is better for blocking attackers (wastes their time); REJECT is
friendlier for legitimate but misconfigured clients.
Two rules that should be in almost every ruleset:
# Always allow loopback interface โ many services depend on 127.0.0.1 sudo iptables -A INPUT -i lo -j ACCEPT sudo iptables -A OUTPUT -o lo -j ACCEPT # Allow ICMP (ping) โ useful for diagnostics sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT # Or allow all ICMP types sudo iptables -A INPUT -p icmp -j ACCEPT
lo breaks
local services that communicate via 127.0.0.1 โ databases, caching layers, local
APIs. Always explicitly ACCEPT loopback before setting a DROP default policy.
After adding all your ACCEPT rules, set a default DROP policy to block everything not explicitly allowed:
# Set default policy to DROP for INPUT and FORWARD sudo iptables -P INPUT DROP sudo iptables -P FORWARD DROP # Usually leave OUTPUT as ACCEPT (or restrict as needed) sudo iptables -P OUTPUT ACCEPT
-P INPUT DROP
# View rules with line numbers first sudo iptables -L INPUT -n --line-numbers # Delete rule by line number (deletes rule #3 from INPUT) sudo iptables -D INPUT 3 # Delete by matching the exact rule specification sudo iptables -D INPUT -p tcp --dport 8080 -j ACCEPT # Insert a rule at a specific position (position 2) sudo iptables -I INPUT 2 -p tcp --dport 8443 -j ACCEPT # Replace rule at position 3 sudo iptables -R INPUT 3 -p tcp -s 10.0.0.0/8 --dport 22 -j ACCEPT
-A (append) adds to the end of the chain.
-I (insert) adds at a position โ default position 1 means top of chain.
Rules are evaluated top-to-bottom; first match wins.
LOG target writes to syslog without stopping packet processing โ use it before DROP:
# Log and then drop โ two rules, same match sudo iptables -A INPUT -p tcp --dport 23 -j LOG --log-prefix "TELNET ATTEMPT: " --log-level 4 sudo iptables -A INPUT -p tcp --dport 23 -j DROP # Log all dropped packets (place just before your final DROP rule) sudo iptables -A INPUT -j LOG --log-prefix "IPT DROP: " --log-level warning # View the log sudo journalctl -k | grep "IPT DROP" sudo grep "IPT DROP" /var/log/kern.log
Turn a Linux box into a NAT gateway (e.g., sharing a single public IP):
# Enable IP forwarding (also set in /etc/sysctl.conf for persistence) echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward # MASQUERADE outbound traffic on the public interface (eth0) sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE # Allow forwarding between interfaces sudo iptables -A FORWARD -i eth1 -o eth0 -j ACCEPT sudo iptables -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT
Port forwarding (DNAT) โ redirect incoming port to an internal host:
# Forward port 8080 on public IP to internal web server port 80 sudo iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80 # Allow the forwarded traffic through sudo iptables -A FORWARD -p tcp -d 192.168.1.10 --dport 80 -j ACCEPT
MASQUERADE when your public IP
is dynamic (DHCP). Use SNAT --to-source x.x.x.x when your public IP
is static โ it is slightly faster since the kernel does not need to look up
the interface IP each time.
Limit connection attempts to SSH to slow brute-force attacks:
# Allow SSH but limit to 4 new connections per minute per source IP sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW \ -m recent --set --name SSH_CHECK sudo iptables -A INPUT -p tcp --dport 22 -m state --state NEW \ -m recent --update --seconds 60 --hitcount 4 --name SSH_CHECK -j DROP # Simpler limit using the limit module sudo iptables -A INPUT -p tcp --dport 22 -m limit --limit 3/min \ --limit-burst 5 -j ACCEPT sudo iptables -A INPUT -p tcp --dport 22 -j DROP
fail2ban โ it dynamically adds iptables rules based on log analysis
and automatically expires them. More flexible than static rate limits.
Rules are lost on reboot unless explicitly saved:
# Save current rules to a file sudo iptables-save > /etc/iptables/rules.v4 # Restore rules from file sudo iptables-restore < /etc/iptables/rules.v4 # Debian/Ubuntu โ install persistence package sudo apt install iptables-persistent sudo netfilter-persistent save # RHEL/CentOS โ save via service sudo service iptables save # or sudo /sbin/iptables-save > /etc/sysconfig/iptables # Flush ALL rules (nuclear option โ resets to empty) sudo iptables -F # flush rules sudo iptables -X # delete user-defined chains sudo iptables -Z # zero packet/byte counters sudo iptables -t nat -F # flush NAT table
-F flushes rules but
does NOT reset the default policy. If your policy is DROP and you flush all rules,
you will still have a DROP policy with no ACCEPT rules โ effectively locking out
all traffic. Reset the policy first: sudo iptables -P INPUT ACCEPT
| Flag | Meaning | Example |
|---|---|---|
-A CHAIN | Append rule to chain | -A INPUT |
-I CHAIN [n] | Insert at position n (default: 1) | -I INPUT 2 |
-D CHAIN n | Delete rule at line number | -D INPUT 3 |
-R CHAIN n | Replace rule at line number | -R INPUT 2 ... |
-L [CHAIN] | List rules | -L INPUT -n |
-F [CHAIN] | Flush (delete all rules) | -F INPUT |
-P CHAIN TARGET | Set default policy | -P INPUT DROP |
-t TABLE | Specify table (default: filter) | -t nat |
-p PROTO | Protocol: tcp, udp, icmp, all | -p tcp |
-s ADDR | Source address/network | -s 10.0.0.0/8 |
-d ADDR | Destination address/network | -d 192.168.1.5 |
--dport PORT | Destination port | --dport 443 |
--sport PORT | Source port | --sport 1024:65535 |
-i IFACE | Inbound interface | -i eth0 |
-o IFACE | Outbound interface | -o eth0 |
-j TARGET | Jump to target | -j ACCEPT |
-n | Numeric โ no DNS resolution | -L -n |
-v | Verbose โ show counters | -L -v |
--line-numbers | Show rule line numbers | -L --line-numbers |
| Task | Command |
|---|---|
| View all rules (numbered) | sudo iptables -L -n --line-numbers |
| View with packet counts | sudo iptables -L -v -n |
| Allow SSH from anywhere | sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT |
| Allow SSH from subnet | sudo iptables -A INPUT -p tcp -s 10.0.0.0/8 --dport 22 -j ACCEPT |
| Allow established sessions | sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT |
| Allow loopback | sudo iptables -A INPUT -i lo -j ACCEPT |
| Allow HTTP + HTTPS | sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT |
| Block an IP | sudo iptables -I INPUT 1 -s 203.0.113.42 -j DROP |
| Set DROP default policy | sudo iptables -P INPUT DROP |
| Delete rule by line number | sudo iptables -D INPUT 3 |
| Flush all rules | sudo iptables -F |
| Save rules (Debian/Ubuntu) | sudo netfilter-persistent save |
| Save rules (RHEL/CentOS) | sudo service iptables save |
| Save to file (any distro) | sudo iptables-save > /etc/iptables/rules.v4 |
| Restore from file | sudo iptables-restore < /etc/iptables/rules.v4 |
iptables is being replaced by
nftables (kernel 3.13+), with firewalld as a management layer
on RHEL/CentOS/Fedora and ufw on Ubuntu/Debian. On RHEL 8+ and Ubuntu 20.04+,
iptables commands are often translated to nftables rules transparently via
iptables-nft. The concepts โ chains, targets, stateful matching โ carry
directly over. Learn iptables; everything else is a dialect.