Adding one iptables rule per abusive address gets slow once your blocklist runs into the thousands, because every packet is compared against a long chain of rules. ipset IP blacklisting fixes that: addresses live in a kernel hash set and a single iptables rule matches the whole set, so lookup time stays flat as the list grows. This guide creates an ipset, wires it into iptables, loads a blocklist, and makes everything survive a reboot.
Before you start
You need:
- A Linux VPS running Ubuntu 22.04 or 24.04, Debian 12, AlmaLinux 8/9 or Rocky 8/9, with root or sudo access.
- Working SSH access — see Connect to your Linux VPS with SSH if you are setting this up for the first time.
- iptables already in use, or a decision to use it. If you are still choosing, read UFW vs firewalld vs nftables vs iptables.
- A copy of your current rules:
sudo iptables -S > ~/iptables-backup.txt.
Warning: You are editing the packet filter on a remote machine. A wrong rule can lock you out. Keep a second SSH session open, and test from a different connection before closing the first one. Most VPS providers give you a browser console in their control panel if SSH stops responding.
Install ipset
On Debian and Ubuntu:
sudo apt update
sudo apt install -y ipset iptablesOn AlmaLinux and Rocky Linux:
sudo dnf install -y ipset iptablesLoad the kernel module and confirm it is present:
sudo modprobe ipset
lsmod | grep ipset
ipset --versionIf modprobe returns an error, the kernel on your VPS does not ship the module. Open a ticket at /tickets/new and mention the output.
Create the ipset set
A set is a named container with a type. Use hash:ip for single addresses and hash:net when you also want to block CIDR ranges.
sudo ipset create blacklist hash:ip hashsize 4096 maxelem 1000000 timeout 0hashsize 4096is the initial bucket count. It grows automatically, so the default is fine for most lists.maxelem 1000000caps the set at one million entries.timeout 0means entries never expire. Set a number of seconds instead if you want automatic removal.
If you plan to block subnets, create the set as hash:net:
sudo ipset create blacklist hash:net family inet hashsize 4096 maxelem 1000000For IPv6, create a separate set with family inet6:
sudo ipset create blacklist6 hash:net family inet6Add a few entries to test:
sudo ipset add blacklist 203.0.113.10
sudo ipset add blacklist 198.51.100.0/24
sudo ipset add blacklist 203.0.113.77 timeout 3600Add -exist to any ipset add command to make it silent when the address is already in the set. That flag matters when you re-run a loader script.
Add the iptables rule
One rule matches the entire set:
sudo iptables -I INPUT -m set --match-set blacklist src -j DROP-I inserts at the top of the INPUT chain, so the drop happens before your accept rules. Use -A only if you deliberately want it at the end.
To drop matching traffic in both directions, add the same match to FORWARD and OUTPUT, but keep in mind that dropping OUTPUT can break outbound services that talk to a blocked host.
For IPv6:
sudo ip6tables -I INPUT -m set --match-set blacklist6 src -j DROPNote: On Debian 12 and Ubuntu 24.04 the iptables command is backed by nftables. The --match-set syntax still works and is translated into a native nftables set. If you manage a pure nftables ruleset instead, use nftables sets directly and skip this guide.
Load a blocklist
Adding entries one command at a time is slow for large files. Build a restore file and load it in one pass.
Assume /root/blocklist.txt holds one address or CIDR per line:
awk 'NF {print "add blacklist " $1}' /root/blocklist.txt > /root/blacklist.restore
sudo ipset restore -exist < /root/blacklist.restoreipset restore reads the same format that ipset save writes, so you can also merge a saved set from another server.
If you prefer a loop, wrap it in a single root shell so the redirection works:
sudo bash -c 'while read -r ip; do
[ -z "$ip" ] && continue
ipset add blacklist "$ip" -exist
done < /root/blocklist.txt'Check how many entries landed:
sudo ipset list blacklist | head -n 8The header shows Number of entries. If it is far lower than the line count in your file, some lines were malformed — usually CIDR ranges in a hash:ip set.
Persist the set across reboots
Sets live in kernel memory and disappear on reboot, so save them and restore at boot.
sudo ipset save > /etc/ipset.confCreate a systemd unit at /etc/systemd/system/ipset-restore.service:
[Unit]
Description=Restore ipset sets
Before=network-pre.target
Wants=network-pre.target
[Service]
Type=oneshot
ExecStart=/sbin/ipset restore -exist -f /etc/ipset.conf
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetEnable it:
sudo systemctl daemon-reload
sudo systemctl enable --now ipset-restore.serviceYour iptables rules need their own persistence. On Debian and Ubuntu install iptables-persistent and save with sudo netfilter-persistent save. On the RHEL family use sudo service iptables save or the firewalld equivalent described in Configure firewalld on AlmaLinux and Rocky Linux.
Warning: Order matters. If the firewall rules load before the set exists, iptables fails to apply the rule and the chain may end up incomplete. The Before=network-pre.target line above restores sets early for that reason.
Check it works
Test membership directly:
sudo ipset test blacklist 203.0.113.10Expected output: 203.0.113.10 is in set blacklist.
Confirm the rule is present and counting packets:
sudo iptables -L INPUT -n -v --line-numbersThe pkts column for the match-set blacklist src line increases as blocked traffic arrives. If it stays at zero, nothing is hitting that rule yet.
From a different machine, try to reach the blocked address:
curl --max-time 5 http://203.0.113.10/The connection should hang and time out rather than return a response.
Finally, reboot once and re-check:
sudo rebootAfter reconnecting, run sudo ipset list blacklist | head -n 8 and sudo iptables -S INPUT. Both should show your set and rule.
Troubleshooting
Set blacklist doesn't existwhen adding the iptables rule — the set was not created yet, or it was lost at boot. Create the set first, and make sure the restore unit runs before the firewall loads.ipset addreturnsSyntax error: cannot parse 198.51.100.0/24— you are adding a CIDR range to ahash:ipset. Recreate the set ashash:netand reload the list.- Everything is gone after a reboot — sets and rules are both volatile. Run
ipset saveandiptables-saveafter every change, or automate it with a cron job as described in Schedule tasks with cron on Linux. - A legitimate visitor is blocked — remove the entry with
sudo ipset del blacklist 203.0.113.10, or add new entries with atimeoutso they expire on their own. ipset: command not foundor the module will not load — the package is missing or the host kernel lacks the module. Install the package first; ifmodprobe ipsetstill fails, contact support.
Next steps
- Review the wider firewall picture in UFW vs firewalld vs nftables vs iptables.
- Harden the rest of the box with First steps after your Linux VPS is ready.
- Watch for blocked traffic spikes with Monitor CPU, memory, disk and logs on a Linux server.
- Automate blocklist refreshes using Schedule tasks with cron on Linux.
