Security

Block thousands of IPs with firewalld ipsets

Create a hash:net ipset in firewalld, load a blocklist file into it, attach it to the drop zone and update entries later without rebuilding your whole firewall.

AdvancedLinux server5 min read

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/25

Remove comments, blank lines and Windows line endings, and drop any duplicates:

Bash
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.txt

Check your own IP is not covered. A plain grep catches exact matches; also look over any wide ranges by eye:

Bash
grep -n '203.0.113.10' /etc/firewalld/blocklist.txt

Create 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:

Bash
sudo firewall-cmd --permanent --new-ipset=blocklist --type=hash:net \
  --option=family=inet --option=maxelem=262144
sudo firewall-cmd --reload

For 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:

Bash
sudo firewall-cmd --permanent --ipset=blocklist --add-entries-from-file=/etc/firewalld/blocklist.txt
sudo firewall-cmd --reload

Confirm the count:

Bash
sudo firewall-cmd --ipset=blocklist --get-entries | wc -l

Attach 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:

Bash
sudo firewall-cmd --permanent --zone=drop --add-source=ipset:blocklist
sudo firewall-cmd --reload

Your public interface stays in public (or whichever zone it was in), so everyone else is handled as before. Check:

Bash
sudo firewall-cmd --zone=drop --list-sources
sudo firewall-cmd --get-active-zones

Update 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:

Bash
# 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.77

Runtime commands take effect immediately with no reload. The --permanent copy makes sure the change survives a reboot.

To check one address:

Bash
sudo firewall-cmd --ipset=blocklist --query-entry=198.51.100.99

Replace 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:

Bash
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 --reload

Clean 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

Bash
sudo firewall-cmd --info-ipset=blocklist | head
sudo nft list ruleset | grep -i blocklist

The 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:blocklist first.
  • 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

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.