$linuxjunkies
>

How to Set Up a Firewall with firewalld

Learn how to configure firewalld using zones, services, rich rules, and source bindings — with a clear explanation of runtime vs permanent changes.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Root or sudo access on the target system
  • Basic familiarity with network interfaces and IP addressing
  • SSH access confirmed working before making firewall changes

firewalld is the default firewall management layer on Fedora, RHEL, Rocky Linux, AlmaLinux, and openSUSE. It wraps nftables (or legacy iptables on older systems) in a zone-based model that separates policy from interface assignment. Unlike editing raw nftables rules, firewalld lets you make changes at runtime, inspect them, then commit only what works — without a typo dropping your SSH session permanently.

How firewalld Thinks: Zones, Sources, and Services

Every packet is matched to exactly one zone. The match order is:

  1. Source IP/CIDR — if the packet's source matches a zone's source list, that zone wins.
  2. Interface — if no source match, the zone bound to the incoming interface is used.
  3. Default zonefallback if nothing else matches.

Built-in zones you'll actually use:

ZoneDefault policyTypical use
dropDrop all inbound, no replyHostile networks
blockReject all inboundControlled rejection
publicReject except allowed servicesInternet-facing NICs
internalMore permissiveLAN segments
trustedAccept everythingLoopback, VPN tun

Installation and Service Status

Install firewalld

On Fedora/RHEL family it is already installed. On Debian/Ubuntu and Arch it needs adding:

# Debian / Ubuntu
sudo apt install firewalld
# Arch Linux
sudo pacman -S firewalld

If ufw is active on Ubuntu, disable it first to avoid conflicts:

sudo ufw disable
sudo systemctl disable --now ufw

Enable and start the daemon

sudo systemctl enable --now firewalld
sudo firewall-cmd --state

Output should read running.

Runtime vs Permanent — the Core Concept

Every firewall-cmd change is runtime-only by default. It takes effect immediately but disappears on reload or reboot. Add --permanent to write the rule to disk. The safest workflow is:

  1. Make the runtime change (test it).
  2. Confirm it works.
  3. Re-run with --permanent to persist it.

Alternatively, add --permanent first, then reload:

sudo firewall-cmd --reload

Warning: --reload drops all runtime-only rules. Use it only after you're satisfied with permanent config, or you'll lose temporary additions.

Step 1 — Assign Interfaces to Zones

Find your interface names:

ip -brief link show

Check what zone an interface is currently in:

sudo firewall-cmd --get-active-zones

Assign an interface permanently (replace eth0 with your NIC name):

sudo firewall-cmd --zone=public --change-interface=eth0 --permanent
sudo firewall-cmd --reload

On NetworkManager-managed systems, firewalld reads the zone from the connection profile. You can also set it there:

sudo nmcli connection modify "Wired connection 1" connection.zone public

Step 2 — Allow Services

firewalld ships with named service definitions (XML files under /usr/lib/firewalld/services/) for hundreds of applications. List them:

sudo firewall-cmd --get-services

Allow SSH and HTTPS in the public zone permanently:

sudo firewall-cmd --zone=public --add-service=ssh --permanent
sudo firewall-cmd --zone=public --add-service=https --permanent

To allow a raw port instead of a named service:

sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent

Remove a service (runtime example — add --permanent to persist removal):

sudo firewall-cmd --zone=public --remove-service=dhcpv6-client

Step 3 — Create a Custom Service Definition

When your app uses a non-standard port, define a reusable service rather than using raw port rules:

sudo firewall-cmd --new-service=myapp --permanent
sudo firewall-cmd --service=myapp --add-port=9000/tcp --permanent
sudo firewall-cmd --service=myapp --set-description="My custom app" --permanent
sudo firewall-cmd --zone=public --add-service=myapp --permanent
sudo firewall-cmd --reload

Custom service files are saved to /etc/firewalld/services/ and survive package upgrades.

Step 4 — Use Rich Rules for Granular Control

Rich rules let you match on source IP, destination, port, and action in one expression. They live within a zone and are evaluated before simple service/port rules.

Allow SSH only from a management subnet:

sudo firewall-cmd --zone=public \
  --add-rich-rule='rule family=ipv4 source address=192.168.10.0/24 service name=ssh accept' \
  --permanent

Block a specific abusive IP outright:

sudo firewall-cmd --zone=public \
  --add-rich-rule='rule family=ipv4 source address=203.0.113.45 drop' \
  --permanent

Rate-limit new connections to a service (useful against brute-force):

sudo firewall-cmd --zone=public \
  --add-rich-rule='rule service name=ssh accept limit value=5/m' \
  --permanent

Log and drop a subnet (logging goes to syslog/journald):

sudo firewall-cmd --zone=public \
  --add-rich-rule='rule family=ipv4 source address=10.0.99.0/24 log prefix="DROP-MGMT" level=warning drop' \
  --permanent

Rich rules inside a zone are ordered: drop/reject rules are evaluated before accept rules when using the same specificity. To see all rich rules in a zone:

sudo firewall-cmd --zone=public --list-rich-rules

Step 5 — Source-Based Zone Binding

Assign all traffic from an IP range to a specific zone regardless of interface. Useful for VPN or management networks:

sudo firewall-cmd --zone=internal --add-source=10.10.0.0/16 --permanent
sudo firewall-cmd --reload

Now any packet from that subnet hits the internal zone rules, not public.

Step 6 — Review and Verify the Full Configuration

Inspect everything active right now:

sudo firewall-cmd --list-all

Check a specific zone's permanent config:

sudo firewall-cmd --zone=public --list-all --permanent

List all zones with their permanent settings:

sudo firewall-cmd --list-all-zones --permanent

After confirming runtime rules look right, make them permanent and reload:

sudo firewall-cmd --runtime-to-permanent
sudo firewall-cmd --reload

--runtime-to-permanent copies all current runtime rules into permanent storage in one shot — handy after an interactive tuning session.

Troubleshooting

Changes aren't sticking after reboot

You made runtime changes without --permanent and didn't run --runtime-to-permanent. Always verify with --list-all --permanent before rebooting.

firewalld is running but traffic isn't blocked

Another tool (Docker, libvirt, a raw nftables script) may be inserting rules at a lower level. firewalld uses its own nftables table; check for conflicts:

sudo nft list ruleset

SSH locked out after a change

If you're on a physical console or out-of-band access, run sudo firewall-cmd --reload to revert runtime to permanent state, or sudo systemctl stop firewalld to clear all rules temporarily.

Service name not found

Check spelling: sudo firewall-cmd --get-services | tr ' ' '\n' | grep <keyword>. If a service truly isn't listed, create a custom definition as shown in Step 3.

Rich rules not matching as expected

Rich rules within a zone don't have a user-defined order. If two rich rules conflict, restructure using separate zones with source bindings instead.

tested on:Fedora 40Rocky 9.3Ubuntu 24.04Arch 2024.05

Frequently asked questions

What is the difference between --permanent and runtime rules?
Runtime rules take effect immediately but are lost on reload or reboot. Permanent rules are written to disk and loaded on startup or after firewall-cmd --reload. Use both together: test at runtime, then persist with --permanent.
Can I use firewalld alongside Docker or libvirt?
Yes, but with care. Both Docker and libvirt insert their own nftables or iptables rules. Docker provides a DOCKER-USER chain that firewalld can target; libvirt registers its own zone. Check your platform's documentation and inspect the full ruleset with sudo nft list ruleset to spot conflicts.
How do I temporarily open a port for testing without making it permanent?
Run firewall-cmd --zone=public --add-port=8080/tcp without --permanent. The rule is active immediately and disappears on the next firewall-cmd --reload or system reboot.
Does firewalld use iptables or nftables under the hood?
On modern systems (RHEL 8+, Fedora 32+, recent Debian/Ubuntu) firewalld uses nftables as its backend by default. You can verify or change the backend in /etc/firewalld/firewalld.conf via the FirewallBackend directive. The legacy iptables backend is still available but deprecated.
How do I completely reset firewalld to defaults?
Run sudo firewall-cmd --complete-reload to reload both permanent config and kernel state, or delete custom files from /etc/firewalld/ and restart the service to fall back to compiled-in defaults.

Related guides