firewalld: Zones and Rich Rules
Introduction¶
firewalld is the default firewall manager on RHEL/Fedora and available on
Debian/Ubuntu. It sits on top of nftables (older versions: iptables) and
organises rules into zones — named policy sets you attach interfaces and
source addresses to.
Zones¶
A zone is a trust level with its own rules. Built-in zones, least to most
trusting: drop, block, public, external, dmz, work, home,
internal, trusted.
- An interface belongs to exactly one zone (
publicby default). - A source (IP/CIDR) can also be bound to a zone; source binding takes precedence over interface binding for matching traffic.
trustedallows everything;dropsilently drops all inbound.
firewall-cmd --get-active-zones
firewall-cmd --get-default-zone
firewall-cmd --zone=public --list-all
# Move an interface to a zone
firewall-cmd --permanent --zone=internal --change-interface=eth1
# Bind a source to a zone
firewall-cmd --permanent --zone=trusted --add-source=10.0.0.0/8
Services vs ports¶
A service is a named bundle of ports/protocols (ssh = 22/tcp, https =
443/tcp). Prefer services — they're self-documenting and survive port changes.
firewall-cmd --permanent --zone=public --add-service=https
firewall-cmd --permanent --zone=public --add-port=8080/tcp
firewall-cmd --permanent --zone=public --remove-service=cockpit
firewall-cmd --get-services # all known services
# custom service: /etc/firewalld/services/myapp.xml
Permanent vs runtime — the trap¶
Every command is runtime-only unless --permanent. Runtime changes vanish on
reload/reboot; permanent changes don't apply until --reload.
The safe pattern:
firewall-cmd --permanent --zone=public --add-service=https # persist
firewall-cmd --reload # apply
# verify, THEN you're done. Or, to test first:
firewall-cmd --zone=public --add-service=https # runtime, test it
firewall-cmd --runtime-to-permanent # keep what works
--reload drops runtime-only changes — losing an SSH rule you added but forgot
to persist can lock you out.
Rich rules¶
For anything beyond "allow this service": per-source rules, logging, rate limiting, rejecting with a specific ICMP type.
# Allow SSH only from the office, log new connections, rate-limit
firewall-cmd --permanent --zone=public --add-rich-rule='
rule family="ipv4" source address="203.0.113.0/24"
service name="ssh" log prefix="SSH " level="info" limit value="3/m" accept'
# Block a bad actor
firewall-cmd --permanent --zone=public --add-rich-rule='
rule family="ipv4" source address="198.51.100.66" drop'
# Port forward
firewall-cmd --permanent --zone=public --add-rich-rule='
rule family="ipv4" forward-port port="8080" protocol="tcp" to-port="80"'
Rich rules are evaluated in a fixed priority order (source rules, then service/
port, then a catch-all) — you don't control ordering the way you do raw
nftables. For complex ordering, --add-rich-rule with explicit priority (newer
firewalld) or drop to a direct nftables ruleset.
Masquerading and forwarding¶
firewall-cmd --permanent --zone=external --add-masquerade
# inter-zone forwarding (firewalld 0.9+ policies, or):
firewall-cmd --permanent --new-policy=int-to-ext
firewall-cmd --permanent --policy=int-to-ext --add-ingress-zone=internal
firewall-cmd --permanent --policy=int-to-ext --add-egress-zone=external
firewall-cmd --permanent --policy=int-to-ext --set-target=ACCEPT
Verification and troubleshooting¶
firewall-cmd --list-all-zones
firewall-cmd --get-active-zones
firewall-cmd --zone=public --query-service=https
nft list ruleset | less # the actual rules firewalld generated
journalctl -u firewalld
firewall-cmd --reload && firewall-cmd --runtime-to-permanent
- Rule added but not blocking/allowing — runtime-only (no
--permanent+--reload), or the traffic matches a different zone (a source binding, or the interface is in a zone you didn't edit).firewall-cmd --get-zone-of-source=IP/--get-zone-of-interface=eth0. - Locked out after
--reload— a runtime SSH rule wasn't persisted. Recover via console; always--permanentthe SSH allow first. - Docker bypasses firewalld — Docker inserts its own nftables/iptables
chains.
firewall-cmdrules don't filter published container ports. Bind containers to127.0.0.1or use theDOCKER-USERchain / firewalld's docker integration. --add-portworks but--add-service"unknown service" — the service XML doesn't exist;--get-services, or define one in/etc/firewalld/services/.- Two interfaces, one firewalled and one not — the second interface is in
the default zone (often
public) which you didn't realise; explicitly assign every interface. - Rich rule syntax rejected — quoting; the whole rule is one shell-quoted string. Build it carefully, or use a here-doc.
Related tools and reading¶
- On-site: Port Checker, Subnet Calculator.
- Related posts: nftables for iptables users, fail2ban in practice.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Linux & Sysadmin
resolv.conf and systemd-resolved
On modern distros /etc/resolv.conf points at a stub resolver managed by systemd-resolved. Editing it directly usually does nothing — here's the real model.
October 24, 2026Tuning Open File Limits on Linux
Four different limits control how many files a process can open. 'Too many open files' means you're hitting one specific one — here's how to find and raise it.
October 24, 2026ss and conntrack for Connection Debugging
ss shows you every socket and its send/receive queues; conntrack shows what the firewall is tracking. Together they explain most 'connection refused' and 'no buffer space' problems.
October 24, 2026