Skip to content

Instantly share code, notes, and snippets.

@benlacey57
Last active April 27, 2025 00:10
Show Gist options
  • Save benlacey57/4905f859ba6f7ec69f2dddcd67249db0 to your computer and use it in GitHub Desktop.
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.
#!/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."
@benlacey57
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment