Security

Write firewalld rich rules for IP-based access

Use firewalld rich rules to open a port to one IP, rate-limit new connections, and log or reject traffic. Covers runtime vs permanent rules and how to list and remove them.

AdvancedLinux server5 min read

Plain firewalld commands open a service to everyone in a zone. Rich rules add conditions: a source address, a rate limit, logging, or an explicit reject. They are the right tool when you want MySQL reachable only from your app server, or SSH reachable only from your office, without creating a separate zone for each case.

Before you start

  • A Linux VPS with firewalld running, usually AlmaLinux or Rocky Linux 8 or 9. See Set up firewalld on AlmaLinux and Rocky Linux if it is not set up.
  • Root or a sudo user.
  • The IP addresses you want to allow. Examples here use 203.0.113.10 (your office) and 198.51.100.20 (an app server).
  • Access to the browser console in your VPS provider's control panel, in case a rule blocks SSH.

Warning: If you plan to restrict SSH with a rich rule, add and test the allow rule for your IP before you remove the general ssh service from the zone. Otherwise you can lock yourself out. The browser console in your VPS provider's control panel still works if that happens.

Rich rule syntax

A rich rule is a single quoted string with this shape:

rule [family="ipv4|ipv6"]
     [source address="IP or CIDR" [invert="true"]]
     [destination address="IP or CIDR"]
     service name="NAME" | port port="N" protocol="tcp|udp"
     [log [prefix="TEXT"] [level="info"] [limit value="RATE"]]
     accept | reject | drop  [limit value="RATE"]

A few points that trip people up:

  • family is required whenever you give an address.
  • You can use a firewalld service name (ssh, mysql, http) or a port with protocol, not both.
  • A rule is added to one zone. Without --zone, the default zone is used.

Allow a port from one IP

Open MySQL (TCP 3306) only to the app server:

Bash
sudo firewall-cmd --permanent --zone=public \
  --add-rich-rule='rule family="ipv4" source address="198.51.100.20/32" port port="3306" protocol="tcp" accept'
sudo firewall-cmd --reload

Make sure the zone does not also have the mysql service or port 3306/tcp open to everyone, or the rich rule adds nothing:

Bash
sudo firewall-cmd --zone=public --list-all

To limit SSH to your office IP, add the rich rule, test a new SSH session from that IP, and only then remove the open service:

Bash
sudo firewall-cmd --permanent --zone=public \
  --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" service name="ssh" accept'
sudo firewall-cmd --reload
# open a NEW ssh session from 203.0.113.10 and confirm it works, then:
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --reload

Rate-limit new connections

A limit on the action caps how often the rule matches. Packets above the rate fall through to the next rule, and in a zone without SSH open they are then rejected. This allows at most 10 new SSH connections per minute to the whole server:

Bash
sudo firewall-cmd --permanent --zone=public \
  --add-rich-rule='rule service name="ssh" accept limit value="10/m"'
sudo firewall-cmd --permanent --zone=public --remove-service=ssh
sudo firewall-cmd --reload

The rate is global, not per IP. It slows brute-force bursts but one noisy bot can also use up your allowance. For per-IP banning, pair it with Fail2ban. See Use Fail2ban with firewalld or nftables ban actions.

Rate units are /s, /m, /h and /d.

Log and reject traffic

Reject SSH from a noisy subnet and log a sample of the attempts:

Bash
sudo firewall-cmd --permanent --zone=public \
  --add-rich-rule='rule family="ipv4" source address="198.51.100.0/24" service name="ssh" log prefix="ssh-blocked " level="info" limit value="5/m" reject'
sudo firewall-cmd --reload
  • reject sends back an ICMP error. drop sends nothing, so the client waits until it times out.
  • The limit on log stops the log from flooding.

Read the entries in the kernel log:

Bash
sudo journalctl -k | grep ssh-blocked

firewalld applies rich rules in a fixed order inside a zone: log rules first, then deny (reject and drop), then allow. A reject rule therefore wins over a broader accept for the same traffic.

Runtime vs permanent

  • --add-rich-rule without --permanent is live at once but disappears on reload or reboot. Good for testing.
  • With --permanent it is saved, and you must run --reload to make it live.

A careful workflow is to add the rule at runtime, test it, then save everything:

Bash
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="203.0.113.10/32" port port="8080" protocol="tcp" accept'
# test it
sudo firewall-cmd --runtime-to-permanent

If the test goes wrong, sudo firewall-cmd --reload throws away runtime changes and restores the saved config.

List and remove rules

Bash
sudo firewall-cmd --zone=public --list-rich-rules

To remove a rule, pass the exact same string you used to add it. The easiest way is to copy the line from the list output:

Bash
sudo firewall-cmd --permanent --zone=public \
  --remove-rich-rule='rule family="ipv4" source address="198.51.100.20/32" port port="3306" protocol="tcp" accept'
sudo firewall-cmd --reload

Check it works

From an allowed IP, test the port:

Bash
nc -vz 203.0.113.50 3306

Replace 203.0.113.50 with your server's IP. Run the same test from a different network. The allowed source should connect, and every other source should be refused or time out. Confirm the rules are saved:

Bash
sudo firewall-cmd --permanent --zone=public --list-rich-rules

Troubleshooting

  • "INVALID_RULE" error: usually a missing family when you used an address, or straight quotes replaced by smart quotes when copying. Retype the quotes.
  • Port still open to everyone: the zone also lists the service or port in plain form. Remove it with --remove-service or --remove-port.
  • Rule vanished after reboot: it was added without --permanent. Use --runtime-to-permanent.
  • Cannot remove a rule: the string differs slightly, for example /32 missing. Copy it exactly from --list-rich-rules.
  • Locked out of SSH: log in through the browser console and run sudo firewall-cmd --reload, or add the correct source IP.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.