Skip to content

Block the Spamhaus DROP List with nftables

The Spamhaus DROP list (Don't Route Or Peer) contains netblocks that are leased or stolen outright by professional spam and cybercrime operations — malware distribution, botnet controllers. Spamhaus' advice for these ranges is blunt: drop all traffic, in both directions. There is no legitimate reason for any packet from these networks to reach your server.

This guide blocks the list at the firewall level with nftables. That is deliberately one layer below nginx: the DROP list is designed for network equipment, and a firewall rule protects every service on the host — SSH, mail, game servers — not just HTTP. The sync runs once a day from cron; after the initial setup there is nothing to maintain.

If your site runs behind Cloudflare instead, see Cloudflare WAF: Block the Spamhaus DROP List — at the origin behind a proxy, a local IP block never sees the real client addresses.


Prerequisites

  • A Linux server with nftables (default firewall backend on Ubuntu 22.04+ and Debian 11+)
  • curl and jq installed:
sudo apt install -y curl jq
  • Root access

As of June 2026, the list sizes are roughly 1,700 IPv4 CIDRs and 100 IPv6 CIDRs — trivial for an nftables set.

Download limits

Spamhaus updates the list daily and asks consumers to fetch it at most once per hour, ideally once per day. Excessive downloads get your IP firewalled by Spamhaus itself. The cron schedule below respects this — do not "improve" it to run every few minutes.


How It Works

nftables organizes rules into tables. The trick for a maintenance-free blocklist is to give it its own table: a table can be atomically replaced in a single nft -f run, without touching your existing firewall rules (UFW, custom rulesets — all unaffected, they live in their own tables).

The sync script:

  1. Downloads drop_v4.json and drop_v6.json
  2. Extracts the CIDRs with jq
  3. Generates a complete nftables file containing two sets and the drop rules
  4. Validates and loads it atomically — the old list is replaced in one step, with no gap

Step 1 — The Sync Script

Save as /usr/local/sbin/update-spamhaus-drop.sh:

#!/usr/bin/env bash
set -euo pipefail

NFT_FILE=/etc/nftables-spamhaus.nft

V4=$(curl -sf https://www.spamhaus.org/drop/drop_v4.json | jq -r '.cidr // empty')
V6=$(curl -sf https://www.spamhaus.org/drop/drop_v6.json | jq -r '.cidr // empty')

# Refuse to install an empty list (download or parse failure)
[ -n "$V4" ] || { echo "ERROR: empty IPv4 list, keeping previous version"; exit 1; }

cat > "$NFT_FILE.tmp" <<EOF
#!/usr/sbin/nft -f
# Spamhaus DROP list — (c) Spamhaus Project, https://www.spamhaus.org/drop/
# Generated $(date -u +%Y-%m-%dT%H:%M:%SZ) — do not edit, rebuilt daily by $0

table inet spamhaus
delete table inet spamhaus
table inet spamhaus {
    set drop4 {
        type ipv4_addr
        flags interval
        elements = { $(echo "$V4" | paste -sd, -) }
    }
    set drop6 {
        type ipv6_addr
        flags interval
        elements = { $(echo "$V6" | paste -sd, -) }
    }
    chain input {
        type filter hook input priority -200; policy accept;
        ip saddr @drop4 counter drop
        ip6 saddr @drop6 counter drop
    }
}
EOF

nft -c -f "$NFT_FILE.tmp"
mv "$NFT_FILE.tmp" "$NFT_FILE"
nft -f "$NFT_FILE"
echo "Spamhaus DROP updated: $(echo "$V4" | wc -l) IPv4 + $(echo "$V6" | wc -l) IPv6 entries"

Make it executable and run it once:

sudo chmod +x /usr/local/sbin/update-spamhaus-drop.sh
sudo /usr/local/sbin/update-spamhaus-drop.sh

A few details worth understanding:

  • .cidr // empty — the JSON feeds are NDJSON (one object per line) and end with a metadata line containing the copyright and timestamp instead of a cidr field. The // empty filter skips it. The copyright stays in the generated file's header, as Spamhaus' license requires.
  • table + delete table + redefinition — this is the standard nftables idempotency pattern. The first declaration ensures the table exists, delete removes it, then it is recreated from scratch. The whole file applies atomically: there is no moment where the server runs without the list.
  • priority -200 — hooks the chain well before the standard filter chains (priority 0), so DROP traffic is discarded before UFW or other rules spend time on it.
  • flags interval — required for sets containing CIDR ranges rather than single addresses.
  • nft -c -f — a syntax check before activation. If Spamhaus ever changes the format and parsing breaks, the script fails loudly and the previous list stays active.
  • counter — counts hits per rule, so you can see the list working.

Step 2 — Load on Boot

The generated file must survive a reboot. Include it from the main nftables config:

echo 'include "/etc/nftables-spamhaus.nft"' | sudo tee -a /etc/nftables.conf
sudo systemctl enable nftables

On Ubuntu, the nftables service applies /etc/nftables.conf at boot. The list is then refreshed by the daily cron job.


Step 3 — Daily Refresh via Cron

echo '17 4 * * * root /usr/local/sbin/update-spamhaus-drop.sh 2>&1 | logger -t spamhaus-drop' | sudo tee /etc/cron.d/spamhaus-drop

This runs the sync daily at 04:17 and sends the output to syslog. Picking an odd minute instead of a round hour spreads the load on Spamhaus' mirrors — they explicitly ask for this.


Verify

Check that the table is loaded and count the entries:

sudo nft list table inet spamhaus | grep -c '/'

Watch the counters grow over time — every hit is a connection attempt that went straight to the bin:

sudo nft list chain inet spamhaus input
chain input {
    type filter hook input priority -200; policy accept;
    ip saddr @drop4 counter packets 1337 bytes 80220 drop
    ip6 saddr @drop6 counter packets 0 bytes 0 drop
}

And confirm the cron log after the first scheduled run:

grep spamhaus-drop /var/log/syslog | tail -3

Common Issues

Symptom Cause Fix
Could not process rule: No such file or directory on first run delete table on a non-existent table The script's bare table inet spamhaus declaration prevents this — make sure that line was not removed
Script fails with empty list error Spamhaus unreachable or rate-limiting your IP Wait and retry; check you are not fetching more than once per hour
Rules gone after reboot nftables.service not enabled or include missing systemctl enable nftables and verify the include line in /etc/nftables.conf
UFW rules stopped working Unrelated — separate tables do not interact Check ufw status; the spamhaus table only drops DROP-listed sources
Legitimate user blocked They are inside a DROP-listed range (very unlikely) Verify at Spamhaus IP lookup — if listed, their provider has a problem, not you