firewalld Explained: Zones, Services, and Runtime vs Permanent
If you arrive on Fedora, RHEL, or one of their derivatives from Ubuntu, your firewall commands stop working. ufw is not installed and firewalld is, and its model is different enough that the transition is genuinely confusing.
Both are front ends to nftables. The difference is how they organise rules.
Zones
A zone is a named set of rules, and interfaces or source addresses are assigned to zones.
The point is a machine with more than one network context. A laptop should treat the coffee shop wifi differently from the home network. A router should treat its LAN side differently from its WAN side. With a linear rule list you encode that by naming interfaces in every rule; with zones you assign the interface once.
The predefined zones, from most to least restrictive:
- drop discards everything inbound with no reply
- block rejects inbound with an ICMP refusal
- public allows only explicitly permitted services, the usual default
- external like public, with masquerading enabled for routing
- internal and home allow more, intended for trusted networks
- work sits between them
- trusted accepts everything
firewall-cmd --get-default-zone
firewall-cmd --get-active-zones
firewall-cmd --list-all
firewall-cmd --list-all-zones | less
Runtime versus permanent
This is the thing that catches everyone.
firewalld maintains two configurations. The runtime config is what is currently enforced. The permanent config is what loads at boot or on reload.
# Applies now, lost on reload or reboot
sudo firewall-cmd --add-service=http
# Written to permanent config, does NOT apply yet
sudo firewall-cmd --permanent --add-service=http
# Make permanent config take effect
sudo firewall-cmd --reload
It is deliberate rather than annoying. You can open something risky, test it, and know a reload undoes it if you locked yourself out. The common working pattern is to add it both ways:
sudo firewall-cmd --add-service=http
sudo firewall-cmd --permanent --add-service=http
Or add permanently and reload, which is cleaner:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --reload
Services rather than ports
firewalld ships named service definitions so you open https rather than remembering 443.
firewall-cmd --get-services # everything defined
sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --remove-service=cockpit
Ports directly when there is no service definition:
sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --permanent --add-port=51820/udp # WireGuard
sudo firewall-cmd --permanent --add-port=8000-8100/tcp
Assigning interfaces and sources
# Put an interface in a zone
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1
# Or bind a source subnet to a zone
sudo firewall-cmd --permanent --zone=internal --add-source=192.168.1.0/24
# Then open things only for that zone
sudo firewall-cmd --permanent --zone=internal --add-service=samba
sudo firewall-cmd --reload
That last pattern is the one worth learning: Samba open to the LAN, closed to everything else, expressed without repeating the subnet in every rule. Our Samba and NFS share builder covers the server side.
Rich rules
For conditions the simple commands cannot express:
# Allow SSH only from one subnet
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" service name="ssh" accept'
# Rate limit, blunting brute force
sudo firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="10/m" accept'
# Log and drop from a specific host
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.5" log prefix="blocked " level="info" drop'
Verbose, and readable months later, which is the tradeoff being made.
Masquerading and forwarding
For a machine acting as a gateway:
sudo firewall-cmd --permanent --zone=external --add-masquerade
sudo firewall-cmd --permanent --add-forward-port=port=8080:proto=tcp:toport=80:toaddr=192.168.1.10
sudo firewall-cmd --reload
Masquerade alone is not enough; IP forwarding must be enabled in the kernel too. See sysctl.
Locking yourself out, and not
Test before committing:
# Panic mode drops everything, including your session. Know how to reach the console first.
sudo firewall-cmd --panic-on
sudo firewall-cmd --panic-off
# Safer: make a change in runtime only, and reload to revert if it goes wrong
sudo firewall-cmd --remove-service=ssh # runtime only
# if that was a mistake:
sudo firewall-cmd --reload # permanent config restores ssh
That is the runtime and permanent split earning its keep.
Checking what is really there
sudo firewall-cmd --list-all --zone=public
sudo nft list ruleset | head -50 # the rules firewalld actually generated
sudo journalctl -u firewalld -n 30
Reading the generated nftables ruleset is instructive, and it is the definitive answer when firewalld’s own output does not match what you observe.
Frequently Asked Questions
What is a firewalld zone?
A named set of rules applied to interfaces or source addresses assigned to it. The idea is that the same machine can treat its LAN interface as trusted and its internet-facing interface as hostile, without writing rules that carry interface names throughout.
Why did my rule disappear after a reboot?
Because you did not add --permanent. firewalld keeps a runtime configuration and a permanent one. Changes without the flag apply immediately and are lost on reload, which is a safety feature and the most common confusion.
How do I apply a permanent change without rebooting?
Add the rule with --permanent, then run firewall-cmd --reload. Alternatively add it twice, once with the flag and once without, so it takes effect now and persists later.
What is the difference between firewalld and ufw?
Both are front ends to nftables. ufw is a simple linear rule list, which suits a single-purpose server. firewalld uses zones and named services, which suits machines with multiple interfaces or changing network locations, such as laptops.
Which zone am I actually in?
Run firewall-cmd --get-active-zones, which lists each active zone and the interfaces or sources bound to it. Anything not explicitly assigned falls into the default zone, which you can see with firewall-cmd --get-default-zone.
Can I write raw nftables rules in firewalld?
Yes, through rich rules for structured cases and direct rules for passing syntax straight through. Direct rules are a maintenance liability though, so exhaust zones, services, and rich rules first.