Adding one rich rule per bad IP works for ten addresses, not ten thousand. Every rule is checked in turn, and reloads get slow. An ipset stores all the addresses in a single hashed set, and one rule drops anything in it. Lookups stay fast however big the list gets. This guide builds a blocklist ipset in firewalld, loads it from a file, and shows how to keep it up to date.
Before you start
- A Linux VPS running firewalld, usually AlmaLinux or Rocky Linux 8 or 9. If firewalld is not set up, see Set up firewalld on AlmaLinux and Rocky Linux.
- Root or a sudo user.
- A blocklist: a plain text file with one IPv4 address or CIDR range per line. You can build your own from logs or use a public list you trust.
- Your own public IP, so you can make sure it is not in the list.
- Access to the browser console in your VPS provider's control panel.
Warning: If your own IP, or a range that includes it, ends up in the blocklist, your SSH session stops working. Check the list before you load it, and keep the browser console in your VPS provider's control panel as your way back in. Also make sure ssh stays allowed in your normal zone.
Prepare the blocklist file
Put the list somewhere root-owned, for example /etc/firewalld/blocklist.txt. Keep only valid entries, one per line:
198.51.100.0/24
203.0.113.77
192.0.2.0/25Remove comments, blank lines and Windows line endings, and drop any duplicates:
sudo sed -i -e 's/#.*//' -e 's/\r$//' -e '/^\s*$/d' /etc/firewalld/blocklist.txt
sudo sort -u -o /etc/firewalld/blocklist.txt /etc/firewalld/blocklist.txt
wc -l /etc/firewalld/blocklist.txtCheck your own IP is not covered. A plain grep catches exact matches; also look over any wide ranges by eye:
grep -n '203.0.113.10' /etc/firewalld/blocklist.txtCreate the ipset
hash:net accepts both single addresses and CIDR ranges. Set maxelem above the size of your list, because the default limit is 65536 entries:
sudo firewall-cmd --permanent --new-ipset=blocklist --type=hash:net \
--option=family=inet --option=maxelem=262144
sudo firewall-cmd --reloadFor IPv6 addresses, create a second set with --option=family=inet6, for example blocklist6. One set holds one address family.
Load the entries
Load the whole file into the permanent configuration, then reload:
sudo firewall-cmd --permanent --ipset=blocklist --add-entries-from-file=/etc/firewalld/blocklist.txt
sudo firewall-cmd --reloadConfirm the count:
sudo firewall-cmd --ipset=blocklist --get-entries | wc -lAttach the ipset to the drop zone
The drop zone silently discards all incoming packets. Binding the ipset as a source of that zone sends every listed address there, before your normal zone sees it:
sudo firewall-cmd --permanent --zone=drop --add-source=ipset:blocklist
sudo firewall-cmd --reloadYour public interface stays in public (or whichever zone it was in), so everyone else is handled as before. Check:
sudo firewall-cmd --zone=drop --list-sources
sudo firewall-cmd --get-active-zonesUpdate entries without a full reload
A reload rebuilds the whole ruleset. For one or two changes, update the live set and the saved set separately instead:
# block a new address now and after the next reload
sudo firewall-cmd --ipset=blocklist --add-entry=198.51.100.99
sudo firewall-cmd --permanent --ipset=blocklist --add-entry=198.51.100.99
# unblock an address
sudo firewall-cmd --ipset=blocklist --remove-entry=203.0.113.77
sudo firewall-cmd --permanent --ipset=blocklist --remove-entry=203.0.113.77Runtime commands take effect immediately with no reload. The --permanent copy makes sure the change survives a reboot.
To check one address:
sudo firewall-cmd --ipset=blocklist --query-entry=198.51.100.99Replace the whole list on a schedule
To refresh from a new file, remove the old entries and load the new file in the permanent config, then reload once:
sudo firewall-cmd --permanent --ipset=blocklist --remove-entries-from-file=/etc/firewalld/blocklist.txt
sudo cp /etc/firewalld/blocklist.new /etc/firewalld/blocklist.txt
sudo firewall-cmd --permanent --ipset=blocklist --add-entries-from-file=/etc/firewalld/blocklist.txt
sudo firewall-cmd --reloadClean and check the new file the same way as before. Run this during a quiet period, because a reload briefly rebuilds the rules.
Check it works
sudo firewall-cmd --info-ipset=blocklist | head
sudo nft list ruleset | grep -i blocklistThe first shows the set type and options. The second shows that firewalld created the matching nftables set and rule. From a test machine whose IP you have added on purpose, a connection to the server should now time out. Remove that test entry afterwards.
Troubleshooting
- "INVALID_ENTRY" while loading: a line is not a valid address or range, often a stray comment or
\r. Clean the file as shown and retry. - "ipset is full" or entries missing: the list is larger than
maxelem. Delete the set, recreate it with a higher value and reload. - "ZONE_CONFLICT" when adding the source: the ipset is already bound to another zone. Remove it there with
--remove-source=ipset:blocklistfirst. - Reload takes a long time: very large lists are slow to load. Use the runtime-plus-permanent pattern for small updates.
- Locked out: your IP was in the list. Log in through the browser console, remove it with
--remove-entry, then fix the source file.
Next steps
- Understand where the drop zone fits: firewalld zones explained.
- Ban attackers automatically instead of by hand: Use Fail2ban with firewalld or nftables ban actions.
- Prefer raw nftables? See Use nftables sets for fast allowlists and blocklists.
