firewalld Basics

Zones, services, ports, rich rules, and managing the firewall with Ansible.

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):

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:

# 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
If you forget --permanent: Your change works immediately but disappears on the next reload or reboot. Use --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.