firewalld is the default firewall manager on Red Hat Enterprise Linux and its rebuilds (Rocky Linux, AlmaLinux, CentOS Stream). It groups rules into zones, lets you change them without dropping existing connections, and on RHEL 9 based systems writes the actual filtering rules to nftables. In this tutorial you will check the default configuration on Rocky Linux 9, open services and ports, restrict SSH to trusted addresses, and add rich rules and port forwarding, testing each change at runtime before making it permanent.

Prerequisites

To follow this tutorial, you will need:

  • A server running Rocky Linux 9 or AlmaLinux 9, for example a CubePath VPS. The commands are the same on Rocky Linux 10, AlmaLinux 10 and CentOS Stream 9/10.
  • A non-root user with sudo privileges.
  • Access to the server console (VNC or serial) in case a rule locks you out of SSH. Keep it at hand while you work on SSH rules.

Step 1 - Checking that firewalld is running

firewalld is installed and enabled on standard Rocky Linux images. Confirm that the service is running:

sudo firewall-cmd --state
running

If the command reports not running or is not found, install and start it:

sudo dnf install firewalld
sudo systemctl enable --now firewalld

Before adding rules, look at what is already allowed. The default zone on Rocky Linux 9 is public:

sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones
public
public
  interfaces: eth0

The active zones list tells you which zone each network interface belongs to. Your interface name may be eth0, ens18 or similar. To see everything the zone allows:

sudo firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

Incoming traffic that does not match a service, port or rule in the zone is rejected. Out of the box, only SSH, the Cockpit web console (port 9090) and DHCPv6 client traffic are accepted.

Step 2 - Understanding runtime and permanent configuration

firewalld keeps two configurations:

  • Runtime: what is currently enforced. Changes apply immediately and are lost on reload or reboot.
  • Permanent: what is stored in /etc/firewalld/. Changes apply only after firewall-cmd --reload or a restart.

This split is useful for testing. Add a rule without --permanent, check that everything still works, then save it. There are two ways to save:

sudo firewall-cmd --runtime-to-permanent

This copies the whole current runtime configuration to permanent. Alternatively, repeat the same command with --permanent and reload. Most examples in this tutorial use the second approach because it is explicit about which rule is being saved.

For risky changes, you can also add a runtime rule that removes itself after a number of seconds:

sudo firewall-cmd --add-service=http --timeout=300

Step 3 - Allowing services

A firewalld service is a named set of ports and protocols defined in XML under /usr/lib/firewalld/services/. List the predefined services:

sudo firewall-cmd --get-services

Inspect one to see which ports it opens:

sudo firewall-cmd --info-service=https
https
  ports: 443/tcp
  protocols:
  source-ports:
  modules:
  destination:
  includes:
  helpers:

To run a web server, allow HTTP and HTTPS permanently and reload:

sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload

Verify:

sudo firewall-cmd --list-services
cockpit dhcpv6-client http https ssh

If you do not use the Cockpit web console, remove it to close port 9090:

sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload

Step 4 - Opening individual ports

When no predefined service matches, open a port directly. For an application listening on TCP port 8080:

sudo firewall-cmd --permanent --add-port=8080/tcp
sudo firewall-cmd --reload

A port range uses a hyphen, for example --add-port=60000-61000/udp. Check the result:

sudo firewall-cmd --list-ports
8080/tcp

To close it again:

sudo firewall-cmd --permanent --remove-port=8080/tcp
sudo firewall-cmd --reload

Creating a custom service

If an application uses several ports, a custom service keeps the rules readable. Create a service called myapp that opens TCP 9000 and 9001:

sudo firewall-cmd --permanent --new-service=myapp
sudo firewall-cmd --permanent --service=myapp --set-description="My application API and metrics"
sudo firewall-cmd --permanent --service=myapp --add-port=9000/tcp
sudo firewall-cmd --permanent --service=myapp --add-port=9001/tcp
sudo firewall-cmd --reload

The definition is saved in /etc/firewalld/services/myapp.xml. Allow it like any built-in service:

sudo firewall-cmd --permanent --add-service=myapp
sudo firewall-cmd --reload

Step 5 - Trusting specific source addresses

Zones can match traffic by source address as well as by interface. A source match takes priority over the interface zone, which lets you give a private network or an office IP more access than the public internet.

For example, to accept all traffic from a private network 10.0.0.0/24 (another server in the same private network, a database client, and so on), bind that range to the trusted zone:

sudo firewall-cmd --permanent --zone=trusted --add-source=10.0.0.0/24
sudo firewall-cmd --reload

The trusted zone accepts everything. If you only want to allow specific services from that range, use the internal zone instead and add services to it:

sudo firewall-cmd --permanent --zone=internal --add-source=10.0.0.0/24
sudo firewall-cmd --permanent --zone=internal --add-service=mysql
sudo firewall-cmd --reload

Verify which zones are now active:

sudo firewall-cmd --get-active-zones
internal
  sources: 10.0.0.0/24
public
  interfaces: eth0

Step 6 - Restricting SSH with rich rules

Rich rules express conditions that plain services and ports cannot, such as "allow SSH only from this address". Replace your_admin_ip with the public IP you connect from.

First add the rich rule at runtime:

sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="your_admin_ip/32" service name="ssh" accept'

Then remove the generic ssh service at runtime, so only the rich rule allows SSH:

sudo firewall-cmd --remove-service=ssh

Open a new SSH connection from your workstation. If it works, save the runtime configuration:

sudo firewall-cmd --runtime-to-permanent

If it does not work, run sudo firewall-cmd --reload from the existing session or the console to discard the runtime changes.

List the rich rules in the zone to confirm:

sudo firewall-cmd --list-rich-rules
rule family="ipv4" source address="your_admin_ip/32" service name="ssh" accept

Other useful rich rules:

  • Block a single address completely:

    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.50" drop'
    
  • Allow a port only from a network and log the matches:

    sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" port port="5432" protocol="tcp" log prefix="pg-access " level="info" accept'
    

Reload after adding permanent rules. Remove a rich rule by repeating it with --remove-rich-rule.

Step 7 - Forwarding ports

Port forwarding redirects incoming traffic from one port to another. To make an application listening on 8080 reachable on port 80 of the same server:

sudo firewall-cmd --permanent --add-forward-port=port=80:proto=tcp:toport=8080
sudo firewall-cmd --reload

To forward to another host, for example a backend on a private network, add toaddr and enable masquerading so replies return through this server:

sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --permanent --add-forward-port=port=2222:proto=tcp:toport=22:toaddr=10.0.0.20
sudo firewall-cmd --reload

Check the result:

sudo firewall-cmd --list-forward-ports
port=80:proto=tcp:toport=8080:toaddr=
port=2222:proto=tcp:toport=22:toaddr=10.0.0.20

Step 8 - Logging rejected traffic

When a service is unreachable and you suspect the firewall, enable logging of denied packets:

sudo firewall-cmd --set-log-denied=all

Try to connect again, then read the kernel log:

sudo journalctl -k -g 'REJECT|DROP' --since '5 min ago'

Each line shows the source, destination and port of the rejected packet. Turn logging off again when you are done, because it can be noisy on a public server:

sudo firewall-cmd --set-log-denied=off

Where the configuration lives

  • /usr/lib/firewalld/zones/ and /usr/lib/firewalld/services/: defaults shipped with the package. Do not edit them.
  • /etc/firewalld/zones/ and /etc/firewalld/services/: your permanent changes, stored as XML. A zone file only appears here after you change that zone.
  • /etc/firewalld/firewalld.conf: global settings such as DefaultZone and FirewallBackend (nftables on Rocky Linux 9).

If you edit XML files by hand, validate them before reloading:

sudo firewall-cmd --check-config

To inspect the rules firewalld generates:

sudo nft list ruleset

Troubleshooting

  • A rule works until reboot, then disappears: it was added without --permanent. Run sudo firewall-cmd --runtime-to-permanent or re-add it with --permanent and reload.
  • A permanent rule has no effect: permanent changes need sudo firewall-cmd --reload. Also check that you added it to the right zone with sudo firewall-cmd --get-active-zones; a source-based zone takes priority over the interface zone.
  • Error: INVALID_SERVICE: the service name does not exist. Check the spelling with sudo firewall-cmd --get-services.
  • Locked out of SSH: log in through the server console and run sudo firewall-cmd --permanent --add-service=ssh && sudo firewall-cmd --reload.
  • Docker containers bypass or break rules: Docker manages its own nftables/iptables chains. Published container ports are reachable even if the port is not open in firewalld, so bind containers to 127.0.0.1 when they should stay private.

Conclusion

You now have firewalld running with only the services you need, SSH limited to trusted addresses, and a workflow for testing rules at runtime before saving them. As next steps, consider installing Fail2ban with its firewalld action to block brute-force attempts, keeping SELinux in enforcing mode, and documenting your zone layout alongside your server configuration.