ipset gives your firewall a single named list it can match against instead of hundreds of separate rules, and this guide builds one from scratch. By the end you will have an IPv4 hash:ip set holding a small blocklist, a repeatable way to add and test addresses, and a saved copy you can restore. The examples use ipset 7.19, from Ubuntu package ipset 7.19-1ubuntu2. Allow about 10 minutes, plus time to check that a set is not already in use by your firewall.
You need a Linux host with the ipset package installed and permission to talk to the kernel's netfilter interface. Creating or changing sets normally needs root, so the examples use sudo. Even listing and testing can fail without that permission on a restricted host.
This guide creates only an ipset. It does not add an iptables or nftables rule that uses it, and a set on its own blocks nothing. If a firewall rule already refers to a set, though, changing that set changes live filtering behaviour immediately.
Checkpoint: Choose a set name that is not already in use. The name ssh-blocklist-demo is deliberately specific. Check first:
sudo ipset list -name
Expected output is either an empty result or a list of existing names that does not include ssh-blocklist-demo. The short option -name means names only; it avoids dumping every member of every set.
Record the program and protocol versions before copying a procedure between machines. The installed command reports both, while the package query identifies the distribution package:
ipset --version
dpkg-query -W -f='\${Package} \${Version}\n' ipset
On the system used for this guide, the output includes ipset v7.19, protocol version: 7 and ipset 7.19-1ubuntu2. Do not treat that output as a kernel capability check: a command can still fail if the kernel modules or permissions needed by the chosen set type are unavailable.
Create a hash set for IPv4 addresses, with a five-minute default timeout for new entries:
sudo ipset create ssh-blocklist-demo hash:ip family inet timeout 300
hash:ip stores IP addresses in a hash-backed set.family inet makes the address family explicit; it is also the default for the relevant hash types.timeout 300 is in seconds and applies to new entries unless an entry supplies its own timeout. A zero timeout means permanent, but use that deliberately.Creation is a state change. If the command says the set already exists, stop and inspect it rather than appending options to a guessed state. The -exist option can make repeated automation idempotent, but it only swallows the error when the existing set has identical create parameters.
Checkpoint: Inspect the header before adding anything:
sudo ipset list ssh-blocklist-demo
Look for a header naming Type: hash:ip, the IPv4 family, and the timeout setting. There should be no members yet.
Add a test address for 60 seconds. Replace it with an address you are authorised to block when using this on a real system:
sudo ipset add ssh-blocklist-demo 198.51.100.23 timeout 60
The address comes from the documentation range, so it is safe as an example. The per-entry timeout overrides the set default. Add a second address using the default five-minute lifetime:
sudo ipset add ssh-blocklist-demo 203.0.113.45
Warning: do not put arbitrary public addresses into a production blocklist without checking the incident, owner and change record. If the set is referenced by a live firewall rule, the first add may affect traffic immediately.
Adding an existing entry is an error unless you use -exist. With timeout support, re-adding an entry with -exist can change its timeout:
sudo ipset -exist add ssh-blocklist-demo 198.51.100.23 timeout 600
This makes the example entry live for ten minutes from the re-add. It does not turn a different set type into this one, and it does not make an unknown set safe to modify.
Use test when a script needs a yes-or-no answer rather than a listing:
sudo ipset test ssh-blocklist-demo 198.51.100.23
echo "exit status: $?"
sudo ipset test ssh-blocklist-demo 192.0.2.99
echo "exit status: $?"
A present entry returns status 0. A missing entry returns a non-zero status. The command may print a matching message, but scripts should trust the exit status, not scrape human-readable output.
For a readable listing, use the default plain format. For a machine-friendly saved representation, request the save format instead:
sudo ipset list ssh-blocklist-demo
sudo ipset -o save list ssh-blocklist-demo
The sorted option can make reviews and comparisons easier, although sorting may be slow on large sets:
sudo ipset -sorted list ssh-blocklist-demo
Timed-out entries are removed by garbage collection. Until that work runs, the header count can briefly sit higher than the entries printed. Do not treat that short-lived gap as proof an expired address is still matchable.
Save the set to a root-readable file before making a bulk change. Use a path with a clear ownership and retention policy:
sudo ipset save ssh-blocklist-demo | sudo tee /root/ssh-blocklist-demo.ipset >/dev/null
sudo sed -n '1,20p' /root/ssh-blocklist-demo.ipset
The saved commands feed ipset restore. Saving is not a complete firewall backup: it captures the set, not the rules that refer to it. Protect the file, because it reveals which addresses your policy tracks.
To restore it later, feed the file to the restore command:
sudo ipset restore < /root/ssh-blocklist-demo.ipset
sudo ipset list ssh-blocklist-demo
Restore does not erase existing sets and members unless the restore input tells it to. That is useful for recovery, but it also means stale entries can remain. Review the saved input and current state before restoring into a live host.
When the demonstration is over, first confirm whether a firewall rule refers to the set: a referenced set cannot be destroyed. If it is safe to clear only its members, flush it:
sudo ipset flush ssh-blocklist-demo
sudo ipset list ssh-blocklist-demo
Flush is destructive for the entries and cannot be undone except by restoring a saved copy. If the set is not referenced and you no longer need it, destroy it:
sudo ipset destroy ssh-blocklist-demo
sudo ipset list -name
Warning: never use destroy without a set name on a host whose other sets matter. With no name given, the command attempts to destroy every set that is not protected by a reference. A referenced set survives, but relying on that protection is a poor change-control strategy.
-exist to change an existing entry's timeout.inet6 set for IPv6 addresses; an IPv4 hash set does not accept both.-exist only when ignoring an identical create or repeated add is genuinely wanted.Operation not permitted error can mean the process lacks the required netfilter capability or the host lacks the kernel support, not that the address is invalid.