roles/common: Add Spamhaus DROP lists to firewalld ipsets

This configures the recommended DROP, EDROP, and DROPv6 lists from
Spamhaus as ipsets in firewalld. First we copy an empty placeholder
ipset to seed firewalld, then we use a shell script to download the
real lists and activate them. The same shell script is run daily as
a service (update-spamhaus-lists.service) by a systemd timer.

I am strictly avoiding any direct ipset commands here because I want
to make sure that this works on older hosts where ipsets is used as
well as newer hosts that have moved to nftables such as Ubuntu 20.04.
So far I have tested this on Ubuntu 16.04, 18.04, and 20.04, but ev-
entually I need to abstract the tasks and run them on CentOS 7+ as
well.

See: https://www.spamhaus.org/drop/
This commit is contained in:
Alan Orth 2021-07-21 09:34:51 +03:00
parent ee5f4cdf74
commit d7c34a30a3
Signed by: alanorth
GPG Key ID: 0FB860CC9C45B1B9
7 changed files with 193 additions and 0 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ipset type="hash:net">
<option name="family" value="inet" />
<short>spamhaus-ipv4</short>
<description>Spamhaus DROP and EDROP lists placeholder (IPv4).</description>
</ipset>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ipset type="hash:net">
<option name="family" value="inet6" />
<short>spamhaus-ipv6</short>
<description>Spamhaus DROP list placeholder (IPv6).</description>
</ipset>

View File

@ -0,0 +1,27 @@
[Unit]
Description=Update Spamhaus lists
# This service will fail if firewalld is not running so we use Requires to make
# sure that firewalld is started.
Requires=firewalld.service
# Make sure the network is up and firewalld is started
After=network-online.target firewalld.service
Wants=network-online.target update-spamhaus-lists.timer
[Service]
# https://www.ctrl.blog/entry/systemd-service-hardening.html
# Doesn't need access to /home or /root
ProtectHome=true
# Possibly only works on Ubuntu 18.04+
ProtectKernelTunables=true
ProtectSystem=full
# Newer systemd can use ReadWritePaths to list files, but this works everywhere
ReadWriteDirectories=/etc/firewalld/ipsets
PrivateTmp=true
WorkingDirectory=/var/tmp
SyslogIdentifier=update-spamhaus-lists
ExecStart=/usr/bin/flock -x update-spamhaus-lists.lck \
/usr/local/bin/update-spamhaus-lists.sh
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,107 @@
#!/usr/bin/env bash
#
# update-spamhaus-lists.sh v0.0.5
#
# Download Spamhaus DROP lists and load them into firewalld ipsets. Should work
# with both the iptables and nftables backends.
#
# See: https://www.spamhaus.org/drop/
#
# Copyright (C) 2021 Alan Orth
#
# SPDX-License-Identifier: GPL-3.0-only
# Exit on first error
set -o errexit
firewalld_ipsets=$(firewall-cmd --get-ipsets)
xml_temp=$(mktemp)
spamhaus_ipv4_ipset_path=/etc/firewalld/ipsets/spamhaus-ipv4.xml
spamhaus_ipv6_ipset_path=/etc/firewalld/ipsets/spamhaus-ipv6.xml
function download() {
echo "Downloading $1"
wget -q -O - "https://www.spamhaus.org/drop/$1" > "$1"
}
download drop.txt
download edrop.txt
download dropv6.txt
if [[ -f "drop.txt" && -f "edrop.txt" ]]; then
echo "Processing IPv4 DROP lists"
# Extract all networks from drop.txt and edrop.txt, skipping blank lines and
# comments.
networks=$(cat drop.txt edrop.txt | sed -e '/^$/d' -e '/^;.*/d' -e 's/[[:space:]];[[:space:]].*//')
# If firewalld already has this ipset we should delete it first to emulate
# `ipset flush` (but I don't want to use that because newer hosts might be
# using nftables and firewalld will handle that for us).
if [[ "$firewalld_ipsets" =~ spamhaus-ipv4 ]]; then
echo "Deleting existing spamhaus-ipv4 ipset"
# This deletes the firewalld ipset XML file as well as the ipset itself
firewall-cmd --permanent --delete-ipset=spamhaus-ipv4
else
echo "Creating placeholder spamhaus-ipv4 ipset"
# Create a placeholder ipset so firewalld doesn't complain when we try
# to reload the ipset later after having added a new XML definition. I
# don't know why, but depending on the system state there may not be a
# ipset defined and firewalld errors on INVALID_IPSET.
firewall-cmd --permanent --new-ipset=spamhaus-ipv4 --type=hash:net --option=family=inet
fi
# I'm not proud of this, but writing the XML directly is WAY faster than
# using firewall-cmd to add each entry one by one (and we can't add from
# a file because many of our hosts are using old firewalld).
cat << XML_HEAD > "$xml_temp"
<?xml version="1.0" encoding="utf-8"?>
<ipset type="hash:net">
<option name="family" value="inet" />
<short>spamhaus-ipv4</short>
<description>Spamhaus DROP and EDROP lists (IPv4).</description>
XML_HEAD
for network in $networks; do
echo " <entry>$network</entry>" >> "$xml_temp"
done
echo "</ipset>" >> "$xml_temp"
install -m 0600 "$xml_temp" "$spamhaus_ipv4_ipset_path"
fi
if [[ -f "dropv6.txt" ]]; then
echo "Processing IPv6 DROP list"
networks=$(sed -e '/^$/d' -e '/^;.*/d' -e 's/[[:space:]];[[:space:]].*//' dropv6.txt)
if [[ "$firewalld_ipsets" =~ spamhaus-ipv6 ]]; then
echo "Deleting existing spamhaus-ipv6 ipset"
firewall-cmd --permanent --delete-ipset=spamhaus-ipv6
else
echo "Creating placeholder spamhaus-ipv6 ipset"
firewall-cmd --permanent --new-ipset=spamhaus-ipv6 --type=hash:net --option=family=inet6
fi
cat << XML_HEAD > "$xml_temp"
<?xml version="1.0" encoding="utf-8"?>
<ipset type="hash:net">
<option name="family" value="inet6" />
<short>spamhaus-ipv6</short>
<description>Spamhaus DROP lists (IPv6).</description>
XML_HEAD
for network in $networks; do
echo " <entry>$network</entry>" >> "$xml_temp"
done
echo "</ipset>" >> "$xml_temp"
install -m 0600 "$xml_temp" "$spamhaus_ipv6_ipset_path"
fi
echo "Reloading firewalld"
firewall-cmd --reload
rm -v drop.txt edrop.txt dropv6.txt "$xml_temp"

View File

@ -0,0 +1,12 @@
[Unit]
Description=Update Spamhaus lists
[Timer]
# Once a day at midnight
OnCalendar=*-*-* 00:00:00
# Add a random delay of 03600 seconds
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -54,6 +54,32 @@
loop:
- abusers-ipv4.xml
- abusers-ipv6.xml
- spamhaus-ipv4.xml
- spamhaus-ipv6.xml
notify:
- restart firewalld
- name: Copy Spamhaus update script
when: ansible_distribution_version is version('16.04', '>=')
copy: src=update-spamhaus-lists.sh dest=/usr/local/bin/update-spamhaus-lists.sh mode=0755 owner=root group=root
- name: Copy Spamhaus systemd units
when: ansible_distribution_version is version('16.04', '>=')
copy: src={{ item }} dest=/etc/systemd/system/{{ item }} mode=0644 owner=root group=root
loop:
- update-spamhaus-lists.service
- update-spamhaus-lists.timer
register: spamhaus_systemd_units
# need to reload to pick up service/timer/environment changes
- name: Reload systemd daemon
systemd: daemon_reload=yes
when: spamhaus_systemd_units is changed
- name: Start and enable Spamhaus update timer
when: ansible_distribution_version is version('16.04', '>=')
systemd: name=update-spamhaus-lists.timer state=started enabled=yes
notify:
- restart firewalld

View File

@ -69,4 +69,13 @@
<source ipset="abusers-ipv6"/>
<drop/>
</rule>
<rule>
<source ipset="spamhaus-ipv4"/>
<drop/>
</rule>
<rule>
<source ipset="spamhaus-ipv6"/>
<drop/>
</rule>
</zone>