Red Hat Enterprise Linux 9 uses firewalld as its default firewall management daemon, backed by nftables as the kernel netfilter framework (replacing iptables which was the default in RHEL 7 and earlier). firewalld provides a zone-based model where each network interface is assigned to a trust zone — public, internal, dmz, trusted, and more — and rules are applied per-zone. This architecture makes it straightforward to handle servers with multiple network interfaces, such as a server with a public-facing NIC and a private backend NIC carrying database traffic.
This guide covers everything you need to manage firewalld on RHEL 9: understanding zones, adding and removing services and ports, writing rich rules for more nuanced control, configuring masquerade and NAT for routing, port forwarding, and viewing the underlying nftables ruleset that firewalld generates.
All commands should be run as root or with sudo.
Prerequisites
- RHEL 9 server with root or sudo access
firewalldpackage installed — included in the default RHEL 9 base installation- At least one network interface
- Active SSH session — do not disconnect before verifying SSH remains accessible after each firewall change
Step 1 — Install and Enable firewalld
firewalld is installed by default on RHEL 9. If it was removed, reinstall it:
dnf install -y firewalld
systemctl enable --now firewalld
Confirm the firewall is running:
firewall-cmd --state
Expected output: running
Step 2 — Understand Zones
List all available zones and see which zone each interface belongs to:
firewall-cmd --get-zones
firewall-cmd --get-active-zones
The most commonly used zones are:
- public — for untrusted networks (internet-facing). Only explicitly allowed services are permitted. This is the default zone.
- internal — for internal networks where other machines are more trusted.
- dmz — for servers in a demilitarised zone: limited inbound access from the internet, limited outbound to internal.
- trusted — all connections accepted. Use only for loopback or completely trusted networks (e.g., an internal admin VLAN).
- drop — all incoming packets silently dropped, no response sent. Maximum stealth mode.
- block — incoming packets rejected with an ICMP “host prohibited” response.
View the full configuration of the public zone:
firewall-cmd --zone=public --list-all
Step 3 — Assign a Network Interface to a Zone
# View current interface zone assignment
firewall-cmd --get-zone-of-interface=eth0
# Assign an interface to the public zone permanently
firewall-cmd --permanent --zone=public --change-interface=eth0
# Assign a backend interface to the internal zone
firewall-cmd --permanent --zone=internal --change-interface=eth1
firewall-cmd --reload
After reload, verify the assignments:
firewall-cmd --get-active-zones
Step 4 — Add and Remove Services
firewalld ships with pre-defined service definitions for hundreds of applications in /usr/lib/firewalld/services/. Each file is an XML descriptor specifying port and protocol. Using named services is preferred over raw ports because the names are self-documenting and survive port changes.
# View all available services
firewall-cmd --get-services
# Add SSH and HTTPS to the public zone permanently
firewall-cmd --permanent --zone=public --add-service=ssh
firewall-cmd --permanent --zone=public --add-service=https
# Remove HTTP if you serve only HTTPS
firewall-cmd --permanent --zone=public --remove-service=http
# Apply all permanent changes
firewall-cmd --reload
# Verify
firewall-cmd --zone=public --list-services
Step 5 — Add and Remove Ports
When a pre-defined service does not exist for an application, add the port directly:
# Allow a custom application on TCP port 8080
firewall-cmd --permanent --zone=public --add-port=8080/tcp
# Allow a UDP port range for a media streaming server
firewall-cmd --permanent --zone=public --add-port=10000-20000/udp
# Remove a port
firewall-cmd --permanent --zone=public --remove-port=8080/tcp
firewall-cmd --reload
firewall-cmd --zone=public --list-ports
Step 6 — Create a Custom Service Definition
If you frequently open the same custom port, create a named service definition so it is self-documenting:
vi /etc/firewalld/services/myapp.xml
MyApp
Custom application running on port 8080 TCP
firewall-cmd --reload
firewall-cmd --permanent --zone=public --add-service=myapp
firewall-cmd --reload
Step 7 — Rich Rules for Advanced Control
Rich rules allow combining source IP, destination, port, and action in a single rule. They are essential for IP allowlisting and rate limiting.
# Allow SSH only from a specific IP range
firewall-cmd --permanent --zone=public
--add-rich-rule='rule family="ipv4" source address="203.0.113.0/24" service name="ssh" accept'
# Block a specific IP entirely
firewall-cmd --permanent --zone=public
--add-rich-rule='rule family="ipv4" source address="198.51.100.5" drop'
# Rate-limit new HTTPS connections to 30 per minute
firewall-cmd --permanent --zone=public
--add-rich-rule='rule service name="https" accept limit value="30/m"'
# Log rejected connections for audit
firewall-cmd --permanent --zone=public
--add-rich-rule='rule protocol value="tcp" reject log prefix="REJECT-TCP " level="warning"'
firewall-cmd --reload
firewall-cmd --zone=public --list-rich-rules
Step 8 — Enable IP Masquerade (NAT for Routing)
If your server acts as a router or gateway, enable masquerade on the outbound zone so that traffic from internal hosts is NAT-translated to the server’s public IP:
firewall-cmd --permanent --zone=public --add-masquerade
firewall-cmd --reload
firewall-cmd --zone=public --query-masquerade
Also enable IP forwarding in the kernel:
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-ip-forward.conf
sysctl -p /etc/sysctl.d/99-ip-forward.conf
Step 9 — Port Forwarding
Forward incoming connections on one port to another port or host:
# Forward port 80 locally to port 8080
firewall-cmd --permanent --zone=public
--add-forward-port=port=80:proto=tcp:toport=8080
# Forward port 80 to a different server on the internal network
firewall-cmd --permanent --zone=public
--add-forward-port=port=80:proto=tcp:toport=80:toaddr=192.168.1.50
firewall-cmd --reload
firewall-cmd --zone=public --list-forward-ports
Step 10 — Temporary Runtime Rules for Testing
Rules added without --permanent are runtime-only and disappear on reload or reboot. Use this for testing before committing:
# Add a temporary rule that auto-expires after 5 minutes
firewall-cmd --zone=public --add-port=9000/tcp --timeout=300
# Compare runtime rules vs permanent rules
firewall-cmd --zone=public --list-all # runtime
firewall-cmd --zone=public --list-all --permanent # permanent
Step 11 — View the Underlying nftables Rules
firewalld generates nftables rules under the hood. You can inspect them:
nft list ruleset
This shows all chains, tables, and rules. Useful for debugging and understanding exactly what is in the kernel.
Verification Checklist
# Firewall state
firewall-cmd --state
# Active zones and interfaces
firewall-cmd --get-active-zones
# Full public zone config (runtime)
firewall-cmd --zone=public --list-all
# Full dump of all zones (permanent)
firewall-cmd --list-all-zones --permanent
# Confirm SSH is still accessible from your IP
ss -tlnp | grep :22
Troubleshooting
- Rule added but not taking effect — did you run
firewall-cmd --reloadafter adding a--permanentrule? Without reload, permanent rules are written to disk but not activated in the running kernel ruleset. - Service blocked despite allowing it — check SELinux:
ausearch -m avc -ts recent. SELinux and firewalld are independent layers; both must allow the connection. - Cannot connect after adding a rule — confirm the interface is in the correct zone:
firewall-cmd --get-active-zones. Also confirm the service is actually listening:ss -tlnp | grep PORT. - Docker or Podman containers cannot reach the internet — do not disable firewalld for containers. Docker and Podman manage their own firewalld rules automatically via the
dockerzone andDOCKER-USERchain.
Security Considerations
- Default-deny is the correct security posture. Only open ports that are actively needed.
- Restrict SSH source IPs using rich rules. Never leave SSH open to
0.0.0.0/0in production if your admin IP is static. - Audit your ruleset quarterly:
firewall-cmd --list-all-zonesand remove stale entries from decommissioned services. - Never disable
firewalldin favour of rawiptableson RHEL 9. The two systems conflict with each other and can leave the server unprotected during transitions.
Conclusion
You now have full command of firewalld on RHEL 9: zone management, service and port rules, IP-based rich rules, NAT masquerade, port forwarding, and inspection of the underlying nftables ruleset. A correctly configured firewall is the first network-layer defence for any server and the foundation for every other security control.
Next steps: How to Configure Fail2Ban to Protect SSH on RHEL 9, How to Set Up WireGuard VPN on RHEL 9, and How to Configure nftables Directly on RHEL 9.