feat(waf): edge brute-force throttle for wp-login.php
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 2m8s

The generic rate-limits are tuned high for media-heavy sites, so slow
credential-stuffing on wp-login.php slips under them. Add a dedicated sc1
stick-table (backend wp_bruteforce, 60s window) that counts POSTs to
wp-login.php per real client IP and tarpits once an IP exceeds 30/min.

Only login POSTs are counted (browsing + the login form GET + a legit user's
few attempts are unaffected); an offending IP can still browse, just not keep
hammering login. Honors the existing whitelist (RFC1918 / trusted_ips.list /
trusted_ips.map) and the already-resolved CF/proxy real IP. path_end also
covers subdirectory WP installs. Stops attacks at the edge before they reach
PHP/WordPress, on all edges regardless of Coraza mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 19:52:52 -07:00
parent 3917b6d1ae
commit 6ced2f8797
3 changed files with 24 additions and 2 deletions
+1 -1
View File
@@ -1 +1 @@
2026.06.1 2026.06.2
+14
View File
@@ -64,6 +64,20 @@ frontend web
# High error rate: >100 errors in 30s (scanner/fuzzer behavior) # High error rate: >100 errors in 30s (scanner/fuzzer behavior)
http-request tarpit deny_status 403 if { sc_http_err_rate(0) gt 100 } !is_local !is_trusted_ip !is_whitelisted !is_health_check http-request tarpit deny_status 403 if { sc_http_err_rate(0) gt 100 } !is_local !is_trusted_ip !is_whitelisted !is_health_check
# --- WordPress wp-login.php brute-force protection ---
# The generic limits above are deliberately high (media-heavy sites), so a
# slow credential-stuffing run (dozens of login POSTs/min) slips under them.
# Track POSTs to wp-login.php per real client IP in a DEDICATED 60s table
# (sc1 / backend wp_bruteforce, defined in hap_security_tables.tpl) and
# tarpit once an IP exceeds 30/min. Only login POSTs are counted — GETs of
# the login form, normal browsing, and the handful of POSTs a legit user
# makes are unaffected; an offending IP can still browse, just not keep
# hammering login. path_end also covers subdirectory WP installs. Honors the
# same whitelist (RFC1918 / trusted_ips.list / trusted_ips.map).
acl wp_login_path path_end /wp-login.php
http-request track-sc1 var(txn.real_ip) table wp_bruteforce if METH_POST wp_login_path
http-request tarpit deny_status 429 if METH_POST wp_login_path { sc_http_req_rate(1) gt 30 } !is_local !is_trusted_ip !is_whitelisted
# IP blocking using map file (manual blocks only) # IP blocking using map file (manual blocks only)
# Map file format: /etc/haproxy/blocked_ips.map contains "<ip_or_cidr> 1" per line # Map file format: /etc/haproxy/blocked_ips.map contains "<ip_or_cidr> 1" per line
# Runtime updates: echo "add map #0 IP_ADDRESS 1" | socat stdio /var/run/haproxy.sock # Runtime updates: echo "add map #0 IP_ADDRESS 1" | socat stdio /var/run/haproxy.sock
+8
View File
@@ -6,3 +6,11 @@ frontend stats
stats refresh 30s stats refresh 30s
stats show-legends stats show-legends
stats show-node stats show-node
# Dedicated stick-table for WordPress wp-login.php brute-force tracking.
# Tracked via track-sc1 from the `web` frontend (hap_listener.tpl); counts only
# login POSTs per real client IP over a 60s window. Separate from the generic
# sc0 connection/rate table so the login-attempt threshold is independent of
# the (much higher) flood thresholds.
backend wp_bruteforce
stick-table type ip size 100k expire 30m store http_req_rate(60s)