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.
| Tool | Default on | Model | Backend |
|---|---|---|---|
| ufw | Ubuntu (installed, inactive on server images) | a simple ordered allow list | iptables-nft |
| firewalld | RHEL, Rocky, Alma, Fedora, openSUSE | zones bound to interfaces and sources | nftables since RHEL 8 |
| neither | Debian, Arch, Alpine | you write the ruleset | your choice |
sudo ufw status verbose
sudo firewall-cmd --state
systemctl is-active ufw firewalld 2>/dev/nullRun 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
| Command | Effect |
|---|---|
ufw allow 443/tcp | open a port |
ufw allow 6000:6010/tcp | open a range |
ufw deny 23 | explicit deny, both protocols |
ufw reject 25 | deny with an ICMP error rather than silence |
ufw limit ssh | allow, but rate limit repeat connections per source |
ufw delete 3 | delete rule 3 from status numbered |
ufw delete allow 443/tcp | delete by specification |
ufw insert 1 allow from 10.0.0.5 | insert at a position, order matters |
ufw disable | stop filtering, keep the rules |
ufw reset | back to defaults, backs the old rules up first |
ufw app list | profiles 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
| File | Loaded |
|---|---|
/etc/ufw/before.rules | before your ufw rules, IPv4 |
/etc/ufw/after.rules | after them |
/etc/ufw/before6.rules, after6.rules | the IPv6 equivalents |
/etc/ufw/user.rules | generated, do not hand-edit |
/etc/default/ufw | policies, IPv6 toggle, loaded modules |
Docker publishes ports by writing its own
DOCKER-USERchain, which is evaluated before the ufw chains, so-p 8080:80is reachable from the internet even withufw default deny incoming. Bind the container to127.0.0.1:8080or add rules toDOCKER-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.
| Zone | Intent |
|---|---|
drop | drop everything inbound, no reply |
block | reject everything inbound with an ICMP error |
public | the default, a small allow list, untrusted networks |
external | like public, with masquerading on |
dmz | servers with limited internal access |
work, home, internal | progressively more permitted services |
trusted | accept 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
| Task | Command |
|---|---|
| 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
| Situation | Use |
|---|---|
| Single-purpose Ubuntu server | ufw, its ordered allow list is enough |
| RHEL family, anything | firewalld, it is what the distribution expects |
| Multiple interfaces at different trust levels | firewalld zones map onto this directly |
| A router or complex ruleset | native nftables in a reviewed config file |
| Config-managed fleet | nftables file, or the front end's own config, in Git |
| Container host | check 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
| Rule | Why |
|---|---|
| Pick one front end and disable the others | reloads silently discard foreign rules |
| Read the generated ruleset once | it teaches you what the abstraction really did |
Always pair --permanent with --reload | otherwise the change is not live, or not saved |
Keep --direct rules to a minimum | they bypass the zone model and are easy to forget |
| Test from another host after every change | the front end's own status output is not proof |