Control panels

Using ipset for high-performance IP blacklisting on Linux

Build a kernel-level IP blocklist with ipset and iptables, load thousands of addresses in seconds, and keep the set alive across reboots.

AdvancedLinux server7 min read

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:

Bash
sudo apt update
sudo apt install -y ipset iptables

On AlmaLinux and Rocky Linux:

Bash
sudo dnf install -y ipset iptables

Load the kernel module and confirm it is present:

Bash
sudo modprobe ipset
lsmod | grep ipset
ipset --version

If 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.

Bash
sudo ipset create blacklist hash:ip hashsize 4096 maxelem 1000000 timeout 0
  • hashsize 4096 is the initial bucket count. It grows automatically, so the default is fine for most lists.
  • maxelem 1000000 caps the set at one million entries.
  • timeout 0 means 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:

Bash
sudo ipset create blacklist hash:net family inet hashsize 4096 maxelem 1000000

For IPv6, create a separate set with family inet6:

Bash
sudo ipset create blacklist6 hash:net family inet6

Add a few entries to test:

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

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

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

Bash
sudo ip6tables -I INPUT -m set --match-set blacklist6 src -j DROP

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

Bash
awk 'NF {print "add blacklist " $1}' /root/blocklist.txt > /root/blacklist.restore
sudo ipset restore -exist < /root/blacklist.restore

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

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

Bash
sudo ipset list blacklist | head -n 8

The 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.

Bash
sudo ipset save > /etc/ipset.conf

Create a systemd unit at /etc/systemd/system/ipset-restore.service:

INI
[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.target

Enable it:

Bash
sudo systemctl daemon-reload
sudo systemctl enable --now ipset-restore.service

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

Bash
sudo ipset test blacklist 203.0.113.10

Expected output: 203.0.113.10 is in set blacklist.

Confirm the rule is present and counting packets:

Bash
sudo iptables -L INPUT -n -v --line-numbers

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

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

Bash
sudo reboot

After 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 exist when 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 add returns Syntax error: cannot parse 198.51.100.0/24 — you are adding a CIDR range to a hash:ip set. Recreate the set as hash:net and reload the list.
  • Everything is gone after a reboot — sets and rules are both volatile. Run ipset save and iptables-save after 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 a timeout so they expire on their own.
  • ipset: command not found or the module will not load — the package is missing or the host kernel lacks the module. Install the package first; if modprobe ipset still fails, contact support.

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.