firewalld Zones and Rich Rules in Practice
Assign interfaces to firewalld zones, open services, write rich rules for source-based and rate-limited policies, and manage runtime vs permanent config.
Before you start
- ▸sudo or root access on the target system
- ▸Basic familiarity with network interfaces and TCP/IP ports
- ▸firewalld package installed (pre-installed on Fedora, RHEL, Rocky, openSUSE)
firewalld ships on every Fedora, RHEL, Rocky, and openSUSE system by default, and it is available on Debian and Ubuntu. Its zone model lets you assign different trust levels to different network interfaces and apply fine-grained rules without rewriting a monolithic ruleset. This guide covers the day-to-day operations you will actually reach for: moving interfaces between zones, allowing services, writing rich rules, and making changes stick permanently.
How Zones Work
A zone is a named policy bucket. Every active network interface lives in exactly one zone. firewalld ships with several predefined zones; the ones you will use most are:
- public – default for most interfaces; untrusted, allows selected inbound services only
- internal – for LAN-side interfaces; more permissive by default
- trusted – accepts all connections; use sparingly
- drop – silently drops all inbound packets; outbound allowed
- dmz – limited inbound, no forwarding to internal network
Under the hood, firewalld translates its zone configuration into nftables rules (firewalld 0.9+ uses nftables as the backend by default on modern distros). You can confirm the backend with firewall-cmd --info-policy or by inspecting /etc/firewalld/firewalld.conf.
Installation and Status
On systems where firewalld is not pre-installed:
# Debian / Ubuntu
sudo apt install firewalld
# Arch
sudo pacman -S firewalld
Enable and start the service:
sudo systemctl enable --now firewalld
Check overall status and which zones are active:
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
Sample output (yours will vary):
public
interfaces: eth0
internal
interfaces: eth1
Per-Interface Zone Assignment
Query and change a zone at runtime
Find which zone an interface is currently in, then move it:
sudo firewall-cmd --get-zone-of-interface=eth1
sudo firewall-cmd --zone=internal --change-interface=eth1
Runtime changes take effect immediately but are lost on the next firewalld reload or system reboot. Always add --permanent for changes you want to keep, then reload:
sudo firewall-cmd --permanent --zone=internal --change-interface=eth1
sudo firewall-cmd --reload
Assigning zones via NetworkManager (recommended)
NetworkManager can persist zone assignments independently of firewalld restarts. Set the zone on a connection profile:
nmcli connection modify "Wired connection 1" connection.zone internal
nmcli connection up "Wired connection 1"
NetworkManager writes the zone into the connection profile, so it survives reboots without a separate --permanent flag.
Allowing and Removing Services
firewalld ships with pre-defined service definitions (XML files under /usr/lib/firewalld/services/) that cover common ports and protocols. List available services:
sudo firewall-cmd --get-services
Allow HTTPS in the public zone permanently:
sudo firewall-cmd --permanent --zone=public --add-service=https
sudo firewall-cmd --reload
Verify it is active:
sudo firewall-cmd --zone=public --list-services
Remove a service (here, removing the default dhcpv6-client from public if you do not need it):
sudo firewall-cmd --permanent --zone=public --remove-service=dhcpv6-client
sudo firewall-cmd --reload
To allow a raw port instead of a named service:
sudo firewall-cmd --permanent --zone=public --add-port=8443/tcp
sudo firewall-cmd --reload
Runtime vs Permanent: The Two-Layer Model
This is the single biggest source of confusion for new firewalld users. firewalld maintains two separate configurations:
- Runtime – what is active right now in the kernel; lost on
firewall-cmd --reloador reboot - Permanent – stored in XML files under
/etc/firewalld/; loaded on startup or after--reload
A safe workflow for testing changes:
- Apply the rule without
--permanentto test it immediately. - Confirm it works as expected.
- Re-run the same command with
--permanent, then--reload.
Alternatively, copy the entire running config to permanent in one shot:
sudo firewall-cmd --runtime-to-permanent
Use this carefully — it captures everything in the runtime config, including any test rules you forgot about.
Rich Rules
Rich rules give you per-source, per-destination, logging, and rate-limiting control that simple service entries cannot express. The syntax follows a structured English-like grammar.
Allow a specific source IP to reach a service
sudo firewall-cmd --permanent --zone=public \
--add-rich-rule='rule family="ipv4" source address="203.0.113.42" service name="ssh" accept'
Block a source subnet
sudo firewall-cmd --permanent --zone=public \
--add-rich-rule='rule family="ipv4" source address="192.0.2.0/24" drop'
Rate-limit SSH connections to prevent brute force
Accept SSH but limit each source to 3 new connections per minute:
sudo firewall-cmd --permanent --zone=public \
--add-rich-rule='rule family="ipv4" service name="ssh" limit value="3/m" accept'
Log and reject a specific port
sudo firewall-cmd --permanent --zone=public \
--add-rich-rule='rule family="ipv4" port port="23" protocol="tcp" log prefix="TELNET-ATTEMPT" level="warning" reject'
After adding permanent rich rules, always reload:
sudo firewall-cmd --reload
List all rich rules in a zone:
sudo firewall-cmd --zone=public --list-rich-rules
Inspecting Zone Configuration
Get a full summary of everything active in a zone:
sudo firewall-cmd --zone=public --list-all
Compare runtime vs permanent configs side by side:
sudo firewall-cmd --zone=public --list-all
sudo firewall-cmd --permanent --zone=public --list-all
If the two outputs differ, you have uncommitted runtime changes or permanent rules not yet loaded.
Verification
After making and reloading changes, confirm the nftables rules firewalld generated actually contain what you expect:
sudo nft list ruleset | grep -A5 'firewalld'
Test connectivity from another host using nmap or nc to confirm ports are open or closed as intended. For SSH rate-limiting, a quick local test:
for i in {1..5}; do ssh -o ConnectTimeout=2 user@server echo ok; done
You should see connections drop after the configured limit.
Troubleshooting
Changes not surviving reboot
You forgot --permanent. Run sudo firewall-cmd --runtime-to-permanent if the runtime state is correct, or re-add rules with --permanent followed by --reload.
Interface keeps reverting to the wrong zone
NetworkManager is overriding firewalld on reconnect. Set the zone in the NetworkManager connection profile with nmcli connection modify as shown above, not just in firewalld.
Rich rule syntax errors
firewall-cmd will print an error and refuse to add a malformed rule. Double-check quoting — the entire rule must be a single shell argument (wrap in single quotes), and inner strings use double quotes.
firewalld and Docker / libvirt conflict
Docker and libvirt both manipulate nftables/iptables directly and can conflict with firewalld's backend. On Fedora/RHEL, libvirt integrates cleanly. For Docker, either use the iptables=false Docker option and manage rules manually, or accept that Docker adds its own chains. Do not mix firewalld zones with manually created nftables tables without understanding the evaluation order.
Frequently asked questions
- What is the difference between adding a rule at runtime and adding it permanently?
- Runtime rules take effect immediately in the kernel but are wiped on the next firewall-cmd --reload or reboot. Permanent rules are saved to XML files under /etc/firewalld/ and loaded at startup or after a reload, but they do not affect the running kernel until you reload.
- Can I use firewalld and nftables commands at the same time?
- You can inspect nftables with nft list ruleset, but directly adding nftables rules alongside firewalld is risky — firewalld will overwrite or conflict with manually added rules on the next reload. If you need custom nftables rules, use firewalld direct rules or a firewalld policy, or disable firewalld and manage nftables yourself.
- Why does my interface keep returning to the public zone after a network reconnect?
- NetworkManager re-applies its connection profile on every reconnect, overriding the firewalld runtime assignment. Set the zone persistently in the NetworkManager connection profile using nmcli connection modify with connection.zone.
- How do I create a custom service definition so I can reference it by name?
- Copy an existing XML file from /usr/lib/firewalld/services/ to /etc/firewalld/services/, rename it, and edit the port and protocol fields. After running firewall-cmd --reload, the new service name is available for use with --add-service.
- Does firewalld support IPv6 rich rules?
- Yes. Specify family="ipv6" in the rich rule instead of family="ipv4", or omit the family attribute entirely to match both IPv4 and IPv6 traffic.
Related guides
Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.