From c7c6a8cac9c3d9d8c505b1ff0b122769dbd0d962 Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:49:25 +0100 Subject: [PATCH 1/7] Add Docker Hardened Images pilot for the min flavor (-hardened-test) Applies the decisions from pimcore/DevOps-Tasks#84: - Dockerfile.dhi: experimental `min` image built from dhi.io/php (Debian 13). Extensions missing in DHI (bcmath exif gd pcntl pdo_mysql sockets zip, amqp) are built with phpize in the -dev stage and copied, together with the shared libraries the distroless runtime lacks, into the -fpm runtime. The php CLI comes from the DHI CLI image. Runs as uid 65532, no shell, no package manager. - hardened-test.yml: manual-only workflow that builds the regular and the DHI min image (amd64), runs the same extension/composer smoke checks as test.yml, scans both with Trivy and writes a comparison to the step summary. Pushes php-min-v5-hardened-test only with publish=true. - README: state that -hardened publishing is paused since 2026-07-31 and document the experimental -hardened-test tag and its limits (min only: DHI has no ghostscript, ImageMagick, ffmpeg, poppler, LibreOffice or Chromium). release.yml is unchanged. Co-Authored-By: Claude --- .github/workflows/hardened-test.yml | 154 +++++++++++++++++++++++++++ Dockerfile.dhi | 157 ++++++++++++++++++++++++++++ README.md | 29 ++++- 3 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/hardened-test.yml create mode 100644 Dockerfile.dhi diff --git a/.github/workflows/hardened-test.yml b/.github/workflows/hardened-test.yml new file mode 100644 index 00000000..1d77beed --- /dev/null +++ b/.github/workflows/hardened-test.yml @@ -0,0 +1,154 @@ +name: Hardened test image (DHI pilot) + +# EXPERIMENTAL. Builds the `min` flavor from Docker Hardened Images (Dockerfile.dhi), +# compares its Trivy result with the regular `min` image and -- only when explicitly +# requested -- pushes it as `php-min-v5-hardened-test`. amd64 only. +# Manual dispatch only; never runs on schedule, push or pull_request. +# Context: https://github.com/pimcore/DevOps-Tasks/issues/84 + +on: + workflow_dispatch: + inputs: + php: + description: 'PHP minor version (must exist as dhi.io/php:-debian13-{dev,fpm} and as php:-fpm-trixie)' + required: false + default: '8.5' + type: string + publish: + description: 'Push pimcore/pimcore:php-min-v5-hardened-test to Docker Hub. Off by default: build, test and scan only.' + required: false + default: false + type: boolean + +env: + IMAGE_NAME: pimcore/pimcore + +permissions: + contents: read + +jobs: + dhi-min: + name: "Build, test and scan DHI min (php ${{ inputs.php }})" + runs-on: ubuntu-22.04 + if: github.repository == 'pimcore/docker' + + steps: + - uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + # dhi.io accepts the Docker Hub account credentials (password or access token). + - name: Login to dhi.io + run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login dhi.io -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + + - name: Login to DockerHub Registry + if: ${{ inputs.publish }} + run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + + - name: Install Trivy + run: | + set -euxo pipefail + sudo apt-get update + sudo apt-get install -y wget curl apt-transport-https gnupg lsb-release jq + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install -y trivy + + - name: Build regular min image (baseline) + run: | + set -eux + docker build --load --pull \ + --target pimcore_php_min \ + --build-arg PHP_VERSION="${{ inputs.php }}" \ + --build-arg DEBIAN_VERSION=trixie \ + --tag plain-min . + + - name: Build DHI min image + run: | + set -eux + docker build --load --pull \ + --file Dockerfile.dhi \ + --target pimcore_php_min \ + --build-arg PHP_VERSION="${{ inputs.php }}" \ + --tag dhi-min . + + # The image has no shell: every check is a direct `php` / `php-fpm` / `composer` invocation. + - name: Smoke checks + run: | + set -eux + + # same extension list as test.yml + for ext in amqp bcmath exif gd intl pcntl pdo_mysql sockets zip; do + docker run --rm dhi-min php -m | grep -Fx "$ext" + done + docker run --rm dhi-min php -m | grep -Fx 'Zend OPcache' || echo "::warning::Zend OPcache is not loaded in the CLI of dhi-min (check how DHI enables opcache)" + + # gd must have jpeg, webp and freetype support + docker run --rm dhi-min php -r '$i = gd_info(); foreach (["JPEG Support","WebP Support","FreeType Support"] as $k) { if (empty($i[$k])) { fwrite(STDERR, "gd: $k missing\n"); exit(1); } }' + + # the CLI binary and the fpm binary must agree on the extension ABI + docker run --rm dhi-min php -i | grep -E '^(extension_dir|Thread Safety|Debug Build) ' | tee cli.abi + docker run --rm dhi-min php-fpm -i | grep -E '^(extension_dir|Thread Safety|Debug Build) ' | tee fpm.abi + diff cli.abi fpm.abi + + # fpm config (incl. our zz-www.conf env overrides) is valid and loads all extensions + docker run --rm dhi-min php-fpm -t + + # runs as the DHI nonroot user + docker run --rm dhi-min php -r 'exit(posix_getuid() === 65532 ? 0 : 1);' + + # pimcore.ini env substitution works + docker run --rm -e PHP_MEMORY_LIMIT=333M dhi-min php -r 'exit(ini_get("memory_limit") === "333M" ? 0 : 1);' + + # composer works without root and without a shell + docker run --rm dhi-min composer --version + docker run --rm dhi-min composer create-project pimcore/skeleton:2026.x-dev /tmp/pimcore --no-scripts --no-interaction --prefer-dist --ignore-platform-reqs + + - name: Trivy scan - regular min vs DHI min + run: | + set -euo pipefail + { + echo "## DHI pilot: regular min vs DHI min (php ${{ inputs.php }}, amd64)" + echo "" + echo "| Image | CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN | total | fixable |" + echo "|-------|----------|------|--------|-----|---------|-------|---------|" + } > summary.md + for img in plain-min dhi-min; do + trivy image --pkg-types os,library --format json -o "trivy-${img}.json" "$img" + trivy image --pkg-types os,library --format table -o "trivy-${img}.txt" "$img" + jq -r --arg img "$img" ' + [.Results[]?.Vulnerabilities[]?] as $v + | [$img] + + (["CRITICAL","HIGH","MEDIUM","LOW","UNKNOWN"] | map(. as $s | ($v | map(select(.Severity == $s)) | length))) + + [($v | length), ($v | map(select(.FixedVersion != null and .FixedVersion != "")) | length)] + | "| " + (map(tostring) | join(" | ")) + " |"' "trivy-${img}.json" >> summary.md + done + { + echo "" + echo "Counts are vulnerability rows (one per package and CVE), all severities, fixable and unfixable, OS and library packages." + echo "Full tables: \`trivy-plain-min.txt\` and \`trivy-dhi-min.txt\` in the \`trivy-dhi-pilot\` artifact." + } >> summary.md + cat summary.md >> "$GITHUB_STEP_SUMMARY" + cat summary.md + + - name: Upload Trivy results + if: always() + uses: actions/upload-artifact@v7 + with: + name: trivy-dhi-pilot_php${{ inputs.php }} + path: | + trivy-*.json + trivy-*.txt + summary.md + if-no-files-found: ignore + + - name: Publish -hardened-test tag + if: ${{ inputs.publish }} + run: | + set -eux + TAG="${IMAGE_NAME}:php${{ inputs.php }}-min-v5-hardened-test" + docker tag dhi-min "$TAG" + docker push "$TAG" + echo "Published ${TAG} (amd64 only, experimental)" >> "$GITHUB_STEP_SUMMARY" diff --git a/Dockerfile.dhi b/Dockerfile.dhi new file mode 100644 index 00000000..3df2c585 --- /dev/null +++ b/Dockerfile.dhi @@ -0,0 +1,157 @@ +# syntax=docker/dockerfile:1 +# +# EXPERIMENTAL -- Docker Hardened Images (DHI) pilot for the `min` flavor. +# Published only as `php-min-v5-hardened-test` from .github/workflows/hardened-test.yml. +# Not part of the regular release matrix. See README.md "Hardened images". +# +# Differences to the regular Dockerfile: +# - base is dhi.io/php (Debian 13 only, Apache-2.0, needs `docker login dhi.io`) +# - runtime is distroless: no shell, no apt, runs as uid 65532 (nonroot) +# - PHP lives under /opt/php-; /usr/local/etc/php is a symlink to its ini dir +# - no docker-php-ext-* helpers, so extensions are built with phpize in the -dev stage +# and copied (with the shared libraries they need) into the runtime stage +# - the -fpm runtime is built with --disable-cli; the `php` CLI is taken from the +# DHI CLI runtime image of the same version + +ARG PHP_VERSION="8.5" + +# --------------------------------------------------------------------------- +# builder: root, bash, apt, gcc, pecl, phpize, PHP source in $PHP_SRC_DIR +# --------------------------------------------------------------------------- +FROM dhi.io/php:${PHP_VERSION}-debian13-dev AS builder + +ARG PHP_VERSION + +# Build dependencies. The -dev image's apt points at dhi.io/deb (hardened rebuilds of +# Debian packages) and falls back to upstream Debian for everything else. +# libbrotli1/libbrotli-dev are pinned to one version: the DHI repo intermittently serves +# them out of sync, which breaks libfreetype-dev (docker-hardened-images/catalog#677, #682). +RUN set -eux; \ + apt-get update; \ + BROTLI="$(apt-cache policy libbrotli1 | awk '/Candidate:/ {print $2}')"; \ + apt-get install -y --no-install-recommends \ + "libbrotli1=${BROTLI}" \ + "libbrotli-dev=${BROTLI}" \ + libfreetype-dev \ + libjpeg62-turbo-dev \ + libpng-dev \ + librabbitmq-dev \ + libwebp-dev \ + libzip-dev \ + ; \ + rm -rf /var/lib/apt/lists/* + +# Extensions bundled with PHP but not compiled into the DHI image. +# Each one is built out-of-tree with phpize and installed into `php-config --extension-dir`. +RUN set -eux; \ + for ext in bcmath exif pcntl sockets zip; do \ + ( cd "${PHP_SRC_DIR}/ext/${ext}" && phpize && ./configure && make -j"$(nproc)" && make install ); \ + done; \ + ( cd "${PHP_SRC_DIR}/ext/gd" && phpize \ + && ./configure --with-jpeg --with-webp --with-freetype \ + && make -j"$(nproc)" && make install ); \ + ( cd "${PHP_SRC_DIR}/ext/pdo_mysql" && phpize \ + && ./configure --with-pdo-mysql=mysqlnd \ + && make -j"$(nproc)" && make install ) + +# PECL extension: amqp (librabbitmq from the DHI apt repo). +# `pecl download` + phpize avoids the interactive prompt of `pecl install amqp`. +RUN set -eux; \ + cd /tmp; \ + pecl download amqp; \ + mkdir amqp && tar -xzf amqp-*.tgz -C amqp --strip-components=1; \ + cd amqp && phpize && ./configure && make -j"$(nproc)" && make install; \ + rm -rf /tmp/amqp /tmp/amqp-*.tgz + +# --------------------------------------------------------------------------- +# runtime-probe: the file list of the runtime image, used to copy only the shared +# libraries the runtime does NOT already ship (keeps the DHI base untouched) +# --------------------------------------------------------------------------- +FROM dhi.io/php:${PHP_VERSION}-debian13-fpm AS runtime-probe + +FROM builder AS payload + +ARG PHP_VERSION + +COPY --from=runtime-probe /usr/lib /probe/usr/lib + +# Stage everything the runtime needs under /out: +# - the extension .so files +# - every shared library they link against that the runtime image lacks +# - one ini file per extension in the scan dir ($PHP_INI_DIR/conf.d) +RUN set -eux; \ + EXTDIR="$(php-config --extension-dir)"; \ + mkdir -p "/out${EXTDIR}" "/out${PHP_PREFIX}/etc/php/conf.d"; \ + cp "${EXTDIR}"/*.so "/out${EXTDIR}/"; \ + for so in "${EXTDIR}"/*.so; do \ + ldd "$so" | awk '/=> \// {print $3}'; \ + done | sort -u | while read -r lib; do \ + # merged-/usr: /lib//x -> /usr/lib//x + norm="$(printf '%s' "$lib" | sed -E 's#^/lib/#/usr/lib/#')"; \ + case "$norm" in /usr/lib/*) ;; *) echo "unexpected library path: $lib" >&2; exit 1;; esac; \ + if [ ! -e "/probe${norm}" ]; then \ + mkdir -p "/out$(dirname "$norm")"; \ + cp -L "$lib" "/out${norm}"; \ + fi; \ + done; \ + for ext in bcmath exif gd pcntl pdo_mysql sockets zip amqp; do \ + echo "extension=${ext}.so" > "/out${PHP_PREFIX}/etc/php/conf.d/10-${ext}.ini"; \ + done; \ + find /out -type f | sort + +# --------------------------------------------------------------------------- +# cli: the DHI CLI runtime of the same version, source of the `php` binary +# --------------------------------------------------------------------------- +FROM dhi.io/php:${PHP_VERSION}-debian13 AS cli + +# --------------------------------------------------------------------------- +# runtime: distroless, nonroot (uid 65532), no shell, no package manager +# --------------------------------------------------------------------------- +FROM dhi.io/php:${PHP_VERSION}-debian13-fpm AS pimcore_php_min + +ARG PHP_VERSION + +COPY --from=payload /out/ / +COPY --from=cli /opt/php-${PHP_VERSION}/bin/php /opt/php-${PHP_VERSION}/bin/php + +COPY files/conf/php/php.ini /opt/php-${PHP_VERSION}/etc/php/conf.d/20-pimcore.ini +COPY files/conf/php-fpm/php-fpm.conf /opt/php-${PHP_VERSION}/etc/php-fpm.d/zz-www.conf + +# env php.ini +ENV PHP_TIMEZONE="UTC" +ENV PHP_MEMORY_LIMIT="256M" +ENV PHP_POST_MAX_SIZE="100M" +ENV PHP_UPLOAD_MAX_FILESIZE="100M" +ENV PHP_DISPLAY_STARTUP_ERRORS=1 +ENV PHP_MAX_EXECUTION_TIME="30" +ENV PHP_ERROR_REPORTING="E_ALL" +ENV PHP_EXPOSE_PHP="Off" + +# opcache settings +ENV OPCACHE_ENABLE=1 +ENV OPCACHE_ENABLE_CLI=0 +ENV OPCACHE_MEMORY_CONSUMPTION=128 +ENV OPCACHE_MAX_ACCELERATED_FILES=10000 +ENV OPCACHE_VALIDATE_TIMESTAMPS=1 +ENV OPCACHE_CONSISTENCY_CHECKS=0 + +# fpm settings +ENV PHP_FPM_LISTEN=0.0.0.0:9000 +ENV PHP_FPM_PM=dynamic +ENV PHP_FPM_PM_MAX_CHILDREN=5 +ENV PHP_FPM_PM_START_SERVERS=2 +ENV PHP_FPM_PM_MAX_SPARE_SERVERS=3 +ENV PHP_FPM_PM_MIN_SPARE_SERVERS=1 +ENV PHP_FPM_PM_MAX_REQUESTS=10000 + +# Composer runs as uid 65532 here (not root), so it needs a writable home. +ENV COMPOSER_HOME=/tmp/composer +ENV COMPOSER_MEMORY_LIMIT=-1 +COPY --from=composer/composer:2-bin /composer /usr/local/bin/composer + +WORKDIR /var/www/html + +# DHI sets ENTRYPOINT to the php-fpm binary. Reset it so `docker run php bin/console` +# works exactly like with the regular images (there is no shell for an entrypoint script). +ENTRYPOINT [] +CMD ["php-fpm"] diff --git a/README.md b/README.md index ffd1cf7e..b4013115 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,15 @@ Additionally we're offering 2 special tag suffixes: We're also offering special tags for specific PHP versions, e.g. `php8.2.5-v2.0`. ## Hardened images -For our stable release tags we publish each image in two flavors so you can choose your trade-off: + +> **Status (2026-10-28): publishing of `-hardened` tags is paused.** The last `-hardened` tags were +> pushed on 2026-07-31 and are not refreshed. Use the plain tags; they are the supported images and +> are rebuilt on every scheduled run with all Debian security updates available at build time. +> Hardened publishing resumes once there is a strategy that delivers it consistently +> (see [pimcore/DevOps-Tasks#84](https://github.com/pimcore/DevOps-Tasks/issues/84)). +> The description below documents what the `-hardened` flavor is when it is enabled. + +For our stable release tags the release workflow can publish each image in two flavors so you can choose your trade-off: - **plain** (default, unsuffixed) – the image exactly as built from the Dockerfile, e.g. `php8.5-debug-v5`. Published as-is; it may carry known OS-level CVEs. - **hardened** (`-hardened` suffix) – the same image with OS-level CVEs patched in via [Copacetic (Copa)](https://github.com/project-copacetic/copacetic), e.g. `php8.5-debug-v5-hardened`. @@ -69,6 +77,25 @@ oras pull ghcr.io/pimcore/pimcore@ -o ./sbom Attaching is best-effort (it's skipped if the registry rejects OCI referrers), so if `oras discover` comes up empty, fall back to the guaranteed copy: download the `sboms_*` artifact from the corresponding release workflow run. +### Experimental: `-hardened-test` (Docker Hardened Images pilot) + +`pimcore/pimcore:php8.5-min-v5-hardened-test` is an **experimental, unsupported** image built from +[Docker Hardened Images](https://docs.docker.com/dhi/) (`dhi.io/php`, Apache-2.0) instead of the +official `php` image, see [`Dockerfile.dhi`](Dockerfile.dhi). It exists to measure whether a hardened +base image is a better path than Copa. It may change or disappear without notice. + +- **`min` flavor only, PHP 8.5, Debian 13, amd64 only.** No `default`, `max`, `debug` or `supervisord` + flavor: DHI does not provide ghostscript, ImageMagick, ffmpeg, poppler, LibreOffice or Chromium, so + those tools would come unpatched from plain Debian and a distroless runtime cannot install them. +- **Distroless runtime:** no shell, no package manager, runs as uid/gid `65532` (`nonroot`), not root. + Mounted volumes must be writable by that user. Use `docker debug` for troubleshooting. +- PHP lives under `/opt/php-8.5`; `/usr/local/etc/php` is a symlink to its ini directory, so + `conf.d` overrides work as with the regular images. +- Same extensions as `min` (`amqp bcmath exif gd intl pcntl pdo_mysql sockets zip`), same + `php.ini`/`php-fpm` environment variables, `php` CLI and `composer` included. +- Built and published only by the manual workflow `hardened-test.yml`; it is not part of the release + matrix, has no `-latest`/ghcr tags and no attached SBOM. + ## Known CVEs For every **published stable release image** we publish a CVE and hardening report, From 3ac8887afac406c88f686c3bf467db6ec3ce619d Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:56:08 +0100 Subject: [PATCH 2/7] hardened-test: also run build/test/scan on pull requests touching the pilot workflow_dispatch is only registered once the file is on the default branch, so the pilot could not be exercised from the PR. Run the build + smoke checks + Trivy comparison on pull_request (paths-filtered); publishing stays limited to a manual dispatch with publish=true. Co-Authored-By: Claude --- .github/workflows/hardened-test.yml | 28 +++++++++++++++++++--------- README.md | 5 +++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/hardened-test.yml b/.github/workflows/hardened-test.yml index 1d77beed..2e81276d 100644 --- a/.github/workflows/hardened-test.yml +++ b/.github/workflows/hardened-test.yml @@ -3,10 +3,17 @@ name: Hardened test image (DHI pilot) # EXPERIMENTAL. Builds the `min` flavor from Docker Hardened Images (Dockerfile.dhi), # compares its Trivy result with the regular `min` image and -- only when explicitly # requested -- pushes it as `php-min-v5-hardened-test`. amd64 only. -# Manual dispatch only; never runs on schedule, push or pull_request. +# Runs build + test + scan on pull requests that touch the pilot files, and on manual +# dispatch. Publishing is only possible on a manual dispatch with publish=true. # Context: https://github.com/pimcore/DevOps-Tasks/issues/84 on: + pull_request: + paths: + - 'Dockerfile.dhi' + - '.github/workflows/hardened-test.yml' + - 'files/conf/php/php.ini' + - 'files/conf/php-fpm/php-fpm.conf' workflow_dispatch: inputs: php: @@ -22,13 +29,16 @@ on: env: IMAGE_NAME: pimcore/pimcore + # inputs.* are empty on pull_request runs + PHP: ${{ inputs.php || '8.5' }} + PUBLISH: ${{ github.event_name == 'workflow_dispatch' && inputs.publish == true }} permissions: contents: read jobs: dhi-min: - name: "Build, test and scan DHI min (php ${{ inputs.php }})" + name: "Build, test and scan DHI min (php ${{ inputs.php || '8.5' }})" runs-on: ubuntu-22.04 if: github.repository == 'pimcore/docker' @@ -43,7 +53,7 @@ jobs: run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login dhi.io -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin - name: Login to DockerHub Registry - if: ${{ inputs.publish }} + if: ${{ env.PUBLISH == 'true' }} run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin - name: Install Trivy @@ -61,7 +71,7 @@ jobs: set -eux docker build --load --pull \ --target pimcore_php_min \ - --build-arg PHP_VERSION="${{ inputs.php }}" \ + --build-arg PHP_VERSION="${PHP}" \ --build-arg DEBIAN_VERSION=trixie \ --tag plain-min . @@ -71,7 +81,7 @@ jobs: docker build --load --pull \ --file Dockerfile.dhi \ --target pimcore_php_min \ - --build-arg PHP_VERSION="${{ inputs.php }}" \ + --build-arg PHP_VERSION="${PHP}" \ --tag dhi-min . # The image has no shell: every check is a direct `php` / `php-fpm` / `composer` invocation. @@ -110,7 +120,7 @@ jobs: run: | set -euo pipefail { - echo "## DHI pilot: regular min vs DHI min (php ${{ inputs.php }}, amd64)" + echo "## DHI pilot: regular min vs DHI min (php ${PHP}, amd64)" echo "" echo "| Image | CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN | total | fixable |" echo "|-------|----------|------|--------|-----|---------|-------|---------|" @@ -137,7 +147,7 @@ jobs: if: always() uses: actions/upload-artifact@v7 with: - name: trivy-dhi-pilot_php${{ inputs.php }} + name: trivy-dhi-pilot_php${{ inputs.php || '8.5' }} path: | trivy-*.json trivy-*.txt @@ -145,10 +155,10 @@ jobs: if-no-files-found: ignore - name: Publish -hardened-test tag - if: ${{ inputs.publish }} + if: ${{ env.PUBLISH == 'true' }} run: | set -eux - TAG="${IMAGE_NAME}:php${{ inputs.php }}-min-v5-hardened-test" + TAG="${IMAGE_NAME}:php${PHP}-min-v5-hardened-test" docker tag dhi-min "$TAG" docker push "$TAG" echo "Published ${TAG} (amd64 only, experimental)" >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index b4013115..23ec807d 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,9 @@ base image is a better path than Copa. It may change or disappear without notice `conf.d` overrides work as with the regular images. - Same extensions as `min` (`amqp bcmath exif gd intl pcntl pdo_mysql sockets zip`), same `php.ini`/`php-fpm` environment variables, `php` CLI and `composer` included. -- Built and published only by the manual workflow `hardened-test.yml`; it is not part of the release - matrix, has no `-latest`/ghcr tags and no attached SBOM. +- Built, tested and scanned by `hardened-test.yml` on pull requests that touch it and on manual dispatch; + published only on a manual dispatch with `publish=true`. It is not part of the release matrix, has no + `-latest`/ghcr tags and no attached SBOM. ## Known CVEs From 713e81a180ebf58b3348a8cf9ca35b62c2aead23 Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:00:46 +0100 Subject: [PATCH 3/7] Dockerfile.dhi: add libpcre2-dev and zlib1g-dev to the builder ext/zip failed with 'pcre2.h: No such file or directory': PHP on DHI is built with --with-external-pcre and the -dev image has no pcre2 headers. zlib1g-dev is added for the same reason ahead of the gd build. Co-Authored-By: Claude --- Dockerfile.dhi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile.dhi b/Dockerfile.dhi index 3df2c585..331e7150 100644 --- a/Dockerfile.dhi +++ b/Dockerfile.dhi @@ -24,6 +24,8 @@ ARG PHP_VERSION # Build dependencies. The -dev image's apt points at dhi.io/deb (hardened rebuilds of # Debian packages) and falls back to upstream Debian for everything else. +# libpcre2-dev and zlib1g-dev: PHP is built against the system pcre2/zlib, and the -dev image +# ships only the runtime libraries, not the headers the extension builds need. # libbrotli1/libbrotli-dev are pinned to one version: the DHI repo intermittently serves # them out of sync, which breaks libfreetype-dev (docker-hardened-images/catalog#677, #682). RUN set -eux; \ @@ -34,10 +36,12 @@ RUN set -eux; \ "libbrotli-dev=${BROTLI}" \ libfreetype-dev \ libjpeg62-turbo-dev \ + libpcre2-dev \ libpng-dev \ librabbitmq-dev \ libwebp-dev \ libzip-dev \ + zlib1g-dev \ ; \ rm -rf /var/lib/apt/lists/* From f880acc7a0aac83adaf94d8a58e11d625c3e6342 Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:07:46 +0100 Subject: [PATCH 4/7] Dockerfile.dhi: ship /usr/bin/env so the composer phar can start The distroless runtime has no env binary; composer's '#!/usr/bin/env php' shebang therefore failed with 'no such file or directory'. The phar stub is signed, so the shebang cannot be rewritten; copy env (libc only) from the builder instead. Co-Authored-By: Claude --- Dockerfile.dhi | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile.dhi b/Dockerfile.dhi index 331e7150..395c660e 100644 --- a/Dockerfile.dhi +++ b/Dockerfile.dhi @@ -81,14 +81,17 @@ COPY --from=runtime-probe /usr/lib /probe/usr/lib # Stage everything the runtime needs under /out: # - the extension .so files -# - every shared library they link against that the runtime image lacks +# - /usr/bin/env: the composer phar starts with `#!/usr/bin/env php` (its stub is covered +# by the phar signature, so the shebang cannot be rewritten); env needs only libc +# - every shared library the above link against that the runtime image lacks # - one ini file per extension in the scan dir ($PHP_INI_DIR/conf.d) RUN set -eux; \ EXTDIR="$(php-config --extension-dir)"; \ - mkdir -p "/out${EXTDIR}" "/out${PHP_PREFIX}/etc/php/conf.d"; \ + mkdir -p "/out${EXTDIR}" "/out${PHP_PREFIX}/etc/php/conf.d" /out/usr/bin; \ cp "${EXTDIR}"/*.so "/out${EXTDIR}/"; \ - for so in "${EXTDIR}"/*.so; do \ - ldd "$so" | awk '/=> \// {print $3}'; \ + cp -L /usr/bin/env /out/usr/bin/env; \ + for bin in "${EXTDIR}"/*.so /usr/bin/env; do \ + ldd "$bin" | awk '/=> \// {print $3}'; \ done | sort -u | while read -r lib; do \ # merged-/usr: /lib//x -> /usr/lib//x norm="$(printf '%s' "$lib" | sed -E 's#^/lib/#/usr/lib/#')"; \ From d01058304feb99ffe757a1019c5102e06bfbc55c Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:11:47 +0100 Subject: [PATCH 5/7] hardened-test: replace create-project check, document no-shell limit composer create-project fails in the distroless runtime with 'Unable to launch a new process': composer shells out (rm -rf, git, unzip) and there is no /bin/sh. The image is a runtime image; the application has to be built elsewhere and copied in. Check composer starts and that the pimcore platform extension requirements are met instead. Co-Authored-By: Claude --- .github/workflows/hardened-test.yml | 9 +++++++-- README.md | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/hardened-test.yml b/.github/workflows/hardened-test.yml index 2e81276d..a1751084 100644 --- a/.github/workflows/hardened-test.yml +++ b/.github/workflows/hardened-test.yml @@ -112,9 +112,14 @@ jobs: # pimcore.ini env substitution works docker run --rm -e PHP_MEMORY_LIMIT=333M dhi-min php -r 'exit(ini_get("memory_limit") === "333M" ? 0 : 1);' - # composer works without root and without a shell + # composer starts without root and without a shell. Anything that spawns a + # process (create-project, install with scripts, `rm -rf` in composer's + # Filesystem) needs /bin/sh and fails in a distroless image: the application + # has to be built in another image/stage and copied in. So only --version here. docker run --rm dhi-min composer --version - docker run --rm dhi-min composer create-project pimcore/skeleton:2026.x-dev /tmp/pimcore --no-scripts --no-interaction --prefer-dist --ignore-platform-reqs + + # a real PHP workload: the pimcore composer.json platform requirements are met + docker run --rm dhi-min php -r 'foreach (["SimpleXML","curl","dom","exif","fileinfo","gd","iconv","intl","json","mbstring","openssl","pdo","pdo_mysql","zip","zlib"] as $e) { if (!extension_loaded($e)) { fwrite(STDERR, "missing ext-$e\n"); exit(1); } }' - name: Trivy scan - regular min vs DHI min run: | diff --git a/README.md b/README.md index 23ec807d..648c6f5b 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ base image is a better path than Copa. It may change or disappear without notice `conf.d` overrides work as with the regular images. - Same extensions as `min` (`amqp bcmath exif gd intl pcntl pdo_mysql sockets zip`), same `php.ini`/`php-fpm` environment variables, `php` CLI and `composer` included. +- **Runtime image only.** Without `/bin/sh`, anything that spawns a process fails: `composer + create-project`/`install` (composer shells out for `rm -rf`, `git`, `unzip`), composer scripts and + Pimcore commands that call external tools. Build the application with the regular image or a + `-dev` stage and copy it in. - Built, tested and scanned by `hardened-test.yml` on pull requests that touch it and on manual dispatch; published only on a manual dispatch with `publish=true`. It is not part of the release matrix, has no `-latest`/ghcr tags and no attached SBOM. From 17960db9b769409c5948672a89ec3245597a1666 Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:41:10 +0100 Subject: [PATCH 6/7] Address review: brotli pin anchor, dpkg metadata for copied libs, fork PRs, README link - Pin brotli to the libbrotli-dev candidate (with --allow-downgrades for libbrotli1) instead of the runtime candidate: during the drift from catalog#677/#682 the newest runtime build has no matching -dev package, so the old pin requested a version that does not exist. - Write the dpkg status record of every package whose files are copied into the runtime to /var/lib/dpkg/status.d/, so Trivy sees libzip, librabbitmq, libpng, libjpeg, libwebp, freetype, bz2 and coreutils (env) in the DHI image like it does in the baseline. - Use PHP_PREFIX for the runtime COPY paths instead of the PHP_VERSION build arg (the build arg did expand correctly, this just removes the dependency on ARG/ENV precedence); drop the unused stage ARGs. - Skip the pilot job for fork pull requests: they get no secrets and the dhi.io login would fail. - README: drop the link to the private DevOps-Tasks issue. Co-Authored-By: Claude --- .github/workflows/hardened-test.yml | 6 +++++- Dockerfile.dhi | 33 ++++++++++++++++++----------- README.md | 3 +-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/.github/workflows/hardened-test.yml b/.github/workflows/hardened-test.yml index a1751084..e12bd41f 100644 --- a/.github/workflows/hardened-test.yml +++ b/.github/workflows/hardened-test.yml @@ -40,7 +40,11 @@ jobs: dhi-min: name: "Build, test and scan DHI min (php ${{ inputs.php || '8.5' }})" runs-on: ubuntu-22.04 - if: github.repository == 'pimcore/docker' + # Fork PRs get no repository secrets, and the dhi.io login needs them: run only for + # branches of this repository (pull_request from the same repo, or a dispatch). + if: >- + github.repository == 'pimcore/docker' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) steps: - uses: actions/checkout@v5 diff --git a/Dockerfile.dhi b/Dockerfile.dhi index 395c660e..e37a6b0c 100644 --- a/Dockerfile.dhi +++ b/Dockerfile.dhi @@ -20,18 +20,19 @@ ARG PHP_VERSION="8.5" # --------------------------------------------------------------------------- FROM dhi.io/php:${PHP_VERSION}-debian13-dev AS builder -ARG PHP_VERSION - # Build dependencies. The -dev image's apt points at dhi.io/deb (hardened rebuilds of # Debian packages) and falls back to upstream Debian for everything else. # libpcre2-dev and zlib1g-dev: PHP is built against the system pcre2/zlib, and the -dev image # ships only the runtime libraries, not the headers the extension builds need. # libbrotli1/libbrotli-dev are pinned to one version: the DHI repo intermittently serves # them out of sync, which breaks libfreetype-dev (docker-hardened-images/catalog#677, #682). +# The -dev candidate is the anchor: its exact-version runtime dependency always exists in +# the repo, while the newest runtime build may have no matching -dev package yet. The base +# image can already carry that newer runtime build, hence --allow-downgrades. RUN set -eux; \ apt-get update; \ - BROTLI="$(apt-cache policy libbrotli1 | awk '/Candidate:/ {print $2}')"; \ - apt-get install -y --no-install-recommends \ + BROTLI="$(apt-cache policy libbrotli-dev | awk '/Candidate:/ {print $2}')"; \ + apt-get install -y --no-install-recommends --allow-downgrades \ "libbrotli1=${BROTLI}" \ "libbrotli-dev=${BROTLI}" \ libfreetype-dev \ @@ -75,8 +76,6 @@ FROM dhi.io/php:${PHP_VERSION}-debian13-fpm AS runtime-probe FROM builder AS payload -ARG PHP_VERSION - COPY --from=runtime-probe /usr/lib /probe/usr/lib # Stage everything the runtime needs under /out: @@ -84,12 +83,21 @@ COPY --from=runtime-probe /usr/lib /probe/usr/lib # - /usr/bin/env: the composer phar starts with `#!/usr/bin/env php` (its stub is covered # by the phar signature, so the shebang cannot be rewritten); env needs only libc # - every shared library the above link against that the runtime image lacks +# - the dpkg status record of each package those files come from, in +# /var/lib/dpkg/status.d/ (the distroless convention Trivy reads next to the main +# status file), so the copied libraries stay visible to vulnerability scanners # - one ini file per extension in the scan dir ($PHP_INI_DIR/conf.d) RUN set -eux; \ EXTDIR="$(php-config --extension-dir)"; \ - mkdir -p "/out${EXTDIR}" "/out${PHP_PREFIX}/etc/php/conf.d" /out/usr/bin; \ + mkdir -p "/out${EXTDIR}" "/out${PHP_PREFIX}/etc/php/conf.d" /out/usr/bin /out/var/lib/dpkg/status.d; \ cp "${EXTDIR}"/*.so "/out${EXTDIR}/"; \ cp -L /usr/bin/env /out/usr/bin/env; \ + record_pkg() { \ + pkg="$(dpkg -S "$1" | head -n1 | cut -d: -f1)"; \ + [ -n "$pkg" ] || { echo "no package owns $1" >&2; exit 1; }; \ + [ -e "/out/var/lib/dpkg/status.d/${pkg}" ] || dpkg-query --status "$pkg" > "/out/var/lib/dpkg/status.d/${pkg}"; \ + }; \ + record_pkg /usr/bin/env; \ for bin in "${EXTDIR}"/*.so /usr/bin/env; do \ ldd "$bin" | awk '/=> \// {print $3}'; \ done | sort -u | while read -r lib; do \ @@ -99,6 +107,7 @@ RUN set -eux; \ if [ ! -e "/probe${norm}" ]; then \ mkdir -p "/out$(dirname "$norm")"; \ cp -L "$lib" "/out${norm}"; \ + record_pkg "$norm"; \ fi; \ done; \ for ext in bcmath exif gd pcntl pdo_mysql sockets zip amqp; do \ @@ -116,13 +125,13 @@ FROM dhi.io/php:${PHP_VERSION}-debian13 AS cli # --------------------------------------------------------------------------- FROM dhi.io/php:${PHP_VERSION}-debian13-fpm AS pimcore_php_min -ARG PHP_VERSION - +# Paths below use PHP_PREFIX (ENV from the DHI base, e.g. /opt/php-8.5) rather than the +# PHP_VERSION build arg, so they stay correct whatever the base image sets PHP_VERSION to. COPY --from=payload /out/ / -COPY --from=cli /opt/php-${PHP_VERSION}/bin/php /opt/php-${PHP_VERSION}/bin/php +COPY --from=cli ${PHP_PREFIX}/bin/php ${PHP_PREFIX}/bin/php -COPY files/conf/php/php.ini /opt/php-${PHP_VERSION}/etc/php/conf.d/20-pimcore.ini -COPY files/conf/php-fpm/php-fpm.conf /opt/php-${PHP_VERSION}/etc/php-fpm.d/zz-www.conf +COPY files/conf/php/php.ini ${PHP_PREFIX}/etc/php/conf.d/20-pimcore.ini +COPY files/conf/php-fpm/php-fpm.conf ${PHP_PREFIX}/etc/php-fpm.d/zz-www.conf # env php.ini ENV PHP_TIMEZONE="UTC" diff --git a/README.md b/README.md index 648c6f5b..1880ae62 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,7 @@ We're also offering special tags for specific PHP versions, e.g. `php8.2.5-v2.0` > **Status (2026-10-28): publishing of `-hardened` tags is paused.** The last `-hardened` tags were > pushed on 2026-07-31 and are not refreshed. Use the plain tags; they are the supported images and > are rebuilt on every scheduled run with all Debian security updates available at build time. -> Hardened publishing resumes once there is a strategy that delivers it consistently -> (see [pimcore/DevOps-Tasks#84](https://github.com/pimcore/DevOps-Tasks/issues/84)). +> Hardened publishing resumes once there is a strategy that delivers it consistently. > The description below documents what the `-hardened` flavor is when it is enabled. For our stable release tags the release workflow can publish each image in two flavors so you can choose your trade-off: From d272ed26b3372c6ca6722d9c2c9b6530ef4917ab Mon Sep 17 00:00:00 2001 From: berfinyuksel <99557970+berfinyuksel@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:53:47 +0100 Subject: [PATCH 7/7] hardened-test: drop link to the private issue tracker from the workflow comment Co-Authored-By: Claude --- .github/workflows/hardened-test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/hardened-test.yml b/.github/workflows/hardened-test.yml index e12bd41f..4b7c2abb 100644 --- a/.github/workflows/hardened-test.yml +++ b/.github/workflows/hardened-test.yml @@ -5,7 +5,6 @@ name: Hardened test image (DHI pilot) # requested -- pushes it as `php-min-v5-hardened-test`. amd64 only. # Runs build + test + scan on pull requests that touch the pilot files, and on manual # dispatch. Publishing is only possible on a manual dispatch with publish=true. -# Context: https://github.com/pimcore/DevOps-Tasks/issues/84 on: pull_request: