$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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

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 --reload or reboot
  • Permanent – stored in XML files under /etc/firewalld/; loaded on startup or after --reload

A safe workflow for testing changes:

  1. Apply the rule without --permanent to test it immediately.
  2. Confirm it works as expected.
  3. 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.

tested on:Fedora 40Rocky 9Ubuntu 24.04Arch rolling

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