Add comprehensive anti-scan and brute force protection
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 54s
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 54s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,9 @@ import socket
|
|||||||
import psutil
|
import psutil
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
|
import ipaddress
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
@@ -119,6 +120,14 @@ def init_db():
|
|||||||
''')
|
''')
|
||||||
conn.commit()
|
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():
|
def certbot_register():
|
||||||
"""Register with Let's Encrypt using the certbot client and agree to the terms of service"""
|
"""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)
|
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))
|
log_operation('sync_blocked_ips', False, str(e))
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
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():
|
def generate_config():
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(DB_FILE)
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
68
scripts/manage-blocked-ips.sh
Executable file
68
scripts/manage-blocked-ips.sh
Executable file
@@ -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
|
73
scripts/monitor-attacks.sh
Executable file
73
scripts/monitor-attacks.sh
Executable file
@@ -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
|
@@ -4,21 +4,40 @@ frontend web
|
|||||||
# crt can now be a path, so it will load all .pem files in the path
|
# 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
|
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
|
# Stick tables for tracking and rate limiting
|
||||||
# Can be used by containers to track IPs if needed
|
# Main tracking table: stores request rates, error rates, and abuse counters
|
||||||
stick-table type ip size 200k expire 1h store gpc0,gpc1,http_err_rate(10s)
|
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
|
# 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 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 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
|
# Allow trusted traffic to bypass all protection
|
||||||
http-request allow if trusted_networks or health_check
|
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
|
# Detect real client IP from proxy headers if they exist
|
||||||
# Priority: CF-Connecting-IP (Cloudflare) > X-Real-IP > X-Forwarded-For > src
|
# Priority: CF-Connecting-IP (Cloudflare) > X-Real-IP > X-Forwarded-For > src
|
||||||
acl has_cf_connecting_ip req.hdr(CF-Connecting-IP) -m found
|
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) 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
|
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)
|
# Track the real client IP in stick table for rate limiting
|
||||||
# Kept for potential container-level tracking integrations
|
|
||||||
http-request track-sc0 var(txn.real_ip)
|
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)
|
# IP blocking using map file (no word limit, runtime updates supported)
|
||||||
# Map file: /etc/haproxy/blocked_ips.map
|
# Map file: /etc/haproxy/blocked_ips.map
|
||||||
|
Reference in New Issue
Block a user