fix(shared-ols): unmapped Host gets 421, not a 200 that hides a dead site
The shared-OLS catch-all (`map _health *`) served html/index.html -- HTTP 200, 11 bytes, "shared-ols" -- to any Host no customer vhost claimed. Three live customer sites (joshuaknapp.net, streamers.channel, blog.anti-social.online) sat in exactly that state for ~2 months on whp01 and no monitor noticed, because every uptime check asks "is it 200?" and it was. A tier-wide catch-all that answers 200 makes a missing vhost indistinguishable from a working site. An unmapped Host now gets 421 Misdirected Request with a short generic body. 421 is semantically exact (the server cannot produce a response for the requested authority) and, unlike 404, cannot be confused with a normal answer from a real site. The discriminator is the request path plus the client address, NOT the Host -- the vhost is selected by the listener map, so by the time these rules run the Host is no longer available to branch on: * `/healthz` from an internal client address (loopback, RFC1918) -> 200 "ok" * everything else, every path, every Host, both listeners -> 421 The 421 for `/` is UNCONDITIONAL: no header, source address or Host talks this vhost into a 200 there, so the property the change exists to guarantee does not rest on anything spoofable. The address gate only hardens /healthz, and X-Forwarded-For cannot be used against it because HAProxy replaces that header with the real client IP. Health probes keep passing unchanged. Both forms were run against a container carrying this change and both exit 0 with "ok": curl -fsSk https://127.0.0.1/healthz (Dockerfile.shared-ols HEALTHCHECK) curl -sfk https://localhost/healthz (WHP setup-shared-ols.sh --health-cmd) `docker inspect` reported healthy with failingStreak=0, on a container with a customer site and on a zero-site container. Measured on the lab VM against OLS 1.8.4 (the production base image): unmapped Host, `/`, :443 and :80 -> 421, 356 bytes, identical for every unmapped Host (no enumeration signal) unmapped Host, any deeper path -> the same 421 configured site, both names, :443/:80 -> 200, served normally litespeed -t -> 0 [ERROR] lines (warnings only, and only about the lab fixture's uid/gid) Two OLS behaviours were measured rather than assumed, and both shaped the implementation -- see the comment block in entrypoint-shared-ols.sh: `context / { type redirect statusCode 421 }` silently degrades to a 302 with an unexpanded Location, and the `errorpage 421` body is fetched as a fresh request through the same rewrite rules (so it needs a %{THE_REQUEST} guard, since %{IS_SUBREQ} and %{ENV:REDIRECT_STATUS} are not populated). The old index.html is removed, not just bypassed: if these rules ever stopped applying, `context /` would fall back to the docRoot index, and with no index.html that is a 403 -- wrong-but-loud, rather than a 200 that is wrong-and-silent. Known consumer to land alongside this: whp-monitoring's probe_shared_ols_catchall() currently detects the catch-all by matching `200` + body `shared-ols`, a signature this change deletes. It must also accept 421, or the detector silently stops detecting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -156,8 +156,25 @@ for meta in "$SITES_ROOT"/*/site.meta; do
|
||||
done
|
||||
|
||||
## --- 5. ALWAYS add a health vhost mapped to the catch-all so the server is
|
||||
## valid with zero customer sites and HAProxy health checks (which hit by IP /
|
||||
## unknown Host) get a 200. Exact-domain maps above win over this '*'. ---
|
||||
## valid with zero customer sites. Exact-domain maps above win over this '*'.
|
||||
##
|
||||
## THIS MAP IS WHY AN UNMAPPED HOST GETS AN ANSWER AT ALL. Anything the loop
|
||||
## above did not emit a `map` for -- a customer domain whose site dir went
|
||||
## missing, a stale DNS record, a scanner probing by IP -- lands here. It used
|
||||
## to answer 200 with an 11-byte "shared-ols" body, which is how three live
|
||||
## customer sites stayed silently broken for ~2 months: every uptime monitor
|
||||
## asks "is it 200?" and it was.
|
||||
##
|
||||
## The health vhost (its vhconf.conf is written by entrypoint-shared-ols.sh,
|
||||
## which carries the full rationale) now answers 421 Misdirected Request with a
|
||||
## short generic body for any Host it cannot serve, and keeps 200 ONLY for
|
||||
## GET /healthz from an internal client address -- the Docker HEALTHCHECK and
|
||||
## edge liveness probes. Do NOT reintroduce a 200 here for `/`: probe /healthz.
|
||||
##
|
||||
## The listener `map` itself is unchanged, deliberately. Dropping the catch-all
|
||||
## instead would make OLS answer an unmapped Host from whichever vhost it
|
||||
## considers first, which is worse: an unmapped Host would be served SOMEONE
|
||||
## ELSE'S SITE. ---
|
||||
{
|
||||
echo ""
|
||||
echo "virtualhost _health {"
|
||||
|
||||
Reference in New Issue
Block a user