Using Dynamic Blocklists with IPtables + IPset

In this tutorial, we will learn how to make automatically updated block lists of known bad addresses using ipset and iptables.

This provides a proactive security approach that can use external datasets to discover addresses known for malicious activity and prevent them from accessing your web server.

A quick overview on IPSet

In some of my earlier tutorials, we learned how we could use ipset to create dynamic filtering rules which are independent of iptables.

For example, if we wanted to give our friend temporary access to SSH,

# create ipset
sudo ipset create ssh_ip hash:ip

# add friends ip
sudo ipset add 192.0.2.62 ssh_ip

# remove his ip
sudo ipset del 192.0.2.62

Then in IPTables, we have the following rule

-A INPUT -m state --state NEW -p tcp --dport 22 \
         -m set --match-set ssh_ip src -j ACCEPT

Let's apply this concept to make powerful, automatically updated rule sets.

We pick a blockset from https://github.com/firehol/blocklist-ipsets
These block lists are based on various sensor nodes that monitor what hosts have attempted to intrude into them, and by aggregating them, we get a good idea of which addresses are continual sources of trouble. These lists are ephemeral, so a cronjob is required to keep them up to date.

First, we install the ipset and iprange utilities

$ sudo apt install ipset iprange

Next, we create our first block list

$ sudo ipset create firehol_webserver hash:net

Now we populate it (this may take a while)

# this must be run as root
curl https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_webserver.netset | iprange | while read line; do ipset add firehol_webserver $line; done

Now we create our iptables rules.

We need to make sure the rule to block bad addresses is before the rule to allow http/https, otherwise it will be of no effect.

We get a numbered list of our iptables rules and take note of where we allow 80,443

$ sudo iptables -L -n --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       all  --  0.0.0.0/0            0.0.0.0/0            state INVALID
2    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
3    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0           
4    ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0            state NEW icmptype 8 limit: avg 1/sec burst 5
5    ACCEPT     tcp  --  0.0.0.0/0            10.88.8.8            state NEW tcp flags:0x17/0x02 multiport dports 80,443
6    REJECT     tcp  --  0.0.0.0/0            10.88.8.8            state NEW tcp flags:0x17/0x02 reject-with tcp-reset

In this case, we can see that we need to inset our rule at line 5, so that it immediately precedes the rule where 80,443 is accepted.

$ sudo iptables -I INPUT 5 -m state --state NEW -p tcp -m set --match-set firehol_webserver src -j REJECT --reject-with tcp-reset

Creating the cronscript to automatically update blocklist

Open your favourite text editor and paste the following into /etc/cron.daily/ipset
(make sure to replace URL and BLOCKLIST with your desired chocies)

#! /bin/bash
URL="https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/firehol_webserver.netset"
BLOCKLIST=firehol_webserver

curl $URL | iprange | while read line; do ipset add $BLOCKLIST $line; done

Then make sure the file is executable by running chmod +x /etc/cron.daily/ipset.

Finally, we test the cron script is working as intended by running it (as root)

root@localhost:/etc/cron.daily# ./ipset
ipset v6.29: Element cannot be added to the set: it's already added
ipset v6.29: Element cannot be added to the set: it's already added
ipset v6.29: Element cannot be added to the set: it's already added
[...]

We can monitor the hit counts of our new ruleset

$ sudo iptables -L -nv --line-numbers | grep firehol_webserver
5        0     0 REJECT     tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            state NEW match-set firehol_webserver src reject-with tcp-reset