From e2f350ce95a69531d616f6e4f7dbb45e67cdc407 Mon Sep 17 00:00:00 2001 From: jknapp Date: Mon, 22 Sep 2025 16:50:35 -0700 Subject: [PATCH] Add comprehensive anti-scan and brute force protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement multi-layered security system to protect against exploit scanning and brute force attacks while maintaining legitimate traffic flow. Security Features: - Attack detection for common exploit paths (WordPress, phpMyAdmin, shells) - Malicious user agent filtering (sqlmap, nikto, metasploit, etc.) - SQL injection and directory traversal pattern detection - Progressive rate limiting (50 req/10s, 20 conn/10s, 10 err/10s) - Three-tier response: tarpit → deny → repeat offender blocking - Strict authentication endpoint protection (5 req/10s limit) - Real IP detection through proxy headers (Cloudflare, X-Real-IP) Management Tools: - manage-blocked-ips.sh: Dynamic IP blocking/unblocking - monitor-attacks.sh: Real-time threat monitoring - API endpoints for security stats and temporary blocking - Auto-expiring temporary blocks with cleanup endpoint HAProxy 2.6 Compatibility: - Removed silent-drop (not available in 2.6) - Fixed stick table counter syntax - Using standard tarpit and deny actions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- haproxy_manager.py | 171 +++++++++++++++++++++++++++++++++- scripts/manage-blocked-ips.sh | 68 ++++++++++++++ scripts/monitor-attacks.sh | 73 +++++++++++++++ templates/hap_listener.tpl | 88 ++++++++++++++--- 4 files changed, 388 insertions(+), 12 deletions(-) create mode 100755 scripts/manage-blocked-ips.sh create mode 100755 scripts/monitor-attacks.sh diff --git a/haproxy_manager.py b/haproxy_manager.py index 6644766..3f58f98 100644 --- a/haproxy_manager.py +++ b/haproxy_manager.py @@ -8,8 +8,9 @@ import socket import psutil import functools import logging -from datetime import datetime +from datetime import datetime, timedelta import json +import ipaddress import shutil import tempfile @@ -119,6 +120,14 @@ def init_db(): ''') conn.commit() +def validate_ip_address(ip_string): + """Validate if a string is a valid IP address""" + try: + ipaddress.ip_address(ip_string) + return True + except ValueError: + return False + def certbot_register(): """Register with Let's Encrypt using the certbot client and agree to the terms of service""" result = subprocess.run(['certbot', 'show_account'], capture_output=True) @@ -894,6 +903,166 @@ def sync_blocked_ips(): log_operation('sync_blocked_ips', False, str(e)) return jsonify({'status': 'error', 'message': str(e)}), 500 +@app.route('/api/security/stats', methods=['GET']) +@require_api_key +def get_security_stats(): + """Get current security statistics from HAProxy stick table""" + try: + if os.path.exists(HAPROXY_SOCKET_PATH): + socket_path = HAPROXY_SOCKET_PATH + else: + socket_path = '/tmp/haproxy-cli' + + # Get stick table data + cmd = f'echo "show table web" | socat stdio {socket_path}' + result = subprocess.run(cmd, shell=True, capture_output=True, text=True) + + if result.returncode != 0: + return jsonify({'status': 'error', 'message': 'Failed to get stick table data'}), 500 + + # Parse stick table output + lines = result.stdout.strip().split('\n') + threats = [] + + for line in lines[1:]: # Skip header + parts = line.split() + if len(parts) >= 8: + ip = parts[0] + try: + gpc0 = int(parts[3]) if len(parts) > 3 else 0 + gpc1 = int(parts[4]) if len(parts) > 4 else 0 + req_rate = int(parts[5]) if len(parts) > 5 else 0 + err_rate = int(parts[6]) if len(parts) > 6 else 0 + conn_rate = int(parts[7]) if len(parts) > 7 else 0 + + # Only include IPs with significant activity + if gpc0 > 0 or gpc1 > 0 or req_rate > 30 or err_rate > 5 or conn_rate > 10: + threat_level = 'low' + if gpc1 > 2: + threat_level = 'critical' + elif gpc0 > 0 or err_rate > 10: + threat_level = 'high' + elif req_rate > 40 or conn_rate > 15: + threat_level = 'medium' + + threats.append({ + 'ip': ip, + 'blocked': gpc0 > 0, + 'repeat_offender': gpc1 > 2, + 'offense_count': gpc1, + 'request_rate': req_rate, + 'error_rate': err_rate, + 'connection_rate': conn_rate, + 'threat_level': threat_level + }) + except (ValueError, IndexError): + continue + + # Sort by threat level + threats.sort(key=lambda x: (x['offense_count'], x['error_rate'], x['request_rate']), reverse=True) + + return jsonify({ + 'status': 'success', + 'total_tracked_ips': len(lines) - 1, + 'active_threats': len(threats), + 'threats': threats[:50] # Limit to top 50 + }) + except Exception as e: + log_operation('get_security_stats', False, str(e)) + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/api/security/temporary-block', methods=['POST']) +@require_api_key +def temporary_block(): + """Temporarily block an IP address (auto-unblocks after specified time)""" + data = request.get_json() + ip_address = data.get('ip_address') + duration_minutes = data.get('duration_minutes', 60) # Default 1 hour + + if not ip_address: + return jsonify({'status': 'error', 'message': 'IP address is required'}), 400 + + if not validate_ip_address(ip_address): + return jsonify({'status': 'error', 'message': 'Invalid IP address format'}), 400 + + try: + # Add to blocked IPs with expiration time + expiry_time = datetime.now() + timedelta(minutes=duration_minutes) + + with sqlite3.connect(DB_FILE) as conn: + cursor = conn.cursor() + # Check if table has expiry column, add if not + cursor.execute("PRAGMA table_info(blocked_ips)") + columns = [column[1] for column in cursor.fetchall()] + + if 'expiry_time' not in columns: + cursor.execute('ALTER TABLE blocked_ips ADD COLUMN expiry_time TEXT') + + # Add or update the blocked IP with expiry + cursor.execute(''' + INSERT OR REPLACE INTO blocked_ips (ip_address, reason, expiry_time) + VALUES (?, ?, ?) + ''', (ip_address, f'Temporary block for {duration_minutes} minutes', expiry_time.isoformat())) + + # Update map file and add to runtime + if not update_blocked_ips_map(): + return jsonify({'status': 'error', 'message': 'Failed to update map file'}), 500 + + add_ip_to_runtime_map(ip_address) + + log_operation('temporary_block', True, f'Temporarily blocked {ip_address} for {duration_minutes} minutes') + return jsonify({ + 'status': 'success', + 'message': f'IP {ip_address} temporarily blocked for {duration_minutes} minutes', + 'expires_at': expiry_time.isoformat() + }) + except Exception as e: + log_operation('temporary_block', False, str(e)) + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/api/security/clear-expired', methods=['POST']) +@require_api_key +def clear_expired_blocks(): + """Remove expired temporary IP blocks""" + try: + current_time = datetime.now() + expired_ips = [] + + with sqlite3.connect(DB_FILE) as conn: + cursor = conn.cursor() + + # Check if expiry_time column exists + cursor.execute("PRAGMA table_info(blocked_ips)") + columns = [column[1] for column in cursor.fetchall()] + + if 'expiry_time' in columns: + # Find and remove expired blocks + cursor.execute(''' + SELECT ip_address FROM blocked_ips + WHERE expiry_time IS NOT NULL AND expiry_time < ? + ''', (current_time.isoformat(),)) + + expired_ips = [row[0] for row in cursor.fetchall()] + + # Remove expired IPs + for ip in expired_ips: + cursor.execute('DELETE FROM blocked_ips WHERE ip_address = ?', (ip,)) + remove_ip_from_runtime_map(ip) + + # Update map file if any IPs were removed + if expired_ips: + update_blocked_ips_map() + + log_operation('clear_expired_blocks', True, f'Cleared {len(expired_ips)} expired IP blocks') + return jsonify({ + 'status': 'success', + 'message': f'Cleared {len(expired_ips)} expired IP blocks', + 'cleared_ips': expired_ips + }) + except Exception as e: + log_operation('clear_expired_blocks', False, str(e)) + return jsonify({'status': 'error', 'message': str(e)}), 500 + def generate_config(): try: conn = sqlite3.connect(DB_FILE) diff --git a/scripts/manage-blocked-ips.sh b/scripts/manage-blocked-ips.sh new file mode 100755 index 0000000..2c2e135 --- /dev/null +++ b/scripts/manage-blocked-ips.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# HAProxy IP blocking management script +# Usage: ./manage-blocked-ips.sh [block|unblock|list|clear] [IP_ADDRESS] + +SOCKET="/tmp/haproxy-cli" +MAP_FILE="/etc/haproxy/blocked_ips.map" + +# Ensure map file exists +if [ ! -f "$MAP_FILE" ]; then + touch "$MAP_FILE" + echo "# Blocked IPs - Format: IP_ADDRESS" > "$MAP_FILE" +fi + +case "$1" in + block) + if [ -z "$2" ]; then + echo "Usage: $0 block IP_ADDRESS" + exit 1 + fi + # Add IP to map file + grep -q "^$2" "$MAP_FILE" || echo "$2" >> "$MAP_FILE" + # Add to runtime map + echo "add map /etc/haproxy/blocked_ips.map $2 1" | socat stdio "$SOCKET" + echo "Blocked IP: $2" + ;; + + unblock) + if [ -z "$2" ]; then + echo "Usage: $0 unblock IP_ADDRESS" + exit 1 + fi + # Remove from map file + sed -i "/^$2$/d" "$MAP_FILE" + # Remove from runtime map + echo "del map /etc/haproxy/blocked_ips.map $2" | socat stdio "$SOCKET" + echo "Unblocked IP: $2" + ;; + + list) + echo "Currently blocked IPs:" + echo "show map /etc/haproxy/blocked_ips.map" | socat stdio "$SOCKET" | awk '{print $1}' + ;; + + clear) + echo "Clearing all blocked IPs..." + echo "clear map /etc/haproxy/blocked_ips.map" | socat stdio "$SOCKET" + echo "# Blocked IPs - Format: IP_ADDRESS" > "$MAP_FILE" + echo "All IPs unblocked" + ;; + + stats) + echo "Stick table statistics (showing potential bad actors):" + echo "show table web" | socat stdio "$SOCKET" | head -50 + ;; + + *) + echo "Usage: $0 {block|unblock|list|clear|stats} [IP_ADDRESS]" + echo "" + echo "Commands:" + echo " block IP - Block an IP address" + echo " unblock IP - Unblock an IP address" + echo " list - List all blocked IPs" + echo " clear - Clear all blocked IPs" + echo " stats - Show current stick table stats" + exit 1 + ;; +esac \ No newline at end of file diff --git a/scripts/monitor-attacks.sh b/scripts/monitor-attacks.sh new file mode 100755 index 0000000..65e7428 --- /dev/null +++ b/scripts/monitor-attacks.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Real-time attack monitoring for HAProxy +# Shows blocked requests and suspicious activity + +LOG_FILE="/var/log/haproxy.log" +SOCKET="/tmp/haproxy-cli" + +echo "===================================================" +echo "HAProxy Security Monitor - Real-time Attack Detection" +echo "===================================================" +echo "" + +# Function to show current threats +show_threats() { + echo "Current Threat IPs (from stick table):" + echo "show table web" | socat stdio "$SOCKET" 2>/dev/null | \ + awk '$4 > 0 || $5 > 0 || $6 > 30 || $7 > 5 || $8 > 10 { + printf "%-15s req_rate:%-3s err_rate:%-3s conn_rate:%-3s blocked:%s repeat:%s\n", + $1, $6, $7, $8, $4, $5 + }' | head -20 + echo "---------------------------------------------------" +} + +# Function to show recent blocks +show_recent_blocks() { + echo "Recent Blocked Requests:" + tail -100 "$LOG_FILE" 2>/dev/null | \ + grep -E "(scanner|exploit|ratelimit|repeat|tarpit|denied|dropped)" | \ + tail -10 | \ + awk '{ + if (match($0, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+/)) { + ip = substr($0, RSTART, RLENGTH) + gsub(/:.*/, "", ip) + reason = "" + if ($0 ~ /scanner/) reason = "SCANNER" + else if ($0 ~ /exploit/) reason = "EXPLOIT" + else if ($0 ~ /ratelimit/) reason = "RATE_LIMIT" + else if ($0 ~ /repeat/) reason = "REPEAT_OFFENDER" + else if ($0 ~ /tarpit/) reason = "TARPIT" + else if ($0 ~ /denied/) reason = "DENIED" + else if ($0 ~ /dropped/) reason = "DROPPED" + printf "[%s] %-15s %s\n", strftime("%H:%M:%S"), ip, reason + } + }' + echo "" +} + +# Monitor mode selection +if [ "$1" == "live" ]; then + echo "Live monitoring mode - Press Ctrl+C to exit" + echo "" + + while true; do + clear + echo "===================================================" + echo "HAProxy Security Monitor - $(date '+%Y-%m-%d %H:%M:%S')" + echo "===================================================" + echo "" + show_threats + echo "" + show_recent_blocks + sleep 5 + done +else + # Single run mode + show_threats + echo "" + show_recent_blocks + echo "" + echo "Tip: Run with 'live' parameter for continuous monitoring" + echo "Usage: $0 [live]" +fi \ No newline at end of file diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index c0bdf65..fbe6ccb 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -4,21 +4,40 @@ frontend web # crt can now be a path, so it will load all .pem files in the path bind 0.0.0.0:443 ssl crt {{ crt_path }} alpn h2,http/1.1 - # Stick table kept for potential future use or custom implementations - # Can be used by containers to track IPs if needed - stick-table type ip size 200k expire 1h store gpc0,gpc1,http_err_rate(10s) + # Stick tables for tracking and rate limiting + # Main tracking table: stores request rates, error rates, and abuse counters + stick-table type ip size 200k expire 30m store gpc0,gpc1,http_req_rate(10s),http_err_rate(10s),conn_rate(10s) # Whitelist trusted networks and monitoring systems acl trusted_networks src 127.0.0.1 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12 acl health_check path_beg /health /ping /status /.well-known/ - acl common_missing path /favicon.ico /robots.txt /sitemap.xml /apple-touch-icon.png - + # Allow trusted traffic to bypass all protection http-request allow if trusted_networks or health_check - - # Don't count common missing files against the error count - http-request return status 404 if common_missing - + + # ============================================ + # SECURITY: Anti-Scan and Brute Force Protection + # ============================================ + + # 1. Detect common exploit scan patterns + acl scan_wordpress path_beg /wp-admin /wp-login /xmlrpc.php /wp-content/uploads/ /wp-includes/ + acl scan_admin path_beg /admin /administrator /phpmyadmin /pma /mysql /cpanel /panel + acl scan_exploits path_end .sql .bak .backup .zip .tar.gz .rar .old .orig .save .swp .env .git .svn .DS_Store + acl scan_shells path_beg /shell.php /c99.php /r57.php /wso.php /alfa.php /eval.php /cmd.php + acl scan_dotfiles path_beg /. /.env /.git /.svn /.htaccess /.htpasswd /.ssh /.aws + acl scan_paths path_beg /cgi-bin /scripts /fckeditor /ckfinder /userfiles /console /api/v1/auth/login + + # 2. Detect malicious user agents + acl bot_scanner hdr_sub(user-agent) -i sqlmap nikto nmap masscan zmap dirbuster gobuster wpscan joomscan acunetix nessus openvas metasploit burp zgrab + acl bot_generic hdr_sub(user-agent) -i bot crawler spider scraper scan probe + acl bot_empty hdr_len(user-agent) eq 0 + + # 3. Detect suspicious request patterns + acl suspicious_method method TRACE TRACK OPTIONS CONNECT + acl has_sql_chars url_sub -i select union insert update delete drop create alter exec script + acl has_traversal url_sub ../ ..\\ %2e%2e %252e + acl excessive_params url_len gt 2000 + # Detect real client IP from proxy headers if they exist # Priority: CF-Connecting-IP (Cloudflare) > X-Real-IP > X-Forwarded-For > src acl has_cf_connecting_ip req.hdr(CF-Connecting-IP) -m found @@ -31,9 +50,56 @@ frontend web http-request set-var(txn.real_ip) req.hdr(X-Forwarded-For) if !has_cf_connecting_ip !has_x_real_ip has_x_forwarded_for http-request set-var(txn.real_ip) src if !has_cf_connecting_ip !has_x_real_ip !has_x_forwarded_for - # Track the real client IP in stick table (not the proxy IP) - # Kept for potential container-level tracking integrations + # Track the real client IP in stick table for rate limiting http-request track-sc0 var(txn.real_ip) + + # ============================================ + # APPLY SECURITY RULES + # ============================================ + + # 4. Rate limiting - Check if IP is exceeding limits + acl rate_abuse sc0_http_req_rate gt 50 + acl conn_abuse sc0_conn_rate gt 20 + acl error_abuse sc0_http_err_rate gt 10 + acl marked_bad sc0_get_gpc0 gt 0 + acl repeat_offender sc0_get_gpc1 gt 2 + + # 5. Mark bad actors in stick table + # gpc0: Current bad actor flag (0=good, 1=bad) + # gpc1: Offense counter (increments each time marked bad) + http-request sc-set-gpc0(0) 1 if scan_wordpress or scan_admin or scan_exploits or scan_shells or scan_dotfiles + http-request sc-set-gpc0(0) 1 if bot_scanner or suspicious_method or has_sql_chars or has_traversal + http-request sc-set-gpc0(0) 1 if rate_abuse or conn_abuse or error_abuse + http-request sc-inc-gpc1(0) 1 if marked_bad !repeat_offender + + # 6. Progressive response based on threat level + # Level 1: Deny with tarpit for suspicious scanners (uses tarpit timeout from defaults) + http-request tarpit if scan_wordpress or scan_admin or scan_shells or bot_scanner + http-request tarpit if suspicious_method or has_sql_chars or has_traversal + + # Level 2: Deny for rate abusers and marked bad actors + http-request deny if marked_bad + http-request deny if rate_abuse or conn_abuse or error_abuse + + # Level 3: Reject repeat offenders completely + http-request deny if repeat_offender + + # 7. Additional protections for login/auth endpoints + acl is_login path_end /login /signin /auth /authenticate + acl is_api_auth path_beg /api/login /api/auth /api/v1/auth /api/v2/auth + + # Strict rate limit for authentication endpoints (max 5 requests per 10s) + acl auth_abuse sc0_http_req_rate gt 5 + http-request deny if is_login auth_abuse + http-request deny if is_api_auth auth_abuse + + # 8. Log security events for monitoring + http-request capture var(txn.real_ip) len 40 + http-request capture req.hdr(user-agent) len 150 + http-request set-var(txn.blocked) str(scanner) if bot_scanner + http-request set-var(txn.blocked) str(exploit) if scan_exploits or scan_shells + http-request set-var(txn.blocked) str(ratelimit) if rate_abuse + http-request set-var(txn.blocked) str(repeat) if repeat_offender # IP blocking using map file (no word limit, runtime updates supported) # Map file: /etc/haproxy/blocked_ips.map