iptables & nftables Cheatsheet

ufw and firewalld

Use this iptables & nftables reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Which front end you are dealing with

Both tools generate rules for the kernel filter. You manage the front end, and it manages the ruleset. Mixing raw iptables commands with either one produces rules that disappear on the next reload.

ToolDefault onModelBackend
ufwUbuntu (installed, inactive on server images)a simple ordered allow listiptables-nft
firewalldRHEL, Rocky, Alma, Fedora, openSUSEzones bound to interfaces and sourcesnftables since RHEL 8
neitherDebian, Arch, Alpineyou write the rulesetyour choice
sudo ufw status verbose
sudo firewall-cmd --state
systemctl is-active ufw firewalld 2>/dev/null

Run one, not both. Two front ends writing the same tables is a reliable way to produce a ruleset nobody can explain.

ufw basics

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp                     # or: sudo ufw allow OpenSSH
sudo ufw enable                           # warns that it may drop your session
sudo ufw status numbered
CommandEffect
ufw allow 443/tcpopen a port
ufw allow 6000:6010/tcpopen a range
ufw deny 23explicit deny, both protocols
ufw reject 25deny with an ICMP error rather than silence
ufw limit sshallow, but rate limit repeat connections per source
ufw delete 3delete rule 3 from status numbered
ufw delete allow 443/tcpdelete by specification
ufw insert 1 allow from 10.0.0.5insert at a position, order matters
ufw disablestop filtering, keep the rules
ufw resetback to defaults, backs the old rules up first
ufw app listprofiles from /etc/ufw/applications.d
# source-scoped rules, the form worth memorising
sudo ufw allow from 10.0.0.0/8 to any port 22 proto tcp
sudo ufw allow from 192.0.2.10 to any port 5432 proto tcp comment 'app server'
sudo ufw deny from 203.0.113.0/24
sudo ufw allow in on eth1 to any port 80 proto tcp
sudo ufw allow proto tcp from any to 10.0.0.5 port 443

# ufw understands IPv6 if IPV6=yes in /etc/default/ufw, which is the default
sudo ufw allow from 2001:db8::/32 to any port 22 proto tcp

ufw limit ssh is the one-line brute-force guard: it blocks a source that opens six or more connections in thirty seconds, implemented with the recent module.

ufw for routing and NAT

sudo ufw route allow in on lan0 out on wan0
sudo ufw route allow in on wan0 out on lan0 to 10.0.0.20 port 443 proto tcp
sudo ufw route deny in on wan0 out on lan0

NAT itself is not a ufw command. Edit the config files:

# /etc/default/ufw
DEFAULT_FORWARD_POLICY="ACCEPT"

# /etc/ufw/before.rules, at the very top of the file
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.0.0.0/24 -o wan0 -j MASQUERADE
COMMIT

# then
sudo sysctl -w net.ipv4.ip_forward=1
sudo ufw reload
FileLoaded
/etc/ufw/before.rulesbefore your ufw rules, IPv4
/etc/ufw/after.rulesafter them
/etc/ufw/before6.rules, after6.rulesthe IPv6 equivalents
/etc/ufw/user.rulesgenerated, do not hand-edit
/etc/default/ufwpolicies, IPv6 toggle, loaded modules

Docker publishes ports by writing its own DOCKER-USER chain, which is evaluated before the ufw chains, so -p 8080:80 is reachable from the internet even with ufw default deny incoming. Bind the container to 127.0.0.1:8080 or add rules to DOCKER-USER. This surprises people regularly.

sudo ufw logging on            # or low, medium, high, full
sudo ufw show added            # the commands that built the current ruleset
sudo ufw show raw              # the generated iptables rules
sudo grep -i ufw /var/log/ufw.log | tail

firewalld zones

A zone is a named trust level. Interfaces and source addresses are assigned to zones, and a packet is handled by the zone matching its source first, otherwise by its incoming interface's zone, otherwise by the default zone.

ZoneIntent
dropdrop everything inbound, no reply
blockreject everything inbound with an ICMP error
publicthe default, a small allow list, untrusted networks
externallike public, with masquerading on
dmzservers with limited internal access
work, home, internalprogressively more permitted services
trustedaccept everything
sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --list-all
sudo firewall-cmd --list-all-zones | less
sudo firewall-cmd --get-services            # the known service definitions
sudo firewall-cmd --zone=public --list-services
# put an interface and a source range in specific zones
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1
sudo firewall-cmd --permanent --zone=internal --add-source=10.0.0.0/8
sudo firewall-cmd --permanent --set-default-zone=drop
sudo firewall-cmd --reload

firewalld rules and the permanent flag

Every firewall-cmd change is either runtime (immediate, lost on reload) or permanent (written to /etc/firewalld, applied on reload). Forgetting --permanent is the most common firewalld mistake.

sudo firewall-cmd --add-service=http                       # runtime only
sudo firewall-cmd --permanent --add-service=http           # config only
sudo firewall-cmd --reload                                 # apply the config
sudo firewall-cmd --runtime-to-permanent                   # save what is live
TaskCommand
Open a service--permanent --add-service=https
Open a port--permanent --add-port=8080/tcp
Open a range--permanent --add-port=6000-6010/tcp
Remove--permanent --remove-service=http
Query--query-service=http, exit code answers
Masquerade--permanent --zone=external --add-masquerade
Port forward--permanent --add-forward-port=port=443:proto=tcp:toaddr=10.0.0.20
Trust a source--permanent --zone=trusted --add-source=192.0.2.10
Emergency stop--panic-on, then --panic-off
Reset a zone--permanent --zone=public --remove-service=... per item
# rich rules, for anything the simple options cannot express
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
  source address="10.0.0.0/8" port port="5432" protocol="tcp" accept'

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
  source address="203.0.113.0/24" drop'

sudo firewall-cmd --permanent --add-rich-rule='rule service name="ssh" \
  limit value="6/m" log prefix="ssh-try " level="info" accept'

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" \
  source address="192.0.2.0/24" forward-port port="80" protocol="tcp" \
  to-port="8080"'

sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules

A custom service definition keeps rules readable and reusable:

sudo firewall-cmd --permanent --new-service=myapp
sudo firewall-cmd --permanent --service=myapp --add-port=9000/tcp
sudo firewall-cmd --permanent --service=myapp \
  --set-description='internal app API'
sudo firewall-cmd --permanent --zone=internal --add-service=myapp
sudo firewall-cmd --reload

Choosing, and coexisting

SituationUse
Single-purpose Ubuntu serverufw, its ordered allow list is enough
RHEL family, anythingfirewalld, it is what the distribution expects
Multiple interfaces at different trust levelsfirewalld zones map onto this directly
A router or complex rulesetnative nftables in a reviewed config file
Config-managed fleetnftables file, or the front end's own config, in Git
Container hostcheck DOCKER-USER regardless of the front end
# see what either tool actually generated
sudo nft list ruleset | less
sudo iptables-save | less

# firewalld can pass raw rules through, a last resort
sudo firewall-cmd --permanent --direct \
  --add-rule ipv4 filter INPUT 0 -p tcp --dport 9999 -j ACCEPT
sudo firewall-cmd --reload
RuleWhy
Pick one front end and disable the othersreloads silently discard foreign rules
Read the generated ruleset onceit teaches you what the abstraction really did
Always pair --permanent with --reloadotherwise the change is not live, or not saved
Keep --direct rules to a minimumthey bypass the zone model and are easy to forget
Test from another host after every changethe front end's own status output is not proof