Add and Test firewalld Rich Rules Safely
A rich rule lets you say "SSH from this one address, and log the rest" without rebuilding a zone, and it is also a quick way to lock yourself out. This guide adds a rich rule to a firewalld zone, checks the exact rule that is active, and removes it again. The examples use the syntax documented by the installed firewalld 2.1.1 package, and the whole job takes about 10 minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
- You need a shell account with
sudoaccess, a running firewalld service, and a service or port you can test from a suitable client. - Privileges. Commands that change firewall state need elevation. Listing and querying rules normally work as an ordinary user, but that depends on your local D-Bus policy.
1. Check the version and active zone
Confirm which client you are using and which zone is active. A rich rule belongs to a zone, so adding one without checking is an easy way to protect the wrong interface.
firewall-cmd --version
sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
On the machine used for this guide, the installed manpage identifies firewalld as version 2.1.1. The state command should print running. The active-zone query should show one or more zone names, followed by the interfaces or source ranges assigned to them.
- firewalld stopped? Do not add a rule and assume it is protecting the host. Start or repair the service through your distribution's normal service manager, then repeat this check.
- More than one zone listed? Choose the zone containing the interface that receives the traffic you are changing. In the examples below, replace
publicwith that zone.
Checkpoint
The client reports version 2.1.1 or another version you have accounted for, firewalld reports running, and you have written down the zone that owns the relevant interface.
2. Look up service and ICMP names
Use a predefined service when one describes the traffic you need. Service names are maintained by firewalld and are less error-prone than a port number copied from memory.
sudo firewall-cmd --get-services
sudo firewall-cmd --get-icmptypes
For example, if ssh appears in the service list, a rule can refer to service name="ssh". Rich rules also accept a port number or range with tcp, udp, sctp or dccp. A port rule is more explicit when you are opening a private application rather than a standard service.
3. Add one narrowly scoped rule
This example allows SSH only from one IPv4 address. It is deliberately specific: the family matches the address, the source is a single host, and the action is accept.
ZONE=public
SOURCE_IP=198.51.100.24
RULE="rule family=\"ipv4\" source address=\"$SOURCE_IP\" service name=\"ssh\" accept"
sudo firewall-cmd --zone="$ZONE" --add-rich-rule="$RULE"
The command should return success. Two syntax rules catch people out:
- One line only. Rich language rules are single-line strings.
- Options stay with their element. Each element's options must immediately follow that element. Put
service name="ssh"together, and put the service before the final action.
The change above is runtime configuration. That suits testing, because a reload or restart discards it. If you intend to keep it across a reboot, save the working configuration only after testing it, using your distribution's normal firewalld workflow.
Warning
Saving untested firewall rules can lock out remote administration.
4. Verify the rule is loaded
sudo firewall-cmd --zone="$ZONE" --list-rich-rules
sudo firewall-cmd --zone="$ZONE" --query-rich-rule="$RULE"
echo "$?"
Checkpoint
The list contains a rule equivalent to the one you added, and the query prints yes followed by an exit status of 0.
Do not rely on the list alone when troubleshooting. Comparing the complete string catches a wrong address family, zone, service name or action.
Now test from 198.51.100.24, or substitute the real source address before running the test. A successful client connection proves the service is reachable through the rule, but not that every other source is blocked. Test an unauthorised source as well, and inspect the service logs if the result is unexpected.
5. Add logging without a log flood
Logging is part of the rich rule and must appear before its action. This rule rejects SSH from a known unwanted IPv4 address and logs at most two matching attempts per day.
BLOCKED_IP=198.51.100.25
LOG_RULE="rule family=\"ipv4\" source address=\"$BLOCKED_IP\" service name=\"ssh\" log prefix=\"ssh-block\" level=\"info\" limit value=\"2/d\" reject"
sudo firewall-cmd --zone="$ZONE" --add-rich-rule="$LOG_RULE"
sudo firewall-cmd --zone="$ZONE" --query-rich-rule="$LOG_RULE"
- Reject or drop? A
rejectsends an ICMP or ICMPv6 rejection to the source. Adropdiscards packets without sending anything back. Rejection is easier to diagnose; dropping reveals less to an unwanted client. - Limit format. The manpage wants a positive rate and a duration of seconds, minutes, hours or days, with a maximum of
2/d. - Log level. The default kernel log level for
logiswarning. This example selectsinfo. - Prefix length. The prefix can be up to 127 characters in the rich language, but the iptables backend truncates it to 29.
Check the host's journal or kernel log after a controlled test, rather than generating repeated connection attempts.
6. Remove the test rules
Remove a rule by passing the exact string you used to add it. Keep the variables in your shell until both removals have completed.
sudo firewall-cmd --zone="$ZONE" --remove-rich-rule="$RULE"
sudo firewall-cmd --zone="$ZONE" --remove-rich-rule="$LOG_RULE"
sudo firewall-cmd --zone="$ZONE" --list-rich-rules
Checkpoint
Each removal prints success, and the final list no longer contains either rule.
If a removal says the rule is absent, inspect the list and compare quoting, address family, zone and option order. Do not replace a rule with a broad allow rule just to make a test pass.
Recovery
For a temporary experiment, you can instead reload firewalld and verify the zone again. That discards runtime-only changes, but it can also interrupt other uncommitted firewall changes, so use it only when you understand what else is active.
sudo firewall-cmd --reload
sudo firewall-cmd --zone="$ZONE" --list-rich-rules
Rules that need extra care
- Addresses need a family. If a rule contains a source or destination address, give
family="ipv4"orfamily="ipv6". Without a family a rule applies to both families when possible, but an address cannot match both. - Forwarding is routing. Forward-port rules and port forwarding also require a family. Forwarding to another address implicitly enables IP forwarding, and masquerading does the same, so treat these as routing changes rather than simple service exceptions.
- Priority range. Rule priority runs from -32768 to 32767. Lower numbers take precedence, and equal priorities have undefined ordering.
- Priority placement. Negative priorities run before other firewalld primitives; positive priorities run afterwards. With priority zero, logging, deny actions and allow actions go into separate chains.
- Start without a priority unless you have a documented ordering requirement. Then add one and test the interaction with existing rules.
Warning
The first matching rule wins when rules interact or contradict, so a broad allow rule before a narrower deny rule can defeat the deny. Review the complete list after every change, particularly when combining source allowlists, denylists and logging.
Done means
- The target is confirmed. You checked the installed firewalld version, running state and target zone.
- The rule is specific. It uses the right family, source, service or port, and action.
- The exact rule is present.
--query-rich-ruleconfirmed it. - Both sides were tested. You tried an allowed and an unauthorised source where practical.
- Nothing temporary lingers. Test rules were removed, or you recorded and tested the persistent configuration deliberately.