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.
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:
- Source IP/CIDR — if the packet's source matches a zone's source list, that zone wins.
- Interface — if no source match, the zone bound to the incoming interface is used.
- Default zone — fallback if nothing else matches.
Built-in zones you'll actually use:
| Zone | Default policy | Typical use |
|---|---|---|
| drop | Drop all inbound, no reply | Hostile networks |
| block | Reject all inbound | Controlled rejection |
| public | Reject except allowed services | Internet-facing NICs |
| internal | More permissive | LAN segments |
| trusted | Accept everything | Loopback, 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:
- Make the runtime change (test it).
- Confirm it works.
- Re-run with
--permanentto 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.
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
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.