Last active
April 27, 2025 00:10
-
-
Save benlacey57/4905f859ba6f7ec69f2dddcd67249db0 to your computer and use it in GitHub Desktop.
This script manages IP blocking based on server logs analysis. It detects # suspicious activity patterns and automatically blocks offending IP addresses # for a configurable period. Blocks are managed in a separate file that is # included in the main .htaccess file to prevent overwrites during manual edits.
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 | |
# Multi-Server IP Block Manager | |
# Author: Ben Lacey (https://benlacey.co.uk) | |
# Version: 1.1 | |
# Date: 2025-04-27 | |
# | |
# Description: | |
# This script manages IP blocking across multiple websites and web server types. It detects suspicious activity patterns in logs and automatically blocks offending IP addresses for a configurable period. It supports Apache, Nginx, and LiteSpeed configurations through a central blocklist that is referenced by server-specific include mechanisms. | |
# | |
# Usage: ./ip_block_manager.sh [OPTIONS] | |
# Options: | |
# --block IP DAYS Block specified IP for DAYS days | |
# --unblock IP Remove block for specified IP | |
# --list List all currently blocked IPs | |
# --scan [SITE_NAME] Scan logs for suspicious activity (for specific site or all) | |
# --clean Remove expired blocks | |
# --add-site SITE_NAME Add a new site configuration | |
# --remove-site SITE_NAME Remove a site configuration | |
# --list-sites List all configured sites | |
# --help Show this help message | |
# | |
# For cron usage: | |
# 0 * * * * /path/to/ip_block_manager.sh > /dev/null 2>&1 | |
# | |
# Change Log | |
# v1.1 (2025-04-27) - Multi-server and multi-site support | |
# | |
# Major Changes: | |
# - Added support for multiple web servers (Apache, Nginx, LiteSpeed) | |
# - Implemented central block list approach for unified IP management | |
# - Created site configuration system to handle multiple websites | |
# - Added server-specific syntax handling for different block directives | |
# - Implemented intelligent server reload system | |
# | |
# New Commands: | |
# - --add-site: Configure a new website for monitoring | |
# - --remove-site: Remove a configured website | |
# - --list-sites: Display all configured websites | |
# - --scan [SITE_NAME]: Scan specific site or all sites | |
# | |
# Architecture Changes: | |
# - Centralized configuration in /etc/ip-block-manager/ | |
# - Server-specific include mechanisms (Apache Include vs Nginx include) | |
# - Unified block format with server-specific directives | |
# - Cross-server compatible log scanning | |
# Base configuration | |
CONFIG_DIR="/etc/ip-block-manager" | |
CENTRAL_BLOCK_LIST="$CONFIG_DIR/blocked_ips.conf" | |
SITES_CONFIG="$CONFIG_DIR/sites.conf" | |
BLOCK_LOG="/var/log/ip_blocks.log" | |
DEFAULT_BLOCK_DAYS=7 | |
MAX_FAILED_ATTEMPTS=5 | |
THRESHOLD_PERIOD="10 minutes" | |
# Create config directory and files if they don't exist | |
mkdir -p "$CONFIG_DIR" | |
touch "$BLOCK_LOG" | |
touch "$CENTRAL_BLOCK_LIST" | |
if [ ! -f "$SITES_CONFIG" ]; then | |
echo "# IP Block Manager - Site Configuration" > "$SITES_CONFIG" | |
echo "# Format: SITE_NAME|SERVER_TYPE|ACCESS_LOG|ERROR_LOG|CONFIG_FILE" >> "$SITES_CONFIG" | |
echo "# Example: mysite|apache|/var/log/apache2/access.log|/var/log/apache2/error.log|/var/www/mysite/.htaccess" >> "$SITES_CONFIG" | |
fi | |
# Server-specific configuration handling | |
configure_server() { | |
local site_name=$1 | |
local server_type=$2 | |
local config_file=$3 | |
echo "Configuring $server_type for site $site_name..." | |
case "$server_type" in | |
apache|litespeed) | |
# Check if the include directive already exists | |
if ! grep -q "# IP BLOCKS SECTION" "$config_file" 2>/dev/null; then | |
# Backup existing config | |
cp "$config_file" "${config_file}.bak" 2>/dev/null || true | |
# Add include section | |
echo "" >> "$config_file" | |
echo "# BEGIN IP BLOCKS SECTION - Managed by ip_block_manager.sh" >> "$config_file" | |
echo "# DO NOT EDIT THIS SECTION" >> "$config_file" | |
echo "Include $CENTRAL_BLOCK_LIST" >> "$config_file" | |
echo "# END IP BLOCKS SECTION" >> "$config_file" | |
echo "Added include directive to $config_file" | |
fi | |
;; | |
nginx) | |
# For Nginx, we'll use an include directive in the server block | |
if ! grep -q "# IP BLOCKS SECTION" "$config_file" 2>/dev/null; then | |
# Backup existing config | |
cp "$config_file" "${config_file}.bak" 2>/dev/null || true | |
# Find the server block and add include | |
# This is a simplified approach - may need adjustment for complex configs | |
sed -i '/server {/a \ # BEGIN IP BLOCKS SECTION - Managed by ip_block_manager.sh\n include '"$CENTRAL_BLOCK_LIST"';\n # END IP BLOCKS SECTION' "$config_file" | |
echo "Added include directive to Nginx config $config_file" | |
fi | |
;; | |
*) | |
echo "Warning: Unsupported server type $server_type. Manual configuration required." | |
;; | |
esac | |
} | |
# Function to add IP to block list with expiration date | |
block_ip() { | |
local ip=$1 | |
local days=${2:-$DEFAULT_BLOCK_DAYS} | |
local expiry=$(date -d "+$days days" +"%Y-%m-%d") | |
# Check if IP is already blocked | |
if grep -q "# IP: $ip " "$CENTRAL_BLOCK_LIST"; then | |
echo "IP $ip is already blocked." | |
return | |
fi | |
# Add IP to central block list with expiration date | |
echo "# IP: $ip - EXPIRES: $expiry" >> "$CENTRAL_BLOCK_LIST" | |
# Add server-specific block directives | |
while IFS="|" read -r site_name server_type access_log error_log config_file; do | |
# Skip comment lines | |
[[ $site_name == \#* ]] && continue | |
case "$server_type" in | |
apache|litespeed) | |
echo "Deny from $ip" >> "$CENTRAL_BLOCK_LIST" | |
;; | |
nginx) | |
echo "deny $ip;" >> "$CENTRAL_BLOCK_LIST" | |
;; | |
*) | |
echo "# Unsupported server type $server_type for IP $ip" >> "$CENTRAL_BLOCK_LIST" | |
;; | |
esac | |
# Only add once, not for each site | |
break | |
done < "$SITES_CONFIG" | |
# Log the block | |
echo "$(date +"%Y-%m-%d %H:%M:%S") - BLOCKED: $ip until $expiry" >> "$BLOCK_LOG" | |
echo "Blocked IP $ip until $expiry" | |
} | |
# Function to remove expired blocks | |
remove_expired_blocks() { | |
local today=$(date +"%Y-%m-%d") | |
local temp_file=$(mktemp) | |
local current_ip="" | |
local skip_lines=false | |
# Process the block list line by line | |
while IFS= read -r line; do | |
if [[ $line == "# IP:"* ]]; then | |
# Extract IP and expiry date | |
current_ip=$(echo "$line" | grep -o "IP: [0-9.]*" | cut -d' ' -f2) | |
expiry_date=$(echo "$line" | grep -o "EXPIRES: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" | cut -d' ' -f2) | |
# Compare dates | |
if [[ $(date -d "$today" +%s) -le $(date -d "$expiry_date" +%s) ]]; then | |
# Not expired, keep it | |
echo "$line" >> "$temp_file" | |
skip_lines=false | |
else | |
# Expired, log removal | |
echo "$(date +"%Y-%m-%d %H:%M:%S") - UNBLOCKED: $current_ip (expired)" >> "$BLOCK_LOG" | |
echo "Removed expired block for IP $current_ip" | |
skip_lines=true | |
fi | |
elif [ "$skip_lines" = false ]; then | |
# Keep non-expired IP rules | |
echo "$line" >> "$temp_file" | |
fi | |
done < "$CENTRAL_BLOCK_LIST" | |
# Replace block list with updated content | |
cat "$temp_file" > "$CENTRAL_BLOCK_LIST" | |
rm "$temp_file" | |
} | |
# Function to detect suspicious activity in logs | |
detect_suspicious_activity() { | |
local target_site=$1 | |
echo "Scanning for suspicious activity..." | |
# Process each site configuration | |
while IFS="|" read -r site_name server_type access_log error_log config_file; do | |
# Skip comment lines | |
[[ $site_name == \#* ]] && continue | |
# If target site is specified, only process that site | |
if [ -n "$target_site" ] && [ "$site_name" != "$target_site" ]; then | |
continue | |
fi | |
echo "Processing site: $site_name ($server_type)" | |
# Skip if log files don't exist | |
if [ ! -f "$access_log" ] || [ ! -f "$error_log" ]; then | |
echo "Warning: Log files not found for $site_name" | |
continue | |
fi | |
# Look for 404 errors that exceed threshold | |
echo " Scanning for excessive 404 errors..." | |
grep "\" 404 " "$access_log" | awk '{print $1}' | sort | uniq -c | sort -nr | while read count ip; do | |
if [ "$count" -ge "$MAX_FAILED_ATTEMPTS" ]; then | |
echo " Detected $count 404 errors from IP $ip" | |
block_ip "$ip" "$DEFAULT_BLOCK_DAYS" | |
fi | |
done | |
# Look for failed login attempts | |
echo " Scanning for failed login attempts..." | |
grep -i "failed login\|authentication failure" "$error_log" | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" | sort | uniq -c | sort -nr | while read count ip; do | |
if [ "$count" -ge "$MAX_FAILED_ATTEMPTS" ]; then | |
echo " Detected $count failed login attempts from IP $ip" | |
block_ip "$ip" "$DEFAULT_BLOCK_DAYS" | |
fi | |
done | |
# Look for SQL injection attempts | |
echo " Scanning for SQL injection attempts..." | |
grep -E "SELECT|UNION|INSERT|DROP|UPDATE.*FROM" "$access_log" | awk '{print $1}' | sort | uniq | while read ip; do | |
echo " Detected potential SQL injection attempt from IP $ip" | |
block_ip "$ip" "$DEFAULT_BLOCK_DAYS" | |
done | |
# Look for excessive requests in short time period | |
echo " Scanning for rate limiting violations..." | |
last_period=$(date -d "-$THRESHOLD_PERIOD" +"%Y:%H:%M:%S") | |
awk -v start_time="$last_period" '$4 >= start_time {print $1}' "$access_log" | sort | uniq -c | sort -nr | while read count ip; do | |
if [ "$count" -ge 100 ]; then # 100 requests in threshold period | |
echo " Rate limit exceeded: $count requests from IP $ip" | |
block_ip "$ip" "$DEFAULT_BLOCK_DAYS" | |
fi | |
done | |
done < "$SITES_CONFIG" | |
} | |
# Function to add a new site configuration | |
add_site() { | |
local site_name=$1 | |
local server_type=$2 | |
local access_log=$3 | |
local error_log=$4 | |
local config_file=$5 | |
# Validate inputs | |
if [ -z "$site_name" ] || [ -z "$server_type" ] || [ -z "$access_log" ] || [ -z "$error_log" ] || [ -z "$config_file" ]; then | |
echo "Error: Missing required parameters" | |
echo "Usage: $0 --add-site SITE_NAME SERVER_TYPE ACCESS_LOG ERROR_LOG CONFIG_FILE" | |
return 1 | |
fi | |
# Check if site already exists | |
if grep -q "^$site_name|" "$SITES_CONFIG"; then | |
echo "Site $site_name already exists. Use --remove-site first to update." | |
return 1 | |
fi | |
# Add site to configuration | |
echo "$site_name|$server_type|$access_log|$error_log|$config_file" >> "$SITES_CONFIG" | |
# Configure server | |
configure_server "$site_name" "$server_type" "$config_file" | |
echo "Added site $site_name ($server_type) to configuration" | |
} | |
# Function to remove a site configuration | |
remove_site() { | |
local site_name=$1 | |
if [ -z "$site_name" ]; then | |
echo "Error: Site name required" | |
return 1 | |
fi | |
# Create temporary file without the site | |
local temp_file=$(mktemp) | |
grep -v "^$site_name|" "$SITES_CONFIG" > "$temp_file" | |
# Check if site was found | |
if [ "$(wc -l < "$SITES_CONFIG")" -eq "$(wc -l < "$temp_file")" ]; then | |
echo "Site $site_name not found" | |
rm "$temp_file" | |
return 1 | |
fi | |
# Replace config with updated content | |
cat "$temp_file" > "$SITES_CONFIG" | |
rm "$temp_file" | |
echo "Removed site $site_name from configuration" | |
} | |
# Function to reload web servers | |
reload_servers() { | |
echo "Reloading web servers..." | |
# Get unique server types | |
local server_types=$(cut -d'|' -f2 "$SITES_CONFIG" | grep -v "^#" | sort | uniq) | |
for server_type in $server_types; do | |
case "$server_type" in | |
apache) | |
if command -v apachectl &> /dev/null; then | |
apachectl graceful | |
echo " Reloaded Apache" | |
elif command -v systemctl &> /dev/null; then | |
systemctl reload apache2 || systemctl reload httpd | |
echo " Reloaded Apache" | |
else | |
service apache2 reload || service httpd reload | |
echo " Reloaded Apache" | |
fi | |
;; | |
nginx) | |
if command -v nginx &> /dev/null; then | |
nginx -s reload | |
echo " Reloaded Nginx" | |
elif command -v systemctl &> /dev/null; then | |
systemctl reload nginx | |
echo " Reloaded Nginx" | |
else | |
service nginx reload | |
echo " Reloaded Nginx" | |
fi | |
;; | |
litespeed) | |
if command -v lswsctrl &> /dev/null; then | |
lswsctrl restart | |
echo " Restarted LiteSpeed" | |
elif command -v systemctl &> /dev/null; then | |
systemctl restart lsws | |
echo " Restarted LiteSpeed" | |
else | |
service lsws restart | |
echo " Restarted LiteSpeed" | |
fi | |
;; | |
*) | |
echo " Warning: Don't know how to reload server type $server_type" | |
;; | |
esac | |
done | |
} | |
# Initialize the central block list if empty | |
if [ ! -s "$CENTRAL_BLOCK_LIST" ]; then | |
echo "# IP Block Manager - Central Block List" > "$CENTRAL_BLOCK_LIST" | |
echo "# Managed by ip_block_manager.sh - DO NOT EDIT MANUALLY" >> "$CENTRAL_BLOCK_LIST" | |
echo "# Format: IP entries are added with server-specific syntax" >> "$CENTRAL_BLOCK_LIST" | |
fi | |
# Process command line arguments | |
if [ "$1" == "--help" ] || [ "$1" == "-h" ]; then | |
echo "Multi-Server IP Block Manager by Ben Lacey (https://benlacey.co.uk)" | |
echo "" | |
echo "Usage: $0 [OPTIONS]" | |
echo "Options:" | |
echo " --block IP DAYS Block specified IP for DAYS days" | |
echo " --unblock IP Remove block for specified IP" | |
echo " --list List all currently blocked IPs" | |
echo " --scan [SITE_NAME] Scan logs for suspicious activity (for specific site or all)" | |
echo " --clean Remove expired blocks" | |
echo " --add-site SITE_NAME SERVER_TYPE ACCESS_LOG ERROR_LOG CONFIG_FILE" | |
echo " Add a new site configuration" | |
echo " --remove-site SITE_NAME Remove a site configuration" | |
echo " --list-sites List all configured sites" | |
echo " --help Show this help message" | |
echo "" | |
echo "Server types: apache, nginx, litespeed" | |
echo "" | |
echo "When run without options, performs cleaning and scanning automatically." | |
echo "Ideal for cron job usage: 0 * * * * $0 > /dev/null 2>&1" | |
exit 0 | |
fi | |
case "$1" in | |
--block) | |
if [ -z "$2" ]; then | |
echo "Error: IP address required" | |
exit 1 | |
fi | |
block_days=${3:-$DEFAULT_BLOCK_DAYS} | |
block_ip "$2" "$block_days" | |
reload_servers | |
;; | |
--unblock) | |
if [ -z "$2" ]; then | |
echo "Error: IP address required" | |
exit 1 | |
fi | |
sed -i "/# IP: $2 /,/^$/d" "$CENTRAL_BLOCK_LIST" | |
echo "$(date +"%Y-%m-%d %H:%M:%S") - MANUAL UNBLOCK: $2" >> "$BLOCK_LOG" | |
echo "Unblocked IP $2" | |
reload_servers | |
;; | |
--list) | |
echo "Currently blocked IPs:" | |
grep "# IP:" "$CENTRAL_BLOCK_LIST" | sed 's/# IP: \([0-9.]*\) - EXPIRES: \(.*\)/\1 until \2/' | |
;; | |
--scan) | |
detect_suspicious_activity "$2" | |
reload_servers | |
;; | |
--clean) | |
remove_expired_blocks | |
reload_servers | |
;; | |
--add-site) | |
add_site "$2" "$3" "$4" "$5" "$6" | |
;; | |
--remove-site) | |
remove_site "$2" | |
;; | |
--list-sites) | |
echo "Configured sites:" | |
grep -v "^#" "$SITES_CONFIG" | while IFS="|" read -r site_name server_type access_log error_log config_file; do | |
echo " $site_name ($server_type)" | |
echo " Access log: $access_log" | |
echo " Error log: $error_log" | |
echo " Config file: $config_file" | |
done | |
;; | |
*) | |
# Default: do everything | |
remove_expired_blocks | |
detect_suspicious_activity | |
reload_servers | |
;; | |
esac | |
echo "Done. Check $BLOCK_LOG for block history." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated the script so the blocked ips are added to a separate.htaccess file which is included in the main .htaccess file to avoid overwriting anything.