How to configure firewalld on AlmaLinux and Rocky Linux

Turn on firewalld, open services and ports for good, limit SSH to your own addresses with a zone, and use rich rules, without locking yourself out.

Tested on AlmaLinux 10, Rocky Linux 9

Before you start

  • A server running AlmaLinux 9 or 10 or Rocky Linux 9
  • Root or a user with sudo

To configure firewalld on AlmaLinux or Rocky Linux, start it with systemctl enable --now firewalld, open what you need with firewall-cmd --permanent --add-service=https (or --add-port=8080/tcp), and apply the change with firewall-cmd --reload. Everything else, from limiting SSH to your office to forwarding ports, is the same three moves on a different zone or rule.

firewalld is the standard firewall on RHEL-family systems, in the role ufw plays on Ubuntu. It writes nftables rules for you. This tutorial was run on AlmaLinux 10 (firewalld 2.4) and Rocky Linux 9 (firewalld 1.3); the commands are the same on both and on AlmaLinux 9.

How firewalld thinks

Three ideas cover almost everything:

Idea What it means
Zone A set of rules with a trust level. Each network interface, and optionally each source address range, belongs to one zone. On a server, public is the default, and the one that matters.
Service A named group of ports, like ssh (22/tcp) or https (443/tcp). firewall-cmd --get-services lists the predefined ones.
Runtime vs permanent A command without --permanent changes the running firewall only, and is lost on reload or reboot. With --permanent it is saved, but only takes effect after --reload.

Anything not allowed in a zone is rejected. Replies to connections the server makes itself are always let back in.

Install and start firewalld

Some cloud images ship without firewalld. Install it if firewall-cmd isn't found, then start it now and at every boot:

bash
sudo dnf install -y firewalld
sudo systemctl enable --now firewalld

The public zone allows ssh out of the box, so starting firewalld doesn't cut off your session.

Check it worked

bash
sudo firewall-cmd --state
sudo firewall-cmd --list-all

The first prints running. The second shows the default zone's rules, typically:

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

cockpit is the web console's port, 9090. If you don't run Cockpit, close it (next section).

Open and close services and ports

Use a service name when one exists, a port when it doesn't. Add --permanent and reload:

bash
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --remove-service=cockpit
sudo firewall-cmd --reload

Each command prints success. Adding something that is already open prints Warning: ALREADY_ENABLED: http and changes nothing, which is harmless.

To see what a service opens before you add it:

bash
firewall-cmd --info-service=ssh

Check it worked

bash
sudo firewall-cmd --list-services
sudo firewall-cmd --list-ports

You should see dhcpv6-client http https ssh and 51820/udp. Then confirm from outside, for example with curl -I http://203.0.113.10 from your computer, or with the port checks in check open ports with ss and nmap.

Try a rule before you keep it

A runtime rule with a timeout removes itself, which makes it safe for experiments:

bash
sudo firewall-cmd --add-port=8080/tcp --timeout=10m

If you like the result, save the whole runtime state as the permanent one:

bash
sudo firewall-cmd --runtime-to-permanent

Don't run that while a timed rule you don't want to keep is still active: it saves everything that's running.

Allow SSH only from your own addresses

The cleanest way is a zone for trusted sources. Packets from a source in that zone get its rules; everyone else falls through to public. Here 198.51.100.0/24 stands for your office or VPN range:

bash
sudo firewall-cmd --permanent --new-zone=admin
sudo firewall-cmd --permanent --zone=admin --add-source=198.51.100.0/24
sudo firewall-cmd --permanent --zone=admin --add-service=ssh
sudo firewall-cmd --reload

Check it worked

Open a new SSH session from an address in that range before you change anything else:

bash
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --zone=admin --list-all

admin is listed with sources: 198.51.100.0/24 and services: ssh. Only when your new session works, remove SSH from the public zone:

bash
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --reload

Now SSH answers only from your range. If your home address changes, you'll need the serial console to get back in, so use a range you control (an office or a VPN such as WireGuard).

Rich rules for one-off cases

A rich rule says exactly who may reach which port. For example, PostgreSQL from one network only:

bash
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" port port="5432" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules

To remove it, repeat the command with --remove-rich-rule and the same quoted rule. For more than two or three of these, a source zone like admin above is easier to read.

Forward a port

To send traffic arriving on port 80 to an app listening on 8080 on the same server:

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

A reverse proxy (nginx, Caddy) is usually the better tool for web traffic, because it can also handle HTTPS.

Where the rules live

Permanent rules are XML files under /etc/firewalld/zones/, one per zone you changed (public.xml, admin.xml). You can read them to see the whole picture, and back them up with the rest of /etc. Edit them with firewall-cmd rather than by hand; if you do edit one, run firewall-cmd --reload afterwards.

Troubleshooting

FirewallD is not running. The service is stopped. sudo systemctl enable --now firewalld, then journalctl -u firewalld -n 30 if it won't start.

Error: INVALID_SERVICE: Zone 'public': 'xyz' not among existing services. There's no predefined service by that name. Use --add-port=PORT/tcp instead, or check the name with firewall-cmd --get-services.

A rule works until the next reboot, then disappears. It was added without --permanent. Add it again with --permanent and reload, or run --runtime-to-permanent while it's active.

A --permanent rule does nothing. Permanent changes wait for sudo firewall-cmd --reload.

You're performing an operation over default zone ('public'), but your connections/interfaces are in zone 'admin'. Rocky Linux 9's firewalld prints this once your SSH session comes from a source in the admin zone. It's a reminder, not an error: the change went to public. If you meant the admin zone, repeat the command with --zone=admin.

Docker containers are reachable even though the port isn't open. Docker publishes ports with its own nftables rules, in a zone called docker. Bind published ports to 127.0.0.1 (-p 127.0.0.1:8080:80) and put a reverse proxy in front, or filter them before they reach the server with the panel firewall (below).

Next steps

Doing it on VPSNine

VPSNine servers also have a panel firewall that filters traffic before it reaches the server, so it covers Docker's ports too and still applies if firewalld is stopped. Using both is fine: open a port in both places, and keep SSH allowed in both. Every plan runs AlmaLinux 10, AlmaLinux 9 and Rocky Linux 9.

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly or yearly, no lock-in
  • 14-day money-back guarantee