Default splash for unrouted hosts (404+noindex, answered before the WAF) and in-container access log (2026.11.2) (#15)
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 3m36s
HAProxy Manager Build and Push / Build-and-Push (push) Successful in 3m36s
This commit was merged in pull request #15.
This commit is contained in:
1 parent
1ee5760c1b
commit
50c58c4e9e
16 files changed
+1078
-59
No files matched your search
+23
-32
@@ -2,42 +2,33 @@
|
||||
# Global settings
|
||||
#---------------------------------------------------------------------
|
||||
global
|
||||
# ACCESS LOG DESTINATION.
|
||||
# ACCESS LOG DESTINATION: a UNIX datagram socket INSIDE this container.
|
||||
#
|
||||
# This used to be `log 127.0.0.1 local2`, which was a silent black hole:
|
||||
# 127.0.0.1 is the CONTAINER's own loopback, nothing has ever listened on
|
||||
# udp/514 in the container netns, and there is no /dev/log in the image.
|
||||
# Every access log line -- ~1.5M/day across the whole edge -- was written
|
||||
# to a socket with no receiver and dropped. Nothing errored, nothing
|
||||
# warned, and `haproxy -c` was perfectly happy. The cost only shows up
|
||||
# during an incident: per-IP 429s, tarpits, wp-admin gate redirects,
|
||||
# WAF 403s and `silent-drop`s left no record anywhere, so the edge could
|
||||
# not be asked what it had rejected. Only aggregate stick-table counters
|
||||
# survived.
|
||||
# scripts/haproxy-log-receiver.py (supervised by start-up.sh) reads it and
|
||||
# writes a size-capped, rotating /var/log/haproxy/access.log in the
|
||||
# haproxy-logs volume. No network listener anywhere.
|
||||
#
|
||||
# Now points at the DOCKER BRIDGE GATEWAY, where the host's rsyslog has an
|
||||
# imudp listener bound (installed idempotently by WHP's
|
||||
# setup-haproxy-logrotate.sh, which also writes the logrotate stanza).
|
||||
# The host writes local2 to /var/log/haproxy.log and stops it there, so it
|
||||
# does not also flood /var/log/messages or the Graylog forwarder.
|
||||
# History: `log 127.0.0.1 local2` was a black hole (nothing listened in
|
||||
# the container netns); its successor, the docker bridge gateway
|
||||
# 172.18.0.1:514 with a host rsyslog imudp listener, had no listener on
|
||||
# the fleet either (probable cause, not measured: rsyslog binds that
|
||||
# address once at boot, before docker has created the bridge, and does
|
||||
# not retry). Either way per-IP
|
||||
# 429s, tarpits, gate redirects and WAF 403s left no record.
|
||||
#
|
||||
# WHY NOT `log stdout format raw local0`: it is INCOMPATIBLE with the
|
||||
# `daemon` keyword below, and incompatible SILENTLY. Verified on the
|
||||
# pinned 3.0.11 binary: with `daemon` set, a `log stdout` config serves
|
||||
# traffic normally and emits ZERO log lines, and `haproxy -c` returns 0
|
||||
# with no error and no warning -- so the CI config gate
|
||||
# (scripts/validate-rendered-config.py) cannot catch it either. Making it
|
||||
# work means dropping `daemon` / adding -db so haproxy stays in the
|
||||
# foreground, which in turn breaks the three synchronous
|
||||
# `subprocess.run(['haproxy', '-W', ...], check=True)` launch sites in
|
||||
# haproxy_manager.py (they would block until the 180s timeout and then be
|
||||
# killed). That is a change to the exact code path whose failure mode is
|
||||
# "container Up, ports 80/443 never bound, every site down, /health still
|
||||
# 200". Not worth it for a logging change.
|
||||
# WHY NOT `log stdout`: silently emits NOTHING with `daemon` (below) on
|
||||
# the pinned 3.0.11 (`haproxy -c` still passes), and dropping `daemon`
|
||||
# breaks the synchronous `haproxy -W` launch sites in haproxy_manager.py.
|
||||
# Also, `docker logs haproxy-manager` feeds WHP's AI log monitor, which
|
||||
# must not get a per-request firehose.
|
||||
#
|
||||
# UDP means a dead listener degrades to dropped log lines, never to a
|
||||
# stalled or failing request path -- the correct failure direction for an
|
||||
# edge fronting ~60 customer sites.
|
||||
# The path is relative to the `chroot` below: HAProxy resolves it after
|
||||
# chrooting, so `/log.sock` here is /var/lib/haproxy/log.sock on disk,
|
||||
# which is where the receiver binds (HAPROXY_LOG_SOCKET).
|
||||
#
|
||||
# Sends are non-blocking: a dead receiver or a full socket buffer drops
|
||||
# log lines, never stalls a request. HAPROXY_SYSLOG_TARGET still
|
||||
# overrides the target (`<ip>:<port>` for a remote collector).
|
||||
#
|
||||
# len 2048 accommodates the enriched log-format in hap_listener.tpl
|
||||
# (URL + User-Agent + UUID); the default 1024 would truncate long ones.
|
||||
|
||||
@@ -616,6 +616,47 @@ frontend web
|
||||
http-request set-path /suspended if is_suspended_domain
|
||||
use_backend default-backend if is_suspended_domain
|
||||
{%- endif %}
|
||||
{%- if default_splash %}
|
||||
|
||||
# Default splash for traffic that is NOT routed to any site: the server's
|
||||
# own hostname (registered cert-only, no backend), a bare IP, a stale or
|
||||
# mistyped name. HAProxy answers it ITSELF with a static page and the
|
||||
# request goes no further -- in particular it never reaches the Coraza
|
||||
# SPOE call below.
|
||||
#
|
||||
# Why: `use_backend default-backend` does not short-circuit anything --
|
||||
# HAProxy runs EVERY http-request rule (Coraza included) before it picks
|
||||
# a backend. So the catch-all used to be WAF-inspected with the full
|
||||
# server-wide rule set like customer traffic, and a browser visiting the
|
||||
# server hostname also carries the control panel's cookies (cookies are
|
||||
# not port-scoped: whp01:8443 cookies are sent to whp01:443). Anything
|
||||
# Coraza matched there showed the WAF block page for a host that has no
|
||||
# site behind it to protect. A fixed static reply has nothing to protect.
|
||||
#
|
||||
# "Routed" is decided from two lists generate_config() writes from the
|
||||
# SAME domain rows it renders the routing ACLs from (exact names, and
|
||||
# ".base" suffixes for wildcards), port stripped and lowercased. They
|
||||
# are a SUPERSET of what routes -- the routing ACLs below compare the raw
|
||||
# Host (port included) -- so a host that routes is never splashed; the
|
||||
# worst a mismatch can do is send an unrouted request through the WAF to
|
||||
# default-backend, which is the pre-splash behaviour. Both lists are part
|
||||
# of the rollback set (_config_backup_pairs) so they cannot drift from
|
||||
# the haproxy.cfg that is actually loaded.
|
||||
#
|
||||
# Kept OUT of the splash: ACME http-01 (the server hostname's own cert is
|
||||
# issued through it), blocked IPs (blocked-ip page) and suspended hosts
|
||||
# (suspended page). Rate limits, the IP block and the wp-* gates above
|
||||
# still apply. EVERY path is 404 with the same page, plus
|
||||
# X-Robots-Tag noindex and /robots.txt "Disallow: /", so search engines
|
||||
# never index the server's IP or hostname as a site.
|
||||
# Disable with HAPROXY_DEFAULT_SPLASH=off (back to default-backend).
|
||||
acl splash_routed_host req.hdr(host),field(1,:),lower -f /etc/haproxy/routed_hosts.list
|
||||
acl splash_routed_host req.hdr(host),field(1,:),lower -m end -f /etc/haproxy/routed_wildcards.list
|
||||
acl splash_acme path_beg /.well-known/acme-challenge/
|
||||
acl splash_robots path /robots.txt
|
||||
http-request return status 200 content-type "text/plain; charset=utf-8" hdr cache-control "no-store" hdr x-robots-tag "noindex, nofollow, noarchive" hdr x-content-type-options "nosniff" file /haproxy/errors/robots-unrouted.txt if !splash_routed_host !splash_acme !is_blocked_ip{% if suspension_enabled %} !is_suspended_domain{% endif %} splash_robots
|
||||
http-request return status 404 content-type "text/html; charset=utf-8" hdr cache-control "no-store" hdr x-robots-tag "noindex, nofollow, noarchive" hdr x-content-type-options "nosniff" file /haproxy/errors/default-splash.html if !splash_routed_host !splash_acme !is_blocked_ip{% if suspension_enabled %} !is_suspended_domain{% endif %} !splash_robots
|
||||
{%- endif %}
|
||||
|
||||
# CVE-2026-109618 (GHSA-w8hw-w9fx-j79w, PHP "RCE via OOM corruption"),
|
||||
# vector (a): a multipart/form-data body with no declared length is not
|
||||
|
||||
Reference in new issue
Block a user