Server

firewalld: Zones and Rich Rules

By Prabath Thalangama· October 24, 2026· 4 min read
#linux#firewall#security

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 (public by default).
  • A source (IP/CIDR) can also be bound to a zone; source binding takes precedence over interface binding for matching traffic.
  • trusted allows everything; drop silently 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 --permanent the SSH allow first.
  • Docker bypasses firewalld — Docker inserts its own nftables/iptables chains. firewall-cmd rules don't filter published container ports. Bind containers to 127.0.0.1 or use the DOCKER-USER chain / firewalld's docker integration.
  • --add-port works 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.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.