Dockerfile.litespeed / Dockerfile.lsphp: one global
ARG BASE_IMAGE=litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
used by every stage (incl. the emergency lsphp-src stage, which stays on
upstream repos). The stages that run lsphp-apt-upgrade.sh (litespeed final,
lsphp ext-build + final) take ARG PKG_CACHE=, ship pkg-cache-repos.sh +
keys/pkg-cache/, and wrap the upgrade in `pkg-cache-repos.sh apt` ...
`pkg-cache-repos.sh restore`. Without PKG_CACHE both are no-ops.
lsphp-apt-upgrade.sh: the madison gate also accepts ${PKG_CACHE}/litespeed/
as the lsphp source, but only when PKG_CACHE is set.
build-push.yaml: Build-LiteSpeed-Images and Build-LSPHP-Images resolve the
base digest directly from Docker Hub, probe the cache, set up buildx with
the registry mirror only for a pinned base, and pass BASE_IMAGE + PKG_CACHE.
OLS_VERSION (1.8.4) is now a job-level env read by both the digest lookup
and the build-arg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the pkg-cache BuildKit mirror on, `FROM almalinux/10-base` was resolved
by tag through the cache, so a compromised cache could substitute the image.
Each job using the mirror now looks up the base tag's digest DIRECTLY at
registry-1.docker.io (.gitea/scripts/resolve-base-image.sh, HTTPS, HEAD on
the manifest, regex-validated) and builds FROM image@sha256:... via the
BASE_IMAGE build-arg, so the mirror can only serve content-addressed blobs.
If the lookup fails, the mirror is NOT used for that job and the tag is pulled
straight from Docker Hub. The digest is resolved every run (no manual bumps);
local builds default to the tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All four image matrices (cac, cac-fpm, cac-litespeed, cac-lsphp) and the
emergency lsphp-from-source workflow now build PHP 8.2-8.5 only. 7.4-8.1 are
end of life and got no upstream fix for CVE-2026-109618/109619; no fleet site
runs them any more. Existing :php74/:php80/:php81 tags stay in the registry
but are no longer rebuilt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- build-push.yaml: Build-and-Push, Build-FPM-Images, Build-Shared-httpd
and Build-Shared-OLS probe https://192.168.1.40 and :5000 with the
pinned pkg-cache CA (5 s timeouts). When the cache answers they pass
PKG_CACHE and configure the BuildKit Docker Hub mirror (ca=). When it
doesn't, they build from upstream exactly as before and emit a warning.
Fallback measured from the runner bridge: refused <1 s, silent drop 10 s.
- scripts/host/pkgcache-host-repos.sh: apply/revert/status for WHP hosts.
Each rewritten repo uses baseurl=<cache> <origin> as the real fallback;
mirrorlist/metalink stay commented because measured dnf ignores baseurl
when they are present. Keeps gpgcheck=1 / repo_gpgcheck=1 and backs up
every file. Tested on 192.168.1.148, including cache-down and
silent-drop fallback.
- docs: runner firewall rule (applied), measured fallback results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review of #30: step outputs derived from the LiteSpeed apt index
(fetched over plain HTTP) were spliced into shell via ${{ }} in jobs that
hold registry push secrets — a MITM'd "version" would be shell injection.
- every run: block in build-push.yaml and lsphp-source-emergency.yaml now
receives matrix values, step outputs, inputs and run_id via env: only
(incl. the pre-existing php.net / LiteSpeed resolve steps from #24/#29)
- resolved versions are regex-checked before GITHUB_OUTPUT: PHP
^[0-9]+\.[0-9]+\.[0-9]+$, deb ^[0-9][0-9A-Za-z.+~:-]*$
- LiteSpeed Packages index fetched over HTTPS (--proto =https); measured
rpms.litespeedtech.com serves it (HTTP/2 200)
- lsphp-src-build.sh / lsphp-src-overlay.sh validate PHPVER too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For when a PHP security release is out and the LiteSpeed apt repo lags
(2026-10-21: php2shell CVE-2026-109618/-109619 fixed in 8.2.35/8.3.36/
8.4.27/8.5.12, LiteSpeed still on 8.2.34/8.3.35/8.4.26/8.5.11).
Dockerfile.lsphp / Dockerfile.litespeed gain an `lsphp-src` stage that is
idle unless PHP_SRC_VERSION is set; normal builds are unchanged (measured:
module list, file manifest, ldd, ini and dpkg identical to the published
images).
When set (lsphp-src-build.sh):
- tarball sha256 (php.net releases JSON) + OpenPGP signature against the
committed php.net keyring, signer pinned per branch
- configure flags replayed from the packaged lsphp's own Configure Command
- LiteSpeed's php-litespeed-8.3 SAPI (what the packages actually compile,
measured) instead of php-src's sapi/litespeed; sha256-pinned and checked
against the packaged binary's LSAPI_* names
- packaged compiled-in ini defaults reproduced (session.save_path on 8.2/
8.3) and gated (`php -n` diff)
- ABI gate (lsphp-abi-gate.sh, pahole) before reusing LiteSpeed's packaged
PECL .so files; regression-tested in Shell-Checks
lsphp-src-overlay.sh swaps only php-src-owned files (decided by dpkg) and
fails the build unless extensions, versions, effective ini and feature
lines match the packaged lsphp under both CLI and litespeed SAPIs.
CI: lsphp-source-emergency.yaml (workflow_dispatch only). build-push.yaml
refuses to overwrite a newer php.net image with an older packaged one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Security review of #28: plain-http repo metadata could be replayed to keep a
build on old (validly signed) vulnerable PHP. The origin is now tried over
HTTPS first, with http only as a fallback (its HTTPS timed out/reset under the
2026-10-21 release load). CI resolves php.net's current release per branch
(over HTTPS) and the build fails if the installed PHP is older, so neither a
lagging mirror nor replayed metadata can ship a vulnerable PHP.
Tested on 192.168.1.193 (almalinux/10-base): rewritten baseurl installs PHP
8.5.12; the gate passes at 8.5.12 and fails at 8.5.99.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One failing PHP version (runs 4645/4646: cac-fpm php80, cac php82) cancelled
every other cac/cac-fpm build, so no PHP security build of 8.3-8.5 was pushed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cac-litespeed and cac-lsphp start FROM litespeedtech/openlitespeed:1.8.4-lsphpNN,
which ships lsphp85 8.5.0 / lsphp84 8.4.15 (built 2025-12). lsphp only moved
forward as a side effect of installing lsphpNN-ldap. lsphp-apt-upgrade.sh now
sweeps every installed lsphpNN* package (and OS packages) to the repo's current
release on every build and fails the build if the repo is unreachable or the
result doesn't match what CI resolved from the repo index. CI passes a per-run
CACHE_BUST so the upgrade layer is never a cache hit, and images carry
org.whp.php-version / org.whp.lsphp-package-version labels.
Tested on .148 (2026-10-21): cac-lsphp php85/php84 and cac-litespeed php85 built
from this branch report the same PHP (8.5.11 / 8.4.26), identical module lists
and OLS 1.8.4 as the current registry images; with the hardening flags each
serves/runs PHP as the customer uid, restarts, and stops cleanly (exit 0).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`entrypoint-lsphp.sh` decided whether cac_path_parity was loaded with
printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'
under `set -euo pipefail`. `grep -q` exits on its first match; printf is still
writing the remaining ~40 KB of `lsphp -i`, takes SIGPIPE, exits 141, and
pipefail prefers 141 over grep's 0. The branch therefore evaluated FALSE
*because the extension was present* — present early enough to stop the reader —
and every affected container fell back to the auto_prepend normaliser that a
customer's own .user.ini silently displaces, i.e. the exact failure the
extension exists to remove. Measured on whp02 against the published
cac-lsphp:php83: 5/5 runs status=141 with pipefail, 0 without.
The race is decided by pipe capacity, which is why it reproduced on whp02 and
not on other daemons: while the payload fits the pipe the writer never blocks
and always finishes first. Forced over the limit it is deterministic — 3x the
same `lsphp -i` (122100 bytes) gives 141 every time in the built image.
Fixed by reading with here-strings, which are not pipelines at all, so there is
no second exit status for pipefail to adopt. Same grep/awk patterns; plumbing
only. Same class fixed everywhere it existed under pipefail:
* entrypoint-lsphp.sh parity probe, and the SCAN_DIR awk probe
* entrypoint-litespeed.sh SCAN_DIR probe (a bare assignment: 141 there does
not degrade, `set -e` kills PID 1), and ols_running
* entrypoint-shared-ols.sh ols_running
* render-shared-ols-config.sh site.meta parsing (`sed | head -1`): measured
141 at 6000 duplicate keys, which under `set -e`
aborts the whole render
* fpm-parity-check.sh the `php-fpm -m` pre-flight, whose whole job is to
stop a harness fault being blamed on the extension
Also: the fallback used to announce "cac_path_parity extension not loadable in
this image" for every reason the branch was reached, including its own plumbing
breaking — a false diagnosis that sends operators to rebuild a good image whose
build gate passed. Verdicts now carry the evidence they rest on, and a probe
that produced nothing is reported as a probe failure that establishes nothing
about the image. Fail-open posture is unchanged: no probe failure is fatal.
Adds scripts/tests/lsphp-info-probe.test.sh, which runs the shipped probes
(extracted verbatim, so they cannot drift from what runs in production) under
`set -euo pipefail` against a realistic ~40 KB phpinfo body, and statically
outlaws the shape repo-wide. Against trunk it fails, naming all 9 offending
lines. Wired into CI as a new Shell-Checks job, because no existing gate ever
executed the entrypoint's branch logic — the .phpt suite and the Dockerfile's
own `lsphp -i | grep -q` probe (which has no pipefail) were both green for the
release whose entrypoint declared that same extension missing.
Verified: PHP 8.3 --no-cache build green, 10/10 .phpt, 9/9 FPM harness; the
built image logs `path parity = extension` and reports `Rewriting => active`
with .from/.to populated; ext-removed and probe-broken variants each produce
their own honest message and still start.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
New slim per-site PHP backend that runs 'lsphp -b 0.0.0.0:9000' (detached
LSAPI) and nothing else — the LiteSpeed analogue of cac-fpm, sitting behind
a shared OpenLiteSpeed container. Built on the same litespeedtech prebuilt
base as cac-litespeed so the lsphp runtime/extensions are identical.
- Dockerfile.lsphp: base + lsphpNN-ldap parity, reuses shared lsphp-overrides.ini,
exposes only :9000, no webserver started (guaranteed by entrypoint, not by
stripping OLS binaries).
- entrypoint-lsphp.sh: same uid/user contract + /home/$user/logs layout +
ini drop-in mechanism as entrypoint-litespeed.sh; sizes PHP_LSAPI_CHILDREN
from container memory (detect-memory-lsphp.sh) with panel override precedence;
execs lsphp -b as the per-site user via setpriv (PID 1).
- detect-memory-lsphp.sh: LSAPI_CHILDREN sizing, no OLS daemon reserve.
- healthcheck-lsphp.sh: TCP :9000 + lsphp-alive (LSAPI isn't FastCGI).
- CI: Build-LSPHP-Images job, php81-85 matrix, OLS 1.8.4, cac-lsphp:phpNN.
Verified locally: builds php83+php85; sidecar runs lsphp as the per-site
user (uid 61045) as PID 1, healthcheck green, and a real shared OLS in front
serves PHP over LSAPI (HTTP 200, SAPI=litespeed) with identical docroot path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Separate Apache and PHP-FPM into distinct container roles to reduce
per-customer memory overhead on shared servers. Adds three new images:
- Dockerfile.fpm: PHP-FPM only (no Apache), listens on TCP port 9000
- Dockerfile.shared-httpd: Apache only (no PHP), with SSL and proxy_fcgi
- Existing Dockerfile unchanged for standalone mode
Key changes:
- detect-memory.sh: CONTAINER_ROLE env var (combined/fpm_only/httpd_only)
controls the memory budget split
- create-php-config.sh: FPM_LISTEN env var for TCP port vs Unix socket,
added /fpm-ping and /fpm-status health endpoints
- New entrypoints for each container role
- tune-mpm.sh for hot-adjusting Apache MPM settings
- shared-vhost-template.tpl with proxy_fcgi and SSL on port 443
- CI/CD builds all three image types in parallel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>