refactor(suspension): serve via /suspended route on default-backend, drop bk_suspended
All checks were successful
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 53s

The previous design used a separate whp-suspended container (nginx:alpine
serving a static 503 page) reachable via a dedicated bk_suspended backend.
That was over-engineered — haproxy-manager-base already ships a default-app
Flask server on :8080 that serves /default-page and /blocked-ip via
path-rewrite ACLs. Mirroring that pattern lets the suspension page live
in the SAME container, no extra image to build, no extra container to
run/health-monitor.

Changes:
- Add /suspended Flask route on default_app returning 503 + suspended_page.html
- Add templates/suspended_page.html (dark-themed 503 page)
- hap_listener.tpl: 'http-request set-path /suspended' + 'use_backend
  default-backend' when host is in suspended_domains.list (same pattern
  as is_blocked_ip)
- Rename env var from HAPROXY_SUSPENSION_BACKEND (a target hostport) to
  HAPROXY_SUSPENSION_ENABLED (a bool); accepts 1/true/yes/on (case-insensitive)
- Remove hap_suspended_backend.tpl and its rendering in generate_config

Non-WHP deployments (env var unset) see byte-identical haproxy.cfg as before
(verified via jinja2 render diff).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 12:08:45 -07:00
parent 6fd07b4c54
commit 5e5234cb14
4 changed files with 88 additions and 42 deletions

View File

@@ -56,15 +56,17 @@ frontend web
{%- 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.
# /etc/haproxy/suspended_domains.list is rewritten to /suspended and
# routed through default-backend, which is the same Flask app that
# serves the default page and blocked-ip page (port 8080 inside this
# container). The `/suspended` route returns HTTP 503 with a static
# suspension 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.
acl is_suspended_domain hdr(host),lower -f /etc/haproxy/suspended_domains.list
use_backend bk_suspended if is_suspended_domain
http-request set-path /suspended if is_suspended_domain
use_backend default-backend if is_suspended_domain
{%- endif %}
{%- if coraza_spoe_backend %}

View File

@@ -1,18 +0,0 @@
# 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]
# init-addr last,none: tolerate startup-time DNS resolution failure
# (the upstream container may not be up yet when haproxy-manager starts).
# resolvers docker_dns: re-resolve via Docker's embedded DNS at 127.0.0.11
# so the server picks up the real IP once the upstream becomes available
# (the docker_dns block is defined in hap_header.tpl).
server suspended {{ target }} check inter 30s init-addr last,none resolvers docker_dns

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>Site temporarily unavailable</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
text-align: center;
padding: 50px 20px;
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
margin: 0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e2e8f0;
}
.container {
background: #1e293b;
border: 1px solid #334155;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.4);
max-width: 560px;
width: 100%;
}
h1 {
color: #f1f5f9;
margin: 0 0 20px;
font-size: 1.75em;
font-weight: 600;
}
p {
color: #cbd5e1;
line-height: 1.7;
margin: 0 0 12px;
font-size: 1.05em;
}
.note {
color: #94a3b8;
font-size: 0.9em;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #334155;
}
</style>
</head>
<body>
<div class="container">
<h1>This site is temporarily unavailable.</h1>
<p>The site you are trying to reach is currently offline.</p>
<p class="note">Site owners: please contact support to restore service.</p>
</div>
</body>
</html>