--- title: "How to Configure firewalld on Rocky Linux 9 and AlmaLinux 9" description: "Set up firewalld on Rocky Linux 9: zones, services, ports, trusted sources, rich rules and port forwarding, with runtime testing before making rules permanent." category: "Server Setup" language: en source: https://cubepath.com/docs/server-setup/firewall-configuration-with-firewalld-centos-rocky updated: 2026-09-25 --- # How to Configure firewalld on Rocky Linux 9 and AlmaLinux 9 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: ```bash sudo firewall-cmd --state ``` ```output running ``` If the command reports `not running` or is not found, install and start it: ```bash 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`: ```bash sudo firewall-cmd --get-default-zone sudo firewall-cmd --get-active-zones ``` ```output 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: ```bash sudo firewall-cmd --list-all ``` ```output 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: ```bash 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: ```bash 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: ```bash sudo firewall-cmd --get-services ``` Inspect one to see which ports it opens: ```bash sudo firewall-cmd --info-service=https ``` ```output https ports: 443/tcp protocols: source-ports: modules: destination: includes: helpers: ``` To run a web server, allow HTTP and HTTPS permanently and reload: ```bash sudo firewall-cmd --permanent --add-service=http --add-service=https sudo firewall-cmd --reload ``` Verify: ```bash sudo firewall-cmd --list-services ``` ```output cockpit dhcpv6-client http https ssh ``` If you do not use the Cockpit web console, remove it to close port 9090: ```bash 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: ```bash 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: ```bash sudo firewall-cmd --list-ports ``` ```output 8080/tcp ``` To close it again: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash sudo firewall-cmd --get-active-zones ``` ```output internal sources: 10.0.0.0/24 public interfaces: eth0 ``` > Note: The `internal` zone already allows services such as `ssh`, `mdns`, `samba-client` and `dhcpv6-client`. Run `sudo firewall-cmd --zone=internal --list-all` and 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. > Warning: Test 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: ```bash 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: ```bash sudo firewall-cmd --remove-service=ssh ``` Open a new SSH connection from your workstation. If it works, save the runtime configuration: ```bash 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: ```bash sudo firewall-cmd --list-rich-rules ``` ```output rule family="ipv4" source address="your_admin_ip/32" service name="ssh" accept ``` Other useful rich rules: - Block a single address completely: ```bash 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: ```bash 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: ```bash 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: ```bash 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: ```bash sudo firewall-cmd --list-forward-ports ``` ```output 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: ```bash sudo firewall-cmd --set-log-denied=all ``` Try to connect again, then read the kernel log: ```bash 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: ```bash 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: ```bash sudo firewall-cmd --check-config ``` To inspect the rules firewalld generates: ```bash 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.