feat(suspension): opt-in routing for suspended hosts via bk_suspended backend
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 56s
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 56s
Adds a new env var HAPROXY_SUSPENSION_BACKEND (default unset). When set (e.g. "whp-suspended:80"), generate_config() renders: - A bk_suspended backend pointing at the configured upstream - An ACL `acl is_suspended_domain hdr(host),lower -f /etc/haproxy/suspended_domains.list` + `use_backend bk_suspended if is_suspended_domain` in the frontend, sitting after IP-blocking and before any per-domain routing - An empty /etc/haproxy/suspended_domains.list if missing (haproxy refuses to start with -f pointing at a non-existent file) External tooling (e.g. WHP's site_disable.php) maintains the list via `docker cp` and HUP-reloads the container. Non-WHP deployments (home networks, standalone use) leave the env var unset and see byte-identical haproxy.cfg output. Same opt-in shape as the existing HAPROXY_CORAZA_SPOE_BACKEND integration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1718,6 +1718,24 @@ def generate_config():
|
|||||||
# image) -> the generated haproxy.cfg is byte-identical to today's.
|
# image) -> the generated haproxy.cfg is byte-identical to today's.
|
||||||
coraza_spoe_backend = os.environ.get('HAPROXY_CORAZA_SPOE_BACKEND')
|
coraza_spoe_backend = os.environ.get('HAPROXY_CORAZA_SPOE_BACKEND')
|
||||||
|
|
||||||
|
# Optional site-suspension routing. When HAPROXY_SUSPENSION_BACKEND is
|
||||||
|
# set (e.g. "whp-suspended:80"), we render bk_suspended + a frontend
|
||||||
|
# ACL that routes hosts in /etc/haproxy/suspended_domains.list to it.
|
||||||
|
# Same opt-in shape as Coraza: unset -> config byte-identical to today.
|
||||||
|
# The list file is maintained by external tooling; we just ensure it
|
||||||
|
# exists (haproxy refuses to start with -f pointing at a missing file).
|
||||||
|
suspension_backend_target = os.environ.get('HAPROXY_SUSPENSION_BACKEND')
|
||||||
|
if suspension_backend_target:
|
||||||
|
suspended_list_path = '/etc/haproxy/suspended_domains.list'
|
||||||
|
if not os.path.exists(suspended_list_path):
|
||||||
|
try:
|
||||||
|
with open(suspended_list_path, 'w') as f:
|
||||||
|
f.write('')
|
||||||
|
os.chmod(suspended_list_path, 0o644)
|
||||||
|
logger.info(f"Created empty {suspended_list_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create {suspended_list_path}: {e}")
|
||||||
|
|
||||||
# Add Haproxy Default Headers
|
# Add Haproxy Default Headers
|
||||||
default_headers = template_env.get_template('hap_header.tpl').render()
|
default_headers = template_env.get_template('hap_header.tpl').render()
|
||||||
config_parts.append(default_headers)
|
config_parts.append(default_headers)
|
||||||
@@ -1729,6 +1747,7 @@ def generate_config():
|
|||||||
listener_block = template_env.get_template('hap_listener.tpl').render(
|
listener_block = template_env.get_template('hap_listener.tpl').render(
|
||||||
crt_path = SSL_CERTS_DIR,
|
crt_path = SSL_CERTS_DIR,
|
||||||
coraza_spoe_backend = coraza_spoe_backend,
|
coraza_spoe_backend = coraza_spoe_backend,
|
||||||
|
suspension_enabled = bool(suspension_backend_target),
|
||||||
)
|
)
|
||||||
config_parts.append(listener_block)
|
config_parts.append(listener_block)
|
||||||
|
|
||||||
@@ -1849,6 +1868,14 @@ backend default-backend
|
|||||||
# Add Backends
|
# Add Backends
|
||||||
config_parts.append('\n' .join(config_backends) + '\n')
|
config_parts.append('\n' .join(config_backends) + '\n')
|
||||||
|
|
||||||
|
# Suspended-site backend (only when env var set). Inserted before the
|
||||||
|
# Coraza backend so config_parts ordering remains deterministic.
|
||||||
|
if suspension_backend_target:
|
||||||
|
suspended_backend_block = template_env.get_template(
|
||||||
|
'hap_suspended_backend.tpl'
|
||||||
|
).render(target=suspension_backend_target)
|
||||||
|
config_parts.append(suspended_backend_block + '\n')
|
||||||
|
|
||||||
# Coraza WAF backend + SPOE engine config file (only when env var set).
|
# Coraza WAF backend + SPOE engine config file (only when env var set).
|
||||||
# Writing /etc/haproxy/coraza-spoe.cfg here keeps it in sync with the
|
# Writing /etc/haproxy/coraza-spoe.cfg here keeps it in sync with the
|
||||||
# filter line that hap_listener.tpl just rendered into the frontend.
|
# filter line that hap_listener.tpl just rendered into the frontend.
|
||||||
|
|||||||
@@ -53,6 +53,19 @@ frontend web
|
|||||||
acl is_blocked_ip var(txn.real_ip),map_ip(/etc/haproxy/blocked_ips.map,0) -m int gt 0
|
acl is_blocked_ip var(txn.real_ip),map_ip(/etc/haproxy/blocked_ips.map,0) -m int gt 0
|
||||||
http-request set-path /blocked-ip if is_blocked_ip
|
http-request set-path /blocked-ip if is_blocked_ip
|
||||||
use_backend default-backend if is_blocked_ip
|
use_backend default-backend if is_blocked_ip
|
||||||
|
{%- if suspension_enabled %}
|
||||||
|
|
||||||
|
# Site suspension routing. Any Host header listed in
|
||||||
|
# /etc/haproxy/suspended_domains.list is routed to bk_suspended (a
|
||||||
|
# backend serving a static 503 "site unavailable" page). External
|
||||||
|
# tooling (e.g. WHP's site_disable.php) maintains the list file via
|
||||||
|
# `docker cp`. An empty list is safe — the ACL simply doesn't match.
|
||||||
|
# Sits after IP-blocking (so 429/403 still trigger first) and before
|
||||||
|
# any per-domain use_backend rules, so suspension takes precedence
|
||||||
|
# over normal site routing.
|
||||||
|
acl is_suspended_domain hdr(host),lower -f /etc/haproxy/suspended_domains.list
|
||||||
|
use_backend bk_suspended if is_suspended_domain
|
||||||
|
{%- endif %}
|
||||||
{%- if coraza_spoe_backend %}
|
{%- if coraza_spoe_backend %}
|
||||||
|
|
||||||
# Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block
|
# Coraza WAF inspection via SPOE. Runs AFTER rate-limit and IP-block
|
||||||
|
|||||||
13
templates/hap_suspended_backend.tpl
Normal file
13
templates/hap_suspended_backend.tpl
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Suspended-site backend. Used when external tooling adds a host to
|
||||||
|
# /etc/haproxy/suspended_domains.list (read by an ACL in the frontend).
|
||||||
|
# The backend points at a single upstream that serves a static 503
|
||||||
|
# "site temporarily unavailable" page. Only rendered when the
|
||||||
|
# HAPROXY_SUSPENSION_BACKEND env var is set on the haproxy-manager
|
||||||
|
# container; non-WHP deployments (home networks, standalone use) see
|
||||||
|
# no change to haproxy.cfg.
|
||||||
|
backend bk_suspended
|
||||||
|
mode http
|
||||||
|
option http-server-close
|
||||||
|
http-request set-header X-Forwarded-Proto https if { ssl_fc }
|
||||||
|
http-request set-header X-Forwarded-For %[src]
|
||||||
|
server suspended {{ target }} check inter 30s
|
||||||
Reference in New Issue
Block a user