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>
69 lines
3.6 KiB
Docker
69 lines
3.6 KiB
Docker
## shared-ols — the shared OpenLiteSpeed webserver tier.
|
|
##
|
|
## One OLS container fronting MANY tenants' detached cac-lsphp sidecars — the
|
|
## OLS analogue of the shared-httpd container. Runs NO PHP locally: every site's
|
|
## PHP goes to its own cac-lsphp:phpNN sidecar over LSAPI (extProcessor type
|
|
## lsapi, address <sidecar>:9000). HAProxy stays the TLS/WAF/SNI edge and routes
|
|
## OLS-type hostnames here on :443.
|
|
##
|
|
## Built on the SAME litespeedtech prebuilt base as cac-litespeed / cac-lsphp so
|
|
## the OLS build + plumbing (lscgid, cgid socket — see feedback_ols_packaging_landmines)
|
|
## are the proven ones. The base is lsphp-tagged but we never run that lsphp;
|
|
## the tag just selects the OLS build. Pinned to lsphp83 / OLS 1.8.4.
|
|
##
|
|
## Config model (established by PoC 2026-06-10): OLS has NO top-level `include`,
|
|
## so render-shared-ols-config.sh assembles httpd_config.conf from the panel's
|
|
## per-site files at boot + on every change. See that script + the plan.
|
|
|
|
ARG OLS_VERSION=1.8.4
|
|
ARG PHPVER=83
|
|
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
|
|
|
## Tooling the shared tier needs on top of the base:
|
|
## - inotify-tools: the .htaccess watcher (spec 5.3)
|
|
## - gettext-base: envsubst for render-shared-ols-config.sh
|
|
## - openssl: self-signed cert for the :443 listener (HAProxy verifies none)
|
|
## - curl/ca-certificates: HEALTHCHECK
|
|
## - procps: provides pgrep, which entrypoint-shared-ols.sh's ols_running()
|
|
## liveness check depends on. Only transitively present via the base image
|
|
## today (Ubuntu 24.04 pulls it in) — pin it explicitly so it can't be
|
|
## pruned as "unused" and silently break the supervisor's crash detection.
|
|
RUN apt-get update && \
|
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
inotify-tools gettext-base openssl ca-certificates curl procps && \
|
|
apt-get clean && \
|
|
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
|
|
|
## Snapshot the stock httpd_config.conf so render-shared-ols-config.sh always has
|
|
## a pristine base to strip-and-rebuild from (the base image keeps it at conf/).
|
|
RUN mkdir -p /usr/local/lsws/.conf && \
|
|
cp /usr/local/lsws/conf/httpd_config.conf /usr/local/lsws/.conf/httpd_config.conf
|
|
|
|
COPY ./scripts/entrypoint-shared-ols.sh \
|
|
./scripts/render-shared-ols-config.sh \
|
|
./scripts/ols-htaccess-watcher.sh \
|
|
/scripts/
|
|
RUN chmod +x /scripts/entrypoint-shared-ols.sh /scripts/render-shared-ols-config.sh /scripts/ols-htaccess-watcher.sh
|
|
COPY ./configs/shared-ols/ /etc/shared-ols-templates/
|
|
|
|
## Admin console unreachable from tenant/edge networks (spec 5.2): bind the
|
|
## WebAdmin listener to loopback. Same sed as Dockerfile.litespeed.
|
|
RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0.0.1:|' \
|
|
/usr/local/lsws/admin/conf/admin_config.conf 2>/dev/null || true
|
|
|
|
EXPOSE 80 443
|
|
|
|
## Health: the entrypoint renders a catch-all _health vhost serving /healthz, so
|
|
## this passes from boot (zero customer sites) onward. Self-signed :443.
|
|
##
|
|
## MUST stay on /healthz, and must stay a LOOPBACK request. That vhost answers
|
|
## 421 for every other path/Host so an unmapped customer hostname can never look
|
|
## "up" to a monitor; /healthz answers 200 only for an internal client address
|
|
## (loopback here). Probing `/` instead would fail the healthcheck and restart
|
|
## the whole shared tier. WHP's setup-shared-ols.sh overrides this with the
|
|
## equivalent `curl -sfk https://localhost/healthz`; keep the two in step.
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
|
CMD curl -fsSk https://127.0.0.1/healthz || exit 1
|
|
|
|
ENTRYPOINT ["/scripts/entrypoint-shared-ols.sh"]
|