diff --git a/roles/common/files/spamhaus-ipv4.xml b/roles/common/files/spamhaus-ipv4.xml
new file mode 100644
index 0000000..6854d00
--- /dev/null
+++ b/roles/common/files/spamhaus-ipv4.xml
@@ -0,0 +1,6 @@
+
+
+
+ spamhaus-ipv4
+ Spamhaus DROP and EDROP lists placeholder (IPv4).
+
diff --git a/roles/common/files/spamhaus-ipv6.xml b/roles/common/files/spamhaus-ipv6.xml
new file mode 100644
index 0000000..445a2a7
--- /dev/null
+++ b/roles/common/files/spamhaus-ipv6.xml
@@ -0,0 +1,6 @@
+
+
+
+ spamhaus-ipv6
+ Spamhaus DROP list placeholder (IPv6).
+
diff --git a/roles/common/files/update-spamhaus-lists.service b/roles/common/files/update-spamhaus-lists.service
new file mode 100644
index 0000000..ca0bf30
--- /dev/null
+++ b/roles/common/files/update-spamhaus-lists.service
@@ -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
diff --git a/roles/common/files/update-spamhaus-lists.sh b/roles/common/files/update-spamhaus-lists.sh
new file mode 100755
index 0000000..a05e679
--- /dev/null
+++ b/roles/common/files/update-spamhaus-lists.sh
@@ -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"
+
+
+
+ spamhaus-ipv4
+ Spamhaus DROP and EDROP lists (IPv4).
+XML_HEAD
+
+ for network in $networks; do
+ echo " $network" >> "$xml_temp"
+ done
+
+ echo "" >> "$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"
+
+
+
+ spamhaus-ipv6
+ Spamhaus DROP lists (IPv6).
+XML_HEAD
+
+ for network in $networks; do
+ echo " $network" >> "$xml_temp"
+ done
+
+ echo "" >> "$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"
diff --git a/roles/common/files/update-spamhaus-lists.timer b/roles/common/files/update-spamhaus-lists.timer
new file mode 100644
index 0000000..626b3ae
--- /dev/null
+++ b/roles/common/files/update-spamhaus-lists.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=Update Spamhaus lists
+
+[Timer]
+# Once a day at midnight
+OnCalendar=*-*-* 00:00:00
+# Add a random delay of 0–3600 seconds
+RandomizedDelaySec=3600
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/roles/common/tasks/firewall_Ubuntu.yml b/roles/common/tasks/firewall_Ubuntu.yml
index d27661e..eb3619f 100644
--- a/roles/common/tasks/firewall_Ubuntu.yml
+++ b/roles/common/tasks/firewall_Ubuntu.yml
@@ -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
diff --git a/roles/common/templates/public.xml.j2 b/roles/common/templates/public.xml.j2
index 949dbb0..6fe0969 100644
--- a/roles/common/templates/public.xml.j2
+++ b/roles/common/templates/public.xml.j2
@@ -69,4 +69,13 @@
+
+
+
+
+
+
+
+
+