Last active
June 11, 2025 17:44
-
-
Save klepsydra/ecf975984b32b1c8291a to your computer and use it in GitHub Desktop.
Block globally reported hack attempts using your local iptables firewall rules
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
## Update fail2ban iptables with globally known attackers. | |
## Actually, runs 100% independently now, without needing fail2ban installed. | |
## | |
## /etc/cron.daily/sync-fail2ban | |
## | |
## Author: Marcos Kobylecki <[email protected]> | |
## http://www.reddit.com/r/linux/comments/2nvzur/shared_blacklists_from_fail2ban/ | |
## Quit if fail2ban is missing. Maybe this fake requirement can be skipped? YES. | |
#PROGRAM=/etc/init.d/fail2ban | |
#[ -x $PROGRAM ] || exit 0 | |
datadir=/etc/fail2ban | |
[[ -d "$datadir" ]] || datadir=/tmp | |
## Get default settings of fail2ban (optional?) | |
[ -r /etc/default/fail2ban ] && . /etc/default/fail2ban | |
umask 000 | |
blacklistf=$datadir/blacklist.blocklist.de.txt | |
mv -vf $blacklistf $blacklistf.last | |
badlisturls="http://antivirus.neu.edu.cn/ssh/lists/base_30days.txt http://lists.blocklist.de/lists/ssh.txt http://lists.blocklist.de/lists/bruteforcelogin.txt" | |
iptables -vN fail2ban-ssh # Create the chain if it doesn't exist. Harmless if it does. | |
# Grab list(s) at https://www.blocklist.de/en/export.html . Block. | |
echo "Adding new blocks:" | |
time curl -s http://lists.blocklist.de/lists/ssh.txt http://lists.blocklist.de/lists/bruteforcelogin.txt \ | |
|sort -u \ | |
|tee $blacklistf \ | |
|grep -v '^#\|:' \ | |
|while read IP; do iptables -I fail2ban-ssh 1 -s $IP -j DROP; done | |
# Which listings had been removed since last time? Unblock. | |
echo "Removing old blocks:" | |
if [[ -r $blacklistf.diff ]]; then | |
# comm is brittle, cannot use sort -rn | |
time comm -23 $blacklistf.last $blacklistf \ | |
|tee $blacklistf.delisted \ | |
|grep -v '^#\|:' \ | |
|while read IP; do iptables -w -D fail2ban-ssh -s $IP -j DROP || iptables -wv -D fail2ban-ssh -s $IP -j LOGDROP; done | |
fi | |
# prepare for next time. | |
diff -wbay $blacklistf.last $blacklistf > $blacklistf.diff | |
# Saves a copy of current iptables rules, should you like to check them later. | |
(set -x; iptables -wnv -L --line-numbers; iptables -wnv -t nat -L --line-numbers) &> /tmp/iptables.fail2ban.log & | |
exit | |
# iptables v1.4.21: host/network `2a00:1210:fffe:145::1' not found | |
# So weed out IPv6, try |grep -v ':' | |
## http://ix.io/fpC | |
# Option: actionban | |
# Notes.: command executed when banning an IP. Take care that the | |
# command is executed with Fail2Ban user rights. | |
# Tags: See jail.conf(5) man page | |
# Values: CMD | |
# | |
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype># Option: actionunban | |
# Notes.: command executed when unbanning an IP. Take care that the | |
# command is executed with Fail2Ban user rights. | |
# Tags: See jail.conf(5) man page | |
# Values: CMD | |
# | |
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype> |
@klepsydra, check out this LLM-assisted revision, works on Ubuntu and adds logging and handles duplicates within the same set, not duplicate lines in general. The script now handles the first run gracefully by:
- Creating the main ipsets before trying to use them
- Not failing if iptables rules can't be added immediately
- Using a fallback copy method if the atomic swap fails
- Providing sensible defaults (0) when counting entries in non-existent sets
#!/bin/bash
# Daily blocklist updater script for ipset v7.19+
# Logs to /var/log/blocklist.log
PATH="/sbin:/usr/sbin:/bin:/usr/bin"
LOG_FILE="/var/log/blocklist.log"
TMP_RESTORE="/tmp/ipset_restore.$$"
exec >> "$LOG_FILE" 2>&1
echo "=== Blocklist update started at $(date '+%Y-%m-%d %H:%M:%S') ==="
# Check required commands
for cmd in ipset sipcalc curl; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: $cmd not installed"
exit 1
fi
done
# Random sleep to distribute load
DELAY=$((RANDOM % 30))
echo "Sleeping for $DELAY seconds..."
sleep "${DELAY}s"
# Function to create ipset
initialize_ipset() {
local set_name=$1
local family=$2
echo "Creating set $set_name (family $family)"
ipset create "$set_name" hash:ip family "$family" hashsize 16384 maxelem 131072 -exist
}
# Create main ipsets if they don't exist (first run)
initialize_ipset blacklist-ip4 inet || exit 1
initialize_ipset blacklist-ip6 inet6 || exit 1
# Create temporary ipsets for atomic swap
initialize_ipset new-blacklist-ip4 inet || exit 1
initialize_ipset new-blacklist-ip6 inet6 || exit 1
# Ensure iptables rules exist
initialize_iptables() {
local cmd=$1
local set=$2
if ! $cmd -n -L INPUT 2>/dev/null | grep -q "match-set $set src"; then
echo "Adding $cmd INPUT rule for $set"
$cmd -I INPUT -m set --match-set "$set" src -j DROP 2>/dev/null || {
echo "Warning: Failed to add $cmd rule for $set (may not be critical)"
}
else
echo "$cmd rule for $set already exists"
fi
}
initialize_iptables iptables blacklist-ip4
initialize_iptables ip6tables blacklist-ip6
# Download and process blocklist
process_blocklist() {
echo "Downloading and processing blocklist..."
OLD_IP4=$(ipset list blacklist-ip4 2>/dev/null | grep -c '^[0-9]' || echo 0)
OLD_IP6=$(ipset list blacklist-ip6 2>/dev/null | grep -c '^[0-9]' || echo 0)
# Clean temporary files
TMP_IP4="/tmp/ip4.$$"
TMP_IP6="/tmp/ip6.$$"
> "$TMP_IP4"
> "$TMP_IP6"
# Download and normalize
curl -s https://lists.blocklist.de/lists/all.txt \
| grep -E '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F:]+)$' \
| xargs -n1 sipcalc 2>/dev/null \
| awk -F '- ' '/Expanded Address/ {print $2} /Host address/ {print $2}' \
| grep -v '^0\.0\.0\.0$' \
| sort -u \
| while read -r ip; do
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$ip" >> "$TMP_IP4"
elif [[ "$ip" =~ : ]]; then
echo "$ip" >> "$TMP_IP6"
fi
done
# Remove duplicates again, generate restore file
{
echo "flush new-blacklist-ip4"
sort -u "$TMP_IP4" | awk '{ print "add new-blacklist-ip4 " $1 }'
echo "flush new-blacklist-ip6"
sort -u "$TMP_IP6" | awk '{ print "add new-blacklist-ip6 " $1 }'
} > "$TMP_RESTORE"
echo "Prepared $(wc -l < "$TMP_RESTORE") ipset entries for restore"
ipset restore < "$TMP_RESTORE" 2>&1 | grep -v 'element already added'
NEW_IP4=$(ipset list new-blacklist-ip4 2>/dev/null | grep -c '^[0-9]' || echo 0)
NEW_IP6=$(ipset list new-blacklist-ip6 2>/dev/null | grep -c '^[0-9]' || echo 0)
echo "Blocklist processed:"
echo "IPv4: $OLD_IP4 (old) → $NEW_IP4 (new)"
echo "IPv6: $OLD_IP6 (old) → $NEW_IP6 (new)"
rm -f "$TMP_IP4" "$TMP_IP6"
}
process_blocklist
# Swap temporary sets into place
swap_sets() {
local set=$1
local temp="new-$set"
local count
count=$(ipset list "$temp" 2>/dev/null | grep -c '^[0-9]' || echo 0)
if (( count >= 1 )); then
echo "Swapping $set with $temp ($count entries)"
if ipset swap "$set" "$temp" 2>/dev/null; then
echo "Swap successful"
else
echo "Swap failed, copying entries instead..."
# Fallback: flush main set and copy from temp
ipset flush "$set" 2>/dev/null
ipset list "$temp" | grep '^[0-9]' | while read -r ip; do
ipset add "$set" "$ip" 2>/dev/null
done
echo "Entries copied to $set"
fi
else
echo "Not enough entries in $temp ($count), skipping swap"
fi
}
swap_sets blacklist-ip4
swap_sets blacklist-ip6
# Cleanup
ipset destroy new-blacklist-ip4 2>/dev/null
ipset destroy new-blacklist-ip6 2>/dev/null
rm -f "$TMP_RESTORE"
# Final report
echo "Blocklist update completed at $(date '+%Y-%m-%d %H:%M:%S')"
echo "IPv4 blocked: $(ipset list blacklist-ip4 2>/dev/null | grep -c '^[0-9]' || echo 0)"
echo "IPv6 blocked: $(ipset list blacklist-ip6 2>/dev/null | grep -c '^[0-9]' || echo 0)"
echo "======================================"
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
That 2014 Server Fault post was removed. It's archived here: make fail2ban use public blacklists. The question and top-placed answer (score: -5) are by the author of this script, @klepsydra.