SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor · Updated 2026-09-04

How to Use firewalld for Rocky or AlmaLinux VPS

Learn firewalld zones, open SSH and web ports, close ports, and make rules survive reboot on Rocky or AlmaLinux. Avoid the --permanent trap.

Wetin be firewalld, and why Rocky and AlmaLinux dey ship am

firewalld na the firewall manager wey dem install by default for Rocky Linux, AlmaLinux and other rebuilds of Red Hat Enterprise Linux (RHEL). Both distributions inherit that default; dem no choose am themselves. This one make more sense when you know how Rocky and AlmaLinux rebuild Red Hat work after CentOS change direction. firewalld no inspect packets by itself. E keep saved configuration and turn that configuration into nftables rules. One command, firewall-cmd, fit edit am while server still dey online. Nothing for this guide change between the two distributions, because the things wey really separate Rocky from AlmaLinux na the compatibility promise and the range of CPUs wey dem still support, not the firewall.

If you already know how ufw dey work for Ubuntu VPS, you already know the main job. firewalld add two ideas wey ufw no get. The first one na zones: named policy wey packets dey sort into. The second one na the separation between live rules and saved rules. Na the --permanent flag represent this, and na the biggest source of confusion for this tool.

Everything below na command wey you go run for your own server. Test every change from another machine, because rule wey look correct for the server fit still wrong from internet.

SSH open first before you do anything else

Most Rocky and AlmaLinux installs get firewalld already installed and running, and the configuration wey come with am dey allow SSH. Some minimal cloud images comot am. Check am instead of assuming.

sudo dnf install -y firewalld
sudo systemctl enable --now firewalld
sudo firewall-cmd --state

firewall-cmd --state dey print running. If the service stop, every other firewall-cmd call go answer FirewallD is not running and exit with non-zero status. Na the first thing to check when command look like say e no do anything.

Now read wetin dey allowed currently.

sudo firewall-cmd --list-all

The real output get some extra lines. Na these ones matter:

public (active)
  target: default
  interfaces: eth0
  sources:
  services: cockpit dhcpv6-client ssh
  ports:
  rich rules:

ssh for the services: line na why your session still dey work. If e no dey there, add am before you touch anything else, because starting firewall without SSH rule go end the session and e no go let you enter again.

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

target: default mean say packet wey no match anything go get rejected with ICMP (internet control message protocol) host-prohibited reply, so client wey hit closed port go see No route to host immediately. If you set the target to DROP, the server go remain silent instead, and scanners go wait for timeout.

sudo firewall-cmd --permanent --zone=public --set-target=DROP
sudo firewall-cmd --reload

Know the cost before you run am: DROP go also stop the server from answering ping, so your own monitoring go quiet too.

Wetin make my rule disappear? The --permanent flag

firewalld dey hold two configurations at once. Runtime configuration na wetin kernel dey enforce this second. Permanent configuration na wetin dey inside /etc/firewalld/zones/public.xml and wetin come back after reload or reboot.

Command wey no get --permanent go change runtime only. E go work immediately, but e go disappear for next reload or boot. Command wey get --permanent go write the file, but e no go change anything wey dey run, so the port go remain closed until you reload. None of these behaviours na bug. Both fit surprise people, because command go print success for either case.

Write the pair every time.

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

You fit read both configurations. Na the fastest way to know which of the two mistakes you make.

sudo firewall-cmd --list-services
sudo firewall-cmd --permanent --list-services

The first one go print the live set. The second one go print the saved set. If live set get service wey saved set no get, that rule go die for next reload. If saved set get one wey live set no get, you forget the reload. sudo firewall-cmd --runtime-to-permanent go copy everything wey dey live into the saved file. This one useful after you don experiment.

--reload go keep connection tracking state, so your SSH session go survive am. --complete-reload go reload the kernel modules too and lose that state, which normally go terminate every open connection, including your own. Use the plain reload.

One safety net dey built in. Runtime rule fit expire by itself.

sudo firewall-cmd --add-service=http --timeout=5m

That rule go remove itself after five minutes. You no fit combine am with --permanent, and na the main point: e dey for testing change wey you no sure about. The older safety net better. Keep a second SSH session open while you dey edit rules, and no close am until fresh login prove say the new rules dey work.

Zones, and why only the default zone matter for a VPS

Zone na named set of permissions wey get trust level attached. firewalld dey put every incoming packet inside exactly one zone. E first dey compare the packet source address with each zone sources: list. If nothing match, e go use the zone wey the incoming interface dey bound to. If the interface no dey bound to any zone, the packet go enter the default zone.

sudo firewall-cmd --get-default-zone
sudo firewall-cmd --get-active-zones

For VPS wey get one network interface, the first answer almost always na public, and na only that zone you go use. firewall-cmd without --zone= argument dey work on the default zone, na why every short command for this guide dey work without naming zone.

Na this failure dey waste whole afternoon. If the interface dey bound to another zone, your rules go enter public while the traffic dey handled somewhere else. So nothing wey you add go get effect, and nothing go warn you. --get-active-zones dey show the binding:

public
  interfaces: eth0

If the interface show under another zone name, either write your rules for there with --zone=, or move the interface.

sudo firewall-cmd --permanent --zone=public --change-interface=eth0
sudo firewall-cmd --reload

NetworkManager dey manage interfaces for Rocky and AlmaLinux, and e dey set the zone again when the connection come up. Set am there too, so reboot no go cancel your work. Take the connection name from the first command, because e rarely be the same as the device name.

sudo nmcli connection show
sudo nmcli connection modify "System eth0" connection.zone public

Source matching get priority over interface matching. Na so one address fit get different policy. The built-in trusted zone dey accept everything.

sudo firewall-cmd --permanent --zone=trusted --add-source=203.0.113.10/32
sudo firewall-cmd --reload

Make you careful with that one. E dey open every port for the server to that address, including the database wey you think say private. Use rich rule when you want allow one port, no be one host.

Wetin be firewalld service?

A service na named bundle of ports wey dem ship as XML file. --add-service=https go open 443/tcp because /usr/lib/firewalld/services/https.xml define wetin https mean.

sudo firewall-cmd --get-services
sudo firewall-cmd --info-service=https

--info-service go print the ports wey dey hide behind the name:

https
  ports: 443/tcp

Use the name when one dey. E go read clear for --list-all six months later, and packages like Cockpit dey install their own service file. Use --add-port for anything wey no get definition.

The gap wey you need watch na this: the ssh service mean 22/tcp and nothing else. If you move SSH go another port while you dey harden SSH access for the server, then --add-service=ssh no go open the port wey you actually dey use.

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

For RHEL rebuild, another lock dey for that door. SELinux (security-enhanced Linux) dey label port numbers, and sshd no get permission to bind to port outside its labels. E go then refuse to start, and the log go read error: Bind to port 2222 on 0.0.0.0 failed: Permission denied. Label the port first.

sudo dnf install -y policycoreutils-python-utils
sudo semanage port -a -t ssh_port_t -p tcp 2222

Wetin dey open right now?

sudo firewall-cmd --list-all
sudo firewall-cmd --list-rich-rules
sudo nft list table inet firewalld | head -n 40

The first two go show wetin firewalld believe. The third one go read the rules wey kernel actually get, for the table wey firewalld dey control. Dem suppose agree.

None of this be proof. Test am from another machine:

nc -zv 203.0.113.20 443

No run the test for the server itself. firewalld dey accept everything wey enter through loopback interface, so curl http://localhost:8080 go succeed no matter wetin your rules talk. That test go show say the service dey alive. E no tell you anything about the firewall.

Allow web port

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

The last command suppose list http https now, together with wetin dey there before. If the site still no answer, firewall fit no be the problem. Rule dey permit packet. But one process still need dey listen for am.

sudo ss -tlnp

Socket wey show as 0.0.0.0:443 or *:443 fit accept connections from any address. The one wey show as 127.0.0.1:443 dey answer for loopback only, and no firewall rule fit ever make e reachable from outside. Ports and listening sockets for Linux explain this difference more.

How I fit close port again?

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

The --permanent rule still apply here, and e dey cause more wahala for this direction. If you remove service from runtime only, the port go look closed. But the next reload or reboot go open am again from the saved file. Na hole be this wey you no go notice, because the check wey you run pass.

If you remove something wey no dey there before, e go print Warning: NOT_ENABLED: http and still exit 0. If you add the same thing twice, e go print Warning: ALREADY_ENABLED: http. Both actions safe. Misspelled name different: Error: INVALID_SERVICE mean say firewalld no get definition with that name, and nothing change at all.

If your --list-all show cockpit and you no dey use Cockpit web console for port 9090, remove am. Every open port na service wey you need keep patched. For the ones wey you decide to keep, dnf-automatic fit install security updates on a timer so the work no depend on you remembering. But installing patch no be the same as running am. needs-restarting dey show which services still dey hold the old libraries after those updates land.

Restrict a port to one source address

Rich rules na long-form rules, for when ordinary service name no fit express wetin you mean. To restrict SSH to one office address, you need two commands, and na the second one people dey forget.

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

Zone na set of permissions, no be numbered list wey dey stop for the first match. The rich rule adds accept for one address. E no deny anybody. As long as ssh still dey for the services: line, the whole internet still fit reach port 22, and the rich rule no change anything wey you fit measure. Remove the broad entry, otherwise the narrow one na just decoration.

For port wey no get service name, name the port instead.

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" port port="5432" protocol="tcp" accept'

To drop noisy network and keep record, put the log element before the action, because na the order wey rich rule language expect.

sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" log prefix="fw-drop " level="info" limit value="3/m" drop'
sudo firewall-cmd --reload
sudo journalctl -k -g fw-drop

The limit value stops too many packets from filling the journal. Before you lock SSH to one address, make sure say the address dey stable. Home connection wey get changing IP address fit lock you out the day e change, so test and confirm say your provider's console access dey work before you do am.

ufw commands and their firewall-cmd equivalents

Na the same tasks, but different tool. Every --permanent line need a sudo firewall-cmd --reload after am. Na only list like this no fit show that part.

  • sudo ufw enable dey become sudo systemctl enable --now firewalld
  • sudo ufw disable dey become sudo systemctl disable --now firewalld
  • sudo ufw status verbose dey become sudo firewall-cmd --list-all
  • sudo ufw allow OpenSSH dey become sudo firewall-cmd --permanent --add-service=ssh
  • sudo ufw allow 443/tcp dey become sudo firewall-cmd --permanent --add-port=443/tcp
  • sudo ufw delete allow 443/tcp dey become sudo firewall-cmd --permanent --remove-port=443/tcp
  • sudo ufw allow from 203.0.113.10 to any port 22 dey become the rich rule wey show above
  • sudo ufw reload dey become sudo firewall-cmd --reload
  • sudo ufw default deny incoming na already how the public zone dey behave, and --set-target=DROP na the silent version of am
  • sudo ufw logging on dey become sudo firewall-cmd --set-log-denied=all

One difference need make we state am clearly. ufw dey keep numbered list, and you fit insert rule for position 1. firewalld no get rule numbers, so “put this rule first” no mean anything here. When two firewalld entries look like say dem dey contradict each other, the broad accept go win because nothing for the set dey deny. Na you go remove the broad entry yourself.

Why my Docker container dey reachable when firewall look closed?

Because published container port no dey reach the part of firewall wey your zone dey control. docker run -d -p 8080:80 nginx tells Docker make e write im own NAT (network address translation) and forwarding rules. Packet wey enter for 8080 go get rewrite and route go the container, so na forwarded packet e be, no be packet delivered to the host. The services: and ports: lines for your zone dey control packets wey dem deliver to the host. Docker rules dey control the forward path, and dem dey accept.

The result na server wey sudo firewall-cmd --list-all show say port 8080 no dey, but nc -zv 203.0.113.20 8080 from another machine still connect. Check wetin Docker install:

sudo iptables -t nat -L DOCKER -n

The fix dey for the publish flag. Bind the port to loopback, then put reverse proxy for front of am.

docker run -d -p 127.0.0.1:8080:80 nginx

The container now dey answer curl http://127.0.0.1:8080 for the server, and nothing from outside fit reach am. Ubuntu users dey hit the same problem, as why Docker containers publish ports straight past ufw explain. Rootful Podman, wey Rocky and AlmaLinux dey ship for the base repositories, dey publish ports with the same NAT approach. So test from another machine instead of trusting the zone list. This overlap na why installing Docker Engine on these distributions need some steps wey Ubuntu guide no mention, starting with Podman already owning the docker command.

Make e survive reboot, and errors wey you go see

sudo systemctl is-enabled firewalld
sudo systemctl status firewalld

enabled and active (running) na wetin you want. Firewall wey dey run but dem never enable am go protect you until the first reboot. Put this check for the list wey you dey go through for the first ten minutes for new VPS, together with SSH keys and updates.

Raw nftables commands and firewalld no dey mix. firewalld dey control table wey dem call inet firewalld. sudo nft flush ruleset dey delete am, so server go open to everything, but firewall-cmd --list-all still dey show the configuration wey you intend, because firewalld dey report wetin e believe, no be wetin kernel dey hold. sudo firewall-cmd --reload go install the rules again. Write rules with firewall-cmd so dem go return after reload.

Two firewall managers for one server. If you install ufw or iptables-services beside firewalld, two programs go dey write rules without knowing wetin the other one dey do, and which one win go depend on the service wey start last. Pick one. For Rocky and AlmaLinux, firewalld na the one wey the distribution support.

The provider firewall wey dey before the server. Plenty VPS panels get separate network firewall. If --list-all show say port open but connection from outside still fail, check the panel before you change anything for the server. E fit happen the other way too: open panel rule no go do anything while firewalld reject the packet.

Running firewall-cmd without sudo. Every change need root. Without am, authorization check go reject the request and nothing go change. For quick look, e fit seem like the command no do anything.

Six commands cover most days: --list-all to read the state, --permanent --add-service or --add-port to open something, --permanent --remove-service to close am, --reload to apply the saved file, and --runtime-to-permanent after you don run some experiments. The zone na public, the flag na --permanent, and the only honest check na from another machine.

FAQ

Why my firewalld rule disappear after reboot?

The rule enter runtime configuration only. sudo firewall-cmd --add-service=http dey apply immediately and dem discard am for next reload or boot, because saved configuration for /etc/firewalld/zones/public.xml no change. Add --permanent, then run sudo firewall-cmd --reload. To keep rules wey you don add by hand, run sudo firewall-cmd --runtime-to-permanent. E go copy the live set enter the saved file.

Why nothing change after I add rule with --permanent?

Na because --permanent dey write the file but e leave the running firewall as e be. The port go remain closed until sudo firewall-cmd --reload load the saved configuration enter the kernel. Compare sudo firewall-cmd --list-services with sudo firewall-cmd --permanent --list-services. If saved list get entry wey live list no get, na reload be wetin you miss.

I suppose use --add-service or --add-port?

Use --add-service when name dey for the thing wey you run. E show the intention, and sudo firewall-cmd --info-service=https show exactly which ports the name cover. Use --add-port when nothing define your service, or when e dey listen on non-standard port. The ssh service mean 22/tcp only, so if SSH move go 2222, you need --add-port=2222/tcp plus SELinux label for that port.

Why my Docker container dey reachable when firewall-cmd show say the port close?

Docker own NAT rules dey rewrite published port and forward am go the container. So the packet no dey reach the host, and zone service and port lists only cover packets wey reach the host. The container fit answer from internet while --list-all show nothing. Publish am to loopback instead with docker run -d -p 127.0.0.1:8080:80 nginx, then put reverse proxy in front of am.

I fit install ufw for Rocky Linux instead of firewalld?

Two firewall managers for one server dey write rules without knowing wetin the other one do. Which rule set go survive depend on which service start last. firewalld na the supported tool for Rocky Linux and AlmaLinux. E don already install, and e dey use the same nftables backend wey ufw for use. Learn the default zone and the --permanent flag once, and you don get the whole tool.