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>
entrypoint-shared-ols.sh's ols_running() liveness check now shells out to
pgrep, but procps was never in Dockerfile.shared-ols's apt-get install
list — pgrep works today only because Ubuntu 24.04's base image pulls
procps in transitively. If that stops being true, pgrep: command not
found -> exit 127 -> ols_running() false forever -> the crash-loop
breaker (MAX_STARTS/WINDOW) escalates to a hard exit 1 at boot. Make
the dependency explicit so it can't be pruned as unused.
One OLS container fronting many tenants' detached cac-lsphp sidecars — the
OLS analogue of shared-httpd. Runs NO PHP locally; every site's PHP goes to
its own sidecar over LSAPI (extProcessor type lsapi, address <sidecar>:9000).
Key design fact (established by PoC): OLS has NO top-level 'include' directive,
so render-shared-ols-config.sh assembles httpd_config.conf from the panel's
per-site files (vhconf.conf + site.meta) at boot and on every change — the
'include' OLS lacks. Per-site detail uses the OLS-native configFile +
vhost-scoped extprocessor model. LSCache is module-level (a configFile-loaded
vhost rejects a bare cache{} block); the WP LiteSpeed plugin controls
cacheability via X-LiteSpeed-Cache-Control headers.
- Dockerfile.shared-ols: litespeed base + inotify-tools/envsubst/openssl,
admin bound to loopback, :80/:443 self-signed, healthz HEALTHCHECK.
- entrypoint-shared-ols.sh: cert + health vhost + render + watcher, then
daemon-mode OLS supervision (reused from cac-litespeed so self-restarts
don't kill PID 1).
- render-shared-ols-config.sh: strip stock (incl local lsphp) + append base +
per-site stanzas + listeners with all maps + catch-all health vhost.
- ols-htaccess-watcher.sh: inotify debounce+floor -> lswsctrl restart (spec 5.3).
- configs/shared-ols/{httpd_config_base,vhconf}.tpl.
- CI: Build-Shared-OLS job.
Verified locally end-to-end: zero-site boot healthy on :443; add site via the
panel contract -> Host-routed to the right sidecar (SAPI=litespeed); real
client IP + HTTPS behind X-Forwarded headers; LSCache miss->hit; .htaccess
change triggers graceful restart; unknown Host hits health catch-all (200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>