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
PartExampleNotes
Table-t natdefaults to filter
Operation-A INPUTappend, insert, delete, replace, check
Protocol-p tcptcp, udp, icmp, all, or a number
Source-s 10.0.0.0/8address, CIDR, or hostname (resolved once, at load)
Destination-d 192.168.1.5same forms
In interface-i eth0only valid in INPUT, FORWARD, PREROUTING
Out interface-o eth0only valid in OUTPUT, FORWARD, POSTROUTING
Ports--dport 443requires -p tcp or -p udp
Match module-m conntrack --ctstate NEWsee the matches topic
Target-j ACCEPTor -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
ChoiceClient seesUse when
DROPnothing, then a connect timeoutinternet-facing, you want scanners to waste time
REJECT --reject-with icmp-port-unreachableimmediate "connection refused"internal networks, so misconfigured clients fail fast
REJECT --reject-with tcp-resetTCP RST, looks like nothing is listeningyou want a closed-port impression rather than a filtered one
REJECT --reject-with icmp-admin-prohibitedexplicit "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-unreachable breaks 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
INPUTyesno
OUTPUTnoyes
FORWARDyesyes
PREROUTINGyesno
POSTROUTINGnoyes

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
DetailBehaviour
Falls off the end of a user chainreturns to the calling chain, next rule after the jump
-j RETURNreturns early
Policiesuser chains have none, -P only works on built-ins
Deletingflush with -F name before -X name
-g instead of -jgoto, 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
PracticeReason
Open a second SSH session before editingthe first one survives on ESTABLISHED, a new one proves the rules
Scheduled iptables-restore rollbacka console-less box does not need a data-centre visit
Add ACCEPT rules before changing a policythe policy is what locks you out, never the rules
Test from the outside with nmap --reasonconfirms the state a client actually sees
Keep -m comment on every ruletomorrow you will not remember which ticket it served