Repository navigation
Add Docker Hardened Images pilot for the min flavor (-hardened-test) #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
berfinyuksel
wants to merge
7
commits into
5.x
Choose a base branch
from
feat/dhi-hardened-test-pilot
base: 5.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c7c6a8c
Add Docker Hardened Images pilot for the min flavor (-hardened-test)
berfinyuksel 3ac8887
hardened-test: also run build/test/scan on pull requests touching the…
berfinyuksel 713e81a
Dockerfile.dhi: add libpcre2-dev and zlib1g-dev to the builder
berfinyuksel f880acc
Dockerfile.dhi: ship /usr/bin/env so the composer phar can start
berfinyuksel d010583
hardened-test: replace create-project check, document no-shell limit
berfinyuksel 17960db
Address review: brotli pin anchor, dpkg metadata for copied libs, for…
berfinyuksel d272ed2
hardened-test: drop link to the private issue tracker from the workfl…
berfinyuksel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| 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<ver>-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. | ||
|
|
||
| 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: | ||
| description: 'PHP minor version (must exist as dhi.io/php:<php>-debian13-{dev,fpm} and as php:<php>-fpm-trixie)' | ||
| required: false | ||
| default: '8.5' | ||
| type: string | ||
| publish: | ||
| description: 'Push pimcore/pimcore:php<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 | ||
| # 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 || '8.5' }})" | ||
| runs-on: ubuntu-22.04 | ||
| # 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 | ||
|
|
||
| - 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: ${{ env.PUBLISH == 'true' }} | ||
| 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="${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="${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 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 | ||
|
|
||
| # 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: | | ||
| set -euo pipefail | ||
| { | ||
| echo "## DHI pilot: regular min vs DHI min (php ${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 || '8.5' }} | ||
| path: | | ||
| trivy-*.json | ||
| trivy-*.txt | ||
| summary.md | ||
| if-no-files-found: ignore | ||
|
|
||
| - name: Publish -hardened-test tag | ||
| if: ${{ env.PUBLISH == 'true' }} | ||
| run: | | ||
| set -eux | ||
| 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| # syntax=docker/dockerfile:1 | ||
| # | ||
| # EXPERIMENTAL -- Docker Hardened Images (DHI) pilot for the `min` flavor. | ||
| # Published only as `php<ver>-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-<ver>; /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 | ||
|
|
||
| # 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 libbrotli-dev | awk '/Candidate:/ {print $2}')"; \ | ||
| apt-get install -y --no-install-recommends --allow-downgrades \ | ||
| "libbrotli1=${BROTLI}" \ | ||
| "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/* | ||
|
|
||
| # 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 | ||
|
|
||
| COPY --from=runtime-probe /usr/lib /probe/usr/lib | ||
|
|
||
| # Stage everything the runtime needs under /out: | ||
| # - the extension .so files | ||
| # - /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 /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 \ | ||
| # merged-/usr: /lib/<arch>/x -> /usr/lib/<arch>/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}"; \ | ||
| record_pkg "$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 | ||
|
|
||
| # 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 ${PHP_PREFIX}/bin/php ${PHP_PREFIX}/bin/php | ||
|
|
||
| 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" | ||
| 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 | ||
|
Copilot marked this conversation as resolved.
Resolved
|
||
|
|
||
| WORKDIR /var/www/html | ||
|
|
||
| # DHI sets ENTRYPOINT to the php-fpm binary. Reset it so `docker run <image> php bin/console` | ||
| # works exactly like with the regular images (there is no shell for an entrypoint script). | ||
| ENTRYPOINT [] | ||
| CMD ["php-fpm"] | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.