firewalld Basics
- What firewalld is
- Zones — which zone are you in?
- Checking current rules
- Opening a service
- Opening a specific port
- Removing a rule
- Rich rules — source-based rules
- Runtime vs permanent
- Reloading firewalld
- Troubleshooting blocked traffic
- Managing firewalld with Ansible
- Masquerading and port forwarding
- Interface management
What firewalld is
firewalld is the default firewall management layer on RHEL/CentOS/Rocky Linux and Fedora. It manages iptables (or nftables) rules for you through a higher-level interface based on zones and services.
You interact with firewalld using the firewall-cmd CLI tool. Changes can be runtime-only (lost on reload/restart) or permanent (written to config files).
Zones — which zone are you in?
A zone defines a trust level for a network connection. Different network interfaces can be in different zones. The default zone for new interfaces is usually public.
# Show which zone each interface is in
firewall-cmd --get-active-zones
# Example output:
# public
# interfaces: eth0
# internal
# interfaces: eth1
Common zones (in order of trust, least to most):
- drop — all incoming connections silently dropped; only outgoing works
- block — all incoming rejected with ICMP prohibited
- public — untrusted public network; only selected services allowed
- internal — internal network; more services allowed by default
- trusted — all connections accepted
For most servers, everything lives in the public zone. You open only the ports you need. Only change the zone if you have multiple interfaces with different trust levels (e.g. a management interface vs a public one).
Checking current rules
# Show all rules for the active zone
firewall-cmd --list-all
# Show for a specific zone
firewall-cmd --zone=public --list-all
# List open services
firewall-cmd --list-services
# List open ports
firewall-cmd --list-ports
# List all zones and their config
firewall-cmd --list-all-zones
Example output of --list-all:
public (active)
target: default
icmp-block-inversion: no
interfaces: eth0
sources:
services: ssh dhcpv6-client
ports: 8080/tcp
protocols:
masquerade: no
rich rules:
Opening a service
Services are named shortcuts that map to port numbers. firewalld ships with definitions for common services.
# List all available service names
firewall-cmd --get-services
# Open a service (runtime — lost on reload)
firewall-cmd --add-service=http
firewall-cmd --add-service=https
# Open permanently (survives reload and reboot)
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
# Apply permanent changes to runtime
firewall-cmd --reload
Service definitions live in /usr/lib/firewalld/services/. If a service is not listed, use --add-port instead.
Opening a specific port
# Open a single port permanently
firewall-cmd --permanent --add-port=8443/tcp
# Open a port range
firewall-cmd --permanent --add-port=8000-9000/tcp
# Open a UDP port
firewall-cmd --permanent --add-port=514/udp
# Apply changes
firewall-cmd --reload
Removing a rule
# Remove a service (runtime)
firewall-cmd --remove-service=http
# Remove a service permanently
firewall-cmd --permanent --remove-service=http
# Remove a port permanently
firewall-cmd --permanent --remove-port=8443/tcp
firewall-cmd --reload
Rich rules — source-based rules
Rich rules let you restrict access based on source IP or subnet — useful for services that should only be accessible from internal networks.
# Only allow SSH from the management subnet
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.0.0/24" service name="ssh" accept'
# Allow port 9200 (Elasticsearch) only from monitoring server
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.50" port port="9200" protocol="tcp" accept'
# Block a specific IP
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.5" reject'
firewall-cmd --reload
Runtime vs permanent
firewalld has two states that must both be updated for changes to survive a reload:
- Runtime — active right now; lost when firewalld reloads or the system reboots
- Permanent — written to config files; applied when firewalld loads
# Method 1: Add to runtime, then make permanent
firewall-cmd --add-service=https # runtime
firewall-cmd --runtime-to-permanent # copy all runtime to permanent
# Method 2: Add permanent directly, then reload to activate
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
--permanent for all production changes and follow with --reload.
Reloading firewalld
# Reload rules (applies permanent config, brief interruption to tracking)
firewall-cmd --reload
# Reload without dropping existing connections
firewall-cmd --complete-reload # use only if --reload fails
# Restart the service
systemctl restart firewalld
Troubleshooting blocked traffic
A service is running but connections are refused. Suspected firewall issue:
# Is the service/port open?
firewall-cmd --list-all
# Test from another host
nc -zv target-host 443
curl -v http://target-host:8080/
# Add a temporary accept log rule to trace traffic
firewall-cmd --add-rich-rule='rule family="ipv4" port port="8443" protocol="tcp" log prefix="FW-ACCEPT" level="info" accept'
# Then watch the log:
journalctl -f | grep FW-ACCEPT
# Remove the rule after debugging
firewall-cmd --remove-rich-rule='rule family="ipv4" port port="8443" protocol="tcp" log prefix="FW-ACCEPT" level="info" accept'
Managing firewalld with Ansible
---
- name: Open HTTP and HTTPS
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
state: enabled
zone: public
loop:
- http
- https
notify: Reload firewalld
- name: Open custom application port
ansible.posix.firewalld:
port: "8443/tcp"
permanent: true
state: enabled
zone: public
notify: Reload firewalld
- name: Allow monitoring from specific IP only
ansible.posix.firewalld:
rich_rule: 'rule family="ipv4" source address="10.10.1.10/32" port port="9200" protocol="tcp" accept'
permanent: true
state: enabled
notify: Reload firewalld
# Handler
- name: Reload firewalld
ansible.builtin.service:
name: firewalld
state: reloaded
Masquerading and port forwarding
Masquerading (NAT/SNAT) rewrites outgoing packets so they appear to come from the server's own IP — used when the server acts as a gateway or router for other hosts.
# Enable masquerading on the public zone (permanent)
firewall-cmd --zone=public --add-masquerade --permanent
firewall-cmd --reload
# Verify
firewall-cmd --zone=public --query-masquerade
Port forwarding (DNAT) redirects traffic arriving on one port to a different port or host — useful for exposing internal services or routing traffic between ports.
# Forward external port 8080 → local port 80
firewall-cmd --zone=public --add-forward-port=port=8080:proto=tcp:toport=80 --permanent
# Forward external port 443 → a different internal host
firewall-cmd --zone=public \
--add-forward-port=port=443:proto=tcp:toport=443:toaddr=192.168.1.20 \
--permanent
firewall-cmd --reload
# List all forwarded ports in a zone
firewall-cmd --zone=public --list-forward-ports
Port forwarding to an external host requires masquerading to be enabled on the same zone, otherwise the reply traffic has no return path.
Interface and zone management
On multi-NIC servers (e.g. a bastion or firewall host) you often need to move an interface between zones or confirm which zone it belongs to.
# See which zone each active interface is in
firewall-cmd --get-active-zones
# Which zone owns a specific interface?
firewall-cmd --get-zone-of-interface=eth0
# Move an interface to a different zone (runtime + permanent)
firewall-cmd --zone=internal --change-interface=eth1
firewall-cmd --zone=internal --change-interface=eth1 --permanent
# Verify the actual nftables rules firewalld generated
nft list ruleset
nft list ruleset shows the low-level nftables rules that firewalld writes. If firewall-cmd says a port is open but traffic is still blocked, check nft list ruleset to confirm the rule is actually there — a missing reload or a conflicting rule in another table will show up here.