The scenario
You just configured a new Fedora server or set up a home lab machine. You need to let a specific subnet access a service, but you do not want to open the port to the entire internet. You run firewall-cmd --add-rich-rule and get a syntax error. Or worse, you add the rule, reload the firewall, and your active SSH session drops. You are left staring at a terminal, wondering why the firewall ignored your CIDR block.
What firewalld is actually doing
Fedora uses firewalld as its default firewall daemon. It sits on top of nftables and translates high-level zone concepts into low-level packet filtering rules. Think of a zone as a security boundary. The public zone assumes untrusted networks. The trusted zone accepts everything. The internal zone is for your LAN. When you add a rule, you are telling firewalld how to treat packets that match specific criteria within a specific zone.
The daemon maintains two separate configurations. The runtime configuration lives in memory and applies immediately. The persistent configuration lives on disk in XML files under /etc/firewalld/. Changes made with --permanent only affect the on-disk configuration. They do not touch the running firewall until you explicitly reload it. This separation prevents accidental lockouts during live configuration. It also means you must always reload after permanent changes.
Fedora's package manager treats /etc/firewalld/ as user-managed. Files in /usr/lib/firewalld/ ship with the package and get overwritten on updates. Never edit the /usr/lib/ copies. Always work through firewall-cmd or the /etc/ directory. The command line interface validates syntax before writing to disk. Manual XML edits bypass that validation and can corrupt zone definitions.
Adding IP ranges with rich rules
Rich rules are the most flexible way to define firewall exceptions. They let you combine source addresses, destination ports, protocols, and actions in a single statement. You will use them when you need to allow a specific IP range to reach a specific port, or when you need to reject traffic from a known bad subnet.
Here is how to add a rule that accepts all IPv4 traffic from the 192.168.1.0/24 subnet on the public zone.
sudo firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" accept'
# --permanent writes to disk so the rule survives a reboot
# --zone=public targets the default zone for external interfaces
# rich rules require single quotes to protect internal double quotes from shell expansion
If you need to allow a broader corporate network, add another rule for the 10.0.0.0/8 range.
sudo firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.0.0.0/8" accept'
# CIDR notation defines the subnet mask length
# /8 means the first 8 bits are fixed, covering roughly 16 million addresses
# firewalld parses this and translates it into nftables set matches
Apply the changes to the live firewall.
sudo firewall-cmd --reload
# --reload merges the persistent config into the runtime config
# existing connections are preserved, but new packets are evaluated against the updated rules
# always run this after every --permanent change
Reload the firewall after every permanent change. The runtime and persistent configs will diverge otherwise, and you will spend hours debugging rules that appear in --list-all but do not actually work.
Testing safely before you commit
A botched firewall rule can lock you out of your own machine. Test new rules in a temporary runtime session before making them permanent. The --timeout flag applies a rule for a set number of seconds, then automatically removes it. This gives you a safe window to verify connectivity without risking a permanent lockout.
sudo firewall-cmd --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" accept' --timeout=60
# --timeout applies the rule to the runtime config only
# the rule automatically expires after 60 seconds
# use this to test connectivity before committing to --permanent
Open a second terminal window or SSH session before testing. If the rule breaks your primary connection, the backup session stays alive long enough to revert the change. Keep a second session open while you modify firewall rules. Future-you will thank you when the network drops.
Verify the rule is active
Do not assume the rule loaded correctly. Check the runtime configuration to confirm the rich rule is present and active.
sudo firewall-cmd --zone=public --list-rich-rules
# displays only rich rules for the specified zone
# runtime rules show what is actually filtering packets right now
# permanent rules require the --permanent flag to list
You should see your CIDR blocks listed exactly as you typed them. If the output is empty, the rule failed to parse or was added to the wrong zone. Check which zone your interface is actually using.
sudo firewall-cmd --get-active-zones
# maps network interfaces to their assigned zones
# a mismatch here is the most common reason rules appear to do nothing
# interfaces default to public unless explicitly assigned elsewhere
Run --list-rich-rules before you test connectivity. Guessing whether a rule loaded is how you end up locked out of SSH.
Common pitfalls and error messages
Rich rule syntax is strict. A missing quote, a misplaced space, or an invalid CIDR block will cause firewall-cmd to reject the command. The daemon does not guess. It prints a parse error and leaves the configuration untouched.
ERROR: INVALID_RICH_RULE: rule family="ipv4" source address="192.168.1.0/24" accept
This error usually means the shell ate your quotes. The single quotes around the entire rule are mandatory. Without them, the shell interprets the double quotes as string delimiters and breaks the command into multiple arguments. Always wrap rich rules in single quotes.
Another frequent issue is zone mismatch. You add a rule to public, but your interface is actually in work or home. The rule loads successfully, but no traffic matches it because the packets arrive on a different zone. Check active zones first. Assign the interface to the correct zone with --change-interface if needed.
CIDR notation must be valid. 192.168.1.0/25 is fine. 192.168.1.0/33 is not. firewalld will reject invalid masks with a clear error. If you are unsure about your subnet mask, calculate it before typing it into the terminal.
Removing rules requires the exact same syntax you used to add them, but with --remove-rich-rule instead.
sudo firewall-cmd --permanent --zone=public --remove-rich-rule='rule family="ipv4" source address="192.168.1.0/24" accept'
# --remove-rich-rule deletes the exact match from the persistent config
# you must quote the rule identically to how you added it
# mismatched quotes or spaces will cause the removal to fail silently
Run firewall-cmd --reload after removing rules. The runtime config does not auto-sync with permanent deletions.
When to use rich rules versus other methods
Use rich rules when you need to combine source IP ranges with specific ports or protocols in a single statement. Use --add-source when you want to allow an entire subnet access to every service already enabled in the zone. Use --add-service or --add-port when you need to open a port to the entire internet without IP restrictions. Use the GNOME Firewall GUI when you prefer a visual interface for managing zones and services. Use nftables directly when you need advanced packet manipulation, traffic shaping, or custom sets that firewalld does not expose.