diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 67ce4e7..1ee2a83 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -84,6 +84,20 @@ jobs: id: bust run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.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 + run: | + set -euo pipefail + v="${{ matrix.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 + echo "php.net current ${branch}: ${min}" + echo "min=${min}" >> "$GITHUB_OUTPUT" + - name: Login to Gitea uses: docker/login-action@v3 with: @@ -99,6 +113,7 @@ jobs: 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' || '' }} @@ -129,6 +144,20 @@ jobs: id: bust run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.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 + run: | + set -euo pipefail + v="${{ matrix.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 + echo "php.net current ${branch}: ${min}" + echo "min=${min}" >> "$GITHUB_OUTPUT" + - name: Login to Gitea uses: docker/login-action@v3 with: @@ -145,6 +174,7 @@ jobs: 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' || '' }} diff --git a/Dockerfile b/Dockerfile index ea01e70..10046c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,6 +4,8 @@ ARG PHPVER=83 # 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= # Install repos, update, install only needed packages, clean up in one layer RUN dnf install -y \ @@ -11,8 +13,12 @@ RUN dnf install -y \ 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. Packages stay GPG-checked (gpgcheck=1). - sed -i -e 's/^#baseurl=/baseurl=/' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \ + # 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 && \ + printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \ dnf update -y && \ 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 && \ @@ -31,6 +37,11 @@ RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \ 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/ && \ /scripts/install-php$PHPVER.sh && \ + # 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/* diff --git a/Dockerfile.fpm b/Dockerfile.fpm index 0c16f90..3733d3f 100644 --- a/Dockerfile.fpm +++ b/Dockerfile.fpm @@ -4,6 +4,8 @@ ARG PHPVER=83 # 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= # Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer RUN dnf install -y \ @@ -11,8 +13,12 @@ RUN dnf install -y \ 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. Packages stay GPG-checked (gpgcheck=1). - sed -i -e 's/^#baseurl=/baseurl=/' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \ + # 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 && \ + printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \ dnf update -y && \ 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 && \ @@ -30,6 +36,11 @@ RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \ # Create needed dirs, install PHP, clean up (no SSL cert, no httpd) RUN mkdir -p /run/php-fpm/ && \ /scripts/install-php$PHPVER.sh && \ + # 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/*