Files
cloud-apache-container/.gitea/workflows/build-push.yaml
T
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

474 lines
21 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Cloud Apache Container
run-name: ${{ gitea.actor }} pushed a change to trunk
on:
push:
branches:
- trunk
workflow_dispatch:
jobs:
# Shell gate. Runs FIRST and costs seconds; the images below do not depend on
# it (a red job here does not block a push that is otherwise fine), but it is
# the only place the ENTRYPOINT logic is executed at all. The .phpt suite and
# the Dockerfile's `lsphp -i` probe both test the extension, and both were
# green for the release whose entrypoint declared that same extension
# missing — see scripts/tests/lsphp-info-probe.test.sh for what went wrong
# and why it needed a test outside the image build to catch it.
Shell-Checks:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# Runner images differ on whether they are root and whether sudo exists.
- name: Install shellcheck
run: |
if ! command -v shellcheck >/dev/null 2>&1; then
(apt-get update && apt-get install -y shellcheck) ||
(sudo apt-get update && sudo apt-get install -y shellcheck)
fi
shellcheck --version
- name: Syntax check every shell script
run: |
set -euo pipefail
find scripts ext -name '*.sh' -print0 | xargs -0 -n1 bash -n
# Deliberately NOT repo-wide. The older scripts (entrypoint.sh,
# entrypoint-fpm.sh, create-vhost.sh, create-php-config.sh,
# detect-memory.sh) carry pre-existing SC2154/SC2027 findings that predate
# this job; listing them here would make the gate red on arrival and
# therefore ignored. This is the set that is clean today — the scripts
# that run `set -o pipefail` plus the tests. Add files as they are fixed;
# do not add one that is not yet clean.
- name: shellcheck (warnings and above, on the clean set)
run: |
shellcheck -S warning \
scripts/entrypoint-lsphp.sh \
scripts/entrypoint-litespeed.sh \
scripts/entrypoint-shared-ols.sh \
scripts/render-shared-ols-config.sh \
scripts/ols-htaccess-watcher.sh \
scripts/create-vhost-litespeed.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/tests/lsphp-info-probe.test.sh \
scripts/tests/lsphp-abi-gate.test.sh
- name: lsphp probe regression test
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:
runs-on: ubuntu-latest
strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix:
phpver: [74, 80, 81, 82, 83, 84, 85]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
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
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
CACHE_BUST=${{ steps.bust.outputs.value }}
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
Build-FPM-Images:
runs-on: ubuntu-latest
strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix:
phpver: [74, 80, 81, 82, 83, 84, 85]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
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
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push FPM Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.fpm
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
CACHE_BUST=${{ steps.bust.outputs.value }}
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
Build-LiteSpeed-Images:
runs-on: ubuntu-latest
strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix:
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
# images stop at older OLS releases for those PHP versions, and the
# cac-litespeed tier is a paid premium offering: 8.1+ is the
# modernization story we're selling.
phpver: [81, 82, 83, 84, 85]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
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
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
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
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.litespeed
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
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
# 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
# test when LiteSpeed publishes lsphp81 on a newer OLS release.
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:latest' || '' }}
Build-LSPHP-Images:
runs-on: ubuntu-latest
strategy:
# One PHP version failing (e.g. an EOL stream) must not cancel the
# security builds of the others.
fail-fast: false
matrix:
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
# backend for the shared-ols tier and shares the litespeed prebuilt
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
# with Build-LiteSpeed-Images.
phpver: [81, 82, 83, 84, 85]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
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
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
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
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.lsphp
platforms: linux/amd64
push: true
build-args: |
PHPVER=${{ matrix.phpver }}
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
# prebuilt base, same lsphp binaries. Bump both together.
tags: |
repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }}
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:latest' || '' }}
Build-Shared-httpd:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push Shared httpd Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.shared-httpd
platforms: linux/amd64
push: true
tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
Build-Shared-OLS:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
- name: Build and Push Shared OLS Image
uses: docker/build-push-action@v6
with:
file: ./Dockerfile.shared-ols
platforms: linux/amd64
push: true
# Single image (runs no PHP). PHPVER just selects the OLS base tag;
# pinned to 83 / OLS 1.8.4 to match the rest of the litespeed family.
build-args: |
PHPVER=83
OLS_VERSION=1.8.4
tags: |
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest