Compare commits

..
Author SHA1 Message Date
shadowdaoandClaude Opus 5.5 2b9e7f302c ci: enable PKG_CACHE via a TLS probe that falls back to upstream; host repo tool
- 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>
2026-10-21 16:46:03 -07:00
shadowdaoandClaude Opus 5.5 18f65c6d84 fix(pkg-cache): HTTPS-only cache, GPG-verified release RPMs, strict restore
Security review follow-up for the opt-in PKG_CACHE path:

- PKG_CACHE must be https:// (http is refused). The cache's certificate
  comes from a name-constrained internal CA (keys/pkg-cache/ca.crt, valid
  only for 192.168.1.40 / pkg-cache.*). The CA is trusted only by the
  rewritten repos (dnf sslcacert bundle, apt per-host CaInfo) and only
  for the duration of the RUN.
- epel-release / remi-release are no longer installed straight from a
  URL. release-install downloads them over HTTPS, checks the package name,
  imports the signing keys committed in keys/pkg-cache/ and requires
  `rpm -K` "signatures OK", then installs with localpkg_gpgcheck=1. This
  applies with and without the cache.
- Rewritten repos get gpgcheck=1. Remi and AlmaLinux also get
  repo_gpgcheck=1, since both sign repomd.xml. EPEL does not publish a
  repomd signature, so it relies on end-to-end HTTPS.
- restore also deletes files it created and any new repo/apt file that
  still references the cache, then fails the build if a reference remains
  under /etc/yum.repos.d, /etc/dnf or /etc/apt.
- docs: trust model, replay risk, proposed openssl/httpd/ImageMagick
  minimum-version gates, TLS registry mirror (BuildKit ca= / dockerd
  certs.d, no insecure-registries).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:53:24 -07:00
shadowdaoandClaude Opus 5.5 b2bec5b3ee feat(build): opt-in PKG_CACHE build arg for the internal package cache
Adds scripts/pkg-cache-repos.sh and wires it into Dockerfile,
Dockerfile.fpm, Dockerfile.shared-httpd and Dockerfile.shared-ols. With
--build-arg PKG_CACHE=http://192.168.1.40 (CT130 pkg-cache), dnf/apt go
through the LAN cache. That cache fetches from the Remi/EPEL/AlmaLinux/
Ubuntu/LiteSpeed origins over HTTPS, with fallback tiers and
stale-on-error. Without the arg the script is a no-op and builds are
unchanged.

- rpm: each baseurl becomes "<cache>/<prefix>/... <origin>/...", so dnf
  fails over to the origin if the cache dies mid-build
- every RUN restores the original repo files before it ends, so no
  shipped layer carries the LAN address
- EPEL/Remi release RPMs come from the cache when it is set
- the php.net version gates are untouched

Dockerfile.litespeed/.lsphp are not wired yet (see docs/pkg-cache.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:29:54 -07:00
jknapp fa7808897f Merge pull request 'feat(lsphp): emergency lsphp build from php.net source (same support as packaged)' (#30) from feat/lsphp-source-emergency into trunk
Cloud Apache Container / Shell-Checks (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (74) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (80) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (81) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (83) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (84) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (85) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (74) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (80) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (81) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (82) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (83) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (84) (push) Canceled after 0s
Cloud Apache Container / Build-FPM-Images (85) (push) Canceled after 0s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Canceled after 0s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Canceled after 0s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Canceled after 0s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Canceled after 0s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Canceled after 0s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Canceled after 0s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Canceled after 0s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Canceled after 0s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Canceled after 0s
Cloud Apache Container / Build-Shared-httpd (push) Canceled after 0s
Cloud Apache Container / Build-Shared-OLS (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (82) (push) Canceled after 0s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Canceled after 0s
2026-10-21 16:48:40 +00:00
shadowdaoandClaude Opus 5.5 42993f42bd ci(security): no ${{ }} in run:, validate resolved versions, LiteSpeed index over HTTPS
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>
2026-10-21 09:47:44 -07:00
shadowdaoandClaude Opus 5.5 83c2ee9c2a feat(lsphp): emergency build path compiling lsphp from php.net source
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>
2026-10-21 09:46:22 -07:00
jknapp be1540f559 Merge pull request 'fix(images): Remi origin over HTTPS first; fail builds older than php.net' (#29) from fix/remi-origin-https into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 15s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 3m35s
Cloud Apache Container / Build-and-Push (74) (push) Successful in 5m36s
Cloud Apache Container / Build-FPM-Images (74) (push) Failing after 43s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 3m25s
Cloud Apache Container / Build-and-Push (83) (push) Failing after 3m43s
Cloud Apache Container / Build-FPM-Images (81) (push) Failing after 4m11s
Cloud Apache Container / Build-FPM-Images (82) (push) Failing after 4m15s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 11m11s
Cloud Apache Container / Build-FPM-Images (83) (push) Failing after 3m29s
Cloud Apache Container / Build-FPM-Images (84) (push) Failing after 3m47s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 7m45s
Cloud Apache Container / Build-FPM-Images (85) (push) Failing after 3m25s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 1m28s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 10m49s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 2m2s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 5m41s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 6m32s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 6m20s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 2m4s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 5m17s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 1m53s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 35s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 4m6s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 5m42s
Cloud Apache Container / Build-and-Push (82) (push) Failing after 3m18s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 4m24s
2026-10-21 15:29:41 +00:00
shadowdaoandClaude Opus 5.5 af7fd193e5 fix(images): Remi origin over HTTPS first; fail builds older than php.net
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>
2026-10-21 08:29:31 -07:00
jknapp e8e3bfd490 Merge pull request 'fix(images): install Remi packages from the origin, not the lagging mirrors' (#28) from fix/remi-origin into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 17s
Cloud Apache Container / Build-and-Push (80) (push) Failing after 4m54s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 6m45s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 6m6s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 4m55s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 6m12s
Cloud Apache Container / Build-FPM-Images (74) (push) Failing after 6m3s
Cloud Apache Container / Build-FPM-Images (82) (push) Failing after 3m30s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 6m22s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 5m57s
Cloud Apache Container / Build-FPM-Images (83) (push) Failing after 4m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Failing after 4m10s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 4m8s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 6m49s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 4m33s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 4m43s
Cloud Apache Container / Build-and-Push (74) (push) Successful in 21m58s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 4m26s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 1m59s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 36s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 4m52s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 3m12s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 5m16s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 5m38s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 5m20s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 6m34s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 4m49s
2026-10-21 15:02:41 +00:00
shadowdaoandClaude Opus 5.5 c49f456336 fix(images): install Remi packages from the origin, not the lagging mirrors
During the php2shell release the Remi mirrors still served 8.2.34 hours after
8.2.35 was published, so cac/cac-fpm builds shipped vulnerable PHP. Enable the
repo files' baseurl (rpms.remirepo.net) and comment the mirrorlist. Verified on
the build server: a clean almalinux/10-base install now gets PHP 8.2.35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:02:38 -07:00
jknapp bcd737a9e2 Merge pull request 'ci: fail-fast false on every image matrix' (#27) from fix/ci-no-fail-fast into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 42s
Cloud Apache Container / Build-and-Push (81) (push) Failing after 2m8s
Cloud Apache Container / Build-and-Push (85) (push) Failing after 3m27s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 5m26s
Cloud Apache Container / Build-and-Push (80) (push) Failing after 13m6s
Cloud Apache Container / Build-FPM-Images (80) (push) Failing after 2m7s
Cloud Apache Container / Build-and-Push (84) (push) Failing after 11m45s
Cloud Apache Container / Build-and-Push (74) (push) Successful in 15m27s
Cloud Apache Container / Build-FPM-Images (82) (push) Failing after 2m22s
Cloud Apache Container / Build-FPM-Images (85) (push) Failing after 1m19s
Cloud Apache Container / Build-FPM-Images (83) (push) Failing after 4m30s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 1m46s
Cloud Apache Container / Build-and-Push (83) (push) Failing after 18m2s
Cloud Apache Container / Build-FPM-Images (81) (push) Failing after 7m47s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 1m31s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 5m20s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 2m26s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 5m22s
Cloud Apache Container / Build-FPM-Images (84) (push) Failing after 8m36s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 2m11s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 36s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 6m5s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 4m5s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 6m14s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 6m21s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 15m24s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 5m2s
2026-10-21 14:55:53 +00:00
shadowdaoandClaude Opus 5.5 955aec4573 ci: fail-fast false on every image matrix
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>
2026-10-21 07:55:49 -07:00
jknapp 998eb17f39 Merge pull request 'fix(php): bake php2shell ini mitigation into cac/cac-fpm/cac-lsphp/cac-litespeed' (#26) from fix/php2shell-ini-mitigation into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 1m25s
Cloud Apache Container / Build-and-Push (74) (push) Canceled after 4m13s
Cloud Apache Container / Build-and-Push (80) (push) Canceled after 3m58s
Cloud Apache Container / Build-and-Push (81) (push) Canceled after 3m41s
Cloud Apache Container / Build-and-Push (83) (push) Canceled after 2m29s
Cloud Apache Container / Build-and-Push (84) (push) Canceled after 31s
Cloud Apache Container / Build-and-Push (85) (push) Canceled after 15s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 5m56s
Cloud Apache Container / Build-FPM-Images (80) (push) Failing after 5m45s
Cloud Apache Container / Build-FPM-Images (81) (push) Canceled after 5m31s
Cloud Apache Container / Build-FPM-Images (82) (push) Canceled after 5m16s
Cloud Apache Container / Build-FPM-Images (83) (push) Canceled after 5m0s
Cloud Apache Container / Build-FPM-Images (84) (push) Canceled after 3m56s
Cloud Apache Container / Build-FPM-Images (85) (push) Canceled after 2m52s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 1m27s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 1m31s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 4m40s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 2m35s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 5m53s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 46s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 5m28s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 6m32s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 6m19s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 6m36s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 4m54s
Cloud Apache Container / Build-and-Push (82) (push) Failing after 2m58s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m40s
2026-10-21 14:22:07 +00:00
shadowdaoandClaude Opus 5.5 f461d22aae fix(php): bake php2shell ini mitigation into cac/cac-fpm/cac-lsphp/cac-litespeed
CVE-2026-109618 / CVE-2026-109619 (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp).
Adds configs/zz-php2shell-mitigation.ini (fatal_error_backtraces=0,
session.upload_progress.enabled=0, max_input_nesting_level=16) and installs it
into each image's PHP scan dir as zz-php2shell-mitigation.ini so it sorts after
99-prod-overrides.ini and the entrypoint-written 99-* fragments:

- Dockerfile / Dockerfile.fpm: COPY to /etc/php.d/ next to prod-php.ini.
- Dockerfile.lsphp / Dockerfile.litespeed: COPY to /etc/lsws-templates/ and
  cp into the computed lsphp $SCAN_DIR in the existing overrides RUN.

fatal_error_backtraces is unknown to (and silently ignored by) PHP < 8.5, so
one file serves every version. Remove once all images ship
PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:21:47 -07:00
jknapp cfde82ae44 Merge pull request 'fix(lsphp-apt-upgrade): no early-exit readers under pipefail' (#25) from fix/lsphp-upgrade-pipefail into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 40s
Cloud Apache Container / Build-and-Push (74) (push) Failing after 1m4s
Cloud Apache Container / Build-and-Push (84) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (85) (push) Canceled after 0s
Cloud Apache Container / Build-and-Push (80) (push) Canceled after 56s
Cloud Apache Container / Build-and-Push (81) (push) Canceled after 41s
Cloud Apache Container / Build-and-Push (83) (push) Canceled after 10s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m54s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m40s
Cloud Apache Container / Build-FPM-Images (80) (push) Failing after 4m2s
Cloud Apache Container / Build-FPM-Images (81) (push) Canceled after 3m46s
Cloud Apache Container / Build-FPM-Images (83) (push) Canceled after 1m3s
Cloud Apache Container / Build-FPM-Images (84) (push) Canceled after 47s
Cloud Apache Container / Build-FPM-Images (85) (push) Canceled after 33s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 4m7s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 4m30s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 4m34s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 6m22s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 1m56s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 2m2s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 39s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 45s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 5m46s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 5m46s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 5m43s
Cloud Apache Container / Build-and-Push (82) (push) Canceled after 26s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 5m15s
2026-10-21 14:07:07 +00:00
shadowdaoandClaude Opus 5.5 132e591503 fix(lsphp-apt-upgrade): no early-exit readers under pipefail
apt-cache madison | grep -q and apt-cache policy | awk '...exit' can SIGPIPE
the writer under pipefail and report lsphp missing when it is there. Read
each output into a variable and match in the shell. Fixes the Shell-Checks
failure on trunk (lsphp-info-probe.test.sh structural scan).

Tested: lsphp-info-probe.test.sh 36/36, shellcheck clean, and the script run
in litespeedtech/openlitespeed:1.8.4-lsphp85 upgrades 8.5.0 -> 8.5.11 OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 07:07:05 -07:00
jknapp 642b97707d Merge pull request 'feat(images): upgrade lsphp from the LiteSpeed repo at build time; label PHP version' (#24) from feat/lsphp-upgrade-at-build into trunk
Cloud Apache Container / Shell-Checks (push) Failing after 12s
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m55s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 1m47s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m45s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 5m50s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 5m55s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 6m29s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 1m52s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 5m37s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 5m53s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 1m39s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 1m43s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 6m32s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 2m6s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 5m30s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 5m24s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 4m58s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 5m33s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 6m41s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 3m1s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 34s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 3m8s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 5m9s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 5m25s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 6m5s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 6m6s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m27s
2026-10-21 13:08:15 +00:00
shadowdaoandClaude Opus 5.5 71ca114529 feat(images): upgrade lsphp from the LiteSpeed repo at build time; label PHP version
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>
2026-10-21 06:07:48 -07:00
jknapp 74e12407f8 Merge pull request 'fix(entrypoint): no recursive chmod, graceful stop, fpm error_log owner, symlink-safe root paths, dotfile deny' (#23) from fix/no-recursive-chmod-on-start into trunk
Cloud Apache Container / Shell-Checks (push) Successful in 12s
Cloud Apache Container / Build-and-Push (74) (push) Successful in 1m58s
Cloud Apache Container / Build-FPM-Images (74) (push) Successful in 2m36s
Cloud Apache Container / Build-FPM-Images (80) (push) Successful in 1m51s
Cloud Apache Container / Build-and-Push (81) (push) Successful in 8m48s
Cloud Apache Container / Build-FPM-Images (81) (push) Successful in 3m16s
Cloud Apache Container / Build-and-Push (80) (push) Successful in 9m14s
Cloud Apache Container / Build-and-Push (84) (push) Successful in 8m24s
Cloud Apache Container / Build-and-Push (83) (push) Successful in 8m51s
Cloud Apache Container / Build-and-Push (85) (push) Successful in 8m43s
Cloud Apache Container / Build-FPM-Images (82) (push) Successful in 2m44s
Cloud Apache Container / Build-LiteSpeed-Images (83) (push) Successful in 59s
Cloud Apache Container / Build-LiteSpeed-Images (81) (push) Successful in 3m16s
Cloud Apache Container / Build-LiteSpeed-Images (82) (push) Successful in 3m11s
Cloud Apache Container / Build-LiteSpeed-Images (84) (push) Successful in 3m38s
Cloud Apache Container / Build-LSPHP-Images (81) (push) Successful in 1m26s
Cloud Apache Container / Build-LSPHP-Images (82) (push) Successful in 1m13s
Cloud Apache Container / Build-LSPHP-Images (85) (push) Successful in 1m57s
Cloud Apache Container / Build-Shared-OLS (push) Successful in 31s
Cloud Apache Container / Build-LSPHP-Images (84) (push) Successful in 3m34s
Cloud Apache Container / Build-LSPHP-Images (83) (push) Successful in 4m10s
Cloud Apache Container / Build-FPM-Images (83) (push) Successful in 9m24s
Cloud Apache Container / Build-FPM-Images (84) (push) Successful in 9m38s
Cloud Apache Container / Build-FPM-Images (85) (push) Successful in 9m38s
Cloud Apache Container / Build-Shared-httpd (push) Successful in 5m13s
Cloud Apache Container / Build-and-Push (82) (push) Successful in 8m45s
Cloud Apache Container / Build-LiteSpeed-Images (85) (push) Successful in 1m22s
2026-10-21 00:01:29 +00:00
26 changed files with 3251 additions and 24 deletions

No files matched your search

+330 -3
View File
@@ -51,26 +51,104 @@ jobs:
scripts/ols-htaccess-watcher.sh \ scripts/ols-htaccess-watcher.sh \
scripts/create-vhost-litespeed.sh \ scripts/create-vhost-litespeed.sh \
scripts/install-lscache-wp.sh \ scripts/install-lscache-wp.sh \
scripts/lsphp-apt-upgrade.sh \
scripts/lsphp-src-build.sh \
scripts/lsphp-src-overlay.sh \
scripts/lsphp-abi-gate.sh \
scripts/tune-mpm.sh \ scripts/tune-mpm.sh \
scripts/tests/lsphp-info-probe.test.sh scripts/tests/lsphp-info-probe.test.sh \
scripts/tests/lsphp-abi-gate.test.sh
- name: lsphp probe regression test - name: lsphp probe regression test
run: ./scripts/tests/lsphp-info-probe.test.sh run: ./scripts/tests/lsphp-info-probe.test.sh
# The emergency php.net lsphp path (lsphp-source-emergency.yaml) keeps
# LiteSpeed's PECL .so files only if this gate says the Zend ABI is
# unchanged; prove here, on every push, that the gate can still FAIL.
- name: lsphp ABI gate regression test
run: |
if ! command -v pahole >/dev/null 2>&1 || ! command -v gcc >/dev/null 2>&1; then
(apt-get update && apt-get install -y gcc dwarves) ||
(sudo apt-get update && sudo apt-get install -y gcc dwarves)
fi
./scripts/tests/lsphp-abi-gate.test.sh
Build-and-Push: Build-and-Push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix: matrix:
phpver: [74, 80, 81, 82, 83, 84, 85] phpver: [74, 80, 81, 82, 83, 84, 85]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
# bad/expired cert) this job builds from the upstream repos and Docker Hub
# exactly as before. The repo cache and the registry mirror are probed
# separately so either can fail back on its own.
- name: Probe package cache
id: pkgcache
run: |
url=""; mirror=""
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx # BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
# so the mirror is configured on the builder; BuildKit itself still falls
# back to Docker Hub if the mirror stops answering mid-run.
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
if: steps.pkgcache.outputs.mirror != ''
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["192.168.1.40:5000"]
[registry."192.168.1.40:5000"]
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
- name: Set up Docker Buildx
if: steps.pkgcache.outputs.mirror == ''
uses: docker/setup-buildx-action@v3
# Per-run cache-bust value. Each job's buildx builder is created fresh by
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
# cached between runs — this keeps it that way if a cache is ever added:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# The minimum PHP version this build must ship: php.net's current release for
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
- name: Resolve current PHP release from php.net
id: phpmin
# Values reach the script via env, never via ${{ }} inside run: (a
# crafted string would otherwise be shell); outputs are regex-checked.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
v="$PHPVER"; branch="${v:0:1}.${v:1}"
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
echo "php.net current ${branch}: ${min}"
echo "min=${min}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -86,6 +164,9 @@ jobs:
push: true push: true
build-args: | build-args: |
PHPVER=${{ matrix.phpver }} PHPVER=${{ matrix.phpver }}
CACHE_BUST=${{ steps.bust.outputs.value }}
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
tags: | tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }} repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }} ${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
@@ -93,17 +174,79 @@ jobs:
Build-FPM-Images: Build-FPM-Images:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix: matrix:
phpver: [74, 80, 81, 82, 83, 84, 85] phpver: [74, 80, 81, 82, 83, 84, 85]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
# bad/expired cert) this job builds from the upstream repos and Docker Hub
# exactly as before. The repo cache and the registry mirror are probed
# separately so either can fail back on its own.
- name: Probe package cache
id: pkgcache
run: |
url=""; mirror=""
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx # BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
# so the mirror is configured on the builder; BuildKit itself still falls
# back to Docker Hub if the mirror stops answering mid-run.
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
if: steps.pkgcache.outputs.mirror != ''
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["192.168.1.40:5000"]
[registry."192.168.1.40:5000"]
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
- name: Set up Docker Buildx
if: steps.pkgcache.outputs.mirror == ''
uses: docker/setup-buildx-action@v3
# Per-run cache-bust value. Each job's buildx builder is created fresh by
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
# cached between runs — this keeps it that way if a cache is ever added:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# The minimum PHP version this build must ship: php.net's current release for
# the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older,
# so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP.
- name: Resolve current PHP release from php.net
id: phpmin
# Values reach the script via env, never via ${{ }} inside run: (a
# crafted string would otherwise be shell); outputs are regex-checked.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
v="$PHPVER"; branch="${v:0:1}.${v:1}"
json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}")
min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")')
if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi
[[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; }
echo "php.net current ${branch}: ${min}"
echo "min=${min}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -120,6 +263,9 @@ jobs:
push: true push: true
build-args: | build-args: |
PHPVER=${{ matrix.phpver }} PHPVER=${{ matrix.phpver }}
CACHE_BUST=${{ steps.bust.outputs.value }}
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
tags: | tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }} repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }} ${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
@@ -127,6 +273,9 @@ jobs:
Build-LiteSpeed-Images: Build-LiteSpeed-Images:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix: matrix:
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base # PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
# images stop at older OLS releases for those PHP versions, and the # images stop at older OLS releases for those PHP versions, and the
@@ -143,6 +292,38 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
# Per-run cache-bust value. Each job's buildx builder is created fresh by
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
# cached between runs — this keeps it that way if a cache is ever added:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
# this (build fails if the repo changed mid-build — just re-run) and writes
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
id: lsphp
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
php="${pkg%%-*}"
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "php=${php}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -150,6 +331,27 @@ jobs:
username: ${{ secrets.CI_USER }} username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }} password: ${{ secrets.CI_TOKEN }}
# A php.net emergency build (lsphp-source-emergency.yaml) may be what
# :phpNN points at right now. While LiteSpeed still lags, a normal build
# would silently put the VULNERABLE packaged lsphp back. Refuse instead;
# the job goes green again by itself once the LiteSpeed repo catches up.
# Fail-open if the tag cannot be inspected (first build, registry blip).
- name: Do not roll back a newer php.net emergency image
env:
PHPVER: ${{ matrix.phpver }}
NEW_PHP: ${{ steps.lsphp.outputs.php }}
run: |
set -euo pipefail
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${PHPVER}"
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
new="$NEW_PHP"
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
exit 1
fi
- name: Build and Push LiteSpeed Image - name: Build and Push LiteSpeed Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -159,6 +361,9 @@ jobs:
build-args: | build-args: |
PHPVER=${{ matrix.phpver }} PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4 OLS_VERSION=1.8.4
CACHE_BUST=${{ steps.bust.outputs.value }}
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images # OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an # for every PHP version we ship (1.8.5 and 1.9.0 don't have an
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild # lsphp81 variant on Docker Hub). Bump alongside a local rebuild
@@ -171,6 +376,9 @@ jobs:
Build-LSPHP-Images: Build-LSPHP-Images:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix: matrix:
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached # Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
# backend for the shared-ols tier and shares the litespeed prebuilt # backend for the shared-ols tier and shares the litespeed prebuilt
@@ -187,6 +395,38 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
# Per-run cache-bust value. Each job's buildx builder is created fresh by
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
# cached between runs — this keeps it that way if a cache is ever added:
# the package-upgrade layers consume CACHE_BUST and can never be reused.
- name: Cache-bust value
id: bust
env:
RUN_ID: ${{ gitea.run_id }}
run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
# Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the
# CURRENT lsphp version. The Dockerfile asserts the installed package equals
# this (build fails if the repo changed mid-build — just re-run) and writes
# it into the image labels org.whp.php-version / org.whp.lsphp-package-version.
- name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo
id: lsphp
# HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via
# env, and both outputs regex-checked before they reach GITHUB_OUTPUT.
env:
PHPVER: ${{ matrix.phpver }}
run: |
set -euo pipefail
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi
php="${pkg%%-*}"
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
[[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; }
echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "php=${php}" >> "$GITHUB_OUTPUT"
- name: Login to Gitea - name: Login to Gitea
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -194,6 +434,27 @@ jobs:
username: ${{ secrets.CI_USER }} username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }} password: ${{ secrets.CI_TOKEN }}
# A php.net emergency build (lsphp-source-emergency.yaml) may be what
# :phpNN points at right now. While LiteSpeed still lags, a normal build
# would silently put the VULNERABLE packaged lsphp back. Refuse instead;
# the job goes green again by itself once the LiteSpeed repo catches up.
# Fail-open if the tag cannot be inspected (first build, registry blip).
- name: Do not roll back a newer php.net emergency image
env:
PHPVER: ${{ matrix.phpver }}
NEW_PHP: ${{ steps.lsphp.outputs.php }}
run: |
set -euo pipefail
ref="repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${PHPVER}"
src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true)
cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true)
new="$NEW_PHP"
echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new"
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
exit 1
fi
- name: Build and Push lsphp Image - name: Build and Push lsphp Image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -203,6 +464,9 @@ jobs:
build-args: | build-args: |
PHPVER=${{ matrix.phpver }} PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4 OLS_VERSION=1.8.4
CACHE_BUST=${{ steps.bust.outputs.value }}
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same # OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
# prebuilt base, same lsphp binaries. Bump both together. # prebuilt base, same lsphp binaries. Bump both together.
tags: | tags: |
@@ -215,10 +479,40 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
# bad/expired cert) this job builds from the upstream repos and Docker Hub
# exactly as before. The repo cache and the registry mirror are probed
# separately so either can fail back on its own.
- name: Probe package cache
id: pkgcache
run: |
url=""; mirror=""
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
# so the mirror is configured on the builder; BuildKit itself still falls
# back to Docker Hub if the mirror stops answering mid-run.
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
if: steps.pkgcache.outputs.mirror != ''
uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["192.168.1.40:5000"]
[registry."192.168.1.40:5000"]
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
- name: Set up Docker Buildx - name: Set up Docker Buildx
if: steps.pkgcache.outputs.mirror == ''
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to Gitea - name: Login to Gitea
@@ -234,6 +528,8 @@ jobs:
file: ./Dockerfile.shared-httpd file: ./Dockerfile.shared-httpd
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
build-args: |
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
tags: | tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
@@ -243,10 +539,40 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
# bad/expired cert) this job builds from the upstream repos and Docker Hub
# exactly as before. The repo cache and the registry mirror are probed
# separately so either can fail back on its own.
- name: Probe package cache
id: pkgcache
run: |
url=""; mirror=""
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
echo "url=${url}" >> "$GITHUB_OUTPUT"
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
# so the mirror is configured on the builder; BuildKit itself still falls
# back to Docker Hub if the mirror stops answering mid-run.
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
if: steps.pkgcache.outputs.mirror != ''
uses: docker/setup-buildx-action@v3
with:
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["192.168.1.40:5000"]
[registry."192.168.1.40:5000"]
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
- name: Set up Docker Buildx - name: Set up Docker Buildx
if: steps.pkgcache.outputs.mirror == ''
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Login to Gitea - name: Login to Gitea
@@ -267,5 +593,6 @@ jobs:
build-args: | build-args: |
PHPVER=83 PHPVER=83
OLS_VERSION=1.8.4 OLS_VERSION=1.8.4
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
tags: | tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
@@ -0,0 +1,142 @@
name: lsphp from php.net source (EMERGENCY)
run-name: EMERGENCY lsphp php.net build by ${{ gitea.actor }}
## NEVER runs on push. Use only when a PHP security release is out and the
## LiteSpeed apt repo (rpms.litespeedtech.com/debian, noble) still ships the
## vulnerable lsphp. Runbook + exact trigger command: docs/lsphp-source-emergency.md
##
## What it builds: the normal Dockerfile.lsphp / Dockerfile.litespeed with
## PHP_SRC_VERSION / PHP_SRC_SHA256 / LSPHP_SOURCE=php.net
## so lsphp${PHPVER} is compiled from the php.net tarball (sha256 from the
## php.net releases JSON + OpenPGP signature against keys/php-src/), configured
## from the packaged lsphp's own Configure Command, and the image build FAILS
## unless the result has the same extensions / ini / features as the packaged
## lsphp (scripts/lsphp-src-overlay.sh).
##
## Tags: always :phpNN-phpnet-X.Y.Z (immutable, for staging / rollback)
## and :phpNN only when push_production_tags = true.
on:
workflow_dispatch:
inputs:
php_versions:
description: "Space-separated PHPVER=X.Y.Z pairs, e.g. '82=8.2.35 83=8.3.36 84=8.4.27 85=8.5.12'"
required: true
type: string
images:
description: "Which images: both | lsphp | litespeed"
required: true
default: both
type: string
push_production_tags:
description: "Also push :phpNN (what WHP pulls). false = only :phpNN-phpnet-X.Y.Z for staging."
required: true
default: false
type: boolean
jobs:
Build-lsphp-from-source:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
phpver: [81, 82, 83, 84, 85]
image: [lsphp, litespeed]
steps:
- name: Select this job's target (skip if not requested)
id: t
# inputs via env, never spliced into the script text
env:
IN_VERSIONS: ${{ inputs.php_versions }}
IN_IMAGES: ${{ inputs.images }}
IN_PROD: ${{ format('{0}', inputs.push_production_tags) }}
PHPVER: ${{ matrix.phpver }}
IMG: ${{ matrix.image }}
run: |
set -euo pipefail
case "$IN_IMAGES" in both|lsphp|litespeed) ;; *) echo "::error::images must be both|lsphp|litespeed, got '$IN_IMAGES'"; exit 1 ;; esac
case "$IN_PROD" in true|false) ;; *) echo "::error::push_production_tags must be true|false, got '$IN_PROD'"; exit 1 ;; esac
v=""
for pair in $IN_VERSIONS; do
[[ "$pair" =~ ^(8[1-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 81..85)"; exit 1; }
[ "${BASH_REMATCH[1]}" = "$PHPVER" ] && v="${BASH_REMATCH[2]}"
done
echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT"
if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != "$IMG" ]; }; then
echo "not requested: php${PHPVER} / ${IMG}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0
fi
echo "skip=0" >> "$GITHUB_OUTPUT"
echo "version=$v" >> "$GITHUB_OUTPUT"
case "$IMG" in lsphp|litespeed) ;; *) echo "::error::unexpected matrix image '$IMG'"; exit 1 ;; esac
echo "image_name=cac-${IMG}" >> "$GITHUB_OUTPUT"
echo "dockerfile=./Dockerfile.${IMG}" >> "$GITHUB_OUTPUT"
- name: Checkout
if: steps.t.outputs.skip == '0'
uses: actions/checkout@v4
# sha256 of php-X.Y.Z.tar.xz from php.net's own releases JSON. The build
# re-verifies it AND the OpenPGP signature; this step only refuses early
# if the release does not exist or is not newer than what LiteSpeed ships.
- name: Resolve php.net release + packaged lsphp
if: steps.t.outputs.skip == '0'
id: v
env:
V: ${{ steps.t.outputs.version }}
PHPVER: ${{ matrix.phpver }}
RUN_ID: ${{ gitea.run_id }}
run: |
set -euo pipefail
v="$V"
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed version '$v'"; exit 1; }
json=$(curl -fsS --retry 3 --max-time 60 "https://www.php.net/releases/index.php?json&version=$v")
sha=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); v=sys.argv[1]; print(next((s["sha256"] for s in d.get("source", []) if s.get("filename")=="php-%s.tar.xz"%v), ""))' "$v")
[ -n "$sha" ] || { echo "::error::php.net has no release php-$v.tar.xz (EOL branch? typo?): $json"; exit 1; }
[[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::no sha256 for php-$v.tar.xz in the php.net releases JSON"; exit 1; }
idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages)
pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1)
[ -n "$pkg" ] || { echo "::error::lsphp${PHPVER} not in the LiteSpeed repo index"; exit 1; }
[[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; }
pphp=${pkg%%-*}
[[ "$pphp" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '$pphp' from lsphp${PHPVER} $pkg"; exit 1; }
if [ "$(printf '%s\n%s\n' "$pphp" "$v" | sort -V | tail -n1)" != "$v" ] || [ "$pphp" = "$v" ]; then
echo "::error::LiteSpeed already ships lsphp${PHPVER} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)."
exit 1
fi
echo "php.net $v sha256=$sha ; LiteSpeed lsphp${PHPVER}=$pkg"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "pkg=$pkg" >> "$GITHUB_OUTPUT"
echo "pkgphp=$pphp" >> "$GITHUB_OUTPUT"
echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
if: steps.t.outputs.skip == '0'
uses: docker/setup-buildx-action@v3
- name: Login to Gitea
if: steps.t.outputs.skip == '0'
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and push (php.net lsphp)
if: steps.t.outputs.skip == '0'
uses: docker/build-push-action@v6
with:
file: ${{ steps.t.outputs.dockerfile }}
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
OLS_VERSION=1.8.4
CACHE_BUST=${{ steps.v.outputs.bust }}
EXPECT_LSPHP_PKG=${{ steps.v.outputs.pkg }}
EXPECT_PHP_VERSION=${{ steps.v.outputs.pkgphp }}
PHP_SRC_VERSION=${{ steps.t.outputs.version }}
PHP_SRC_SHA256=${{ steps.v.outputs.sha }}
LSPHP_SOURCE=php.net
tags: |
repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }}:php${{ matrix.phpver }}-phpnet-${{ steps.t.outputs.version }}
${{ steps.t.outputs.prod == 'true' && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:php{1}', steps.t.outputs.image_name, matrix.phpver) || '' }}
${{ steps.t.outputs.prod == 'true' && matrix.phpver == 85 && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:latest', steps.t.outputs.image_name) || '' }}
+41 -4
View File
@@ -1,13 +1,36 @@
FROM almalinux/10-base FROM almalinux/10-base
ARG PHPVER=83 ARG PHPVER=83
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
# never be a layer-cache hit, so every build gets the repos' current packages.
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
ARG CACHE_BUST=
# php.net's current release for this branch (CI); the build fails on older PHP.
ARG EXPECT_PHP_MIN=
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
# GPG-verified against keys/pkg-cache/ either way. Repo files
# are restored at the end of every RUN, so no layer keeps the cache address.
# See scripts/pkg-cache-repos.sh.
ARG PKG_CACHE=
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
# Install repos, update, install only needed packages, clean up in one layer # Install repos, update, install only needed packages, clean up in one layer
RUN dnf install -y \ RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \ /usr/local/sbin/pkg-cache-repos.sh release-install epel remi && \
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \ # Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
# because the origin's HTTPS dropped/timed out under that day's load. A
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
/usr/local/sbin/pkg-cache-repos.sh rpm && \
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
dnf update -y && \ dnf update -y && \
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \ dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \ nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
dnf clean all && \ dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/* rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
@@ -16,13 +39,23 @@ COPY ./scripts/ /scripts/
RUN chmod +x /scripts/* RUN chmod +x /scripts/*
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support # Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \ RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
dnf clean all dnf clean all
# Generate self-signed cert, create needed dirs, install PHP, clean up # Generate self-signed cert, create needed dirs, install PHP, clean up
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \ RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
mkdir -p /run/php-fpm/ && \ mkdir -p /run/php-fpm/ && \
/usr/local/sbin/pkg-cache-repos.sh rpm && \
/scripts/install-php$PHPVER.sh && \ /scripts/install-php$PHPVER.sh && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
# CI passes php.net's current release for the branch; never ship older PHP.
if [ -n "$EXPECT_PHP_MIN" ]; then \
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
rm -rf /tmp/* rm -rf /tmp/*
# Download and install wp-cli (consider pinning version for reproducibility) # Download and install wp-cli (consider pinning version for reproducibility)
@@ -36,6 +69,10 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
# Copy configs and web files # Copy configs and web files
COPY ./configs/default-index.conf /etc/httpd/conf.d/ COPY ./configs/default-index.conf /etc/httpd/conf.d/
COPY ./configs/prod-php.ini /etc/php.ini COPY ./configs/prod-php.ini /etc/php.ini
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
COPY ./configs/phpinfo.php /var/www/html/ COPY ./configs/phpinfo.php /var/www/html/
COPY ./configs/mariadb.repo /etc/yum.repos.d/ COPY ./configs/mariadb.repo /etc/yum.repos.d/
COPY ./configs/index.php /var/www/html/ COPY ./configs/index.php /var/www/html/
+41 -4
View File
@@ -1,13 +1,36 @@
FROM almalinux/10-base FROM almalinux/10-base
ARG PHPVER=83 ARG PHPVER=83
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
# never be a layer-cache hit, so every build gets the repos' current packages.
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
ARG CACHE_BUST=
# php.net's current release for this branch (CI); the build fails on older PHP.
ARG EXPECT_PHP_MIN=
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
# GPG-verified against keys/pkg-cache/ either way. Repo files
# are restored at the end of every RUN, so no layer keeps the cache address.
# See scripts/pkg-cache-repos.sh.
ARG PKG_CACHE=
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer # Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
RUN dnf install -y \ RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \ /usr/local/sbin/pkg-cache-repos.sh release-install epel remi && \
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \ # Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
# because the origin's HTTPS dropped/timed out under that day's load. A
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
/usr/local/sbin/pkg-cache-repos.sh rpm && \
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
dnf update -y && \ dnf update -y && \
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \ dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \ nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
dnf clean all && \ dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/* rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
@@ -16,12 +39,22 @@ COPY ./scripts/ /scripts/
RUN chmod +x /scripts/* RUN chmod +x /scripts/*
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support # Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \ RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
dnf clean all dnf clean all
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd) # Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
RUN mkdir -p /run/php-fpm/ && \ RUN mkdir -p /run/php-fpm/ && \
/usr/local/sbin/pkg-cache-repos.sh rpm && \
/scripts/install-php$PHPVER.sh && \ /scripts/install-php$PHPVER.sh && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
# CI passes php.net's current release for the branch; never ship older PHP.
if [ -n "$EXPECT_PHP_MIN" ]; then \
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
rm -rf /tmp/* rm -rf /tmp/*
# Download and install wp-cli # Download and install wp-cli
@@ -34,6 +67,10 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
# Copy configs (PHP only, no Apache configs) # Copy configs (PHP only, no Apache configs)
COPY ./configs/prod-php.ini /etc/php.ini COPY ./configs/prod-php.ini /etc/php.ini
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
COPY ./configs/mariadb.repo /etc/yum.repos.d/ COPY ./configs/mariadb.repo /etc/yum.repos.d/
# Set up cron job for log rotation # Set up cron job for log rotation
+63 -2
View File
@@ -22,6 +22,26 @@
## after FROM for any RUN steps that need it. ## after FROM for any RUN steps that need it.
ARG OLS_VERSION=1.8.5 ARG OLS_VERSION=1.8.5
ARG PHPVER=83 ARG PHPVER=83
## ---- EMERGENCY ONLY: lsphp compiled from php.net source ----------------------
## Idle unless PHP_SRC_VERSION is set (then /lsphp-src-out stays empty and the
## overlay step below is a no-op). Same stage as Dockerfile.lsphp — see there and
## docs/lsphp-source-emergency.md.
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS lsphp-src
ARG PHPVER=83
ARG CACHE_BUST=
ARG EXPECT_LSPHP_PKG=
ARG EXPECT_PHP_VERSION=
ARG PHP_SRC_VERSION=
ARG PHP_SRC_SHA256=
RUN --mount=type=bind,source=scripts,target=/mnt/scripts \
--mount=type=bind,source=keys/php-src,target=/usr/local/share/php-src-keys \
set -e; \
if [ -n "$PHP_SRC_VERSION" ]; then \
install -m 0755 /mnt/scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh; \
fi; \
PHPVER="${PHPVER}" bash /mnt/scripts/lsphp-src-build.sh
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
ARG PHPVER=83 ARG PHPVER=83
ENV PHPVER=${PHPVER} ENV PHPVER=${PHPVER}
@@ -45,14 +65,40 @@ ENV PHPVER=${PHPVER}
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs ## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell ## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier. ## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
RUN apt-get update && \ ##
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh also upgrades the OS and
## sweeps EVERY installed lsphp${PHPVER}* package to the LiteSpeed repo's current
## release, failing the build if the repo is unreachable — so a rebuild ships a
## PHP security fix as soon as LiteSpeed publishes the .deb, without waiting for
## a new litespeedtech/openlitespeed base tag. See the script header.
##
## CACHE_BUST: CI passes a per-run value so this layer can never be a cache hit
## (an ARG's first use in a RUN is part of that layer's cache key). Local builds:
## pass --build-arg CACHE_BUST=$(date +%s) or use --no-cache, or you may get the
## lsphp from your last build. EXPECT_* are resolved by CI from the repo index
## and asserted here; the same values become the image labels below.
ARG CACHE_BUST=
ARG EXPECT_LSPHP_PKG=
ARG EXPECT_PHP_VERSION=
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
RUN /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
gettext-base sudo cron \ gettext-base sudo cron \
ca-certificates curl wget \ ca-certificates curl wget \
lsphp${PHPVER}-ldap && \ lsphp${PHPVER}-ldap && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
## EMERGENCY PATH: overlay the php.net lsphp (no-op on normal builds) and prove
## it has the same extension/feature surface as the packaged one just installed
## — see scripts/lsphp-src-overlay.sh. Bind mounts: no extra image layer content.
ARG PHP_SRC_VERSION=
ARG LSPHP_SOURCE=litespeed-apt
RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \
--mount=type=bind,source=scripts,target=/mnt/scripts \
install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \
bash /mnt/scripts/lsphp-src-overlay.sh runtime "${PHPVER}" && \
rm -f /usr/local/share/lsphp-src-probe.php
## Composer (matches the wp-cli pattern in the existing CAC Dockerfile — ## Composer (matches the wp-cli pattern in the existing CAC Dockerfile —
## phar download, no `php install` pipe since lsphp's CLI mode is fine). ## phar download, no `php install` pipe since lsphp's CLI mode is fine).
RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \ RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \
@@ -68,6 +114,10 @@ COPY ./scripts/entrypoint-litespeed.sh \
/scripts/ /scripts/
RUN chmod +x /scripts/* RUN chmod +x /scripts/*
COPY ./configs/litespeed/ /etc/lsws-templates/ COPY ./configs/litespeed/ /etc/lsws-templates/
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir ## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own ## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
@@ -77,6 +127,7 @@ RUN bash -c 'set -e; \
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \ SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
mkdir -p "$SCAN_DIR"; \ mkdir -p "$SCAN_DIR"; \
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \ cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
echo "wrote overrides to $SCAN_DIR"' echo "wrote overrides to $SCAN_DIR"'
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin ## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
@@ -87,6 +138,16 @@ RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0
## Cron entry for log rotation (mirrors cac:phpXX). ## Cron entry for log rotation (mirrors cac:phpXX).
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
## Inspect without running the image:
## docker buildx imagetools inspect <ref> --format '{{json .Image.Config.Labels}}'
## Values come from CI's repo-index lookup and were asserted against the
## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build).
## Emergency (php.net) builds: see the same LABEL block in Dockerfile.lsphp.
LABEL org.whp.php-version="${PHP_SRC_VERSION:-${EXPECT_PHP_VERSION:-unverified}}" \
org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \
org.whp.lsphp-source="${LSPHP_SOURCE}" \
org.whp.php-flavour="lsphp${PHPVER}"
EXPOSE 80 443 EXPOSE 80 443
## Healthcheck: the entrypoint drops a static /healthz into the customer ## Healthcheck: the entrypoint drops a static /healthz into the customer
+96 -8
View File
@@ -22,6 +22,29 @@
ARG OLS_VERSION=1.8.4 ARG OLS_VERSION=1.8.4
ARG PHPVER=83 ARG PHPVER=83
## ---- stage 0 (EMERGENCY ONLY): lsphp compiled from php.net source -----------
## Idle unless PHP_SRC_VERSION is set: lsphp-src-build.sh then exits at once and
## /lsphp-src-out stays empty, so every normal build is unchanged (the overlay
## steps below are no-ops). Set PHP_SRC_VERSION + PHP_SRC_SHA256 + LSPHP_SOURCE=php.net
## ONLY when a PHP security release is out and the LiteSpeed apt repo still lags
## — CI: .gitea/workflows/lsphp-source-emergency.yaml; runbook:
## docs/lsphp-source-emergency.md. The tarball is sha256- AND OpenPGP-verified
## against keys/php-src/; configure flags are replayed from the packaged lsphp.
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS lsphp-src
ARG PHPVER=83
ARG CACHE_BUST=
ARG EXPECT_LSPHP_PKG=
ARG EXPECT_PHP_VERSION=
ARG PHP_SRC_VERSION=
ARG PHP_SRC_SHA256=
RUN --mount=type=bind,source=scripts,target=/mnt/scripts \
--mount=type=bind,source=keys/php-src,target=/usr/local/share/php-src-keys \
set -e; \
if [ -n "$PHP_SRC_VERSION" ]; then \
install -m 0755 /mnt/scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh; \
fi; \
PHPVER="${PHPVER}" bash /mnt/scripts/lsphp-src-build.sh
## ---- stage 1: build the cac_path_parity extension -------------------------- ## ---- stage 1: build the cac_path_parity extension --------------------------
## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced ## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced
## from RINIT so a customer's .user.ini cannot displace it — see ## from RINIT so a customer's .user.ini cannot displace it — see
@@ -60,9 +83,20 @@ ARG PHPVER=83
## Toolchain layer, deliberately BEFORE the source COPY so editing the extension ## Toolchain layer, deliberately BEFORE the source COPY so editing the extension
## does not re-resolve the PHP version (which is what caused the skew above). ## does not re-resolve the PHP version (which is what caused the skew above).
## Records the exact lsphp version the headers belong to; stage 2 pins to it. ## Records the exact lsphp version the headers belong to; stage 2 pins to it.
##
## lsphp-apt-upgrade.sh (see its header) refuses to run on stale/unreachable
## repo lists and sweeps every lsphp${PHPVER}* package to the repo's current
## release. CACHE_BUST is per-CI-run so this layer — and therefore the recorded
## version stage 2 pins to — is never a cache hit; it is re-declared in stage 2.
## --no-os-upgrade: this stage is thrown away, OS updates would only cost time.
## EXPECT_* (CI's repo-index lookup) are asserted here too, so a mismatch fails
## before the compile rather than after it.
ARG CACHE_BUST=
ARG EXPECT_LSPHP_PKG=
ARG EXPECT_PHP_VERSION=
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
RUN set -e; \ RUN set -e; \
apt-get update; \ /usr/local/sbin/lsphp-apt-upgrade.sh --no-os-upgrade "${PHPVER}" \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential autoconf pkg-config \ build-essential autoconf pkg-config \
lsphp${PHPVER}-dev; \ lsphp${PHPVER}-dev; \
RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \ RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \
@@ -90,6 +124,17 @@ RUN set -e; \
## - a run that executes ZERO tests also exits 0, so assert the summary shows ## - a run that executes ZERO tests also exits 0, so assert the summary shows
## every .phpt in the directory both ran and passed. Same reasoning as the ## every .phpt in the directory both ran and passed. Same reasoning as the
## `lsphp -i` probe below: an assertion that cannot fail is worse than none. ## `lsphp -i` probe below: an assertion that cannot fail is worse than none.
## EMERGENCY PATH: swap the packaged lsphp + its -dev files for the php.net build
## (no-op when stage 0 was idle), so cac_path_parity below compiles against, and
## its .phpt suite runs on, the PHP this image will actually ship.
ARG PHP_SRC_VERSION=
ARG LSPHP_SOURCE=litespeed-apt
RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \
--mount=type=bind,source=scripts,target=/mnt/scripts \
install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \
bash /mnt/scripts/lsphp-src-overlay.sh dev "${PHPVER}" && \
rm -f /usr/local/share/lsphp-src-probe.php
COPY ./ext/cac-path-parity /usr/src/cac-path-parity COPY ./ext/cac-path-parity /usr/src/cac-path-parity
RUN set -e; \ RUN set -e; \
cd /usr/src/cac-path-parity; \ cd /usr/src/cac-path-parity; \
@@ -141,17 +186,29 @@ ENV PHPVER=${PHPVER}
## cache hit, new version => this layer re-runs and lands on the same one. The ## cache hit, new version => this layer re-runs and lands on the same one. The
## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead ## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead
## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05). ## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05).
##
## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh upgrades the Ubuntu OS
## packages and every other lsphp${PHPVER}* package (extensions such as -msgpack
## are NOT dragged along by the -common pin) to the repos' current versions, and
## fails the build if the repo is unreachable. CACHE_BUST makes this layer a
## guaranteed cache miss per CI run; EXPECT_* are CI's repo-index lookup,
## asserted by the script and written into the image labels below.
ARG CACHE_BUST=
ARG EXPECT_LSPHP_PKG=
ARG EXPECT_PHP_VERSION=
COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
RUN set -e; \ RUN set -e; \
V=$(cat /etc/cac-lsphp-build.version); \ V=$(cat /etc/cac-lsphp-build.version); \
apt-get update; \ if ! /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
if ! DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \ ca-certificates \
lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \ lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \
echo "FATAL: lsphp${PHPVER} $V is what cac_path_parity was compiled against," >&2; \ echo "FATAL: lsphp-apt-upgrade.sh failed (reason above). If apt said" >&2; \
echo " but the LiteSpeed repo no longer offers it (it keeps only the" >&2; \ echo " \"Version '$V' ... was not found\" or refused a downgrade: $V is what" >&2; \
echo " current release). The ext-build stage is almost certainly a stale" >&2; \ echo " cac_path_parity was compiled against, but the LiteSpeed repo (which" >&2; \
echo " cache hit — rebuild with --no-cache." >&2; \ echo " keeps only the current release) now offers a newer one — either it" >&2; \
echo " published mid-build or ext-build was a stale cache hit. Re-run the" >&2; \
echo " build (CI) / rebuild with --no-cache (local)." >&2; \
exit 1; \ exit 1; \
fi; \ fi; \
apt-get clean; \ apt-get clean; \
@@ -166,6 +223,18 @@ RUN set -e; \
fi; \ fi; \
echo "runtime lsphp${PHPVER} pinned to $V (the version cac_path_parity was built against)" echo "runtime lsphp${PHPVER} pinned to $V (the version cac_path_parity was built against)"
## EMERGENCY PATH: overlay the php.net lsphp (no-op on normal builds) and prove
## it has the same extension/feature surface as the packaged one just installed
## — see scripts/lsphp-src-overlay.sh. Bind mounts, so neither the staged build
## nor the scripts add a layer to the image.
ARG PHP_SRC_VERSION=
ARG LSPHP_SOURCE=litespeed-apt
RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \
--mount=type=bind,source=scripts,target=/mnt/scripts \
install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \
bash /mnt/scripts/lsphp-src-overlay.sh runtime "${PHPVER}" && \
rm -f /usr/local/share/lsphp-src-probe.php
## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed ## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed
## image — same runtime, same tuning). Scripts layer last (they change most). ## image — same runtime, same tuning). Scripts layer last (they change most).
COPY ./scripts/entrypoint-lsphp.sh \ COPY ./scripts/entrypoint-lsphp.sh \
@@ -178,10 +247,15 @@ RUN chmod +x /scripts/entrypoint-lsphp.sh /scripts/detect-memory-lsphp.sh /scrip
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by ## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed). ## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
RUN bash -c 'set -e; \ RUN bash -c 'set -e; \
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \ SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
mkdir -p "$SCAN_DIR"; \ mkdir -p "$SCAN_DIR"; \
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \ cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
echo "wrote overrides to $SCAN_DIR"' echo "wrote overrides to $SCAN_DIR"'
## Install the cac_path_parity extension into lsphp's own extension_dir and load ## Install the cac_path_parity extension into lsphp's own extension_dir and load
@@ -218,6 +292,20 @@ RUN bash -c 'set -e; \
## default so the runuser exec path can't drop it. ## default so the runuser exec path can't drop it.
ENV LSPHP_ENABLE_USER_INI=on ENV LSPHP_ENABLE_USER_INI=on
## Inspect without running the image:
## docker buildx imagetools inspect <ref> --format '{{json .Image.Config.Labels}}'
## Values come from CI's repo-index lookup and were asserted against the
## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build).
## Emergency (php.net) builds: org.whp.php-version is the php.net version the
## overlay step asserted; org.whp.lsphp-package-version still names the LiteSpeed
## packages underneath (their ini/PECL files are what the image keeps);
## org.whp.lsphp-source says which one is running. /etc/cac-lsphp-source has the
## tarball sha256 + signer.
LABEL org.whp.php-version="${PHP_SRC_VERSION:-${EXPECT_PHP_VERSION:-unverified}}" \
org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \
org.whp.lsphp-source="${LSPHP_SOURCE}" \
org.whp.php-flavour="lsphp${PHPVER}"
EXPOSE 9000 EXPOSE 9000
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping). ## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
+13 -2
View File
@@ -1,10 +1,21 @@
FROM almalinux/10-base FROM almalinux/10-base
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
# GPG-verified against keys/pkg-cache/ either way. Repo files
# are restored at the end of every RUN, so no layer keeps the cache address.
# See scripts/pkg-cache-repos.sh.
ARG PKG_CACHE=
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
# Install Apache and minimal dependencies (no PHP at all) # Install Apache and minimal dependencies (no PHP at all)
RUN dnf install -y \ RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm && \ /usr/local/sbin/pkg-cache-repos.sh release-install epel && \
/usr/local/sbin/pkg-cache-repos.sh rpm && \
dnf update -y && \ dnf update -y && \
dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \ dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
dnf clean all && \ dnf clean all && \
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/* rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
+12 -1
View File
@@ -19,6 +19,15 @@ ARG OLS_VERSION=1.8.4
ARG PHPVER=83 ARG PHPVER=83
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
## OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
## (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
## GPG-verified against keys/pkg-cache/ either way. Repo files
## are restored at the end of every RUN, so no layer keeps the cache address.
## See scripts/pkg-cache-repos.sh.
ARG PKG_CACHE=
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
## Tooling the shared tier needs on top of the base: ## Tooling the shared tier needs on top of the base:
## - inotify-tools: the .htaccess watcher (spec 5.3) ## - inotify-tools: the .htaccess watcher (spec 5.3)
## - gettext-base: envsubst for render-shared-ols-config.sh ## - gettext-base: envsubst for render-shared-ols-config.sh
@@ -28,9 +37,11 @@ FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
## liveness check depends on. Only transitively present via the base image ## 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 ## 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. ## pruned as "unused" and silently break the supervisor's crash detection.
RUN apt-get update && \ RUN /usr/local/sbin/pkg-cache-repos.sh apt && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
inotify-tools gettext-base openssl ca-certificates curl procps && \ inotify-tools gettext-base openssl ca-certificates curl procps && \
/usr/local/sbin/pkg-cache-repos.sh restore && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
+5
View File
@@ -0,0 +1,5 @@
; php2shell mitigation (CVE-2026-109618 / CVE-2026-109619, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp)
; Remove once all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
fatal_error_backtraces = 0
session.upload_progress.enabled = 0
max_input_nesting_level = 16
+139
View File
@@ -0,0 +1,139 @@
# Emergency lsphp from php.net source
Use this only when PHP has published a security release and the LiteSpeed apt
repo (`rpms.litespeedtech.com/debian`, noble) still ships the vulnerable lsphp.
The emergency build replaces the PHP inside `cac-lsphp` and `cac-litespeed` with
the same PHP version compiled from the php.net tarball. Everything else in the
image stays as LiteSpeed packages it. The build fails unless the result offers
the same support as the packaged lsphp.
This path was first built for php2shell (CVE-2026-109618/-109619, 2026-10-21):
php.net released 8.2.35, 8.3.36, 8.4.27 and 8.5.12, and LiteSpeed was still on
8.2.34, 8.3.35, 8.4.26 and 8.5.11.
## Trigger
```bash
# 1. Staging tags only (:phpNN-phpnet-X.Y.Z). Test these on .148 first.
curl -fsS -X POST -H "Authorization: token $TEA_TOKEN" -H 'Content-Type: application/json' \
https://repo.anhonesthost.net/api/v1/repos/cloud-hosting-platform/cloud-apache-container/actions/workflows/lsphp-source-emergency.yaml/dispatches \
-d '{"ref":"trunk","inputs":{"php_versions":"82=8.2.35 83=8.3.36 84=8.4.27 85=8.5.12","images":"both","push_production_tags":"false"}}'
# 2. Production tags (:phpNN, :latest for 85). WHP pulls these on recreate.
# The JSON is the same as step 1 except push_production_tags is "true".
```
Inputs:
- `php_versions`: space-separated `NN=X.Y.Z` pairs, with NN between 81 and 85.
- `images`: `both`, `lsphp` or `litespeed`.
- `push_production_tags`: `true` or `false`.
The job for a given PHP version refuses to run if php.net has no such release.
It also refuses if LiteSpeed already ships that version or a newer one; in that
case, run the normal build instead.
After pushing, check the labels on the pushed image:
```bash
docker buildx imagetools inspect repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php85 \
--format '{{json .Image.Config.Labels}}'
# org.whp.lsphp-source = php.net, org.whp.php-version = 8.5.12
```
Inside the container, `/etc/cac-lsphp-source` records the tarball sha256, the
signer fingerprint and the packaged release that was replaced.
Running containers do not change until they are recreated. Follow the normal
deploy order: test.148, whp02, sdbees, then whp01.
## Getting back to the packaged lsphp
When LiteSpeed publishes the fixed lsphp, run the normal `build-push.yaml`
build, either with a push to trunk or a dispatch. It overwrites `:phpNN` with
the packaged build once the packaged version is the same as or newer than the
php.net version.
Until that happens, the normal build refuses to overwrite a newer php.net image.
It fails at the "Do not roll back a newer php.net emergency image" step. This is
intentional, so a routine push during the emergency does not put the vulnerable
PHP back. That step lets the build through if it cannot inspect the published
tag.
## Local build
```bash
docker build -f Dockerfile.lsphp --build-arg PHPVER=85 --build-arg OLS_VERSION=1.8.4 \
--build-arg CACHE_BUST=$(date +%s) \
--build-arg PHP_SRC_VERSION=8.5.12 \
--build-arg PHP_SRC_SHA256=<sha256 of php-8.5.12.tar.xz from https://www.php.net/releases/index.php?json&version=8.5.12> \
--build-arg LSPHP_SOURCE=php.net -t localhost/cac-lsphp-src:php85-test .
```
The same build args work with `Dockerfile.litespeed`. Without
`PHP_SRC_VERSION`, both Dockerfiles build exactly as before: the `lsphp-src`
stage and the overlay steps do nothing.
## What the build does and what makes it fail
The build has four parts.
1. The `lsphp-src` stage runs `scripts/lsphp-src-build.sh`, FROM the same
`litespeedtech/openlitespeed` base as the shipped image.
2. In `Dockerfile.lsphp`, the ext-build stage runs `scripts/lsphp-src-overlay.sh dev`.
This builds `cac_path_parity` and runs its `.phpt` suite against the php.net
PHP.
3. The shipped stage runs `scripts/lsphp-src-overlay.sh runtime`.
4. The labels record the source: `org.whp.php-version` is the php.net version
and `org.whp.lsphp-source` is `php.net`. `org.whp.lsphp-package-version`
still names the LiteSpeed packages underneath.
Each of the following fails the build:
| Check | Fails when |
|---|---|
| Tarball sha256 | It does not match `PHP_SRC_SHA256`, which CI takes from the php.net releases JSON. |
| OpenPGP signature | `gpgv` rejects the `.asc` against `keys/php-src/php-keyring.asc`, or the signer's primary fingerprint is not listed for that branch in `keys/php-src/release-managers.txt`. |
| Version sanity | The version is not X.Y.Z, is not in the PHPVER branch, or is not newer than the packaged version. |
| Configure | Replaying the packaged lsphp's own `Configure Command` fails. The usual cause is a `-dev` package missing from `DEVPKGS`. |
| LSAPI SAPI | `php-litespeed-8.3.tgz` does not match its pinned sha256, or the built lsphp's set of `LSAPI_*` names differs from the packaged binary's. |
| Compiled-in ini defaults | Any `php -n` ini default differs from the packaged binary. Directives that are new in the release are allowed and printed. |
| ABI gate | A struct member's offset or size changed, or a non-`*_globals` struct changed size. See `scripts/lsphp-abi-gate.sh`. |
| File replacement | A php-src ELF file owned by a package at the core version has no php.net counterpart. |
| Same-support gate (both SAPIs) | Loaded extensions differ, a PECL or Zend extension changes version, a php-src extension is not at the new version, an effective ini value changes or disappears, or a gd/curl/openssl/icu/imagick/redis/memcached/password/hash/stream feature line changes. |
| Build-env consistency | `Configure Command` differs beyond CFLAGS, an ELF file has an unresolved library, or the packaged release in the builder stage differs from the one in the shipped stage. |
## Differences from the packaged build
These were measured on 2026-10-21 for php85 and php82 (`cac-lsphp`) and php83
(`cac-litespeed`):
- Version strings, build date and build host.
- `memory_hard_limit_percent` exists. The php2shell release added this ini
directive.
- The bundled Olson timezone DB is 2026.5 instead of 2026.3 for 8.5. It comes
from the newer tarball.
- LDAP "Vendor Version" is 20610 instead of 20607. This is the version of the
OpenLDAP headers it was compiled against; noble's libldap-dev is newer now.
The runtime library is the same Ubuntu libldap.
- CFLAGS has no `-ffile-prefix-map` or `-fdebug-prefix-map` pointing at
LiteSpeed's build directory.
- One more `_chk` (FORTIFY) symbol is used. Shared extensions get BIND_NOW,
which is stricter than the packaged build.
- The image is about 100 MB larger. The overlay layer holds a second copy of
lsphp, php and php8.M.
## Maintenance: what must stay in sync
| Item | When it needs attention | How you find out |
|---|---|---|
| `keys/php-src/release-managers.txt` and `php-keyring.asc` | A new release manager, or a new PHP branch (8.6). | The build fails because the signer is not pinned or there is no key. Refresh from <https://www.php.net/gpg-keys.php> and <https://www.php.net/distributions/php-keyring.gpg>. |
| `keys/php-src/litespeed-sapi.txt` | LiteSpeed publishes a new php-litespeed release and builds lsphp with it. | The build fails on the sha256 mismatch or on the LSAPI name-set difference. Re-measure the packaged binary, then re-pin. |
| `DEVPKGS` in `lsphp-src-build.sh` | LiteSpeed adds a configure flag that needs a new `-dev` library. | configure fails and prints the error. |
| session.save_path patch (step 2c) | LiteSpeed patches some other compiled-in default. | The compiled-in ini defaults check fails and shows a diff. |
| PECL reuse | A PHP patch release changes a struct layout. | The ABI gate fails. PECL extensions would then have to be rebuilt from source, which this path does not do. Without that, the emergency path cannot be used for that release. |
| 8.1 | php.net no longer publishes 8.1 (EOL). | The workflow refuses because there is no release. |
No configure flags, extension lists or PECL versions are kept in this repo.
They are read from the packaged lsphp in the same build, so they cannot go out
of date unnoticed.
+208
View File
@@ -0,0 +1,208 @@
# Internal package + image cache (`PKG_CACHE`)
Opt-in build support for the LAN package cache **CT130 `pkg-cache` at `https://192.168.1.40`** (Proxmox node `pve`, storage `shared-storage-tn`).
Without `PKG_CACHE`, builds use the upstream repositories. The one change from before is that the EPEL and Remi release RPMs are now **GPG-verified** against `keys/pkg-cache/` before they are installed.
Why it exists: during the 2026-10-21 PHP security release, builds failed because:
- Remi mirrors lagged hours behind origin;
- Remi's origin reset or timed out HTTPS under load;
- an EPEL mirror had a broken certificate.
On that day a build without the cache spent **1031 s** in its first dnf step. Through the cache the same step took **40 s cold and 21 s warm**.
## What the cache serves
| URL (https, pkg-cache CA) | Upstream tiers, tried in order (stale copy last) |
|---|---|
| `https://192.168.1.40/remi/` | `https://rpms.remirepo.net`, then `http://rpms.remirepo.net` (origin only, no mirrors) |
| `https://192.168.1.40/epel/` | `https://dl.fedoraproject.org/pub/epel`, `https://mirror.math.princeton.edu/pub/epel`, `https://mirrors.kernel.org/fedora-epel` |
| `https://192.168.1.40/almalinux/` | `https://repo.almalinux.org/almalinux`, `https://mirror.math.princeton.edu/pub/almalinux` |
| `https://192.168.1.40/ubuntu/` | `https://archive.ubuntu.com/ubuntu`, `https://us.archive.ubuntu.com/ubuntu` |
| `https://192.168.1.40/ubuntu-security/` | `https://security.ubuntu.com/ubuntu`, `https://archive.ubuntu.com/ubuntu` |
| `https://192.168.1.40/litespeed/` | `https://rpms.litespeedtech.com`, then `http://rpms.litespeedtech.com` |
| `192.168.1.40:5000` (TLS, pkg-cache CA) | Docker Hub pull-through cache (`registry-1.docker.io`) |
Plain `http://192.168.1.40/` answers **403** to LAN clients.
Cache rules:
- **Metadata** has a 3-minute TTL. After that it is revalidated upstream (`If-Modified-Since`). Metadata means:
- `repomd.xml*`
- repodata files whose names don't start with a hash
- apt `dists/`, except `by-hash/`
- `*-latest*.rpm` and `remi-release-N.rpm`
- `RPM-GPG-KEY*`
- directory listings
- **Packages** (`*.rpm`, `*.deb`, hash-named repodata, `by-hash/`) are cached for 30 days and evicted after 60 days unused. Total size is capped at 110 GB, with 15 GB kept free.
- On a connect error, timeout or 5xx, the request falls through to the next tier. Only the **last** tier may answer from a stale copy, so the order is primary, then fallback, then stale.
- `proxy_cache_lock` is on.
- Responses carry `X-Cache-Status` and `X-Pkgcache-Tier`.
## Building with the cache
```bash
docker buildx build --build-arg PKG_CACHE=https://192.168.1.40 --build-arg PHPVER=85 -f Dockerfile .
```
`scripts/pkg-cache-repos.sh` (installed as `/usr/local/sbin/`) and `keys/pkg-cache/` (installed as `/usr/local/share/pkg-cache/`) are wired into `Dockerfile`, `Dockerfile.fpm`, `Dockerfile.shared-httpd` and `Dockerfile.shared-ols`. The script has four modes.
**`rpm`** handles AlmaLinux, EPEL and Remi:
- Each `baseurl` becomes `<cache>/<prefix>/... <origin>/...`. dnf fails over to the origin if the cache dies.
- That section's mirrorlist/metalink is commented out.
- The section gets `sslcacert=` pointing at a bundle of the system CAs plus the pkg-cache CA. The bundle lives in the state dir and is deleted by `restore`.
- It also gets `gpgcheck=1`.
- AlmaLinux and Remi also get `repo_gpgcheck=1`, because both sign `repomd.xml` (measured 2026-10-21: `repomd.xml.asc` returns 200). EPEL publishes no repomd signature (404), so its metadata integrity rests on HTTPS end to end.
**`apt`** handles Ubuntu archive/security and LiteSpeed:
- Their URIs are rewritten to the cache.
- `/etc/apt/apt.conf.d/99pkg-cache` sets `Acquire::https::192.168.1.40::CaInfo`, so the CA is trusted for the cache host only.
- apt verifies the signed `InRelease`/`Release.gpg` as always. It has no failover, so CI must probe the cache first.
**`release-install epel [remi]`**:
- Downloads the release RPM over HTTPS, from the cache or from the origin.
- Checks the package name.
- Imports the key committed in `keys/pkg-cache/`.
- Requires `rpm -K` to report `digests signatures OK`.
- Installs with `--setopt=localpkg_gpgcheck=1`.
**`restore`**:
- Puts rewritten files back.
- Deletes files the script created.
- Deletes any repo/apt file that **appeared during the RUN** and still references the cache.
- Then **fails the build** if any file under `/etc/yum.repos.d`, `/etc/dnf` or `/etc/apt` still mentions the cache.
Every RUN that rewrites ends with `restore`, so no shipped layer carries the LAN address. This was verified on the built images: 0 references, no state dir, no apt drop-in.
`PKG_CACHE` must start with `https://`. `http://` is refused.
`Dockerfile.litespeed` and `Dockerfile.lsphp` are **not wired yet**. Two things are needed first:
- the multi-stage `lsphp-apt-upgrade.sh` flow needs a `PKG_CACHE` ARG in each stage;
- its madison check must accept the cache URL. It currently requires `*rpms.litespeedtech.com*`, and via the cache madison shows `https://192.168.1.40/litespeed/debian`.
## Trust model
`keys/pkg-cache/ca.crt` is the *pkg-cache internal CA*:
- EC P-256, valid to 2036.
- **Name-constrained** to IP `192.168.1.40` and DNS `pkg-cache.internal.cloud-hosting.io` / `pkg-cache.geekhome.local`. Even its private key, which exists only on CT130 at `/etc/pkgcache-ca/ca.key`, cannot mint a certificate that clients would accept for any other host.
- It is trusted only by the rewritten repos, the apt per-host option and the BuildKit/dockerd registry config. It is never added to a system trust store.
The server certificate `/etc/pkgcache-ca/server.crt` **expires 2029-10-20**. `pkgcache-stats` prints the expiry. Renew it with the same `openssl x509 -req ... -extfile srv.cnf` steps in `/etc/pkgcache-ca/`.
Remaining risks:
- **Replay of old but validly signed metadata.** Signatures stop tampering, not freshness. The cache's own plain-http tiers for Remi and LiteSpeed are the leg where an internet MITM could replay. The php.net gates (`EXPECT_PHP_MIN`, `EXPECT_LSPHP_PKG`/`EXPECT_PHP_VERSION`) fail an image build that gets old PHP. **Josh to decide** whether to drop those http tiers: you gain replay resistance and lose availability when Remi's HTTPS is down, as it was all day on 2026-10-21.
- **Proposed, not implemented:** the same kind of minimum-version gate for other security-relevant packages:
- CI resolves the newest `openssl`/`openssl-libs` and `httpd` from AlmaLinux BaseOS/AppStream, and `ImageMagick` from EPEL, **directly from the origin over HTTPS** (e.g. `dnf repoquery --repofrompath=o,https://repo.almalinux.org/almalinux/10/BaseOS/x86_64/os/ --repo=o --latest-limit=1 --qf '%{evr}' openssl-libs`).
- It passes the results as `EXPECT_OPENSSL_MIN` / `EXPECT_HTTPD_MIN` / `EXPECT_IMAGEMAGICK_MIN`.
- The Dockerfile asserts `rpm -q --qf '%{EVR}'` is not older (`rpm --eval "%{lua: print(rpm.vercmp('A','B'))}"`).
## CI integration (APPLIED 2026-10-21)
1. **Runner network.** Ephemeral runners are NATed and `iptables FORWARD` drops
all RFC1918 destinations:
- `eph-pve-*` on `pve`, vmbr2, 10.99.98.0/24;
- `eph-pve3-*` on `pve3`, vmbr1, 10.99.99.0/24.
One rule per node now sits **above** those DROPs. It is persistent through a
`post-up`/`post-down` pair placed before the first DROP line in
`/etc/network/interfaces.d/vmbr2-ci-isolated` (pve) and
`/etc/network/interfaces.d/vmbr1-ci-isolated` (pve3). The Proxmox firewall is
disabled on both nodes, so its reloads do not touch these chains. Backups:
`/root/vmbr{2,1}-ci-isolated.bak-pkgcache-20261021`.
```
iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40/32 -p tcp -m multiport --dports 443,5000 \
-m comment --comment pkg-cache-ct130 -j ACCEPT
```
Verified from throwaway CTs on vmbr2 and vmbr1:
- :443 and :5000 work with TLS verified;
- CT130 :22/:80, pve :8006, .193 :22, the router's :53 and 10.10.14.20 stay **blocked**;
- internet access is unchanged.
The LAN runner VM *Git-Builders* (VM103, 192.168.1.126 on vmbr0) needs no
rule. `gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which
is unroutable from both the LAN and the DC (stale record?).
2. **Workflow** (`.gitea/workflows/build-push.yaml`). Jobs Build-and-Push,
Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS run a
**"Probe package cache"** step. It curls `https://192.168.1.40/` and
`https://192.168.1.40:5000/v2/` with `--cacert keys/pkg-cache/ca.crt
--max-time 5`.
- If both answer: `PKG_CACHE=https://192.168.1.40` is passed as a build arg,
and buildx is set up with the Docker Hub mirror (`buildkitd-config-inline`,
`ca=`).
- If either fails: that output is empty, the build uses upstream repos /
Docker Hub, and a `::warning::` is emitted.
Litespeed and lsphp jobs are unchanged (their Dockerfiles are not wired).
**Fail-back measured** with the workflow's own step code, from a runner-bridge CT:
| cache state | probe result | time |
|---|---|---|
| up | url + mirror set | <1 s |
| nginx + registry stopped | both empty, warning | <1 s (connection refused) |
| silently dropped | both empty, warning | 10 s (2 x 5 s) |
Mid-build cache loss is also covered: dnf has the origin as a 2nd baseurl, and
BuildKit falls back from the mirror to Docker Hub (measured with the registry stopped).
3. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart.
Use TLS, no `insecure-registries`:
```
install -D -m0644 keys/pkg-cache/ca.crt /etc/docker/certs.d/192.168.1.40:5000/ca.crt
# /etc/docker/daemon.json: { "registry-mirrors": ["https://192.168.1.40:5000"] } then restart docker
```
## WHP hosts (`scripts/host/pkgcache-host-repos.sh`)
`pkgcache-host-repos.sh apply https://192.168.1.40` does the following:
- backs up every `/etc/yum.repos.d/*.repo` to `/root/pkgcache-repo-backup-<UTC>/`;
- installs the name-constrained CA as a system anchor;
- rewrites the AlmaLinux, EPEL and Remi sections to `baseurl=<cache>/... <origin>/...`;
- comments out mirrorlist/metalink in those sections;
- sets `gpgcheck=1`, plus `repo_gpgcheck=1` for AlmaLinux and Remi.
`revert [DIR]` restores byte-identical files (verified on AL9 and AL10) and
removes the anchor. `status` shows the current state.
**Why not "cache first, mirrorlist as fallback":** measured on AL10, with
mirrorlist/metalink left in place dnf **ignores `baseurl` completely** (0 cache
requests). The origin as second baseurl is the fallback dnf really performs.
**Measured on 192.168.1.148** (AlmaLinux 10.2), 2026-10-21:
| test | result |
|---|---|
| baseline `dnf makecache --refresh` (original mirrorlists) | rc=0, **1209 s** |
| via cache `makecache --refresh` | rc=0, **22 s** (2nd run, non-interactive: 2 s) |
| via cache `dnf download php-cli` | rc=0, 1 s |
| `dnf -y --downloadonly update php-cli` | "Nothing to do" (already 8.5.12) |
| cache **down** (nginx stopped): makecache --refresh / download | rc=0 **16 s** / rc=0 24 s, served by origin |
| cache **silently dropped** (iptables DROP): makecache --refresh / download | rc=0 **225 s** / rc=0 59 s (dnf's 30 s connect timeout per attempt) |
**Datacenter hosts** (whp01, whp02, sdbees, TrueSelfCA) cannot reach
192.168.1.40 and are **not configured**. That needs CT130 on NetBird (see the
report: setup key / route pending).
## Operating the cache (CT130)
- **nginx config.**
- `/etc/nginx/conf.d/pkgcache-http.conf` holds the cache zone, maps and log format.
- `/etc/nginx/pkgcache/{server-common,meta,pkg}.conf` hold the shared settings.
- `/etc/nginx/sites-available/pkgcache` is **generated** by `/usr/local/sbin/pkgcache-gen-nginx`. Edit the `UPSTREAMS` table, then run `pkgcache-gen-nginx > /etc/nginx/sites-available/pkgcache && nginx -t && systemctl reload nginx`.
- Listeners: :443 TLS for the LAN, `127.0.0.1:80` for local tools, and :80 for the LAN answers 403.
- **Registry.** Config is `/etc/docker/registry/config.yml` (Debian `docker-registry` 2.8.3, the same code as `registry:2`, with TLS). Service `docker-registry`.
- **TLS.** Everything is in `/etc/pkgcache-ca/`: `ca.key` (0600), `ca.crt`, `server.key`, `server-chain.crt`, and the `ca.cnf`/`srv.cnf` openssl configs.
- **Data.** nginx uses `/srv/cache/nginx`, the registry uses `/srv/cache/registry`. Both live on the 150 GB mount point `mp0` (`backup=0`).
- **Security-release day.** `pkgcache-refresh` re-fetches every cached metadata entry right away. Variants:
- `pkgcache-refresh remi` refreshes one prefix;
- `--list` lists the entries;
- `--purge` deletes them instead.
A refresh bypasses the cache from localhost only and replaces each entry in place. If upstream is down, the old copy stays. Metadata is at most 3 minutes old anyway.
- **Monitoring.** `pkgcache-stats` shows:
- disk usage;
- hit ratio;
- fallback-tier use;
- the STALE count;
- upstream errors;
- certificate expiry.
Logs are `/var/log/nginx/pkgcache.access.log` (fields `cache=`, `tier=`, `up=`) and `journalctl -u docker-registry`. Suggested Uptime Kuma checks are `https://192.168.1.40/` and `https://192.168.1.40:5000/v2/`, with certificate checking against the CA.
- **Docker Hub rate limits.** Anonymous pulls are limited per egress IP, and all clients share it. Add `username`/`password` (a Hub access token) under `proxy:` if needed.
+13
View File
@@ -0,0 +1,13 @@
# LiteSpeed's own LSAPI SAPI source, which the LiteSpeed lsphp packages compile
# INSTEAD of php-src's bundled sapi/litespeed (measured 2026-10-21 on packaged
# lsphp81..85: the binaries carry LSAPI_MAX_CMD_SCRIPT_PATH_LEN, report
# "Copyright (c) 1997-2016" and phpversion('litespeed') = '' — all three are
# php-litespeed-8.3, none is php-src 8.5.12's sapi/litespeed).
# LiteSpeed publishes no signature for it; this sha256 was taken on 2026-10-21
# (Last-Modified: Sun, 07 Sep 2025 20:35:32 GMT) and lsphp-src-build.sh refuses
# any other file. lsphp-src-build.sh additionally checks that the resulting
# binary has the same LSAPI_* feature/env-name set as the packaged lsphp, so a
# LiteSpeed SAPI upgrade we have not pinned yet fails the build instead of
# shipping an older SAPI.
# <url> <sha256>
https://www.litespeedtech.com/packages/lsapi/php-litespeed-8.3.tgz cd616b82cc853e5e3187fccd060228b9cd1a2009ce72d8da5950eef950ac29b1
+804
View File
@@ -0,0 +1,804 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=SWGM
-----END PGP PUBLIC KEY BLOCK-----
+22
View File
@@ -0,0 +1,22 @@
# PHP release-manager signing keys, per branch: <branch> <PRIMARY fingerprint> <who>
# Transcribed from https://www.php.net/gpg-keys.php (2026-10-21). The key material
# is php-keyring.asc in this directory, the official keyring from
# https://www.php.net/distributions/php-keyring.gpg (saved unmodified; it is ASCII-armoured despite its .gpg name).
# lsphp-src-build.sh accepts a tarball only if gpgv validates its .asc against
# php-keyring.asc AND the signing key's PRIMARY fingerprint is listed here for
# the tarball's branch. A new release manager = a reviewed commit to both files.
8.1 528995BFEDFBA7191D46839EF9BA0ADA31CBD89E Joe Watkins
8.1 39B641343D8C104B2B146DC3F9C39DC0B9698544 Ben Ramsey
8.1 F1F692238FBC1666E5A5CCD4199F9DFEF6FFBAFD Patrick Allaert
8.2 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron
8.2 39B641343D8C104B2B146DC3F9C39DC0B9698544 Ben Ramsey
8.2 E60913E4DF209907D8E30D96659A97C9CF2A795A Sergey Panteleev
8.3 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron
8.3 AFD8691FDAEDF03BDF6E460563F15A9B715376CA Eric A Mann
8.3 C28D937575603EB4ABB725861C0779DC5C0A9DE4 Jakub Zelenka
8.4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA Eric A Mann
8.4 9D7F99A0CB8F05C8A6958D6256A97AF7600A39A6 Calvin Buckley
8.4 0616E93D95AF471243E26761770426E17EBBB3DD Saki Takamachi
8.5 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron
8.5 49D9AF6BC72A80D6691719C8AA23F5BE9C7097D4 Volker Dusch
8.5 D95C03BC702BE9515344AE3374E44BC9067701A5 Daniel Scherzer
+20
View File
@@ -0,0 +1,20 @@
Trust material for the OPT-IN internal package cache (scripts/pkg-cache-repos.sh,
docs/pkg-cache.md). Copied into images at /usr/local/share/pkg-cache/; nothing
here is added to the system trust store.
ca.crt "pkg-cache internal CA (CT130)", EC P-256, valid to 2036-10-18.
NAME-CONSTRAINED to IP 192.168.1.40 and DNS
pkg-cache.internal.cloud-hosting.io / pkg-cache.geekhome.local.
SHA-256 fingerprint BB:5B:9A:08:05:ED:2F:58:83:92:D0:99:9D:11:0A:30:14:88:68:F0:F0:88:65:5E:94:0B:75:EB:C2:A2:BD:F3
(private key lives only on CT130: /etc/pkgcache-ca/ca.key)
RPM-GPG-KEY-EPEL-10 Fedora (epel10) 7D8D 15CB FC4E 6268 8591 FB26 33D9 8517 E37E D158
fetched 2026-10-21 over HTTPS from dl.fedoraproject.org and
mirror.math.princeton.edu (identical); fingerprint matches
https://fedoraproject.org/security/
RPM-GPG-KEY-remi2024 Remi's RPM repository 2024 CF1D F005 7CE8 5DFF 5B2F 2A37 C2FD 3B2C 2A09 48E4
fetched 2026-10-21 over HTTPS from rpms.remirepo.net; byte-identical
to the key shipped in remi-release-10.2-1.el10.remi. Not found on
keys.openpgp.org / keyserver.ubuntu.com - confirm out of band.
Remi rotates keys (remi2025, ...): when remi-release starts being
signed by a new key, `release-install remi` fails loudly - add the
new key here (and to pkg-cache-repos.sh).
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQINBGV4X6kBEAC3eQxgiWuo08uc3mHo4ELux++uqTnYz/tJzEf9Ou3h36WnhumA
Nvs+Ts5h8PBx879Y9/aIX1Z20p1kf6tBCinZnEJu59n+TAAsph0+XQlV1l5YkleK
Z2ff/Fg65k8QcLXWaIGykA/FaKznRiSurGuD6tRGhJw7DawEwBJr8QZSkRUpnH1L
URW97Q/iKrRPiE5VEayE0y8eAL28jIIiFvR+4oJMzvCsRRB/2wYZ2MlJOW91hcYf
mbUoXKOBD5UzsJylu7kj25K/ge8rEJ7KicOOwcdYddxsU3DxGSSfwF8AMagENcm2
XROeXknjm84A8sNlUkFZBJwfuc7eRTiZGJrnQQVYLrkKj8Mxpq9Ts7hU51TqAWNI
uvGDlJdYNE3D2RMqjMEsZ8ej08Thrib6xslu4NzTBkt+6QNnXL4E3hEgYtoyio60
GswSz2ulogKg7X4JrNdJYE8/qNowyF3hoVgj5TG1/wQRq+5HlMMOLjgGu9wzLUix
fnVfEUnzaofbrUf4/GabCaeY8xRe4tFQrvzigQ4g+kgwKKnfAeqBmPov0yljkw9z
BYJWR5zvaw0ffg9Ing00KUSaXBXA5jSlgk1603Y+LefY1SlXsTyqohiRvGH6FI77
HNMo72DwoJfFcYjncZUzKgXWJECR4nhVsdj6pKoOjcQ4aSuyVxtsR86ASQARAQAB
tChGZWRvcmEgKGVwZWwxMCkgPGVwZWxAZmVkb3JhcHJvamVjdC5vcmc+iQJOBBMB
CAA4FiEEfY0Vy/xOYmiFkfsmM9mFF+N+0VgFAmV4X6kCGw8FCwkIBwIGFQoJCAsC
BBYCAwECHgECF4AACgkQM9mFF+N+0Vhv/A/+PlhPLSctGRCUEahE+cN4764Acc3p
l40ZYzXRhqR0/Tc1/cSDjlA3qVTc8SPohi5OJXwCyr9EiMqKoyoDN097euqbYpyp
yN/Pj0lBjsXwcpdDtZ21WGeQU0Khb04N68bMtJbDaxeBciTvDDQravZuPPh0m4Rg
Z6myEoa6Aa6EK0hI1Qwi1qIWeRiuEkVT671IaKVETBW5XiUpNBXDAB/L+6DzUF9u
scBzfsUDiPO6NrpYDtV3jwq22y6gWluIct/Ka8brwPbqK2sBfFzrHboRhfqlTGjs
7F9qUGwIQZn/A8iozXZYQ0+JG1bhQyvjA8eN1GOcRpT+O7H7JXN49o6IG2As4+iK
F04+qjqAu2sVfpD8mzM2VubFNllcKKiyCzRYHhSbObRCPzsudDL9GPiXeGGaCuWg
sDkiA1MESvf2tLETAGBs/TziO4GwmXUtlKbRiq1FYm90mVq9mBxPZ/Idn+yZusNB
0O5SXIbI8lYZw5n4XTK4b+byHRBYsOTHiTsGvjTF2Y7oSwW2CVUmL6RZ23mI4qoY
1p5kzRS+GjT1acnTei/FTsOlIKCsjfeHx7uxCkX6xpAD8P3UtLQqfsgH0CL4vSZt
TGO6L1InQlp4ZG3OYIomTKbD3/R0wod3U3dTqdulQMXL895u6OLTY3spY2m2MO2k
p9Dfd2pKuxK9Mys=
=mhQZ
-----END PGP PUBLIC KEY BLOCK-----
+52
View File
@@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=meaU
-----END PGP PUBLIC KEY BLOCK-----
+15
View File
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICPzCCAeagAwIBAgIUJI0QJZ+PCrSQ/nKZFENwwcp8Zr0wCgYIKoZIzj0EAwIw
TDEmMCQGA1UEAwwdcGtnLWNhY2hlIGludGVybmFsIENBIChDVDEzMCkxIjAgBgNV
BAoMGWNsb3VkLWhvc3RpbmcuaW8gaW50ZXJuYWwwHhcNMjYxMDIxMTkzNjIyWhcN
MzYxMDE4MTkzNjIyWjBMMSYwJAYDVQQDDB1wa2ctY2FjaGUgaW50ZXJuYWwgQ0Eg
KENUMTMwKTEiMCAGA1UECgwZY2xvdWQtaG9zdGluZy5pbyBpbnRlcm5hbDBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABBJh75sC5vSzTnkYl0CYYEA/TKUHEXhbtanV
OVNW4NlD3za5+bu5YuKjpbFAva4TCZqWsaS9IMd/JfsaFbf5XVejgaUwgaIwEgYD
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP7UCTf/
v7KU1KBrs1vqa71/TZl0MF0GA1UdHgEB/wRTMFGgTzAKhwjAqAEo/////zAlgiNw
a2ctY2FjaGUuaW50ZXJuYWwuY2xvdWQtaG9zdGluZy5pbzAaghhwa2ctY2FjaGUu
Z2Vla2hvbWUubG9jYWwwCgYIKoZIzj0EAwIDRwAwRAIgOod4wPHu0kAzYAw8KeGg
cIrFMwQ4ATKVTkDioiSFOqICIEhY+sGj/YZ4oBMst3b/AAz9zMEJAXpnCkSTI6tM
YAIl
-----END CERTIFICATE-----
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
## pkgcache-host-repos.sh — point a WHP HOST's dnf repos (AlmaLinux 9/10 BaseOS/
## AppStream/CRB/extras/..., EPEL, Remi) at the internal package cache, with the
## ORIGIN as automatic fallback. Not used by image builds (see pkg-cache-repos.sh).
##
## pkgcache-host-repos.sh apply https://192.168.1.40 # backs up, then rewrites
## pkgcache-host-repos.sh status
## pkgcache-host-repos.sh revert [BACKUP_DIR] # default: newest backup
##
## Fallback design (measured 2026-10-21 on AlmaLinux 10.2):
## * each rewritten section gets baseurl=<cache>/<prefix>/<path> <origin>/<path>
## — dnf/librepo treats the second URL as a failover mirror.
## * the section's mirrorlist=/metalink= is COMMENTED OUT. Measured: with a
## mirrorlist/metalink present dnf ignores baseurl entirely (0 cache requests),
## so "cache first, mirrorlist as fallback" is not something dnf can do.
## * gpgcheck=1 everywhere; repo_gpgcheck=1 for AlmaLinux + Remi (both sign
## repomd.xml); EPEL has no repomd signature and relies on HTTPS end to end.
## * trust: the name-constrained pkg-cache CA is installed as a system anchor
## (/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt). Its nameConstraints
## (IP 192.168.1.40, pkg-cache.* DNS) are enforced by OpenSSL — a cert it
## signs for any other name fails ("permitted subtree violation").
## Every changed file is copied to /root/pkgcache-repo-backup-<UTC>/ first.
set -euo pipefail
MODE="${1:-status}"
CA_SRC="$(dirname "$0")/pkg-cache-ca.crt"
ANCHOR=/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt
die() { echo "FATAL(pkgcache-host-repos): $*" >&2; exit 1; }
same() { [ "$(sha256sum < "$1")" = "$(sha256sum < "$2")" ]; } # cmp/diffutils is not on minimal hosts
rewrite() { # $1 file $2 cache url
python3 - "$1" "$2" <<'PY'
import re, sys
f, C = sys.argv[1], sys.argv[2].rstrip('/')
maps = [ # regex on a (commented or live) baseurl, cache prefix, origin base, repomd signed?
(r'^#?\s*baseurl\s*=\s*https?://repo\.almalinux\.org/almalinux/(\S+)', 'almalinux', 'https://repo.almalinux.org/almalinux/', True),
(r'^#?\s*baseurl\s*=\s*https?://download\.example/pub/epel/(\S+)', 'epel', 'https://dl.fedoraproject.org/pub/epel/', False),
(r'^#?\s*baseurl\s*=\s*https?://rpms\.remirepo\.net/(\S+)', 'remi', 'https://rpms.remirepo.net/', True),
]
out, sec = [], []
state = {'hit': False, 'sig': False}
def flush():
for x in sec:
if state['hit']:
if re.match(r'^(mirrorlist|metalink)\s*=', x):
x = '#' + x
elif re.match(r'^(gpgcheck|repo_gpgcheck)\s*=', x):
continue
out.append(x)
if state['hit']:
# keep blank-line layout: insert before trailing blanks
tail = []
while out and out[-1].strip() == '':
tail.append(out.pop())
out.append('gpgcheck=1')
if state['sig']:
out.append('repo_gpgcheck=1')
out.extend(tail)
sec.clear(); state['hit'] = state['sig'] = False
for line in open(f).read().split('\n'):
if line.startswith('['):
flush()
for rx, prefix, origin, sig in maps:
m = re.match(rx, line)
if m:
p = m.group(1)
line = f'baseurl={C}/{prefix}/{p} {origin}{p}'
state['hit'] = True; state['sig'] = state['sig'] or sig
break
sec.append(line)
flush()
open(f, 'w').write('\n'.join(out))
PY
}
case "$MODE" in
apply)
C="${2:?usage: apply https://<cache>}"; C="${C%/}"
case "$C" in https://*) ;; *) die "cache URL must be https://" ;; esac
[ -s "$CA_SRC" ] || die "missing $CA_SRC (copy keys/pkg-cache/ca.crt next to this script)"
B=/root/pkgcache-repo-backup-$(date -u +%Y%m%dT%H%M%SZ)
mkdir -p "$B/yum.repos.d"
cp -a /etc/yum.repos.d/*.repo "$B/yum.repos.d/"
[ -e "$ANCHOR" ] && cp -a "$ANCHOR" "$B/" || touch "$B/.no-anchor-before"
install -m 0644 "$CA_SRC" "$ANCHOR" && update-ca-trust
for f in /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/epel*.repo /etc/yum.repos.d/remi*.repo; do
[ -f "$f" ] || continue
grep -q -E '^#?\s*baseurl\s*=\s*https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" && rewrite "$f" "$C"
done
echo "backup: $B"
echo "changed files:"; for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || echo " /etc/yum.repos.d/$(basename "$f")"; done
;;
revert)
B="${2:-$(ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sort | tail -1)}"
[ -d "$B/yum.repos.d" ] || die "no backup dir ($B)"
for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || { cp -a "$f" /etc/yum.repos.d/; echo "restored /etc/yum.repos.d/$(basename "$f")"; }; done
if [ -e "$B/.no-anchor-before" ]; then rm -f "$ANCHOR"; update-ca-trust; echo "removed $ANCHOR"; fi
dnf clean metadata -q || true
echo "reverted from $B"
;;
status)
grep -l -E '^baseurl=https://[^ ]+/(almalinux|epel|remi)/' /etc/yum.repos.d/*.repo 2>/dev/null | sed 's/^/via cache: /' || echo "no repo uses the cache"
ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sed 's/^/backup: /' || true
[ -e "$ANCHOR" ] && echo "CA anchor: $ANCHOR" || echo "CA anchor: absent"
;;
*) die "usage: apply URL | status | revert [DIR]" ;;
esac
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
## lsphp-abi-gate.sh — may PHP extensions compiled against OLD_INC be loaded into
## a PHP built with NEW_INC? Used by lsphp-src-build.sh (emergency php.net build)
## before it keeps LiteSpeed's packaged PECL .so files (redis, imagick,
## memcached, igbinary, msgpack) next to a newer PHP patch release.
##
## WHY: PHP refuses a module whose ZEND_MODULE_API_NO / build-id differ (loud),
## but a changed struct layout under the SAME API number loads fine and corrupts
## memory (silent). Patch releases are not supposed to change layouts, yet the
## php2shell fix (8.x.y, 2026-10-21) did add fields — so this is measured, not
## assumed.
##
## HOW: compile one TU including the public PHP headers (each header in the list
## below that exists in BOTH trees) with -g against each tree, dump every struct/
## union with pahole (incl. anonymous typedef'd ones), and compare:
## * every pre-existing member keeps its offset and size (else FAIL)
## * a new member may only use bytes no old member used (else FAIL)
## (padding, or appended at the end)
## * a struct may change size only if it is *_globals — engine/module globals
## are singletons behind one exported symbol (executor_globals, ps_globals);
## no extension embeds or allocates them (else FAIL)
## A member that is renamed or retyped counts as removed: a false alarm that fails
## loud, never a silent pass.
##
## Usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR (each *_INC contains php/)
## Env: ABI_MIN_MEMBERS (default 1000) — fewer members extracted from OLD_INC
## means the probe itself is broken; refuse rather than pass vacuously.
## Exit 0 = compatible (report on stdout), 1 = incompatible (report on stderr).
## Tested by scripts/tests/lsphp-abi-gate.test.sh.
set -euo pipefail
OLD_INC="${1:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
NEW_INC="${2:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
WORK="${3:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
MIN="${ABI_MIN_MEMBERS:-1000}"
mkdir -p "$WORK"
HDRS=(php.h php_ini.h SAPI.h ext/standard/info.h ext/standard/php_var.h
ext/standard/php_string.h ext/standard/basic_functions.h ext/standard/url.h
ext/standard/php_incomplete_class.h ext/standard/php_smart_string.h
ext/session/php_session.h ext/json/php_json.h ext/date/php_date.h
ext/hash/php_hash.h ext/random/php_random.h ext/spl/spl_exceptions.h
ext/pcre/php_pcre.h zend_exceptions.h zend_interfaces.h zend_smart_str.h
zend_closures.h zend_generators.h zend_observer.h zend_attributes.h
zend_enum.h zend_weakrefs.h zend_fibers.h zend_extensions.h)
: >"$WORK/abi.c"
for h in "${HDRS[@]}"; do
for t in "$OLD_INC/php" "$NEW_INC/php"; do
[ -e "$t/$h" ] || [ -e "$t/main/$h" ] || [ -e "$t/Zend/$h" ] || continue 2
done
printf '#include "%s"\n' "$h" >>"$WORK/abi.c"
done
[ -s "$WORK/abi.c" ] || { echo "ABI gate: none of the probe headers exist in both trees" >&2; exit 1; }
dump() { # $1 = include root, $2 = tag
local inc="$1/php" o="$WORK/abi-$2.o"
gcc -g -O0 -c -fno-eliminate-unused-debug-types \
-I"$inc" -I"$inc/main" -I"$inc/TSRM" -I"$inc/Zend" -I"$inc/ext" -I"$inc/ext/date/lib" \
-o "$o" "$WORK/abi.c"
## M <struct> <decl>#<n> <offset[:bit]> <size> and S <struct> <size>
## Named: "struct _zend_object {" ... "};" /* size: N ... */
## Anonymous: "typedef struct {" ... "} zend_fcall_info_cache;"
pahole -a -F dwarf "$o" | awk '
/^(typedef )?(struct|union) ([A-Za-z_0-9]+ )?\{/ {
depth = 1; nm = 0; sz = ""; delete seen
name = ($1 == "typedef") ? "" : $2; next }
depth == 0 { next }
depth == 1 && /^\}/ {
if (name == "") { n = $0; sub(/^\}[ \t]*/, "", n); sub(/;.*$/, "", n); name = n }
for (i = 1; i <= nm; i++) print "M", name, mem[i]
if (sz != "") print "S", name, sz
depth = 0; next }
depth == 1 && /\/\* size: [0-9]+/ { t = $0; sub(/^.*size: /, "", t); sub(/[^0-9].*$/, "", t); sz = t; next }
/\{[ \t]*$/ { depth++; next }
/^[ \t]+\}/ { depth-- }
/;[ \t]*\/\*[ \t]+[0-9]+(:[ \t]*[0-9]+)?[ \t]+[0-9]+[ \t]+\*\// {
d = $0; sub(/;.*$/, "", d); gsub(/[ \t]+/, " ", d); sub(/^ /, "", d); gsub(/ /, "_", d)
c = $0; sub(/^.*\/\*[ \t]+/, "", c); sub(/[ \t]+\*\/.*$/, "", c); gsub(/:[ \t]+/, ":", c)
k = split(c, w, /[ \t]+/)
seen[d]++
mem[++nm] = d "#" seen[d] " " w[1] " " w[k] }
' >"$WORK/abi-$2.txt"
}
dump "$OLD_INC" old
dump "$NEW_INC" new
NOLD=$(awk '$1 == "M"' "$WORK/abi-old.txt" | wc -l)
NSTRUCT=$(awk '$1 == "S"' "$WORK/abi-old.txt" | wc -l)
if [ "$NOLD" -lt "$MIN" ]; then
echo "ABI gate: only $NOLD struct members extracted from $OLD_INC (< $MIN) — the probe is broken; refusing to call that compatible." >&2
exit 1
fi
REPORT=$(awk '
FNR == NR { if ($1 == "M") { old[$2 " " $3] = $4 " " $5; ost[$2] = 1 } else if ($1 == "S") osz[$2] = $3; next }
$1 == "M" { new[$2 " " $3] = $4 " " $5; nst[$2] = 1 }
$1 == "S" { nsz[$2] = $3 }
END {
for (k in old) { split(k, p, " "); if (!(p[1] in nst)) continue
if (!(k in new)) print "BAD removed/renamed member " k " (was " old[k] ")"
else if (old[k] != new[k]) print "BAD moved member " k ": " old[k] " -> " new[k] }
for (k in new) { if (k in old) continue; split(k, p, " "); if (!(p[1] in ost)) continue
split(new[k], q, " "); s = q[1] + 0; e = s + q[2]
hit = ""
for (o in old) { split(o, r, " "); if (r[1] != p[1]) continue; split(old[o], z, " ")
if (s < z[1] + z[2] && z[1] + 0 < e) { hit = o; break } }
if (hit != "") print "BAD new member " k " at " new[k] " overlaps old " hit
else print "OK new member " k " at " new[k] " (padding/appended)" }
for (st in osz) { if (!(st in nsz) || osz[st] == nsz[st]) continue
if (st ~ /_globals$/) print "OK size " st " " osz[st] " -> " nsz[st] " (*_globals singleton)"
else print "BAD size " st " " osz[st] " -> " nsz[st] }
}' "$WORK/abi-old.txt" "$WORK/abi-new.txt" | sort)
if [[ $'\n'"$REPORT" == *$'\nBAD '* ]]; then
printf '%s\n' "$REPORT" >&2
echo "ABI gate: INCOMPATIBLE ($NOLD members / $NSTRUCT structs compared)" >&2
exit 1
fi
[ -z "$REPORT" ] || printf '%s\n' "$REPORT"
echo "ABI gate: compatible ($NOLD members / $NSTRUCT structs compared)"
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
## lsphp-apt-upgrade.sh — BUILD-TIME ONLY. Brings every installed lsphp${PHPVER}*
## package (and, by default, the Ubuntu OS packages) up to what the repos offer
## RIGHT NOW, then proves it.
##
## Why this exists: cac-litespeed / cac-lsphp start FROM the LiteSpeed prebuilt
## image litespeedtech/openlitespeed:<OLS>-lsphpNN, which LiteSpeed republishes
## rarely (the 1.8.4 tags were built 2025-12-16 and ship lsphp85 8.5.0 /
## lsphp84 8.4.15, measured 2026-10-21, while rpms.litespeedtech.com/debian
## offers 8.5.11 / 8.4.26). The base image already carries that apt repo, so a
## PHP security release reaches us the moment LiteSpeed publishes the .deb —
## provided every build actually asks for it. Before this script the lsphp core
## only moved forward as a SIDE EFFECT of `apt-get install lsphpNN-ldap` (whose
## exact-version dependency on lsphpNN-common drags the family along); lsphp
## extension packages without that coupling (e.g. lsphpNN-msgpack) and every
## Ubuntu package stayed at the base image's December versions.
##
## Fails LOUDLY — never ships the base image's PHP by accident:
## * `apt-get update --error-on=any`: plain `apt-get update` only WARNS when a
## repo is unreachable and carries on with no/old lists, exit 0.
## * the LiteSpeed repo must be the candidate source for lsphp${PHPVER}.
## * after upgrading, no lsphp${PHPVER}* package may remain upgradable.
## * if CI passed the version it resolved from the repo index (EXPECT_LSPHP_PKG,
## EXPECT_PHP_VERSION), the installed package / running PHP must match it;
## that same value becomes the image LABEL, so the label cannot lie.
##
## Usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [extra apt packages / pkg=ver pins...]
## Extra packages are installed in the same transaction set (after the OS
## upgrade, before the lsphp sweep). Does NOT clean apt lists — the caller
## decides (multi-stage builders want them; shipped layers delete them).
set -euo pipefail
OS_UPGRADE=1
if [ "${1:-}" = "--no-os-upgrade" ]; then OS_UPGRADE=0; shift; fi
PHPVER="${1:?usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [pkgs...]}"
shift
export DEBIAN_FRONTEND=noninteractive
APT=(apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold)
LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
PHPCLI="/usr/local/lsws/lsphp${PHPVER}/bin/php"
die() { echo "FATAL(lsphp-apt-upgrade): $*" >&2; exit 1; }
echo "lsphp-apt-upgrade: PHPVER=${PHPVER} os_upgrade=${OS_UPGRADE} cache_bust=${CACHE_BUST:-<none>}"
echo "lsphp-apt-upgrade: base image lsphp${PHPVER}=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}" 2>/dev/null || echo none)"
"${APT[@]}" update --error-on=any || die "apt-get update failed (repo unreachable?). Refusing to build with stale package lists."
# The candidate for lsphpNN must come from the LiteSpeed repo — not "none", and
# not merely whatever is already installed.
# Read each output into a variable and match in the shell: piping into an
# early-exit reader (grep -q, awk ... exit) under pipefail can SIGPIPE the writer
# and report "missing" for a package that is there (scripts/tests/lsphp-info-probe.test.sh).
MADISON=$(apt-cache madison "lsphp${PHPVER}")
if [[ "$MADISON" != *rpms.litespeedtech.com* ]]; then
apt-cache policy "lsphp${PHPVER}" >&2 || true
die "the LiteSpeed apt repo does not offer lsphp${PHPVER} (repo missing from sources, or its index is empty)."
fi
POLICY=$(apt-cache policy "lsphp${PHPVER}")
CAND=$(awk '/Candidate:/ {print $2; exit}' <<<"$POLICY")
echo "lsphp-apt-upgrade: repo candidate lsphp${PHPVER}=${CAND}"
if [ "$OS_UPGRADE" = 1 ]; then
# --with-new-pkgs: let an upgrade pull in a new dependency instead of being
# silently "kept back". Never removes packages (that would be dist-upgrade).
"${APT[@]}" upgrade --with-new-pkgs
fi
if [ $# -gt 0 ]; then
"${APT[@]}" install --no-install-recommends "$@"
fi
# Sweep EVERY installed lsphpNN* package (core + all extensions) to the candidate.
mapfile -t PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' "lsphp${PHPVER}*" 2>/dev/null | awk '$1 == "ii" {print $2}')
[ "${#PKGS[@]}" -gt 0 ] || die "no lsphp${PHPVER}* packages installed — wrong base image?"
"${APT[@]}" install --only-upgrade --no-install-recommends "${PKGS[@]}"
LEFT=$(apt list --upgradable 2>/dev/null | grep "^lsphp${PHPVER}" || true)
[ -z "$LEFT" ] || die "lsphp packages still upgradable after the sweep:
$LEFT"
INST=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
RUNV=$("$PHPCLI" -r 'echo PHP_VERSION;') || die "lsphp${PHPVER} CLI does not run"
if [ -n "${EXPECT_LSPHP_PKG:-}" ] && [ "$INST" != "$EXPECT_LSPHP_PKG" ]; then
die "installed lsphp${PHPVER}=$INST but CI resolved $EXPECT_LSPHP_PKG from the repo index (repo changed mid-build? re-run)."
fi
if [ -n "${EXPECT_PHP_VERSION:-}" ] && [ "$RUNV" != "$EXPECT_PHP_VERSION" ]; then
die "lsphp${PHPVER} reports PHP $RUNV but the image will be labelled $EXPECT_PHP_VERSION."
fi
if [ -z "${EXPECT_PHP_VERSION:-}" ]; then
echo "lsphp-apt-upgrade: WARNING no EXPECT_PHP_VERSION build-arg — image label will read 'unverified' (fine for local builds; CI always sets it)." >&2
fi
printf '%s\n' "$RUNV" > /etc/cac-php-version
echo "lsphp-apt-upgrade: ===== version report ====="
dpkg-query -W -f=' ${Package} ${Version}\n' "lsphp${PHPVER}*"
echo " lsphp -v: $("$LSPHP" -v | head -n1)"
echo " OS packages still upgradable: $(apt list --upgradable 2>/dev/null | grep -vc '^Listing' || true)"
echo "lsphp-apt-upgrade: OK lsphp${PHPVER}=${INST} (PHP ${RUNV})"
+304
View File
@@ -0,0 +1,304 @@
#!/bin/bash
## lsphp-src-build.sh — BUILD-TIME ONLY, EMERGENCY PATH. Compiles lsphp from the
## official php.net release tarball, configured EXACTLY like the LiteSpeed-packaged
## lsphp${PHPVER} it will replace, for the window in which a PHP security release
## is out but rpms.litespeedtech.com/debian has not published it yet.
##
## Runs in the throwaway `lsphp-src` stage of Dockerfile.lsphp / Dockerfile.litespeed,
## FROM the same litespeedtech/openlitespeed base as the shipped stage. The output
## (/lsphp-src-out) is applied to the shipped stage by lsphp-src-overlay.sh, which
## is where the "same support as the packaged lsphp" assertions live.
##
## PHP_SRC_VERSION empty (every normal build) => does nothing but create an empty
## /lsphp-src-out, in well under a second. Nothing is downloaded or installed.
##
## WHAT "CONFIGURED EXACTLY LIKE THE PACKAGE" MEANS, and why it cannot drift:
## the configure arguments are NOT kept in this repo. They are read, at build
## time, from the `Configure Command` line of the packaged lsphp that the same
## build just installed from the LiteSpeed repo (php -i), and replayed verbatim —
## only CFLAGS is edited, to drop the two -f*-prefix-map flags that encode
## LiteSpeed's build directory. When LiteSpeed changes their flags, this follows
## automatically; if a new flag needs a -dev package we do not install, configure
## FAILS (loud), it does not silently drop the feature. CPPFLAGS/LDFLAGS are not
## recorded by PHP; they come from Ubuntu noble's dpkg-buildflags with
## hardening=+all, which reproduces the packaged binary's measured hardening
## (PIE, full RELRO, BIND_NOW, FORTIFY _chk symbols) — checked again below.
##
## TRUST: the tarball must (1) match PHP_SRC_SHA256 (CI takes it from
## https://www.php.net/releases/index.php?json&version=X.Y; the build fails if it
## is not supplied) AND (2) carry a valid OpenPGP signature (.asc from php.net)
## by a key in keys/php-src/php-keyring.asc (the official keyring from
## https://www.php.net/distributions/php-keyring.gpg, committed
## unmodified as php-keyring.asc — it is ASCII-armoured despite the name) whose PRIMARY
## fingerprint is listed for this branch in keys/php-src/release-managers.txt
## (transcribed from https://www.php.net/gpg-keys.php). Either check failing
## aborts the build. A new release manager key therefore needs a reviewed commit.
##
## The LSAPI SAPI itself is LiteSpeed's php-litespeed release (pinned by sha256
## in keys/php-src/litespeed-sapi.txt), NOT php-src's bundled sapi/litespeed —
## that is what LiteSpeed's packages compile (measured, see that file).
##
## PECL extensions (igbinary, imagick, memcached, msgpack, redis) are NOT built
## here: the shipped stage keeps LiteSpeed's own packaged .so files — the same
## binaries the primary image ships, which LiteSpeed itself does not rebuild per
## PHP patch release (e.g. lsphp85-msgpack 3.0.1-1 is from June, core 8.5.11 from
## September). That is only safe while the Zend ABI is unchanged, so this script
## also runs an ABI gate (scripts/lsphp-abi-gate.sh: pahole over every struct the
## public PHP headers define, packaged -dev headers vs the new headers); any
## changed offset/size of an existing member fails the build.
##
## Usage (env): PHPVER=85 PHP_SRC_VERSION=8.5.12 PHP_SRC_SHA256=<64 hex> lsphp-src-build.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd) # sibling scripts (lsphp-abi-gate.sh)
OUT=/lsphp-src-out
KEYDIR=${KEYDIR:-/usr/local/share/php-src-keys}
mkdir -p "$OUT"
die() { echo "FATAL(lsphp-src-build): $*" >&2; exit 1; }
log() { echo "lsphp-src-build: $*"; }
PHPVER="${PHPVER:?PHPVER required}"
[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || die "PHPVER='$PHPVER' is not NN."
if [ -z "${PHP_SRC_VERSION:-}" ]; then
[ -z "${PHP_SRC_SHA256:-}" ] || die "PHP_SRC_SHA256 given without PHP_SRC_VERSION."
log "PHP_SRC_VERSION empty — packaged lsphp build, nothing to do."
exit 0
fi
BRANCH="${PHPVER:0:1}.${PHPVER:1}" # 85 -> 8.5
[[ "$PHP_SRC_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "PHP_SRC_VERSION='$PHP_SRC_VERSION' is not X.Y.Z (RCs/alphas are refused)."
[ "${PHP_SRC_VERSION%.*}" = "$BRANCH" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION does not belong to PHPVER=$PHPVER (branch $BRANCH)."
[[ "${PHP_SRC_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || die "PHP_SRC_SHA256 must be the 64-hex sha256 of php-${PHP_SRC_VERSION}.tar.xz from the php.net releases JSON."
P="/usr/local/lsws/lsphp${PHPVER}"
START=$(date +%s)
## ---- 1. toolchain + every -dev library the packaged configure line needs -----
## Same script as the shipped stage: refuses stale/unreachable repo lists and
## brings lsphp${PHPVER}* (core + -dev) to the repo's current release, so the
## Configure Command we read is the one the shipped stage's packages carry
## (lsphp-src-overlay.sh re-checks that the versions match).
## -dev packages: one per library in the packaged Configure Command. Extras are
## harmless; a missing one makes configure fail, which is the loud outcome.
DEVPKGS=(
build-essential autoconf pkg-config file dwarves
curl ca-certificates gpg gpgv xz-utils
libxml2-dev libssl-dev libcurl4-openssl-dev libenchant-2-dev libgmp-dev
libonig-dev libedit-dev libbz2-dev libxslt1-dev libzip-dev libsodium-dev
libargon2-dev libjpeg-dev libxpm-dev libpng-dev libwebp-dev libfreetype-dev
libldap-dev libsasl2-dev libpq-dev libsnmp-dev libsqlite3-dev freetds-dev
libtidy-dev libicu-dev zlib1g-dev libkrb5-dev
)
case "$PHPVER" in
81) DEVPKGS+=(libc-client2007e-dev libpspell-dev) ;; # --with-imap, --with-pspell
82|83) DEVPKGS+=(libpspell-dev) ;; # --with-pspell (gone in 8.4)
esac
/usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" "${DEVPKGS[@]}" "lsphp${PHPVER}-dev"
PKGV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
DEVV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}-dev")
[ "$PKGV" = "$DEVV" ] || die "lsphp${PHPVER}=$PKGV but lsphp${PHPVER}-dev=$DEVV — packaged headers do not match the packaged binary."
PKG_PHPV=$("$P/bin/php" -r 'echo PHP_VERSION;')
log "packaged reference: lsphp${PHPVER} ${PKGV} (PHP ${PKG_PHPV}); building php.net ${PHP_SRC_VERSION}"
if [ "$(printf '%s\n%s\n' "$PKG_PHPV" "$PHP_SRC_VERSION" | sort -V | tail -n1)" != "$PHP_SRC_VERSION" ] \
|| [ "$PKG_PHPV" = "$PHP_SRC_VERSION" ]; then
die "PHP_SRC_VERSION=$PHP_SRC_VERSION is not NEWER than the packaged $PKG_PHPV — the emergency path is pointless (or a downgrade). Use the normal build."
fi
## ---- 2. fetch + verify -------------------------------------------------------
WORK=/usr/src/php-src
mkdir -p "$WORK"
cd "$WORK"
TARBALL="php-${PHP_SRC_VERSION}.tar.xz"
for f in "$TARBALL" "$TARBALL.asc"; do
curl -fsSL --retry 3 --max-time 600 -o "$f" "https://www.php.net/distributions/$f" \
|| die "download of https://www.php.net/distributions/$f failed."
done
GOT=$(sha256sum "$TARBALL" | awk '{print $1}')
[ "$GOT" = "$PHP_SRC_SHA256" ] || die "sha256 mismatch for $TARBALL: got $GOT, expected $PHP_SRC_SHA256 (php.net releases JSON)."
log "sha256 OK $GOT"
[ -s "$KEYDIR/php-keyring.asc" ] || die "pinned keyring $KEYDIR/php-keyring.asc missing."
## The committed file is php.net's keyring byte-for-byte (ASCII-armoured); gpgv
## only reads binary keyrings.
gpg --batch --yes --dearmor -o "$WORK/php-keyring.gpg" "$KEYDIR/php-keyring.asc" || die "could not dearmor $KEYDIR/php-keyring.asc."
[ -s "$KEYDIR/release-managers.txt" ] || die "pinned fingerprint list $KEYDIR/release-managers.txt missing."
STATUS=$(gpgv --status-fd 1 --keyring "$WORK/php-keyring.gpg" "$TARBALL.asc" "$TARBALL" 2>"$WORK/gpgv.err") || {
cat "$WORK/gpgv.err" >&2; die "OpenPGP signature verification of $TARBALL FAILED."; }
cat "$WORK/gpgv.err"
## [GNUPG:] VALIDSIG <signing-key-fpr> <date> <ts> <expire> <ver> <rsv> <pk-algo> <hash> <class> <PRIMARY-fpr>
PRIMARY=$(awk '$1 == "[GNUPG:]" && $2 == "VALIDSIG" {print $NF}' <<<"$STATUS")
[ -n "$PRIMARY" ] || die "gpgv reported no VALIDSIG for $TARBALL."
ALLOWED=$(awk -v b="$BRANCH" '$1 == b {print $2}' "$KEYDIR/release-managers.txt")
[ -n "$ALLOWED" ] || die "no release-manager fingerprints pinned for PHP $BRANCH in release-managers.txt."
OK=0
while read -r fp; do [ "$fp" = "$PRIMARY" ] && OK=1; done <<<"$ALLOWED"
[ "$OK" = 1 ] || die "$TARBALL is validly signed by $PRIMARY, which is NOT a pinned PHP $BRANCH release manager (release-managers.txt)."
log "signature OK: primary key $PRIMARY is a pinned PHP $BRANCH release manager"
tar -xJf "$TARBALL"
SRC="$WORK/php-${PHP_SRC_VERSION}"
grep -F "PHP ${PHP_SRC_VERSION}" "$SRC/NEWS" >/dev/null || die "NEWS in $TARBALL has no 'PHP ${PHP_SRC_VERSION}' entry."
## ---- 2b. LiteSpeed's LSAPI SAPI, as the packages use (keys/php-src/litespeed-sapi.txt)
read -r SAPI_URL SAPI_SHA < <(awk '!/^#/ && NF == 2 {print; exit}' "$KEYDIR/litespeed-sapi.txt")
[ -n "${SAPI_URL:-}" ] && [[ "${SAPI_SHA:-}" =~ ^[0-9a-f]{64}$ ]] || die "no pinned LiteSpeed SAPI in $KEYDIR/litespeed-sapi.txt."
curl -fsSL --retry 3 --max-time 300 -o "$WORK/lsapi.tgz" "$SAPI_URL" || die "download of $SAPI_URL failed."
GOT=$(sha256sum "$WORK/lsapi.tgz" | awk '{print $1}')
[ "$GOT" = "$SAPI_SHA" ] || die "LiteSpeed SAPI $SAPI_URL sha256 $GOT != pinned $SAPI_SHA (LiteSpeed republished it? re-measure against the packaged lsphp before re-pinning)."
mkdir -p "$WORK/lsapi"
tar -xzf "$WORK/lsapi.tgz" -C "$WORK/lsapi"
## Per its README: copy only the *.c/*.h over php-src's sapi/litespeed and keep
## php-src's config.m4 / Makefile.frag / arginfo.
N=0
for f in "$WORK"/lsapi/*/*.c "$WORK"/lsapi/*/*.h; do
[ -f "$f" ] || continue
install -m 0644 "$f" "$SRC/sapi/litespeed/$(basename "$f")"; N=$((N + 1))
done
[ "$N" -ge 4 ] || die "LiteSpeed SAPI archive had only $N .c/.h files."
log "LiteSpeed SAPI $(basename "$SAPI_URL") (sha256 OK) replaces php-src sapi/litespeed sources ($N files)"
## ---- 2c. compiled-in ini defaults LiteSpeed patched ----------------------------
## Measured 2026-10-21 with `php -n`: LiteSpeed's lsphp82/83 carry Debian's
## session.save_path patch (default '/var/lib/php/sessions' instead of php-src's
## ''); lsphp85 does not. Reproduce whatever the packaged binary reports. Any
## OTHER compiled-in default that differs is caught by the gate in step 4b.
PKG_SAVE_PATH=$("$P/bin/php" -n -r 'echo ini_get("session.save_path");')
if [ -n "$PKG_SAVE_PATH" ]; then
[[ "$PKG_SAVE_PATH" =~ ^[A-Za-z0-9/._-]+$ ]] || die "unexpected packaged session.save_path default '$PKG_SAVE_PATH'."
sed -i -E 's|(STD_PHP_INI_ENTRY\("session\.save_path",[[:space:]]*)""|\1"'"$PKG_SAVE_PATH"'"|' "$SRC/ext/session/session.c"
grep -F "\"session.save_path\"" "$SRC/ext/session/session.c" | grep -F "\"$PKG_SAVE_PATH\"" >/dev/null \
|| die "could not apply the packaged session.save_path default '$PKG_SAVE_PATH' to ext/session/session.c."
log "compiled-in session.save_path default set to '$PKG_SAVE_PATH' (as the packaged lsphp${PHPVER})"
fi
## ---- 3. replay the packaged configure line -----------------------------------
CFG_LINE=$("$P/bin/php" -i | awk -F' => ' '$1 == "Configure Command " || $1 == "Configure Command" {sub(/^[^=]*=> /, ""); print}')
[ -n "$CFG_LINE" ] || die "could not read 'Configure Command' from the packaged $P/bin/php -i."
## xargs does the shell-style single-quote splitting without an eval.
mapfile -t CFG_ARGS < <(printf '%s\n' "$CFG_LINE" | xargs -n1 printf '%s\n')
[ "${CFG_ARGS[0]}" = "./configure" ] || die "unexpected Configure Command shape (first word '${CFG_ARGS[0]}')."
CFG_ARGS=("${CFG_ARGS[@]:1}")
ARGS=()
PKG_CFLAGS=""
for a in "${CFG_ARGS[@]}"; do
case "$a" in
CFLAGS=*) PKG_CFLAGS="${a#CFLAGS=}" ;;
*) ARGS+=("$a") ;;
esac
done
[ -n "$PKG_CFLAGS" ] || die "packaged Configure Command carries no CFLAGS — refusing to guess."
CFLAGS=""
for w in $PKG_CFLAGS; do
case "$w" in -ffile-prefix-map=*|-fdebug-prefix-map=*) continue ;; esac
CFLAGS+="${CFLAGS:+ }$w"
done
CFLAGS+=" -ffile-prefix-map=${SRC}=."
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
CPPFLAGS=$(dpkg-buildflags --get CPPFLAGS)
LDFLAGS=""
for w in $(dpkg-buildflags --get LDFLAGS); do
## CFLAGS ends in -fno-lto (LiteSpeed's choice); keep the link step consistent.
case "$w" in -flto*|-ffat-lto-objects) continue ;; esac
LDFLAGS+="${LDFLAGS:+ }$w"
done
CXXFLAGS="$CFLAGS"
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
log "configure args (from packaged lsphp${PHPVER} ${PKGV}): ${ARGS[*]}"
log "CFLAGS=$CFLAGS"
log "CPPFLAGS=$CPPFLAGS"
log "LDFLAGS=$LDFLAGS"
## Build-environment shim (builder stage only, never shipped): ext/pdo_dblib's
## config.m4 looks for libsybdb in $prefix/lib, not the multiarch dir Ubuntu's
## freetds-dev uses. pdo_dblib is built but not shipped (the packaged image has
## no lsphpNN-dblib), exactly as in the packaged build.
MA=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
[ -e /usr/lib/libsybdb.so ] || ln -s "/usr/lib/$MA/libsybdb.so" /usr/lib/libsybdb.so
cd "$SRC"
if ! ./configure "${ARGS[@]}" "CFLAGS=$CFLAGS" >"$WORK/configure.log" 2>&1; then
tail -n 60 "$WORK/configure.log" >&2
die "configure FAILED with the packaged flags — most likely a -dev package missing from DEVPKGS in this script (see the error above)."
fi
## Unknown options are expected for a handful of historic flags LiteSpeed still
## passes (--enable-gd-native-ttf, --with-vpx-dir, ...); they are just as unknown
## to the packaged build. Print them so a NEW unknown flag is visible in the log.
grep -E "^configure: WARNING: unrecognized options" "$WORK/configure.log" || true
T0=$(date +%s)
make -j"$(nproc)" >"$WORK/make.log" 2>&1 || { tail -n 80 "$WORK/make.log" >&2; die "make FAILED."; }
log "make took $(( $(date +%s) - T0 ))s on $(nproc) CPUs"
STAGE="$WORK/stage"
make install INSTALL_ROOT="$STAGE" >"$WORK/install.log" 2>&1 || { tail -n 40 "$WORK/install.log" >&2; die "make install FAILED."; }
SP="$STAGE$P"
[ -x "$SP/bin/lsphp${BRANCH}" ] || die "make install produced no $P/bin/lsphp${BRANCH} (did --enable-litespeed vanish from the packaged flags?)."
[ -x "$SP/bin/php${BRANCH}" ] || die "make install produced no $P/bin/php${BRANCH}."
## --program-suffix names everything <tool>8.M; LiteSpeed's packages ship the
## SAPI as bin/lsphp and the CLI as both bin/php and bin/php8.M (two identical
## regular files, measured). Reproduce those names; the overlay copies only the
## files the packaged image actually has.
mv "$SP/bin/lsphp${BRANCH}" "$SP/bin/lsphp"
cp -p "$SP/bin/php${BRANCH}" "$SP/bin/php"
## Strip like dh_strip does for the packaged binaries (measured: no .comment /
## debug sections in the packaged lsphp).
find "$SP" -type f \( -name '*.so' -o -path "*/bin/*" \) -print0 | while IFS= read -r -d '' f; do
case "$(file -b "$f")" in
ELF*shared\ object*|ELF*pie\ executable*|ELF*executable*)
strip --remove-section=.comment --remove-section=.note --strip-unneeded "$f" ;;
esac
done
NEWV=$("$SP/bin/php${BRANCH}" -n -r 'echo PHP_VERSION;' 2>/dev/null || true)
[ "$NEWV" = "$PHP_SRC_VERSION" ] || die "built php reports '$NEWV', expected $PHP_SRC_VERSION."
## ---- 4b. compiled-in ini defaults must match the packaged binary ---------------
## `php -n` = no php.ini, no scan dir, no shared extensions: what is left is
## what the binary itself defaults to. New directives (a security release may
## add one, e.g. memory_hard_limit_percent in 8.x.y php2shell fix) are allowed
## and printed; a removed or changed default fails.
ini_defaults() { "$1" -n -r 'foreach (ini_get_all(null, false) as $k => $v) echo $k, "=", var_export($v, true), "\n";' | sort; }
INI_PKG=$(ini_defaults "$P/bin/php")
INI_NEW=$(ini_defaults "$SP/bin/php${BRANCH}")
INI_BAD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^<' || true)
INI_ADD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^>' || true)
[ -z "$INI_BAD" ] || die "compiled-in ini defaults differ from the packaged lsphp${PHPVER} (< packaged, > php.net build):
$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") || true)
LiteSpeed patched a default this script does not reproduce (see step 2c)."
log "compiled-in ini defaults identical to packaged ($(printf '%s\n' "$INI_PKG" | wc -l) directives)${INI_ADD:+; new in $PHP_SRC_VERSION: $(printf '%s' "$INI_ADD" | tr '\n' ' ')}"
## Same LSAPI SAPI as the packaged binary? Compare the LSAPI_*/LSCAPI_* names
## (its env knobs + feature switches) compiled into each. A difference means
## LiteSpeed moved to a SAPI release we have not pinned (or the copy failed).
lsapi_names() { grep -aoE 'LS(C)?API_[A-Z0-9_]+' "$1" | sort -u; }
LSAPI_DIFF=$(diff <(lsapi_names "$P/bin/lsphp") <(lsapi_names "$SP/bin/lsphp") || true)
[ -z "$LSAPI_DIFF" ] || die "LSAPI SAPI differs from the packaged lsphp (< packaged, > php.net build):
$LSAPI_DIFF"
log "LSAPI SAPI matches the packaged lsphp ($(lsapi_names "$SP/bin/lsphp" | wc -l) LSAPI_* names identical)"
## ---- 4. ABI gate: may LiteSpeed's packaged PECL .so files load into this? -----
## See scripts/lsphp-abi-gate.sh (and its test). Measured for 8.5.11 -> 8.5.12:
## the php2shell fix adds CG(interned_strings_fallible) in padding and appends
## EG(delayed_oom) / PS(in_rfc1867_update); no existing member moved.
ABI_OUT=$(bash "$HERE/lsphp-abi-gate.sh" "$P/include" "$SP/include" "$WORK/abi" 2>&1) || {
printf '%s\n' "$ABI_OUT" >&2
die "ABI gate: PHP $PHP_SRC_VERSION changed struct layouts relative to the packaged $PKG_PHPV headers. LiteSpeed's packaged PECL .so files (redis, imagick, memcached, igbinary, msgpack) were compiled against the old layout and must NOT be loaded into this build — they would have to be rebuilt from source, which this emergency path does not do (docs/lsphp-source-emergency.md)."
}
printf '%s\n' "$ABI_OUT" | sed 's/^/lsphp-src-build: /'
## ---- 5. hand-off ----------------------------------------------------------------
mkdir -p "$OUT/root"
cp -a "$STAGE/." "$OUT/root/"
cat >"$OUT/meta.env" <<EOF
LSPHP_SRC_PHP_VERSION=$PHP_SRC_VERSION
LSPHP_SRC_SHA256=$PHP_SRC_SHA256
LSPHP_SRC_SIGNER=$PRIMARY
LSPHP_SRC_PKG_REFERENCE=$PKGV
LSPHP_SRC_PKG_PHP_VERSION=$PKG_PHPV
EOF
printf '%s\n' "${ARGS[@]}" >"$OUT/configure-args.txt"
log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})"
+203
View File
@@ -0,0 +1,203 @@
#!/bin/bash
## lsphp-src-overlay.sh — BUILD-TIME ONLY, EMERGENCY PATH. Applies the php.net
## build produced by lsphp-src-build.sh (staged in /lsphp-src-out by the
## `lsphp-src` stage) over the LiteSpeed-packaged lsphp${PHPVER} that this stage
## has just installed, then PROVES the result offers the same support.
##
## Usage: lsphp-src-overlay.sh runtime|dev PHPVER [OUT_DIR=/lsphp-src-out]
## runtime — the shipped stage: replace the php-src binaries (lsphp, php,
## php8.M, phar) and every php-src shared extension the packages
## installed. Everything else stays exactly as packaged: php.ini,
## mods-available/*.ini, the LiteSpeed PECL .so files (see
## lsphp-src-build.sh for why), helper scripts, man pages.
## dev — Dockerfile.lsphp's ext-build stage: runtime + the -dev files
## (headers, phpize, php-config, lib/php/build) so cac_path_parity is
## compiled against, and its .phpt suite run on, the php.net build.
##
## PHP_SRC_VERSION empty and no staged build => no-op (normal builds).
##
## WHAT IS REPLACED is decided by dpkg, not by a list kept here: a file is
## php-src output iff it is owned by an installed lsphp${PHPVER}* package whose
## version equals lsphp${PHPVER}'s (core, -common, -curl, -intl, -ldap, -mysql,
## -opcache, -dev ...; the PECL packages carry their own versions). Every ELF
## file among those MUST have a php.net counterpart — a missing one fails the
## build, so a new LiteSpeed sub-package can never keep an old binary silently.
##
## THE "SAME SUPPORT" GATE (all fatal): lsphp-src-probe.php is run under both
## SAPIs before and after; the loaded extension set (incl. Zend extensions) must
## be identical, every PECL extension must report its same version, every
## php-src extension the new version, no effective ini value may change or
## disappear (new directives are allowed and printed), and the
## gd/curl/openssl/icu/imagick/redis/memcached/password/hash/stream feature
## lines must be unchanged. `php -i` Configure Command must match the packaged
## one except CFLAGS. No ELF under the prefix may have an unresolved library.
set -euo pipefail
MODE="${1:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
PHPVER="${2:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
OUT="${3:-/lsphp-src-out}"
case "$MODE" in runtime|dev) ;; *) echo "FATAL(lsphp-src-overlay): mode must be runtime|dev" >&2; exit 1 ;; esac
[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || { echo "FATAL(lsphp-src-overlay): PHPVER='$PHPVER' is not NN" >&2; exit 1; }
die() { echo "FATAL(lsphp-src-overlay): $*" >&2; exit 1; }
log() { echo "lsphp-src-overlay[$MODE]: $*"; }
LSPHP_SOURCE="${LSPHP_SOURCE:-litespeed-apt}"
if [ ! -f "$OUT/meta.env" ]; then
[ -z "${PHP_SRC_VERSION:-}" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION but no staged php.net build in $OUT (lsphp-src stage did not run?)."
[ "$LSPHP_SOURCE" = "litespeed-apt" ] || die "LSPHP_SOURCE=$LSPHP_SOURCE requires PHP_SRC_VERSION."
log "no php.net build staged — packaged lsphp${PHPVER} kept as is."
exit 0
fi
[ "$LSPHP_SOURCE" = "php.net" ] || die "a php.net build is staged but LSPHP_SOURCE=$LSPHP_SOURCE (must be 'php.net' so the image label tells the truth)."
# shellcheck disable=SC1091
. "$OUT/meta.env"
[ "${PHP_SRC_VERSION:-}" = "$LSPHP_SRC_PHP_VERSION" ] || die "staged build is $LSPHP_SRC_PHP_VERSION but PHP_SRC_VERSION=${PHP_SRC_VERSION:-<empty>}."
BRANCH="${PHPVER:0:1}.${PHPVER:1}"
P="/usr/local/lsws/lsphp${PHPVER}"
SRCROOT="$OUT/root"
PROBE=/usr/local/share/lsphp-src-probe.php
[ -f "$PROBE" ] || die "$PROBE missing."
CORE="lsphp${PHPVER}"
COREV=$(dpkg-query -W -f='${Version}' "$CORE")
[ "$COREV" = "$LSPHP_SRC_PKG_REFERENCE" ] || die "this stage has $CORE $COREV but the php.net build was configured from $LSPHP_SRC_PKG_REFERENCE (the LiteSpeed repo moved between stages? rebuild)."
probe() { # $1 = tag; writes $OUT/probe-<tag>-{cli,lsapi}.txt
"$P/bin/php" "$PROBE" >"$OUT/probe-$1-cli.txt" || die "php probe failed ($1)."
"$P/bin/lsphp" "$PROBE" >"$OUT/probe-$1-lsapi.txt" || die "lsphp probe failed ($1)."
local first
first=$(head -n1 "$OUT/probe-$1-lsapi.txt")
[ "$first" = "SAPI litespeed" ] || die "lsphp did not run the probe under the litespeed SAPI ($1): '$first'."
}
## Configure Command minus CFLAGS, whitespace-normalised (the CFLAGS argument
## sits in a different position in our ./configure invocation).
cfgcmd() { "$P/bin/php" -i | awk -F' => ' '$1 ~ /^Configure Command/ {print $2}' | sed -E "s/'CFLAGS=[^']*'//; s/[[:space:]]+/ /g; s/^ //; s/ $//"; }
probe before
CFG_BEFORE=$(cfgcmd)
OLDV=$(awk '$1 == "VERSION" {print $2}' "$OUT/probe-before-cli.txt")
[ "$OLDV" = "$LSPHP_SRC_PKG_PHP_VERSION" ] || die "packaged PHP is $OLDV, build meta says $LSPHP_SRC_PKG_PHP_VERSION."
## ---- which files are php-src output? (dpkg decides) ---------------------------
mapfile -t SRC_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
| awk -v v="$COREV" '$1 == "ii" && $3 == v {print $2}')
mapfile -t PECL_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
| awk -v v="$COREV" '$1 == "ii" && $3 != v {print $2 "=" $3}')
if [ "$MODE" = runtime ]; then
for p in "${SRC_PKGS[@]}"; do
[ "$p" != "lsphp${PHPVER}-dev" ] || die "runtime mode but lsphp${PHPVER}-dev is installed in this stage."
done
fi
log "php-src packages at $COREV: ${SRC_PKGS[*]}"
log "kept as packaged (own versions): ${PECL_PKGS[*]:-none}"
REPLACED=0
KEPT=()
while IFS= read -r f; do
[ -f "$f" ] && [ ! -L "$f" ] || continue
rel="${f#/}"
is_elf=0
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then is_elf=1; fi
case "$f" in
"$P/bin/phar${BRANCH}.phar"|"$P"/include/php/*|"$P"/lib/php/build/*|"$P/bin/php-config"|"$P/bin/php-config${BRANCH}"|"$P/bin/phpize"|"$P/bin/phpize${BRANCH}")
## text artefacts generated by the php-src build
[ "$MODE" = dev ] || [[ "$f" == "$P/bin/phar${BRANCH}.phar" ]] || { KEPT+=("$f"); continue; }
src="$SRCROOT/$rel"
case "$f" in
"$P/bin/php-config") src="$SRCROOT$P/bin/php-config${BRANCH}" ;;
"$P/bin/phpize") src="$SRCROOT$P/bin/phpize${BRANCH}" ;;
esac
if [ -f "$src" ]; then
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
elif [[ "$f" == "$P"/include/php/* ]]; then
log "header $f no longer exists in php.net $LSPHP_SRC_PHP_VERSION — removing the stale copy"
rm -f "$f"
else
die "php.net build has no counterpart for packaged $f."
fi
;;
*)
if [ "$is_elf" = 1 ]; then
src="$SRCROOT/$rel"
[ "$f" = "$P/bin/php" ] && src="$SRCROOT$P/bin/php${BRANCH}"
[ -f "$src" ] || die "packaged ELF $f (php-src output, owned by a $COREV package) has NO php.net counterpart at $src — refusing to ship an old binary next to the new PHP."
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
else
KEPT+=("$f")
fi
;;
esac
done < <(for p in "${SRC_PKGS[@]}"; do dpkg -L "$p"; done | sort -u)
if [ "$MODE" = dev ]; then
## headers that are NEW in the php.net release (none for most patch releases)
while IFS= read -r -d '' h; do
dst="${h#"$SRCROOT"}"
[ -e "$dst" ] || { mkdir -p "$(dirname "$dst")"; install -m 0644 "$h" "$dst"; log "added new header $dst"; }
done < <(find "$SRCROOT$P/include/php" -type f -print0)
CFV=$("$P/bin/php-config" --version)
[ "$CFV" = "$LSPHP_SRC_PHP_VERSION" ] || die "php-config reports $CFV after overlay."
fi
log "replaced $REPLACED php-src files; kept ${#KEPT[@]} packaging files (ini templates, helper scripts, docs, man pages)"
## ---- no unresolved shared libraries -------------------------------------------
while IFS= read -r -d '' f; do
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then
LDD=$(ldd "$f" 2>&1 || true)
[[ "$LDD" != *"not found"* ]] || die "$f has unresolved libraries:
$LDD"
fi
done < <(find "$P/bin" "$P/lib" -type f -print0)
## ---- the "same support" gate ----------------------------------------------------
probe after
CFG_AFTER=$(cfgcmd)
[ "$CFG_BEFORE" = "$CFG_AFTER" ] || die "Configure Command differs from the packaged build beyond CFLAGS:
packaged: $CFG_BEFORE
php.net : $CFG_AFTER"
FAIL=0
for sapi in cli lsapi; do
B="$OUT/probe-before-$sapi.txt"; A="$OUT/probe-after-$sapi.txt"
NEWV=$(awk '$1 == "VERSION" {print $2}' "$A")
[ "$NEWV" = "$LSPHP_SRC_PHP_VERSION" ] || { echo "[$sapi] reports PHP $NEWV, expected $LSPHP_SRC_PHP_VERSION" >&2; FAIL=1; }
## extension sets + versions
R=$(awk -v o="$OLDV" -v n="$LSPHP_SRC_PHP_VERSION" '
FNR == NR { if ($1 == "EXT" || $1 == "ZEXT") b[$1" "$2] = $3; next }
($1 == "EXT" || $1 == "ZEXT") { a[$1" "$2] = $3 }
END {
for (k in b) {
if (!(k in a)) { print "MISSING " k; continue }
want = (b[k] == o) ? n : b[k]
if (a[k] != want) print "VERSION " k " " b[k] " -> " a[k] " (want " want ")"
}
for (k in a) if (!(k in b)) print "EXTRA " k " " a[k]
}' "$B" "$A" | sort)
if [ -n "$R" ]; then echo "[$sapi] extension surface changed:"$'\n'"$R" >&2; FAIL=1; fi
## feature lines must be byte-identical
R=$(diff <(grep '^FEAT ' "$B") <(grep '^FEAT ' "$A") || true)
if [ -n "$R" ]; then echo "[$sapi] feature surface changed:"$'\n'"$R" >&2; FAIL=1; fi
## effective ini (packaged php.ini + mods-available/*.ini): no directive may
## vanish or change value; directives NEW in the release are printed.
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = $0; next } $1 == "INI" { a[$2] = $0 }
END { for (k in b) { if (!(k in a)) print "REMOVED " b[k]; else if (a[k] != b[k]) print "CHANGED " b[k] " -> " a[k] } }' "$B" "$A" | sort)
if [ -n "$R" ]; then echo "[$sapi] effective ini changed:"$'\n'"$R" >&2; FAIL=1; fi
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = 1; next } $1 == "INI" && !($2 in b) { print " " $0 }' "$B" "$A")
[ -z "$R" ] || log "[$sapi] ini directives new in $LSPHP_SRC_PHP_VERSION (informational):"$'\n'"$R"
log "[$sapi] $(grep -c '^EXT ' "$A") extensions + $(grep -c '^ZEXT ' "$A") zend extensions, identical to packaged; $(grep -c '^FEAT ' "$A") feature lines identical"
done
[ "$FAIL" = 0 ] || die "the php.net build does NOT offer the same support as packaged lsphp${PHPVER} $COREV (details above)."
printf '%s\n' "$LSPHP_SRC_PHP_VERSION" >/etc/cac-php-version
cat >/etc/cac-lsphp-source <<EOF
source=php.net
php_version=$LSPHP_SRC_PHP_VERSION
tarball_sha256=$LSPHP_SRC_SHA256
signer_primary_fpr=$LSPHP_SRC_SIGNER
replaces_packaged=$CORE=$COREV (PHP $OLDV)
pecl_kept=${PECL_PKGS[*]:-none}
EOF
log "OK: lsphp${PHPVER} is now php.net $LSPHP_SRC_PHP_VERSION (was packaged $OLDV); /etc/cac-lsphp-source written"
## $OUT is a BuildKit bind mount (rw = writes discarded); nothing to clean up.
+47
View File
@@ -0,0 +1,47 @@
<?php
// lsphp-src-probe.php — prints the runtime "support surface" of the PHP that
// runs it, one fact per line, for lsphp-src-overlay.sh to diff before/after the
// php.net overlay. Run under BOTH SAPIs: `bin/php probe.php` and
// `bin/lsphp probe.php` (lsphp executes a script argument under the litespeed
// SAPI — measured; it is only `-m`/`-r` it does not understand).
// Line kinds: SAPI <name> | VERSION <v> | EXT <name> <version> | ZEXT <name> <version>
// INI <name> <value> | FEAT <key> <value>
// The overlay script relies on the stable "<KIND> <key> <rest>" shape; keep it.
echo 'SAPI ', PHP_SAPI, "\n";
echo 'VERSION ', PHP_VERSION, "\n";
echo 'FEAT ZTS ', PHP_ZTS, "\n";
echo 'FEAT DEBUG ', PHP_DEBUG, "\n";
echo 'FEAT INT_SIZE ', PHP_INT_SIZE, "\n";
foreach (get_loaded_extensions() as $e) {
echo 'EXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
}
foreach (get_loaded_extensions(true) as $e) {
echo 'ZEXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
}
foreach (ini_get_all(null, false) as $k => $v) {
echo 'INI ', $k, ' ', var_export($v, true), "\n";
}
if (function_exists('gd_info')) {
foreach (gd_info() as $k => $v) {
echo 'FEAT gd:', str_replace(' ', '_', $k), ' ', var_export($v, true), "\n";
}
}
if (function_exists('curl_version')) {
$c = curl_version();
echo 'FEAT curl:version ', $c['version'], "\n";
echo 'FEAT curl:ssl ', $c['ssl_version'], "\n";
echo 'FEAT curl:protocols ', implode(',', $c['protocols']), "\n";
}
echo 'FEAT openssl ', defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : 'none', "\n";
if (defined('INTL_ICU_VERSION')) echo 'FEAT icu ', INTL_ICU_VERSION, "\n";
if (class_exists('Imagick')) echo 'FEAT imagick:formats ', count(Imagick::queryFormats()), "\n";
if (function_exists('sodium_crypto_aead_aes256gcm_is_available')) echo 'FEAT sodium:aes256gcm ', var_export(sodium_crypto_aead_aes256gcm_is_available(), true), "\n";
echo 'FEAT password_algos ', implode(',', password_algos()), "\n";
echo 'FEAT hash_algos ', count(hash_algos()), "\n";
echo 'FEAT stream_wrappers ', implode(',', stream_get_wrappers()), "\n";
echo 'FEAT stream_filters ', implode(',', stream_get_filters()), "\n";
if (function_exists('mb_list_encodings')) echo 'FEAT mb_encodings ', count(mb_list_encodings()), "\n";
if (function_exists('opcache_get_status')) echo 'FEAT opcache:loaded 1', "\n";
if (class_exists('Redis')) echo 'FEAT redis:serializers ', implode(',', array_filter(['IGBINARY', 'MSGPACK', 'JSON'], fn($s) => defined("Redis::SERIALIZER_$s"))), "\n";
if (class_exists('Redis')) echo 'FEAT redis:compression ', implode(',', array_filter(['LZF', 'ZSTD', 'LZ4'], fn($s) => defined("Redis::COMPRESSION_$s"))), "\n";
if (class_exists('Memcached')) echo 'FEAT memcached:igbinary ', var_export(Memcached::HAVE_IGBINARY, true), ' msgpack ', var_export(Memcached::HAVE_MSGPACK, true), ' sasl ', var_export(Memcached::HAVE_SASL, true), "\n";
+223
View File
@@ -0,0 +1,223 @@
#!/bin/bash
## pkg-cache-repos.sh — BUILD-TIME ONLY, OPT-IN. Points dnf/apt at the internal
## package cache (nginx proxy_cache, CT130 "pkg-cache" on the Proxmox cluster)
## when PKG_CACHE is set; without it the repo files are left alone and builds
## use the upstream repositories as before.
##
## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh rpm # AlmaLinux/EPEL/Remi
## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh apt # Ubuntu + LiteSpeed
## pkg-cache-repos.sh release-install epel [remi] # GPG-verified release RPMs
## pkg-cache-repos.sh restore # undo everything above
##
## Every RUN that rewrites must `restore` before it ends: it puts rewritten files
## back, deletes files this script created, deletes any NEW repo/apt file that
## still references the cache, then FAILS if any reference is left — so no shipped
## layer carries the cache's LAN address (images run on hosts that can't reach it).
##
## Why: on 2026-10-21 (PHP security release) builds failed on Remi mirrors lagging
## hours behind origin, Remi's origin timing out over HTTPS, and an EPEL mirror
## with a broken certificate. The cache fetches from the ORIGINS over HTTPS (with
## origin/mirror fallbacks and stale-on-error), keeps metadata ~3 minutes and
## immutable .rpm/.deb files long-term.
##
## Integrity (security review 2026-10-21):
## * PKG_CACHE must be https://. The cache's certificate is issued by the
## name-constrained "pkg-cache internal CA" (keys/pkg-cache/ca.crt, valid only
## for 192.168.1.40 / pkg-cache.*). That CA is trusted ONLY by the rewritten
## repos (dnf sslcacert= bundle, apt per-host CaInfo) and only during the RUN —
## never added to the system trust store.
## * gpgcheck=1 is forced on every rewritten repo; repo_gpgcheck=1 on Remi and
## AlmaLinux, which sign repomd.xml (measured: repomd.xml.asc 200). EPEL
## publishes no repomd signature (404), so its metadata relies on HTTPS end
## to end (client->cache->dl.fedoraproject.org/mirrors, all verified).
## * release-install downloads epel-release / remi-release (from the cache, or
## from the HTTPS origin without PKG_CACHE), checks the package name, imports
## the signing keys committed in keys/pkg-cache/ and requires
## `rpm -K` "signatures OK" before installing with localpkg_gpgcheck=1.
## * The php.net version gates (EXPECT_PHP_MIN / EXPECT_PHP_VERSION) still fail
## a build that gets old PHP (replay of old-but-signed metadata).
##
## rpm: each rewritten baseurl keeps the upstream origin as a SECOND url, so if
## the cache dies mid-build dnf fails over to the origin by itself.
## apt: apt has no per-source failover; CI only sets PKG_CACHE after probing the
## cache (with the CA), and a build without PKG_CACHE uses upstream sources.
set -euo pipefail
MODE="${1:?usage: pkg-cache-repos.sh rpm|apt|release-install NAME...|restore}"
shift || true
STATE=/var/lib/pkg-cache-repos # saved originals + manifest + created files
SHARE="${PKG_CACHE_SHARE:-/usr/local/share/pkg-cache}" # ca.crt + RPM-GPG-KEY-*
C="${PKG_CACHE:-}"
C="${C%/}"
die() { echo "FATAL(pkg-cache-repos): $*" >&2; exit 1; }
if [ -n "$C" ]; then
case "$C" in
https://*) ;;
*) die "PKG_CACHE must be an https:// URL (plain http is refused), got '$C'" ;;
esac
HOSTPORT="${C#https://}"; HOSTPORT="${HOSTPORT%%/*}"
[ -s "$SHARE/ca.crt" ] || die "PKG_CACHE set but $SHARE/ca.crt (pkg-cache CA) is missing"
fi
# Files that may hold repo configuration; restore re-checks all of them.
repo_files() {
find /etc/yum.repos.d /etc/dnf /etc/apt -type f 2>/dev/null | sort || true
}
start_session() {
[ -d "$STATE" ] && return 0
mkdir -p "$STATE/orig"
repo_files > "$STATE/manifest" # what existed before this RUN touched anything
: > "$STATE/created"
printf '%s\n' "$HOSTPORT" > "$STATE/host"
}
save() { # keep the first-seen (pre-rewrite) copy
[ -e "$STATE/orig$1" ] && return 0
mkdir -p "$STATE/orig$(dirname "$1")"
cp -p "$1" "$STATE/orig$1"
}
created() { printf '%s\n' "$1" >> "$STATE/created"; }
if [ "$MODE" = restore ]; then
[ -d "$STATE" ] || exit 0
host=$(cat "$STATE/host")
(cd "$STATE/orig" && find . -type f) | while read -r rel; do
cat "$STATE/orig/$rel" > "/${rel#./}"
done
while read -r f; do [ -n "$f" ] && rm -f -- "$f"; done < "$STATE/created"
# A repo/apt file that appeared during this RUN and still names the cache
# (e.g. written by a package from the rewritten repos) is removed too.
repo_files | while read -r f; do
if ! grep -qxF -- "$f" "$STATE/manifest" && grep -qF -- "$host" "$f"; then
echo "pkg-cache-repos: removing new file that references the cache: $f"
rm -f -- "$f"
fi
done
left=$(repo_files | xargs -r grep -lF -- "$host" 2>/dev/null || true)
rm -rf "$STATE"
[ -z "$left" ] || die "cache address $host still present after restore in: $left"
echo "pkg-cache-repos: original repository files restored (no reference to $host left)"
exit 0
fi
if [ "$MODE" = release-install ]; then
[ $# -gt 0 ] || die "release-install needs: epel and/or remi"
tmp=$(mktemp -d)
curl_opts=(-fsS --proto '=https' --retry 5 --retry-all-errors --max-time 120)
if [ -n "$C" ]; then
start_session
cat /etc/pki/tls/certs/ca-bundle.crt "$SHARE/ca.crt" > "$STATE/ca-bundle.pem"
curl_opts+=(--cacert "$STATE/ca-bundle.pem")
fi
files=()
for name in "$@"; do
case "$name" in
epel) pkg=epel-release key=RPM-GPG-KEY-EPEL-10
path=/epel/epel-release-latest-10.noarch.rpm
origin=https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm ;;
remi) pkg=remi-release key=RPM-GPG-KEY-remi2024
path=/remi/enterprise/remi-release-10.rpm
origin=https://rpms.remirepo.net/enterprise/remi-release-10.rpm ;;
*) die "unknown release package '$name'" ;;
esac
url="$origin"; [ -n "$C" ] && url="$C$path"
f="$tmp/$pkg.rpm"
curl "${curl_opts[@]}" -o "$f" "$url" || die "download failed: $url"
rpm --import "$SHARE/$key" || die "cannot import $SHARE/$key"
got=$(rpm -qp --nosignature --qf '%{NAME}' "$f" 2>/dev/null || true)
[ "$got" = "$pkg" ] || die "$url is '$got', expected $pkg"
chk=$(rpm -K "$f" 2>&1 || true)
[[ "$chk" == *": digests signatures OK" ]] || die "signature check failed for $url: $chk"
echo "pkg-cache-repos: $pkg verified ($chk) from $url"
files+=("$f")
done
dnf install -y --setopt=localpkg_gpgcheck=1 "${files[@]}"
rm -rf "$tmp"
exit 0
fi
if [ -z "$C" ]; then
echo "pkg-cache-repos($MODE): PKG_CACHE not set - using upstream repositories"
exit 0
fi
rewrite_rpm() {
local f="$1"
grep -q -E '^#? ?baseurl=https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" || return 0
save "$f"
# Per [section]: a (commented or live) baseurl on a known origin becomes
# "baseurl=<cache>/<prefix>/<path> <origin>/<path>"; that section's
# mirrorlist/metalink is commented out (dnf prefers them over baseurl) and it
# gets sslcacert=<system bundle + pkg-cache CA>, gpgcheck=1 and, for repos
# that sign repomd.xml, repo_gpgcheck=1. Other sections are untouched.
awk -v C="$C" -v BUNDLE="$STATE/ca-bundle.pem" '
function flush( i) {
for (i = 1; i <= n; i++) {
if (hit && buf[i] ~ /^(mirrorlist|metalink)=/) buf[i] = "#" buf[i]
if (hit && buf[i] ~ /^(sslcacert|gpgcheck|repo_gpgcheck)[ \t]*=/) continue
print buf[i]
}
if (hit) {
print "sslcacert=" BUNDLE
print "gpgcheck=1"
if (signed) print "repo_gpgcheck=1"
}
n = 0; hit = 0; signed = 0
}
function rw(line, re, prefix, origin, sig, p) {
if (match(line, re)) {
p = substr(line, RSTART + RLENGTH); sub(/[ \t].*$/, "", p)
hit = 1; if (sig) signed = 1
return "baseurl=" C "/" prefix "/" p " " origin "/" p
}
return line
}
/^\[/ { flush() }
{
l = $0
if (l ~ /^#? ?baseurl=/) {
l = rw(l, "^#? ?baseurl=https?://repo\\.almalinux\\.org/almalinux/", "almalinux", "https://repo.almalinux.org/almalinux", 1)
l = rw(l, "^#? ?baseurl=https?://download\\.example/pub/epel/", "epel", "https://dl.fedoraproject.org/pub/epel", 0)
l = rw(l, "^#? ?baseurl=https?://rpms\\.remirepo\\.net/", "remi", "https://rpms.remirepo.net", 1)
}
buf[++n] = l
}
END { flush() }
' "$f" > "$f.pkgcache" && cat "$f.pkgcache" > "$f" && rm -f "$f.pkgcache"
}
rewrite_apt() {
local f="$1"
grep -q -E 'https?://([a-z]{2}\.)?(archive|security)\.ubuntu\.com/|https?://rpms\.litespeedtech\.com/' "$f" || return 0
save "$f"
sed -i -E \
-e "s%https?://(archive|[a-z]{2}\.archive)\.ubuntu\.com/ubuntu/?%$C/ubuntu/%g" \
-e "s%https?://security\.ubuntu\.com/ubuntu/?%$C/ubuntu-security/%g" \
-e "s%https?://rpms\.litespeedtech\.com/%$C/litespeed/%g" \
"$f"
}
case "$MODE" in
rpm)
start_session
cat /etc/pki/tls/certs/ca-bundle.crt "$SHARE/ca.crt" > "$STATE/ca-bundle.pem"
for f in /etc/yum.repos.d/*.repo; do [ -f "$f" ] && rewrite_rpm "$f"; done
echo "pkg-cache-repos(rpm): repos now via $C:"
grep -h '^baseurl=' /etc/yum.repos.d/*.repo | grep -F "$C/" | sort -u | sed 's/^/ /'
;;
apt)
start_session
conf=/etc/apt/apt.conf.d/99pkg-cache
# Trust the pkg-cache CA for the cache host ONLY (apt per-host option).
printf 'Acquire::https::%s::CaInfo "%s";\n' "${HOSTPORT%%:*}" "$SHARE/ca.crt" > "$conf"
created "$conf"
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
[ -f "$f" ] && rewrite_apt "$f"
done
echo "pkg-cache-repos(apt): sources now via $C:"
grep -h -E '^(deb|URIs:)' /etc/apt/sources.list /etc/apt/sources.list.d/* 2>/dev/null | grep -F "$C/" | sort -u | sed 's/^/ /'
;;
*) die "unknown mode '$MODE'" ;;
esac
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
## lsphp-abi-gate.test.sh — proves scripts/lsphp-abi-gate.sh can FAIL.
##
## The gate is what lets the emergency php.net lsphp build keep LiteSpeed's
## packaged PECL .so files. A gate that always says "compatible" would be worse
## than none, so every rule is exercised against synthetic header trees whose
## outcome is known: three layouts that must pass (unchanged; new member in
## padding; member appended to a *_globals struct) and five that must fail
## (member inserted mid-struct; member removed; member retyped/resized; a
## non-globals struct grown at the end; a struct embedded in another growing).
## Plus: a probe that extracts too few members must refuse, not pass.
##
## Needs gcc + pahole (dwarves). Exits 0 only if every case behaves.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
GATE="$HERE/lsphp-abi-gate.sh"
for t in gcc pahole; do
command -v "$t" >/dev/null 2>&1 || { echo "SKIP-FAIL: $t not installed (apt-get install gcc dwarves)" >&2; exit 1; }
done
T=$(mktemp -d)
trap 'rm -rf "$T"' EXIT
## $1 = dir, $2 = body of struct _zend_object, $3 = body of _zend_executor_globals,
## $4 = extra declarations
mk() {
mkdir -p "$1/php/main"
cat >"$1/php/main/php.h" <<EOF
#include <stdint.h>
#include <stdbool.h>
struct _zend_object { $2 };
struct _zend_executor_globals { $3 };
struct _holder { struct _zend_object obj; int after; };
typedef struct { int a; void *b; } anon_cache;
$4
struct _zend_executor_globals executor_globals;
struct _holder holder_sym;
anon_cache anon_sym;
EOF
}
BASE_OBJ='uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers;'
BASE_EG='void *a; int b; void *reserved[4];'
mk "$T/old" "$BASE_OBJ" "$BASE_EG" ''
PASS=0; FAILN=0
expect() { # $1 = pass|fail, $2 = name, $3 = new dir
local rc=0
ABI_MIN_MEMBERS=5 bash "$GATE" "$T/old" "$3" "$T/w-$2" >"$T/out-$2" 2>&1 || rc=$?
if { [ "$1" = pass ] && [ "$rc" = 0 ]; } || { [ "$1" = fail ] && [ "$rc" != 0 ]; }; then
PASS=$((PASS + 1)); echo "ok - $2 ($1, rc=$rc)"
else
FAILN=$((FAILN + 1)); echo "FAIL - $2: expected $1, got rc=$rc"; sed 's/^/ /' "$T/out-$2"
fi
}
mk "$T/same" "$BASE_OBJ" "$BASE_EG" ''
expect pass unchanged "$T/same"
## bool after `flag` lands in the 3 padding bytes before the pointer `ce`
mk "$T/pad" 'uint32_t gc; uint32_t handle; bool flag; bool newflag; void *ce; void *handlers;' "$BASE_EG" ''
expect pass new-member-in-padding "$T/pad"
mk "$T/eg" "$BASE_OBJ" 'void *a; int b; void *reserved[4]; void *delayed_oom;' ''
expect pass globals-appended "$T/eg"
mk "$T/mid" 'uint32_t gc; void *inserted; uint32_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
expect fail member-inserted-mid-struct "$T/mid"
mk "$T/rm" 'uint32_t gc; uint32_t handle; bool flag; void *handlers;' "$BASE_EG" ''
expect fail member-removed "$T/rm"
mk "$T/retype" 'uint32_t gc; uint64_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
expect fail member-resized "$T/retype"
## appended to a NON-globals struct: no old member moves, but sizeof grows —
## extensions that allocate sizeof(zend_object)+x would under-allocate
mk "$T/grow" 'uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers; void *extra;' "$BASE_EG" ''
expect fail non-globals-struct-grew "$T/grow"
## same change seen from the embedding struct: the report must name _holder.after
## as MOVED, i.e. the container check works on its own, not only via the size rule
if grep -E '^BAD moved member _holder int_after#1' "$T/out-non-globals-struct-grew" >/dev/null; then
PASS=$((PASS + 1)); echo "ok - embedded-struct-shifts-container (reported _holder.after moved)"
else
FAILN=$((FAILN + 1)); echo "FAIL - embedded-struct-shifts-container: _holder.after not reported"; sed 's/^/ /' "$T/out-non-globals-struct-grew"
fi
## the anonymous typedef is tracked too
mkdir -p "$T/anon/php/main"
sed 's/typedef struct { int a; void \*b; } anon_cache;/typedef struct { int a; int z; void *b; long c; } anon_cache;/' "$T/old/php/main/php.h" >"$T/anon/php/main/php.h"
expect fail anonymous-typedef-changed "$T/anon"
## a probe that finds (almost) nothing must refuse
rc=0; ABI_MIN_MEMBERS=1000 bash "$GATE" "$T/old" "$T/same" "$T/w-min" >"$T/out-min" 2>&1 || rc=$?
if [ "$rc" != 0 ] && grep 'probe is broken' "$T/out-min" >/dev/null; then PASS=$((PASS + 1)); echo "ok - too-few-members-refused (rc=$rc)"
else FAILN=$((FAILN + 1)); echo "FAIL - too-few-members-refused: rc=$rc"; cat "$T/out-min"; fi
echo "lsphp-abi-gate: $PASS passed, $FAILN failed"
[ "$FAILN" = 0 ]