Managing Services with firewalld on CentOS/RHEL: A Practical Guide

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

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.

Share: