diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 53eff4e..182328d 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -146,6 +146,8 @@ jobs: timeout-minutes: 45 env: TRIVY_IMAGE: ${{ vars.TRIVY_IMAGE }} + PYTHON_BASE_IMAGE: ${{ vars.PYTHON_BASE_IMAGE }} + NGINX_BASE_IMAGE: ${{ vars.NGINX_BASE_IMAGE }} steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -161,7 +163,12 @@ jobs: - name: Build and publish API run: | image="gitea.blyzer.com.br/blyzer/dtf-api" - docker build --pull --file deploy/Dockerfile.api \ + # The Dockerfiles pin digests themselves; these variables let a base be + # moved forward without editing the repository. --pull is intentionally + # absent: a digest already names one immutable image. + set -- + [ -n "$PYTHON_BASE_IMAGE" ] && set -- --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE" + docker build --file deploy/Dockerfile.api "$@" \ --build-arg VCS_REF="${{ gitea.sha }}" \ --tag "$image:latest" --tag "$image:${{ gitea.sha }}" . docker push "$image:latest" @@ -169,27 +176,39 @@ jobs: - name: Build and publish web run: | image="gitea.blyzer.com.br/blyzer/dtf-web" - docker build --pull --file deploy/Dockerfile.web \ + set -- + [ -n "$PYTHON_BASE_IMAGE" ] && set -- --build-arg PYTHON_BASE_IMAGE="$PYTHON_BASE_IMAGE" + [ -n "$NGINX_BASE_IMAGE" ] && set -- "$@" --build-arg NGINX_BASE_IMAGE="$NGINX_BASE_IMAGE" + docker build --file deploy/Dockerfile.web "$@" \ --build-arg VCS_REF="${{ gitea.sha }}" \ --tag "$image:latest" --tag "$image:${{ gitea.sha }}" . docker push "$image:latest" docker push "$image:${{ gitea.sha }}" - # Reported, not blocking. The current bases carry HIGH/CRITICAL findings - # with no fix available upstream, so gating on them would stop every - # deploy without making anything safer. Read the counts each release, and - # see ROADMAP 2.6 for pinning digests and triaging what is fixable. - - name: Image vulnerability report + # CRITICAL blocks, HIGH is reported. Both images carry zero CRITICAL after + # the base pinning and OS upgrades, so this gate holds the line already + # reached. The remaining HIGH findings have no upstream fix, so failing on + # them would stop releases without making anything safer. + - name: Image vulnerabilities run: | image="${TRIVY_IMAGE:-aquasec/trivy:0.58.1}" + failed=0 for target in \ "gitea.blyzer.com.br/blyzer/dtf-api:${{ gitea.sha }}" \ "gitea.blyzer.com.br/blyzer/dtf-web:${{ gitea.sha }}"; do - echo "--- $target" + echo "--- $target (HIGH, reported)" docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \ - image --scanners vuln --severity HIGH,CRITICAL --no-progress \ - --format table --exit-code 0 "$target" || \ - echo "::warning::Could not scan $target" + image --scanners vuln --severity HIGH --no-progress \ + --format table --exit-code 0 "$target" || + echo "::warning::Could not scan $target for HIGH findings" + echo "--- $target (CRITICAL, blocking)" + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock "$image" \ + image --scanners vuln --severity CRITICAL --no-progress \ + --format table --exit-code 1 "$target" || failed=1 done + if [ "$failed" -ne 0 ]; then + echo "::error::A CRITICAL vulnerability was found in a published image." + exit 1 + fi - name: Trigger Portainer redeployment env: diff --git a/PORTAINER.md b/PORTAINER.md index 4bb8a7e..c044a0e 100644 --- a/PORTAINER.md +++ b/PORTAINER.md @@ -24,7 +24,8 @@ What actually gates a deployment: | Browser suites | only when the runner has Chrome; otherwise warns and continues | | Trivy secret scan (HIGH/CRITICAL) | yes | | Source preflight (`deploy/production_preflight.py --source-only`) | only when `ENFORCE_PRODUCTION_PREFLIGHT` is `true` | -| Trivy image vulnerabilities | no — reported after the build | +| Trivy image vulnerabilities, CRITICAL | yes | +| Trivy image vulnerabilities, HIGH | no — reported after the build | The source preflight is advisory by default because it refuses a release while the payment and messaging adapters are fake, which is the deliberate state the @@ -32,10 +33,15 @@ stack runs in today. Enforcing it now would block every deployment. Set the repository variable `ENFORCE_PRODUCTION_PREFLIGHT` to `true` once real adapters land, and it becomes a hard gate. -The image scan is reported rather than enforced because the current bases carry -HIGH/CRITICAL findings with no upstream fix, so failing on them would stop -releases without making anything safer. Triage what is fixable and pin base -digests first; see `ROADMAP.md` 2.6. +CRITICAL image findings block. Both images carry none: the bases are pinned by +digest, both Dockerfiles upgrade their OS packages, and the web image moved off +the 1.28 nginx line, which pins `nginx=1.28.3-r1` in `/etc/apk/world` and so +cannot be patched in place. HIGH findings are reported rather than enforced +because the remainder have no upstream fix. + +`PYTHON_BASE_IMAGE` and `NGINX_BASE_IMAGE` override the digests pinned in the +Dockerfiles. Update the Dockerfile default in the same change, so the repository +still records what a build used. Repository variables: diff --git a/deploy/Dockerfile.api b/deploy/Dockerfile.api index ba71b6b..8f4f6a7 100644 --- a/deploy/Dockerfile.api +++ b/deploy/Dockerfile.api @@ -1,5 +1,8 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_BASE_IMAGE=python:3.12-slim +# Pinned by digest so a rebuild of the same commit produces the same base. +# Override with the PYTHON_BASE_IMAGE repository variable to move it forward +# deliberately, and update this default in the same change. +ARG PYTHON_BASE_IMAGE=python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9 FROM ${PYTHON_BASE_IMAGE} ARG VCS_REF=unknown @@ -7,6 +10,13 @@ LABEL org.opencontainers.image.title="DTF Portal/API" \ org.opencontainers.image.revision="$VCS_REF" \ org.opencontainers.image.source="DTF System repository" +# The base is pinned, so its OS packages are frozen at the digest's build date. +# Upgrade them here or the image ships known-fixed Debian vulnerabilities, which +# is what the production image was doing while the local one already did this. +RUN apt-get update \ + && apt-get upgrade -y \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY local/requirements.txt local/requirements.lock /app/local/ RUN python -m pip install --no-cache-dir --require-hashes -r local/requirements.lock diff --git a/deploy/Dockerfile.web b/deploy/Dockerfile.web index bb1b425..e7cb226 100644 --- a/deploy/Dockerfile.web +++ b/deploy/Dockerfile.web @@ -1,6 +1,10 @@ # syntax=docker/dockerfile:1 -ARG PYTHON_BASE_IMAGE=python:3.12-slim -ARG NGINX_BASE_IMAGE=nginx:1.28-alpine +ARG PYTHON_BASE_IMAGE=python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9 +# Both bases are pinned by digest; override with the repository variables to +# move them forward deliberately. +# nginx 1.31.6. The 1.28 line pins nginx=1.28.3-r1 in /etc/apk/world, so its five +# HIGH findings cannot be upgraded in place; 1.29 scans worse. This one is clean. +ARG NGINX_BASE_IMAGE=nginx:alpine@sha256:62ff2089abf5a9ed33bd232895bef5e22f7bb4b200675cec49a5ebc48e3d4ac8 FROM ${PYTHON_BASE_IMAGE} AS policy WORKDIR /build COPY dtf-site.html /build/dtf-site.html @@ -10,6 +14,8 @@ ENV NGINX_TEMPLATE=/build/deploy/nginx.conf.template RUN python local/compile_web.py FROM ${NGINX_BASE_IMAGE} +# Same reason as the API image: a pinned base freezes its packages. +RUN apk upgrade --no-cache ARG VCS_REF=unknown LABEL org.opencontainers.image.title="DTF Site and Kanban" \ org.opencontainers.image.revision="$VCS_REF" \ diff --git a/local/Dockerfile b/local/Dockerfile index 4a738e3..3553d7d 100644 --- a/local/Dockerfile +++ b/local/Dockerfile @@ -1,4 +1,6 @@ -FROM python:3.12-slim +# Same pinned base as deploy/Dockerfile.api, so the integration suite exercises +# the image that ships rather than a different one. +FROM python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9 RUN apt-get update \ && apt-get upgrade -y \ && rm -rf /var/lib/apt/lists/* diff --git a/local/Dockerfile.web b/local/Dockerfile.web index 83a4602..023421c 100644 --- a/local/Dockerfile.web +++ b/local/Dockerfile.web @@ -1,10 +1,11 @@ -FROM python:3.12-slim AS policy +FROM python:3.12-slim@sha256:2f17fc044b579bab302c2e8054d3a686e2cb9a83de48e70534b94cd8ebbe06a9 AS policy WORKDIR /build COPY dtf-site.html /build/dtf-site.html COPY local /build/local RUN python local/compile_web.py -FROM nginx:1.28-alpine +# Same pinned base as deploy/Dockerfile.web. +FROM nginx:alpine@sha256:62ff2089abf5a9ed33bd232895bef5e22f7bb4b200675cec49a5ebc48e3d4ac8 RUN apk upgrade --no-cache ENV WEB_INDEX=index.html COPY --from=policy /build/default.conf.template /etc/nginx/templates/default.conf.template