From b88da4c58fc4ae54c2c920ac60a9fc9f4222e585 Mon Sep 17 00:00:00 2001 From: jknapp Date: Sun, 24 Aug 2025 19:33:10 -0700 Subject: [PATCH] Implement HAProxy tarpit escalation and CLI monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add full tarpit escalation logic with gpc1 tracking (levels 0-3) - Implement progressive delays: 2-5s → 8-15s → 20-45s → 60s - Increase initial threshold from 5 to 10 errors (more tolerant) - Reduce tracking duration from 2h to 1h (faster cleanup) - Add show-tarpit-ips.sh script for monitoring tarpitted IPs via CLI - Script shows IP, scan count, escalation level, and tarpit status 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- scripts/show-tarpit-ips.sh | 117 +++++++++++++++++++++++++++++++++++++ templates/hap_listener.tpl | 46 ++++++++++++--- 2 files changed, 155 insertions(+), 8 deletions(-) create mode 100755 scripts/show-tarpit-ips.sh diff --git a/scripts/show-tarpit-ips.sh b/scripts/show-tarpit-ips.sh new file mode 100755 index 0000000..52485b2 --- /dev/null +++ b/scripts/show-tarpit-ips.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# Script to display IPs that have been tarpitted by HAProxy 3.0 +# Uses HAProxy stats socket to query stick-table data +# +# Usage in Docker container: +# docker exec -it haproxy-manager /haproxy/scripts/show-tarpit-ips.sh + +SOCKET="/tmp/haproxy-cli" + +# Check if socket exists +if [ ! -S "$SOCKET" ]; then + echo "Error: HAProxy socket not found at $SOCKET" + echo "Make sure HAProxy is running with stats socket enabled" + exit 1 +fi + +echo "===================================================================" +echo " HAProxy Tarpitted IPs Report " +echo "===================================================================" +echo +echo "Showing IPs tracked in the stick-table with scan detection counters:" +echo "(gpc0 = total scan attempts, gpc1 = escalation level)" +echo + +# In HAProxy 3.0, we need to use the proper process prefix +# The web frontend table is in the worker process, not master +# First check which process has the table +# Note: grep for actual worker line, not the header +PROCESS_ID=$(echo "show proc" | socat stdio "$SOCKET" 2>/dev/null | grep -E '^[0-9]+.*worker' | awk '{print $1}' | head -1) + +if [ -z "$PROCESS_ID" ]; then + echo "Error: Could not find HAProxy worker process" + echo "Try: echo 'show proc' | socat stdio $SOCKET" + exit 1 +fi + +# Show stick-table entries from the web frontend using the worker process +# Use printf to avoid bash history expansion issues with ! +printf "@!%s show table web\n" "${PROCESS_ID}" | socat stdio "$SOCKET" 2>/dev/null | { + # Skip the header line + read header + + # Check if we got an error or empty response + if echo "$header" | grep -q "No such table"; then + echo "Error: Table 'web' not found. HAProxy may need to be reloaded." + exit 1 + fi + + has_data=false + echo "IP Address | Scan Count | Level | HTTP Err Rate | Status" + echo "---------------------|------------|-------|---------------|------------------" + + # Process each line + while IFS= read -r line; do + # Skip empty lines and comments + if [ -z "$line" ] || echo "$line" | grep -q "^#"; then + continue + fi + + # HAProxy 3.0 format: 0x... key= use=... exp=... gpc0=... gpc1=... http_err_rate(10s)=... + if echo "$line" | grep -q "key="; then + has_data=true + + # Extract IP and counters + ip=$(echo "$line" | grep -o 'key=[^ ]*' | cut -d'=' -f2) + gpc0=$(echo "$line" | grep -o 'gpc0=[0-9]*' | cut -d'=' -f2) + gpc1=$(echo "$line" | grep -o 'gpc1=[0-9]*' | cut -d'=' -f2) + err_rate=$(echo "$line" | grep -o 'http_err_rate([^)]*=[0-9]*' | grep -o '[0-9]*$') + + # Set defaults if values are empty + gpc0=${gpc0:-0} + gpc1=${gpc1:-0} + err_rate=${err_rate:-0} + + # Determine status based on scan count + status="" + if [ "$gpc0" -ge 50 ]; then + status="BLOCKED (critical)" + elif [ "$gpc0" -ge 35 ]; then + status="TARPITTED (high)" + elif [ "$gpc0" -ge 20 ]; then + status="TARPITTED (medium)" + elif [ "$gpc0" -ge 10 ]; then + status="MONITORED (low)" + else + status="Normal" + fi + + # Format output + printf "%-20s | %10s | %5s | %13s | %s\n" "$ip" "$gpc0" "$gpc1" "$err_rate/10s" "$status" + fi + done + + if [ "$has_data" = false ]; then + echo "(No IPs currently tracked - table is empty)" + fi +} + +echo +echo "===================================================================" +echo "Legend:" +echo " - Scan Count 10-19: Potential scanner (monitored/low tarpit)" +echo " - Scan Count 20-34: Likely scanner (tarpitted with medium delay)" +echo " - Scan Count 35-49: Confirmed scanner (tarpitted with high delay)" +echo " - Scan Count 50+: Aggressive scanner (blocked with 429 status)" +echo "===================================================================" +echo "Note: IPs are tracked for 1 hour since last activity" +echo +echo "To clear a specific IP from the table:" +echo " printf '@!${PROCESS_ID} del table web key \\n' | socat stdio $SOCKET" +echo +echo "To clear all entries:" +echo " printf '@!${PROCESS_ID} clear table web\\n' | socat stdio $SOCKET" +echo +echo "Debug: Worker PID is ${PROCESS_ID}" +echo \ No newline at end of file diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index 76d6e1a..b744759 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -7,7 +7,7 @@ frontend web # Stick table for tracking attacks with escalating timeouts # gpc0 = total scan attempts # gpc1 = escalation level (0=none, 1=level1, 2=level2, 3=level3) - stick-table type ip size 200k expire 2h store gpc0,gpc1,http_err_rate(10s) + stick-table type ip size 200k expire 1h store gpc0,gpc1,http_err_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 @@ -40,21 +40,51 @@ frontend web # Define threat levels based on accumulated error responses from backends # These will be checked on subsequent requests after errors are tracked - acl scanner_low sc0_get_gpc0 ge 5 # 5+ errors = potential scanner - acl scanner_medium sc0_get_gpc0 ge 15 # 15+ errors = likely scanner - acl scanner_high sc0_get_gpc0 ge 30 # 30+ errors = confirmed scanner + acl scanner_low sc0_get_gpc0 ge 10 # 10+ errors = potential scanner + acl scanner_medium sc0_get_gpc0 ge 20 # 20+ errors = likely scanner + acl scanner_high sc0_get_gpc0 ge 35 # 35+ errors = confirmed scanner acl scanner_critical sc0_get_gpc0 ge 50 # 50+ errors = aggressive scanner # Rate-based detection (burst of errors) acl burst_scanner sc0_http_err_rate gt 5 # >5 errors in 10 seconds + # Escalation levels (tracks how many times we've escalated this IP) + acl escalation_level_0 sc0_get_gpc1 eq 0 # First offense + acl escalation_level_1 sc0_get_gpc1 eq 1 # Second offense + acl escalation_level_2 sc0_get_gpc1 eq 2 # Third offense + acl escalation_level_3 sc0_get_gpc1 ge 3 # Repeat offender + # BLOCKING RULES - Block aggressive scanners completely # Only block after significant error accumulation http-request deny deny_status 429 if scanner_critical - # TARPIT RULES - Slow down detected scanners - # Apply progressive delays based on error count - http-request tarpit if scanner_high - http-request tarpit if scanner_medium burst_scanner + # ESCALATING TARPIT RULES - Progressive delays based on offense level + # Level 0 (first offense): Short delays + http-request tarpit deny_status 429 timeout 2s if scanner_low escalation_level_0 + http-request tarpit deny_status 429 timeout 3s if scanner_medium escalation_level_0 + http-request tarpit deny_status 429 timeout 5s if scanner_high escalation_level_0 + http-request tarpit deny_status 429 timeout 5s if burst_scanner escalation_level_0 + + # Level 1 (second offense): Medium delays + http-request tarpit deny_status 429 timeout 8s if scanner_low escalation_level_1 + http-request tarpit deny_status 429 timeout 12s if scanner_medium escalation_level_1 + http-request tarpit deny_status 429 timeout 15s if scanner_high escalation_level_1 + http-request tarpit deny_status 429 timeout 10s if burst_scanner escalation_level_1 + + # Level 2 (third offense): Long delays + http-request tarpit deny_status 429 timeout 20s if scanner_low escalation_level_2 + http-request tarpit deny_status 429 timeout 30s if scanner_medium escalation_level_2 + http-request tarpit deny_status 429 timeout 45s if scanner_high escalation_level_2 + http-request tarpit deny_status 429 timeout 25s if burst_scanner escalation_level_2 + + # Level 3+ (repeat offender): Maximum delays + http-request tarpit deny_status 429 timeout 60s if scanner_low escalation_level_3 + http-request tarpit deny_status 429 timeout 60s if scanner_medium escalation_level_3 + http-request tarpit deny_status 429 timeout 60s if scanner_high escalation_level_3 + http-request tarpit deny_status 429 timeout 60s if burst_scanner escalation_level_3 + + # Increment escalation level when we apply tarpit + # This tracks how many times this IP has been tarpitted + http-request sc-inc-gpc1(0) if scanner_low or scanner_medium or scanner_high or burst_scanner # Note: The backend will increment sc0_get_gpc0 when it sees 400/401/403/404 responses