iptables & nftables Cheatsheet
Filter Rules
Use this iptables & nftables reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Rule syntax
iptables [-t table] {-A|-I|-D|-R|-C} CHAIN [rule-spec] -j TARGET| Part | Example | Notes |
|---|---|---|
| Table | -t nat | defaults to filter |
| Operation | -A INPUT | append, insert, delete, replace, check |
| Protocol | -p tcp | tcp, udp, icmp, all, or a number |
| Source | -s 10.0.0.0/8 | address, CIDR, or hostname (resolved once, at load) |
| Destination | -d 192.168.1.5 | same forms |
| In interface | -i eth0 | only valid in INPUT, FORWARD, PREROUTING |
| Out interface | -o eth0 | only valid in OUTPUT, FORWARD, POSTROUTING |
| Ports | --dport 443 | requires -p tcp or -p udp |
| Match module | -m conntrack --ctstate NEW | see the matches topic |
| Target | -j ACCEPT | or -g to jump without returning |
Negate almost any match with !:
sudo iptables -A INPUT ! -s 10.0.0.0/8 -p tcp --dport 22 -j DROP sudo iptables -A INPUT -p tcp ! --dport 80 -j LOG sudo iptables -A FORWARD ! -i eth0 -j DROP
DROP against REJECT
sudo iptables -A INPUT -p tcp --dport 3306 -j DROP sudo iptables -A INPUT -p tcp --dport 3306 -j REJECT --reject-with tcp-reset sudo iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable
| Choice | Client sees | Use when |
|---|---|---|
DROP | nothing, then a connect timeout | internet-facing, you want scanners to waste time |
REJECT --reject-with icmp-port-unreachable | immediate "connection refused" | internal networks, so misconfigured clients fail fast |
REJECT --reject-with tcp-reset | TCP RST, looks like nothing is listening | you want a closed-port impression rather than a filtered one |
REJECT --reject-with icmp-admin-prohibited | explicit "a firewall said no" | you want the block to be diagnosable |
DROP on a LAN is a support ticket generator: every failed connection becomes a
multi-second hang instead of an instant error. Reserve it for the edge.
A minimal host firewall
# start from a known state sudo iptables -F sudo iptables -X # loopback is never filtered sudo iptables -A INPUT -i lo -j ACCEPT # let replies to our own traffic back in sudo iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # drop obviously broken packets sudo iptables -A INPUT -m conntrack --ctstate INVALID -j DROP # the services this host actually offers sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j ACCEPT # useful ICMP, rate limited sudo iptables -A INPUT -p icmp --icmp-type echo-request \ -m limit --limit 5/second -j ACCEPT sudo iptables -A INPUT -p icmp --icmp-type destination-unreachable -j ACCEPT sudo iptables -A INPUT -p icmp --icmp-type time-exceeded -j ACCEPT # default deny, stated as a rule so a flush cannot strand you sudo iptables -A INPUT -j REJECT --reject-with icmp-host-prohibited # only now tighten the policies sudo iptables -P FORWARD DROP sudo iptables -P OUTPUT ACCEPT sudo iptables -P INPUT DROP
The order above is deliberate. lo first, then state, then the allow list, then
the deny, and the -P INPUT DROP last. Reverse any of those steps over SSH and
the session dies mid-script.
Do not block all ICMP. Dropping
destination-unreachablebreaks path MTU discovery, which shows up later as large transfers hanging while small ones work. That failure mode is very hard to trace back to the firewall.
Interfaces and directions
# per-interface policy on a router sudo iptables -A FORWARD -i lan0 -o wan0 -j ACCEPT sudo iptables -A FORWARD -i wan0 -o lan0 \ -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT sudo iptables -A FORWARD -i wan0 -j DROP # wildcard interface names sudo iptables -A INPUT -i eth+ -p tcp --dport 22 -j ACCEPT sudo iptables -A FORWARD -i docker0 -j ACCEPT # anti-spoofing, private sources must not arrive from the internet sudo iptables -A INPUT -i wan0 -s 10.0.0.0/8 -j DROP sudo iptables -A INPUT -i wan0 -s 172.16.0.0/12 -j DROP sudo iptables -A INPUT -i wan0 -s 192.168.0.0/16 -j DROP sudo iptables -A INPUT -i wan0 -s 127.0.0.0/8 -j DROP
| Chain | -i valid | -o valid |
|---|---|---|
INPUT | yes | no |
OUTPUT | no | yes |
FORWARD | yes | yes |
PREROUTING | yes | no |
POSTROUTING | no | yes |
Forwarding needs the kernel switch as well as the rules:
sudo sysctl -w net.ipv4.ip_forward=1 echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-forward.conf
User-defined chains
Custom chains keep long rulesets readable and cheap. Jump into one from a broad match, and the specific rules only run for traffic that qualifies.
sudo iptables -N web-allow sudo iptables -A web-allow -s 203.0.113.0/24 -j ACCEPT sudo iptables -A web-allow -s 198.51.100.10 -j ACCEPT sudo iptables -A web-allow -j LOG --log-prefix "web-denied: " sudo iptables -A web-allow -j REJECT sudo iptables -A INPUT -p tcp -m multiport --dports 80,443 -j web-allow
| Detail | Behaviour |
|---|---|
| Falls off the end of a user chain | returns to the calling chain, next rule after the jump |
-j RETURN | returns early |
| Policies | user chains have none, -P only works on built-ins |
| Deleting | flush with -F name before -X name |
-g instead of -j | goto, does not come back to the caller |
Restricting by user, process, and rate
OUTPUT can match the local originator, which is useful for egress control on
servers.
# only the backup user may reach the backup host sudo iptables -A OUTPUT -d 10.0.5.20 -m owner --uid-owner backup -j ACCEPT sudo iptables -A OUTPUT -d 10.0.5.20 -j REJECT # block a group from outbound SMTP sudo iptables -A OUTPUT -p tcp --dport 25 -m owner --gid-owner www-data -j REJECT # allow DNS and HTTPS out, deny the rest sudo iptables -A OUTPUT -o lo -j ACCEPT sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT sudo iptables -A OUTPUT -p tcp -m multiport --dports 53,443 -j ACCEPT sudo iptables -A OUTPUT -j REJECT sudo iptables -P OUTPUT DROP
-m owner works in OUTPUT only, because that is the one chain where the
kernel still knows which local socket produced the packet.
Testing changes without losing the box
# a dead-man switch, restores the last good ruleset in 5 minutes sudo iptables-save > /root/fw.good ( sleep 300 && sudo iptables-restore < /root/fw.good ) & # ... make changes, open a SECOND ssh session to prove they work ... kill %1 # cancel the rollback once you are sure
| Practice | Reason |
|---|---|
| Open a second SSH session before editing | the first one survives on ESTABLISHED, a new one proves the rules |
Scheduled iptables-restore rollback | a console-less box does not need a data-centre visit |
| Add ACCEPT rules before changing a policy | the policy is what locks you out, never the rules |
Test from the outside with nmap --reason | confirms the state a client actually sees |
Keep -m comment on every rule | tomorrow you will not remember which ticket it served |