Skip to content

Cloudflare WAF: Block the Spamhaus DROP List (Free Plan)

The Spamhaus DROP list contains netblocks controlled by professional spam and cybercrime operations. Blocking it in the Cloudflare WAF keeps that traffic away from your sites entirely — before it ever reaches your origin.

If you have tried this with WAF custom rules and given up: pasting IP ranges directly into a rule expression hits the expression size limit almost immediately, on the Free plan and on Pro alike. That is the wrong mechanism. The right one is a custom IP list — and those are available on every plan:

Plan IP lists Total items
Free 1 10,000
Pro / Business 10 10,000

The DROP list currently holds roughly 1,700 IPv4 + 100 IPv6 CIDRs (June 2026) — comfortably within the 10,000-item quota of the Free plan. The list is referenced by a single WAF rule that never changes; a daily cron job replaces the list contents through the API.

For servers not behind Cloudflare, see the companion guide: Block the Spamhaus DROP List with nftables.


Prerequisites

  • A domain proxied through Cloudflare (any plan)
  • Your Account ID — visible in the Cloudflare dashboard on any zone's Overview page, right sidebar
  • A Linux machine with curl and jq to run the sync (any always-on box — the server itself, a Pi, an LXC)

Free plan: one list

The Free plan includes exactly one custom list. If you already use it for something else, this approach needs Pro — or you merge both use cases into the one list.


Step 1 — Create an API Token

The sync script needs a token that can edit account-level lists — nothing more. In the dashboard: My Profile → API Tokens → Create Token → Create Custom Token with a single permission:

Scope Permission Access
Account Account Filter Lists Edit

Restrict the token to the one account, set no zone permissions. Note the token — it is shown only once.


Step 2 — Create the IP List

Once via the API (or in the dashboard under Manage Account → Configurations → Lists):

curl -s -X POST \
  "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/rules/lists" \
  -H "Authorization: Bearer <API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"name": "spamhaus_drop", "kind": "ip", "description": "Spamhaus DROP - synced daily"}'

The response contains the list's id — note it, the sync script needs it. List names may only contain lowercase letters, numbers, and underscores.

IP lists accept individual addresses and CIDR ranges (IPv4 /8/32, IPv6 /12/128), so the DROP entries can be inserted as-is.


Step 3 — The WAF Rule (Once, Never Touched Again)

In the dashboard of the zone you want to protect: Security → WAF → Custom rules → Create rule:

  • Rule name: Spamhaus DROP
  • Expression (use the expression editor): (ip.src in $spamhaus_drop)
  • Action: Block

Repeat for each zone that should enforce the list — the list itself is account-wide, so all rules reference the same data. When the cron job updates the list, every referencing rule picks up the change automatically. This is the entire point: the rule is static, only the list contents rotate.


Step 4 — The Sync Script

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

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

ACCOUNT_ID="your-account-id"
LIST_ID="your-list-id"
API_TOKEN="your-api-token"

BODY=$(curl -sf https://www.spamhaus.org/drop/drop_v4.json \
            https://www.spamhaus.org/drop/drop_v6.json \
  | jq -s '[ .[] | select(.cidr != null) | {ip: .cidr, comment: "Spamhaus DROP"} ]')

COUNT=$(echo "$BODY" | jq 'length')
[ "$COUNT" -gt 100 ] || { echo "ERROR: only $COUNT entries parsed, aborting"; exit 1; }

RESULT=$(echo "$BODY" | curl -sf -X PUT \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/rules/lists/$LIST_ID/items" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  --data @-)

echo "$RESULT" | jq -e '.success' >/dev/null \
  || { echo "ERROR: API call failed: $RESULT"; exit 1; }

echo "Spamhaus DROP synced to Cloudflare: $COUNT entries"

Fill in the three variables, then protect and test it:

sudo chmod 700 /usr/local/sbin/update-spamhaus-cf.sh
sudo /usr/local/sbin/update-spamhaus-cf.sh

How it works:

  • One curl, two URLs — both feeds are fetched in a single call and concatenated; jq -s slurps the NDJSON lines into one array. The metadata line of each feed has no cidr field and is filtered out by select.
  • PUT .../items replaces the entire list — no diffing needed. The old entries are removed and the new set is installed in one operation. The call is asynchronous on Cloudflare's side; for a daily blocklist sync, waiting for completion is unnecessary.
  • The sanity check (-gt 100) prevents an empty or truncated download from wiping your list.
  • chmod 700 because the script contains the API token. If other admins share the box, move the token into a separate root-owned file and source it.

Download limits

Spamhaus asks consumers to fetch the feeds at most once per hour, ideally once per day — excessive polling gets your IP blocked by Spamhaus. Daily is also plenty: the list changes slowly.


Step 5 — Daily Cron

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

Output goes to syslog; an odd minute avoids hammering Spamhaus on the full hour like everyone else.


Verify

Check the item count via API:

curl -s "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/rules/lists/<LIST_ID>" \
  -H "Authorization: Bearer <API_TOKEN>" | jq '.result.num_items'

In the dashboard, Security → Events shows blocked requests with the rule name Spamhaus DROP once traffic from listed ranges arrives — for most sites that takes hours, not weeks; these networks scan constantly.


What About ASN-DROP?

Spamhaus also publishes ASN-DROP (~420 autonomous systems). Cloudflare supports ASN lists, but only on Enterprise plans. On Free/Pro the only route is an inline expression (ip.src.asnum in {…}) maintained by a script — possible, but it fights the expression size limit the whole way and adds little: the DROP IP ranges already cover the worst of these networks. Recommendation: skip ASN-DROP unless you are on Enterprise.


Common Issues

Symptom Cause Fix
403 from the Cloudflare API Token lacks the permission or is zone-scoped Token needs Account → Account Filter Lists → Edit
List creation fails with name error Invalid characters Lowercase letters, numbers, underscores only
10001 / item validation error Malformed entry in the body Check the jq output — every item must be {"ip": "<cidr>"}
Rule not blocking Rule created in the wrong zone, or list empty Verify num_items via API and the rule's zone
Sync works but events show nothing No DROP traffic yet, or another rule blocks first Check rule order under Custom rules; patience otherwise