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+)
curlandjqinstalled:
- 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:
- Downloads
drop_v4.jsonanddrop_v6.json - Extracts the CIDRs with
jq - Generates a complete nftables file containing two sets and the drop rules
- 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:
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 acidrfield. The// emptyfilter 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,deleteremoves 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:
Watch the counters grow over time — every hit is a connection attempt that went straight to the bin:
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:
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 |