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
sudoprivileges. - 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 afterfirewall-cmd --reloador 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
NoteThe
internalzone already allows services such asssh,mdns,samba-clientanddhcpv6-client. Runsudo firewall-cmd --zone=internal --list-alland remove any you do not need.
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.
WarningTest this step with a second SSH session open and the server console available. If your IP changes, you will be locked out of SSH.
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 asDefaultZoneandFirewallBackend(nftableson 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. Runsudo firewall-cmd --runtime-to-permanentor re-add it with--permanentand reload. - A permanent rule has no effect: permanent changes need
sudo firewall-cmd --reload. Also check that you added it to the right zone withsudo 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 withsudo 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.1when 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.
