From 01badf5c01a135260b8c0ccb374b0b9392ecda9f Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:42:50 -0500 Subject: [PATCH 1/2] feat: add rootless nginx development image --- .dockerignore | 14 ++++ .gitattributes | 1 - .github/workflows/ci.yml | 138 ++++++++++++++++++++++++++++++++++ CHANGELOG.md | 11 +++ Containerfile | 71 +++++++++++++++++ README.md | 28 +++++++ compose.yaml | 16 ++++ container/conf.d/default.conf | 18 +++++ container/html/index.html | 12 +++ container/nginx.conf | 29 +++++++ docs/CI.md | 42 +++++------ docs/ROADMAP.md | 23 +----- tests/smoke.ps1 | 97 ++++++++++++++++++++++++ tests/smoke.sh | 60 +++++++++++++++ 14 files changed, 518 insertions(+), 42 deletions(-) create mode 100644 .dockerignore create mode 100644 Containerfile create mode 100644 compose.yaml create mode 100644 container/conf.d/default.conf create mode 100644 container/html/index.html create mode 100644 container/nginx.conf create mode 100644 tests/smoke.ps1 create mode 100644 tests/smoke.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..db54a33 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.github +.pre-commit-cache +.venv +build +dist +docs +security-results +tests +*.key +*.pem +*.sarif +*.spdx.json +grype*.json diff --git a/.gitattributes b/.gitattributes index 11c93a6..2a72d17 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,7 +2,6 @@ *.bat text eol=crlf *.cmd text eol=crlf -*.ps1 text eol=crlf *.gif binary *.ico binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05c51b8..d1721c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,3 +66,141 @@ jobs: format: table exit-code: "1" severity: CRITICAL,HIGH + + image: + name: image (${{ matrix.architecture }}) + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + platform: linux/amd64 + machine: x86_64 + - architecture: arm64 + runner: ubuntu-24.04-arm + platform: linux/arm64 + machine: aarch64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + permissions: + contents: read + security-events: write + env: + TEST_IMAGE: localhost/nginx-ubi9:test-${{ matrix.architecture }} + SBOM_FILE: nginx-ubi9-${{ matrix.architecture }}.spdx.json + GRYPE_SARIF: grype-${{ matrix.architecture }}.sarif + GRYPE_ALL: grype-all-${{ matrix.architecture }}.json + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up Buildx + uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + + - name: Confirm native runner architecture + env: + EXPECTED_MACHINE: ${{ matrix.machine }} + run: test "$(uname -m)" = "${EXPECTED_MACHINE}" + + - name: Build test image + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . + file: Containerfile + platforms: ${{ matrix.platform }} + load: true + push: false + tags: ${{ env.TEST_IMAGE }} + cache-from: type=gha,scope=image-${{ matrix.architecture }} + cache-to: type=gha,mode=max,scope=image-${{ matrix.architecture }} + + - name: Confirm loaded image architecture + env: + EXPECTED_ARCHITECTURE: ${{ matrix.architecture }} + run: >- + test "$(docker image inspect --format '{{.Architecture}}' "${TEST_IMAGE}")" + = "${EXPECTED_ARCHITECTURE}" + + - name: Run restricted-runtime smoke tests + env: + CONTAINER_RUNTIME: docker + IMAGE: ${{ env.TEST_IMAGE }} + run: bash tests/smoke.sh + + - name: Scan image with Trivy + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + image-ref: ${{ env.TEST_IMAGE }} + format: table + exit-code: "1" + ignore-unfixed: true + severity: CRITICAL,HIGH + + - name: Generate SPDX SBOM with Syft + uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2 + with: + image: ${{ env.TEST_IMAGE }} + format: spdx-json + output-file: ${{ env.SBOM_FILE }} + syft-version: v1.51.1 + upload-artifact: false + upload-release-assets: false + + - name: Scan Syft SBOM with Grype + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: ${{ env.SBOM_FILE }} + output-format: sarif + output-file: ${{ env.GRYPE_SARIF }} + severity-cutoff: high + only-fixed: true + fail-build: true + cache-db: true + grype-version: v0.118.0 + + - name: Record all Grype findings + if: ${{ always() && hashFiles(env.SBOM_FILE) != '' }} + uses: anchore/scan-action@27805bf3b4e84b4a5c980df22ed233c00390a439 # v7.4.2 + with: + sbom: ${{ env.SBOM_FILE }} + output-format: json + output-file: ${{ env.GRYPE_ALL }} + severity-cutoff: negligible + only-fixed: false + fail-build: false + cache-db: true + grype-version: v0.118.0 + + - name: Retain image security artifacts + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: image-security-${{ github.sha }}-${{ matrix.architecture }} + path: | + ${{ env.SBOM_FILE }} + ${{ env.GRYPE_SARIF }} + ${{ env.GRYPE_ALL }} + if-no-files-found: warn + retention-days: 14 + + - name: Publish Grype findings to code scanning + if: ${{ always() && github.event_name != 'pull_request' && hashFiles(env.GRYPE_SARIF) != '' }} + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + sarif_file: ${{ env.GRYPE_SARIF }} + category: grype-image-${{ matrix.architecture }} + + image-result: + name: image + if: ${{ always() }} + needs: image + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - name: Require every native image job + env: + MATRIX_RESULT: ${{ needs.image.result }} + run: test "${MATRIX_RESULT}" = success diff --git a/CHANGELOG.md b/CHANGELOG.md index 4464666..3b8d79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,3 +25,14 @@ but container releases use the upstream-derived format documented in hash-locked CI Python environment. - Documented local checks, GitHub automation, planned image assurance, and the evidence boundary of Podman Desktop or a remote Podman machine. +- Added an initial package-manager-free UBI 9 Micro development image using an + exact Red Hat NGINX RPM build. +- Added rootless NGINX defaults for unprivileged HTTP, read-only-root operation, + container log streams, `/tmp` runtime state, health checking, and graceful + shutdown. +- Added a restricted-runtime smoke suite, static landing page, and hardened + Compose development service. +- Added native PowerShell smoke testing for Podman Desktop development. +- Extended CI with native AMD64 and ARM64 builds, restricted-runtime tests, + Trivy image scanning, Syft SPDX inventories, blocking Grype analysis, full + finding retention, and an aggregate image result. diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..34956f7 --- /dev/null +++ b/Containerfile @@ -0,0 +1,71 @@ +# syntax=docker/dockerfile:1.7 + +ARG UBI_MINIMAL_IMAGE="registry.access.redhat.com/ubi9/ubi-minimal:9.8@sha256:7fbeae18dc9476399f565e68255f602a3374ea8614ba3d14843565131a13ff93" +ARG UBI_MICRO_IMAGE="registry.access.redhat.com/ubi9/ubi-micro:9.8@sha256:f332c99eb8f798a8486821c91937f10ad64ee83d7e739303be2df051040918f6" + +FROM ${UBI_MINIMAL_IMAGE} AS builder + +ARG NGINX_RPM_VERSION="2:1.20.1-28.el9_8.5" + +# Install the exact Red Hat NGINX build and its runtime dependency closure into +# a separate root. The UBI Micro final stage receives no package-management +# commands or builder caches. +# hadolint ignore=DL3041 +RUN microdnf install -y dnf \ + && mkdir -p /runtime \ + && dnf install -y \ + --installroot=/runtime \ + --releasever=9 \ + --setopt=install_weak_deps=0 \ + --setopt=keepcache=0 \ + "nginx-core-${NGINX_RPM_VERSION}" \ + ca-certificates tzdata \ + && dnf clean all \ + && microdnf clean all \ + && rm -rf \ + /runtime/run/* \ + /runtime/tmp/* \ + /runtime/var/cache/dnf \ + /runtime/var/cache/nginx \ + /runtime/var/log/* \ + /runtime/var/tmp/* \ + && mkdir -p /runtime/var/log/nginx \ + && ln -s /dev/stdout /runtime/var/log/nginx/access.log \ + && ln -s /dev/stderr /runtime/var/log/nginx/error.log \ + && chown -R 0:0 /runtime/etc/nginx /runtime/usr/share/nginx \ + && chmod -R g=u /runtime/etc/nginx /runtime/usr/share/nginx + +FROM ${UBI_MICRO_IMAGE} + +ARG NGINX_VERSION="1.20.1" +ARG NGINX_RPM_VERSION="2:1.20.1-28.el9_8.5" + +LABEL org.opencontainers.image.title="NGINX on Red Hat UBI 9" \ + org.opencontainers.image.description="A security-oriented, rootless NGINX image built on Red Hat UBI 9 Micro" \ + org.opencontainers.image.source="https://github.com/datopsis/nginx-ubi9" \ + org.opencontainers.image.documentation="https://github.com/datopsis/nginx-ubi9#readme" \ + org.opencontainers.image.licenses="BSD-2-Clause AND Apache-2.0" \ + org.opencontainers.image.vendor="Datopsis" \ + org.opencontainers.image.version="${NGINX_VERSION}" \ + io.datopsis.nginx.rpm-version="${NGINX_RPM_VERSION}" + +COPY --from=builder /runtime/ / +COPY --chown=0:0 --chmod=0644 container/nginx.conf /etc/nginx/nginx.conf +COPY --chown=0:0 --chmod=0644 container/conf.d/default.conf /etc/nginx/conf.d/default.conf +COPY --chown=0:0 --chmod=0644 container/html/index.html /usr/share/nginx/html/index.html + +ENV LANG="C.UTF-8" \ + TZ="UTC" + +USER 999:0 +WORKDIR /usr/share/nginx/html + +EXPOSE 8080 8443 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD ["nginx", "-t", "-q"] + +STOPSIGNAL SIGQUIT + +ENTRYPOINT ["nginx"] +CMD ["-g", "daemon off;"] diff --git a/README.md b/README.md index 8517296..45c7f7b 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,34 @@ python -m pip install --require-hashes --only-binary=:all: \ pre-commit run --all-files --show-diff-on-failure ``` +Build and exercise the current AMD64 development image with Podman on Linux, +WSL, or Git Bash: + +```console +podman build --format docker --file Containerfile \ + --tag localhost/nginx-ubi9:development . +CONTAINER_RUNTIME=podman IMAGE=localhost/nginx-ubi9:development \ + bash tests/smoke.sh +``` + +From PowerShell with Podman Desktop: + +```powershell +podman build --format docker --file Containerfile ` + --tag localhost/nginx-ubi9:development . +.\tests\smoke.ps1 +``` + +Or start the hardened default service with Compose: + +```console +podman compose up --build +curl --fail http://127.0.0.1:8080/healthz +``` + +CI supplies the canonical Linux shell execution and native architecture +evidence. + See the [continuous integration guide](docs/CI.md) for hook installation and Podman evidence boundaries. Until the first signed release is published, this repository should be treated as development material rather than a supported diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..1d5ed47 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,16 @@ +services: + nginx: + image: localhost/nginx-ubi9:development + build: + context: . + dockerfile: Containerfile + ports: + - "127.0.0.1:8080:8080" + read_only: true + tmpfs: + - /tmp:size=64m,mode=1777 + cap_drop: + - ALL + security_opt: + - no-new-privileges:true + stop_signal: SIGQUIT diff --git a/container/conf.d/default.conf b/container/conf.d/default.conf new file mode 100644 index 0000000..23fd89a --- /dev/null +++ b/container/conf.d/default.conf @@ -0,0 +1,18 @@ +server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location = /healthz { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/container/html/index.html b/container/html/index.html new file mode 100644 index 0000000..5c03ac3 --- /dev/null +++ b/container/html/index.html @@ -0,0 +1,12 @@ + + + + + + NGINX on UBI 9 + + +

NGINX on UBI 9

+

The rootless development image is running.

+ + diff --git a/container/nginx.conf b/container/nginx.conf new file mode 100644 index 0000000..0aeefd8 --- /dev/null +++ b/container/nginx.conf @@ -0,0 +1,29 @@ +worker_processes auto; +pid /tmp/nginx.pid; +error_log /dev/stderr notice; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /dev/stdout main; + + client_body_temp_path /tmp/nginx-client-body; + proxy_temp_path /tmp/nginx-proxy; + fastcgi_temp_path /tmp/nginx-fastcgi; + uwsgi_temp_path /tmp/nginx-uwsgi; + scgi_temp_path /tmp/nginx-scgi; + + sendfile on; + keepalive_timeout 65; + server_tokens off; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docs/CI.md b/docs/CI.md index 9e0efdc..8102ffc 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -1,15 +1,14 @@ # Continuous integration -The initial automation protects repository and workflow development before the -container implementation lands. Image build, runtime, SBOM, vulnerability, and -SCAP jobs will be added with the corresponding implementations so a green job -never represents a test that could not actually run. +The automation protects repository, workflow, and container development. A +green job represents only checks that have an implemented target and an +inspectable result; planned controls are not represented as passing jobs. ## Current workflows | Workflow | Triggers | Current purpose | | --- | --- | --- | -| `CI` | Pull requests, `main`, weekly, manual | Run pinned pre-commit checks, audit Actions with Zizmor, and scan repository configuration with Trivy. | +| `CI` | Pull requests, `main`, weekly, manual | Run pinned repository checks, audit Actions, scan configuration, and build, test, inventory, and scan native AMD64 and ARM64 images. | | `CodeQL` | Workflow changes, `main`, weekly, manual | Analyze GitHub Actions with the security-extended query suite. | | `OpenSSF Scorecard` | `main`, branch-protection changes, weekly, manual | Publish repository supply-chain findings and SARIF. | @@ -53,26 +52,27 @@ non-root UID while its Linux VM engine itself operates rootfully. That proves the image's non-root process behavior but does not qualify rootless-host user namespace behavior. Release evidence will distinguish these cases. -After the `Containerfile` and smoke suite are implemented, the README will -publish their exercised Podman commands. Native AMD64 and ARM64 CI remains -required before an image receives supported multi-architecture status. +The README contains the exercised Podman build and smoke commands. The local +PowerShell suite is suitable for Podman Desktop on Windows; the Bash suite is +used on Linux CI. Native AMD64 and ARM64 CI remains required before an image +receives supported multi-architecture status. -## Planned image assurance +## Image assurance -The stable protected check names will be `lint` and `image`. The aggregate -`image` check will require both native architecture jobs after they exist. The -image pipeline will add, in dependency order: +The stable protected check names are `lint`, `configuration security`, and +`image`. The aggregate `image` check requires both native architecture jobs. +The implemented image pipeline performs: 1. Trivy build-configuration scanning. 2. Native architecture builds and restricted-runtime smoke tests. -3. Tailored OpenSCAP evaluation against an ownership-preserving filesystem - export. -4. Trivy image vulnerability scanning. -5. SPDX inventory generation with Syft. -6. Independent fixed High/Critical vulnerability gating with Grype and a +3. Trivy image vulnerability scanning. +4. SPDX inventory generation with Syft. +5. Independent fixed High/Critical vulnerability gating with Grype and a retained full finding inventory. -7. Architecture-specific artifacts and non-pull-request SARIF publication. +6. Architecture-specific artifacts and non-pull-request SARIF publication. -Branch protection must not require `image` until that aggregate check exists -on the default branch. Once present and proven, both `lint` and `image` become -strict, required, up-to-date checks. +Tailored OpenSCAP evaluation against an ownership-preserving filesystem export +will be inserted after the runtime tests when its profile and result semantics +are reviewed. Branch protection must not require `image` until its aggregate +check exists on the default branch. Once present and proven, `image` becomes a +strict, required, up-to-date check alongside the current repository checks. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0810bee..e688b13 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -65,18 +65,9 @@ are separately approved. They must not delay the core first release. pin every base image and downloaded artifact by digest or checksum. - [ ] Record source, redistribution, licensing, support lifecycle, and update ownership for every runtime component. -- [ ] Build a package-manager-free UBI 9 Micro final stage. -- [ ] Run NGINX directly as a fixed non-root identity compatible with an - arbitrary UID in group `0`; do not include a root entrypoint phase. -- [ ] Listen on `8080` and `8443`; make PID, cache, temporary, and state paths - explicit and compatible with a read-only root filesystem. -- [ ] Send access and error logs to stdout/stderr without symlink mutation at - startup. -- [ ] Add a health check that does not require a shell when practical. -- [ ] Add smoke tests for image identity, process identity, static serving, - reverse proxying, graceful shutdown, read-only root, dropped capabilities, - `no-new-privileges`, arbitrary UID, writable-path failures, and absence of a - package manager. +- [ ] Add negative tests for invalid configuration and unavailable writable + runtime paths with actionable failure diagnostics. +- [ ] Add graceful reload and shutdown assertions to the runtime suite. **Fast-release checkpoint:** after Package 2, a development image is usable for local evaluation but is not yet a supported release. @@ -99,14 +90,6 @@ local evaluation but is not yet a supported release. ## Package 4: CI and supply-chain controls -- [ ] Build and smoke-test on native AMD64 and ARM64 GitHub-hosted runners. -- [ ] Run Trivy configuration and image scanning with fixed High/Critical - findings blocking. -- [ ] Generate architecture-specific SPDX SBOMs with Syft and scan them with - Grype as an independent fixed High/Critical gate; retain the complete - non-blocking finding inventory for review. -- [ ] Retain evidence even when a blocking scan fails, without leaking secrets - into artifacts. - [ ] After the native image jobs exist on `main`, require the stable `lint` and aggregate `image` checks in the default-branch ruleset. diff --git a/tests/smoke.ps1 b/tests/smoke.ps1 new file mode 100644 index 0000000..813f96a --- /dev/null +++ b/tests/smoke.ps1 @@ -0,0 +1,97 @@ +param( + [string]$ContainerRuntime = "podman", + [string]$Image = "localhost/nginx-ubi9:development" +) + +$ErrorActionPreference = "Stop" +$prefix = "nginx-ubi9-smoke-$PID" +$fixedName = "$prefix-fixed" +$arbitraryName = "$prefix-arbitrary" + +function Invoke-ContainerRuntime { + param([Parameter(ValueFromRemainingArguments)] [string[]]$Arguments) + + & $ContainerRuntime @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$ContainerRuntime command failed: $($Arguments -join ' ')" + } +} + +function Wait-Nginx { + param([string]$Name) + + foreach ($attempt in 1..30) { + & $ContainerRuntime exec $Name nginx -t -q 2>$null + if ($LASTEXITCODE -eq 0) { + return + } + Start-Sleep -Seconds 1 + } + + & $ContainerRuntime logs $Name + throw "NGINX did not become ready in $Name" +} + +try { + $configuredUser = & $ContainerRuntime image inspect --format "{{.Config.User}}" $Image + if ($LASTEXITCODE -ne 0 -or $configuredUser -ne "999:0") { + throw "Expected image user 999:0; received $configuredUser" + } + + Invoke-ContainerRuntime run --detach --name $fixedName ` + --read-only ` + --tmpfs "/tmp:size=64m,mode=1777" ` + --cap-drop ALL ` + --security-opt "no-new-privileges:true" ` + --publish "127.0.0.1::8080" ` + $Image | Out-Null + Wait-Nginx $fixedName + + $fixedUid = & $ContainerRuntime exec $fixedName id -u + $fixedGid = & $ContainerRuntime exec $fixedName id -g + if ($fixedUid -ne "999" -or $fixedGid -ne "0") { + throw "Expected fixed identity 999:0; received ${fixedUid}:${fixedGid}" + } + + Invoke-ContainerRuntime exec $fixedName nginx -t -q + Invoke-ContainerRuntime exec $fixedName sh -c "test ! -w /etc/nginx/nginx.conf" + Invoke-ContainerRuntime exec $fixedName sh -c ` + "! command -v dnf && ! command -v microdnf && ! command -v yum" + + $binding = & $ContainerRuntime port $fixedName "8080/tcp" + if ($LASTEXITCODE -ne 0) { + throw "Unable to determine the published health port" + } + $hostPort = ($binding -split ":")[-1] + $health = & curl.exe --fail --silent --show-error ` + "http://127.0.0.1:$hostPort/healthz" + if ($LASTEXITCODE -ne 0 -or $health -ne "ok") { + throw "Unexpected health response: $health" + } + $index = & curl.exe --fail --silent --show-error ` + "http://127.0.0.1:$hostPort/" + $indexText = $index -join "`n" + if ($LASTEXITCODE -ne 0 -or $indexText -notmatch "NGINX on UBI 9") { + throw "The static landing page was not served" + } + + Invoke-ContainerRuntime run --detach --name $arbitraryName ` + --read-only ` + --tmpfs "/tmp:size=64m,mode=1777" ` + --cap-drop ALL ` + --security-opt "no-new-privileges:true" ` + --user "10001:0" ` + $Image | Out-Null + Wait-Nginx $arbitraryName + + $arbitraryUid = & $ContainerRuntime exec $arbitraryName id -u + $arbitraryGid = & $ContainerRuntime exec $arbitraryName id -g + if ($arbitraryUid -ne "10001" -or $arbitraryGid -ne "0") { + throw "Expected arbitrary identity 10001:0; received ${arbitraryUid}:${arbitraryGid}" + } + + Write-Output "Restricted-runtime smoke tests passed for $Image" +} +finally { + & $ContainerRuntime rm --force $fixedName $arbitraryName 2>$null | Out-Null +} diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100644 index 0000000..6f37ab9 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +runtime="${CONTAINER_RUNTIME:-podman}" +image="${IMAGE:-localhost/nginx-ubi9:development}" +prefix="nginx-ubi9-smoke-${RANDOM}-$$" +primary="${prefix}-primary" +arbitrary="${prefix}-arbitrary" + +cleanup() { + "${runtime}" rm --force "${primary}" "${arbitrary}" >/dev/null 2>&1 || true +} +trap cleanup EXIT + +run_restricted() { + local name="$1" + shift + "${runtime}" run --detach --name "${name}" \ + --read-only \ + --tmpfs /tmp:size=64m,mode=1777 \ + --cap-drop ALL \ + --security-opt no-new-privileges:true \ + "$@" \ + "${image}" >/dev/null +} + +wait_for_nginx() { + local name="$1" + local _ + for _ in {1..30}; do + if "${runtime}" exec "${name}" nginx -t -q >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + "${runtime}" logs "${name}" >&2 + return 1 +} + +test "$("${runtime}" image inspect --format '{{.Config.User}}' "${image}")" = "999:0" + +run_restricted "${primary}" --publish 127.0.0.1::8080 +wait_for_nginx "${primary}" +test "$("${runtime}" exec "${primary}" id -u)" = "999" +test "$("${runtime}" exec "${primary}" id -g)" = "0" +"${runtime}" exec "${primary}" nginx -t -q +"${runtime}" exec "${primary}" test ! -w /etc/nginx/nginx.conf +"${runtime}" exec "${primary}" sh -c '! command -v dnf && ! command -v microdnf && ! command -v yum' + +binding="$("${runtime}" port "${primary}" 8080/tcp)" +host_port="${binding##*:}" +test "$(curl --fail --silent --show-error "http://127.0.0.1:${host_port}/healthz")" = "ok" +curl --fail --silent --show-error "http://127.0.0.1:${host_port}/" | grep -Fq 'NGINX on UBI 9' + +run_restricted "${arbitrary}" --user 10001:0 +wait_for_nginx "${arbitrary}" +test "$("${runtime}" exec "${arbitrary}" id -u)" = "10001" +test "$("${runtime}" exec "${arbitrary}" id -g)" = "0" + +echo "Rootless restricted-runtime smoke tests passed for ${image}" From 7d86f3feb9052c210c958d4ec7cbd7e4fae64379 Mon Sep 17 00:00:00 2001 From: joey-huckabee <138994589+joey-huckabee@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:51:02 -0500 Subject: [PATCH 2/2] fix: use patched nginx module stream --- Containerfile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Containerfile b/Containerfile index 34956f7..2c3848a 100644 --- a/Containerfile +++ b/Containerfile @@ -5,7 +5,8 @@ ARG UBI_MICRO_IMAGE="registry.access.redhat.com/ubi9/ubi-micro:9.8@sha256:f332c9 FROM ${UBI_MINIMAL_IMAGE} AS builder -ARG NGINX_RPM_VERSION="2:1.20.1-28.el9_8.5" +ARG NGINX_MODULE_STREAM="1.26" +ARG NGINX_RPM_VERSION="2:1.26.3-9.module+el9.8.0+24599+8fde0ff7.3" # Install the exact Red Hat NGINX build and its runtime dependency closure into # a separate root. The UBI Micro final stage receives no package-management @@ -13,6 +14,10 @@ ARG NGINX_RPM_VERSION="2:1.20.1-28.el9_8.5" # hadolint ignore=DL3041 RUN microdnf install -y dnf \ && mkdir -p /runtime \ + && dnf module enable -y \ + --installroot=/runtime \ + --releasever=9 \ + "nginx:${NGINX_MODULE_STREAM}" \ && dnf install -y \ --installroot=/runtime \ --releasever=9 \ @@ -37,8 +42,8 @@ RUN microdnf install -y dnf \ FROM ${UBI_MICRO_IMAGE} -ARG NGINX_VERSION="1.20.1" -ARG NGINX_RPM_VERSION="2:1.20.1-28.el9_8.5" +ARG NGINX_VERSION="1.26.3" +ARG NGINX_RPM_VERSION="2:1.26.3-9.module+el9.8.0+24599+8fde0ff7.3" LABEL org.opencontainers.image.title="NGINX on Red Hat UBI 9" \ org.opencontainers.image.description="A security-oriented, rootless NGINX image built on Red Hat UBI 9 Micro" \