Step 7: The blacklist update script
Save this as /usr/local/bin/update-abuse-blacklist.sh and make it executable:
sudo chmod +x /usr/local/bin/update-abuse-blacklist.sh
#!/bin/bash
# update-abuse-blacklist.sh
# Fetches crowd-sourced IP blocklists and loads them into nftables.
# Run as a nightly cron job: 0 2 * * * /usr/local/bin/update-abuse-blacklist.sh
API_KEY="YOUR_ABUSEIPDB_API_KEY"
# --------------------------------------------------
# 1. Fetch Blacklists
# --------------------------------------------------
# AbuseIPDB — plaintext, top 10,000 entries at 90%+ confidence
ABUSE=$(curl -s -L -G "https://api.abuseipdb.com/api/v2/blacklist" \
-d "plaintext=true" \
-d "limit=10000" \
-d "confidenceMinimum=90" \
-H "Key: $API_KEY" \
-H "Accept: application/json")
# Bitwire — community-maintained inbound threat lists (IPv4 + IPv6)
BITWIRE4=$(curl -s -L "https://raw.githubusercontent.com/bitwire-it/ip-blocklists/main/lists/inbound.txt")
BITWIRE6=$(curl -s -L "https://raw.githubusercontent.com/bitwire-it/ip-blocklists/main/lists/inbound-ipv6.txt")
# Spamhaus DROP/DROPv6 — industry-leading threat intelligence.
# EDROP was consolidated into DROP in April 2024; the EDROP fetch
# is included for backwards compatibility but returns the same data.
SPAM4=$(curl -s https://www.spamhaus.org/drop/drop.txt | grep -v "^;" | awk '{print $1}')
SPAM4_EXT=$(curl -s https://www.spamhaus.org/drop/edrop.txt | grep -v "^;" | awk '{print $1}')
SPAM6=$(curl -s https://www.spamhaus.org/drop/dropv6.txt | grep -v "^;" | awk '{print $1}')
# --------------------------------------------------
# 2. Combine and Deduplicate
# --------------------------------------------------
IPV4=$(printf "%s\n%s\n%s\n%s\n" "$ABUSE" "$BITWIRE4" "$SPAM4" "$SPAM4_EXT" \
| grep -E "^[0-9]" \
| sort -u)
IPV6=$(printf "%s\n%s\n" "$BITWIRE6" "$SPAM6" \
| grep ":" \
| sort -u)
# --------------------------------------------------
# 3. Update nftables Sets
# --------------------------------------------------
# Flush existing sets to start fresh
nft flush set inet f2b-table abuse-blacklist
nft flush set inet f2b-table abuse-blacklist6
# IPv4: validate with regex, sort, add in batches of 400
echo "$IPV4" | \
grep -E "^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/([0-9]|[12][0-9]|3[0-2]))?$" | \
sort -u | \
xargs -r -n 400 sh -c 'nft add element inet f2b-table abuse-blacklist { $(echo "$*" | tr " " ",") } 2>/dev/null' _
# IPv6: validate with regex, sort, add in batches of 400
echo "$IPV6" | \
grep -E "^[0-9a-fA-F:]+(/[0-9]{1,3})?$" | \
sort -u | \
xargs -r -n 400 sh -c 'nft add element inet f2b-table abuse-blacklist6 { $(echo "$*" | tr " " ",") } 2>/dev/null' _
# --------------------------------------------------
# 4. Log Summary
# --------------------------------------------------
COUNT4=$(echo "$IPV4" | wc -l)
COUNT6=$(echo "$IPV6" | wc -l)
NGX_BANNED=$(fail2ban-client status nginx-forbidden | grep "Currently banned:" | awk '{print $NF}')
SSHD_BANNED=$(fail2ban-client status sshd | grep "Currently banned:" | awk '{print $NF}')
echo "[$(date)] Blacklists updated: $COUNT4 IPv4, $COUNT6 IPv6 entries loaded"
echo "[$(date)] Fail2ban — nginx-forbidden: $NGX_BANNED banned, sshd: $SSHD_BANNED banned"
The script fetches three sources:
AbuseIPDB — 10,000 worst offenders from the last 30 days at 90%+ confidence. A crowd-sourced database where thousands of sysadmins report abusive IPs. Consensus-driven: the more reporters flag an IP, the higher its confidence score.
Bitwire — An aggregator pulling from ~15 established upstream open-source threat intelligence feeds — including FireHOL, ThreatFox, IPSum, Tor exit node lists, C2IntelFeeds, and others. Updated every 2 hours. Covers both IPv4 and IPv6.
Spamhaus DROP — An industry-leading non-profit threat intelligence organization. The DROP (Don't Route Or Peer) list contains IP ranges Spamhaus has determined are so dangerous they should never be routed — hijacked netblocks, cybercriminal infrastructure, and the worst-of-the-worst traffic sources. (The EDROP list was consolidated into DROP in April 2024 and is now the same dataset.)
A note on the numbers: 10,000–11,000 entries might sound modest, but many are CIDR ranges covering huge swaths of address space. A single Spamhaus /24 blocks 256 IPv4 addresses. A /17 blocks 32,768. On the IPv6 side, a single /48 covers an entire ISP-sized allocation — and the blacklist typically contains dozens of them. In practice, these lists represent millions of individual addresses, weighted toward the worst netblocks on the internet. It's not 11,000 IPs — it's 11,000 entries, many of which are entire neighborhoods, data centers, and ISP ranges that have been deemed irredeemably hostile.
Why batch at 400 entries? nftables processes add element commands atomically per batch. Dumping 10,000+ IPs as a single command can exceed kernel limits and time out. 400 at a time is a safe middle ground — fast, but won't choke the parser.
Step 8: Schedule the update
Add a cron job that runs nightly — 2 AM is a quiet window and doesn't interfere with peak traffic:
# /etc/cron.d/abuse-blacklist
0 2 * * * root /usr/local/bin/update-abuse-blacklist.sh > /dev/null 2>&1