Getting Started with firewalld in 5 Minutes
My first encounter with firewalld was when I switched from Ubuntu to CentOS 7 to manage a production server. Coming from ufw, firewalld felt unfamiliar at first. The concept of “zones” sounded complicated. But after a few months of real-world use, I realized it’s far more convenient than plain iptables — especially when a server has 2–3 NICs that need different policies.
Here are 4 commands to run right away:
# Check firewalld status
sudo systemctl status firewalld
# Enable firewalld and allow it to start at boot
sudo systemctl enable --now firewalld
# View the current zone and attached interfaces
sudo firewall-cmd --get-active-zones
# View all currently applied rules
sudo firewall-cmd --list-all
Running these 4 commands gives you a clear picture of your server’s firewall state. The default zone is usually public, and the output of --list-all shows which services are currently open.
Opening Ports or Services Immediately
# Allow HTTP and HTTPS
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
# Open a custom port (e.g., Node.js app running on port 3000)
sudo firewall-cmd --permanent --add-port=3000/tcp
# Apply changes — required after adding permanent rules
sudo firewall-cmd --reload
Pay attention to the --permanent flag: without it, rules only survive until the next reboot. --reload activates permanent rules immediately — no need to restart the entire service.
Understanding Zones — The Heart of firewalld
Zones are what set firewalld completely apart from plain iptables. Instead of writing rules by chain (INPUT/OUTPUT/FORWARD), you assign interfaces to zones — each zone has its own policy, managed independently.
Default zones:
- drop — Reject all traffic, no response sent
- block — Reject all traffic, sends ICMP rejection
- public — Default zone, only trusts specific services
- external — External-facing interface, with NAT masquerade
- internal — Internal network, more trusted than public
- trusted — Allow all traffic
- dmz — DMZ area, restricted inbound traffic
# List all zones
sudo firewall-cmd --get-zones
# Change the default zone
sudo firewall-cmd --set-default-zone=public
# Assign an interface to a specific zone (e.g., internal NIC)
sudo firewall-cmd --permanent --zone=internal --add-interface=eth1
sudo firewall-cmd --reload
Managing Predefined Services
firewalld ships with over 60 predefined services, stored in /usr/lib/firewalld/services/. Each XML file declares the corresponding ports and protocols — much more convenient than memorizing port numbers manually.
# List available services
sudo firewall-cmd --get-services
# Add/remove services
sudo firewall-cmd --permanent --add-service=mysql
sudo firewall-cmd --permanent --remove-service=telnet
# View services allowed in the public zone
sudo firewall-cmd --zone=public --list-services
If the service you need doesn’t have a predefined definition, create a custom XML file:
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>MyApp</short>
<description>Custom application on port 8080</description>
<port protocol="tcp" port="8080"/>
</service>
# Save the file to /etc/firewalld/services/myapp.xml and reload
sudo firewall-cmd --reload
sudo firewall-cmd --permanent --add-service=myapp
sudo firewall-cmd --reload
Advanced: Rich Rules and Port Forwarding
Rich Rules — More Granular Control
After CentOS 8 reached EOL, I had to urgently migrate 5 servers to Rocky Linux within a week. That was the first time I really dug into rich rules — several servers needed IP whitelisting for SSH, and plain --add-source wasn’t flexible enough.
# Only allow SSH from a specific IP
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.10" service name="ssh" accept'
# Block an IP entirely
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.5" reject'
# Rate-limit SSH to prevent brute force (max 5 connections/minute)
sudo firewall-cmd --permanent --add-rich-rule='rule service name="ssh" limit value="5/m" accept'
# Log traffic before dropping (for debugging or auditing)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="http" log prefix="HTTP-BLOCK" level="warning" drop'
sudo firewall-cmd --reload
Port Forwarding
Running an app on port 8080 but want users to access port 80 without setting up Nginx just for forwarding? Port forwarding handles it cleanly:
# Forward port 80 to 8080 on the same machine
sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
# Forward to another machine on the internal network
sudo firewall-cmd --permanent --add-forward-port=port=3306:proto=tcp:toaddr=192.168.1.20:toport=3306
sudo firewall-cmd --reload
Practical Tips from Production
1. Test Temporarily Before Committing Permanently
I learned this the hard way: tested a new firewall rule, forgot the --permanent flag, thought it was saved — then it vanished after a reboot. Fortunately that server wasn’t critical.
# Step 1: Test without permanent — rule disappears after reboot
sudo firewall-cmd --add-service=https
# Check connectivity to see if it works
# Step 2: If it works, then save permanently
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
2. Back Up Config Before Major Changes
# firewalld config is stored in /etc/firewalld/zones/
ls /etc/firewalld/zones/
# Back up the entire config with a timestamp
sudo cp -r /etc/firewalld /etc/firewalld.bak.$(date +%Y%m%d)
# Restore if needed
sudo cp -r /etc/firewalld.bak.20240115/* /etc/firewalld/
sudo firewall-cmd --reload
3. Debugging When Rules Don’t Behave as Expected
# Compare runtime vs permanent (these two states often diverge)
sudo firewall-cmd --list-all # runtime
sudo firewall-cmd --list-all --permanent # permanent
# View firewalld logs
sudo journalctl -u firewalld -f
# Panic mode — block everything immediately (use when under attack)
sudo firewall-cmd --panic-on
sudo firewall-cmd --panic-off
4. Combining with SELinux — Don’t Forget This Layer
firewalld and SELinux are two completely independent security layers. Opening a port in firewalld doesn’t automatically allow a service to bind to that port if SELinux is blocking it. I once spent 30 minutes debugging why traffic couldn’t get through even though the firewall was open — then remembered SELinux. Ever since, whenever I hit a network issue, I check both layers from the start:
# Example: nginx running on port 8080 requires both steps
# 1. Open firewalld
sudo firewall-cmd --permanent --add-port=8080/tcp
# 2. Allow in SELinux (if enforcing)
sudo semanage port -a -t http_port_t -p tcp 8080
sudo firewall-cmd --reload
5. Script to Sync Firewall Rules Across Multiple Servers
5 servers running different firewall configs is a nightmare to audit. After the Rocky Linux migration, I wrote a small script to ensure all servers have consistent rules:
#!/bin/bash
# sync-firewall.sh — Apply consistent firewall rules across multiple servers
SERVERS=("server1.example.com" "server2.example.com" "server3.example.com")
RULES=(
"--add-service=http"
"--add-service=https"
"--add-service=ssh"
"--add-port=8080/tcp"
)
for server in "${SERVERS[@]}"; do
echo "Configuring $server..."
for rule in "${RULES[@]}"; do
ssh root@$server "firewall-cmd --permanent $rule"
done
ssh root@$server "firewall-cmd --reload"
echo "Done: $server"
done
Three things to remember when using firewalld: zones determine policy, --permanent is what actually saves rules, and firewalld is just one layer — SELinux still has veto power above it. After migrating 5 CentOS 8 servers to Rocky Linux, the firewalld configs transferred over without a single line needing to change.

