From d7a9fe53db2db7e856d4951fc1898aa7b9ed33ca Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 23:01:41 +0200 Subject: [PATCH] Adopt Alpine as the sanctioned base; release candidate scans clean Operator adopted Alpine/musl as the base and trivy as the scanner. Containerfile.alpine is promoted to Containerfile and the Debian slim variant is retired rather than kept as an option -- it shipped three perl-base CRITICALs with no upstream fix, in a package this service never invokes. Promoting Alpine left 7 HIGH and 1 MEDIUM, all libuuid 2.42.1-r0 as shipped by the pinned Alpine 3.24.1, and all with fixes in 2.42.3. The runtime stage now requires libuuid>=2.42.3-r1. That is a version floor, not a floating upgrade: the base stays digest-pinned and reproducible, and a vulnerable libuuid fails the build instead of shipping. The candidate scans 0 CRITICAL / 0 HIGH / 0 MEDIUM / 0 LOW. Verified on that exact artifact: non-root uid 10001, pip absent, schema v3, tenant default tenant:platform, fresh-store migrate and verify clean, restart persistence via re-verify on the same volume, both production fail-closed refusals, 111 tests on musl, kubectl dry-run passing. The gate is now reproducible instead of a one-off. make image-scan fails on any CRITICAL or HIGH, and make image-release runs build then scan then push, so a failing scan blocks the push by construction rather than by whoever remembers to look. Not released. docker push was attempted and refused by this session's sandbox as an outward-facing publish, and was not worked around. No release digest exists, so the manifest deliberately keeps REPLACE_WITH_RELEASE_DIGEST -- it must be pinned to the registry manifest digest, never the tag and never the local image id. T03 stays wait on that push plus the still-unmaterialized KeyCape registrations and audit sender credential. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D Assistant: claude-code Assistant-Model: opus Assistant-Process: 715850@bnt-lap001 Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e --- Containerfile | 38 +++++--- Containerfile.alpine | 47 --------- Makefile | 19 +++- docs/image-scan-2026-09-06.md | 95 ++++++++++++++++--- ...duction-readiness-and-consumer-adoption.md | 30 ++++++ 5 files changed, 154 insertions(+), 75 deletions(-) delete mode 100644 Containerfile.alpine diff --git a/Containerfile b/Containerfile index 27ee80f..acc65a2 100644 --- a/Containerfile +++ b/Containerfile @@ -1,5 +1,17 @@ -# Build stage: pip exists here and is never copied into the runtime image. -FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS build +# Sanctioned base — musl/Alpine, adopted 2026-09-06. +# +# Alpine carries no perl, which is where the Debian slim variant's three +# unfixable CRITICALs lived (CVE-2026-13221, CVE-2026-42496, CVE-2026-8376 in +# perl-base, no upstream fix). Debian slim scanned 3 CRITICAL / 51 HIGH / 56 +# MEDIUM against this image's 0 / 7 / 1. +# +# pip is deliberately absent from the runtime stage: it is a build-time tool, +# it held every Python-layer finding, and a running approval service has no +# business installing packages. Do not add it back. +# +# Base is pinned by digest, never a tag. See docs/image-scan-2026-09-06.md. + +FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ @@ -11,34 +23,32 @@ WORKDIR /app COPY pyproject.toml README.md ./ COPY approval_engine ./approval_engine RUN pip install --no-cache-dir '.[serve]' \ - && pip uninstall -y pip setuptools wheel 2>/dev/null || true \ && rm -rf /opt/venv/bin/pip /opt/venv/bin/pip3 /opt/venv/bin/pip3.12 \ /opt/venv/lib/python3.12/site-packages/pip \ /opt/venv/lib/python3.12/site-packages/pip-*.dist-info \ /opt/venv/lib/python3.12/site-packages/setuptools \ /opt/venv/lib/python3.12/site-packages/setuptools-*.dist-info \ - /opt/venv/lib/python3.12/site-packages/pkg_resources \ - /opt/venv/lib/python3.12/site-packages/wheel \ - /opt/venv/lib/python3.12/site-packages/wheel-*.dist-info + /opt/venv/lib/python3.12/site-packages/pkg_resources -# Runtime stage. -FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea +FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PATH=/opt/venv/bin:$PATH -RUN addgroup --system --gid 10001 approval \ - && adduser --system --uid 10001 --ingroup approval --no-create-home approval \ - && python -m pip uninstall -y pip setuptools wheel 2>/dev/null || true \ +# libuuid is patched explicitly rather than left to the base's pinned version: +# the pinned Alpine 3.24.1 ships 2.42.1-r0, which carries every remaining HIGH +# finding, all fixed in 2.42.3. The floor is a minimum version, not a floating +# upgrade, so the build stays reproducible while refusing a vulnerable libuuid. +RUN apk add --no-cache 'libuuid>=2.42.3-r1' \ + && addgroup -S -g 10001 approval \ + && adduser -S -u 10001 -G approval -H approval \ && rm -rf /usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.12 \ /usr/local/lib/python3.12/site-packages/pip \ /usr/local/lib/python3.12/site-packages/pip-*.dist-info \ /usr/local/lib/python3.12/site-packages/setuptools \ /usr/local/lib/python3.12/site-packages/setuptools-*.dist-info \ - /usr/local/lib/python3.12/site-packages/pkg_resources \ - /usr/local/lib/python3.12/site-packages/wheel \ - /usr/local/lib/python3.12/site-packages/wheel-*.dist-info + /usr/local/lib/python3.12/site-packages/pkg_resources COPY --from=build /opt/venv /opt/venv diff --git a/Containerfile.alpine b/Containerfile.alpine deleted file mode 100644 index bb1d0b8..0000000 --- a/Containerfile.alpine +++ /dev/null @@ -1,47 +0,0 @@ -# Candidate hardened base — musl/Alpine. Carries no perl, which is where the -# Debian variant's three unfixable CRITICALs live (CVE-2026-13221, -42496, -# -8376 in perl-base, no upstream fix as of 2026-09-06). -# -# Not yet the sanctioned base. See docs/image-scan-2026-09-06.md. - -FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a AS build - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PATH=/opt/venv/bin:$PATH - -RUN python -m venv /opt/venv - -WORKDIR /app -COPY pyproject.toml README.md ./ -COPY approval_engine ./approval_engine -RUN pip install --no-cache-dir '.[serve]' \ - && rm -rf /opt/venv/bin/pip /opt/venv/bin/pip3 /opt/venv/bin/pip3.12 \ - /opt/venv/lib/python3.12/site-packages/pip \ - /opt/venv/lib/python3.12/site-packages/pip-*.dist-info \ - /opt/venv/lib/python3.12/site-packages/setuptools \ - /opt/venv/lib/python3.12/site-packages/setuptools-*.dist-info \ - /opt/venv/lib/python3.12/site-packages/pkg_resources - -FROM python:3.12-alpine@sha256:b64631e04e4920160c50fbe8d8df828f7f35f06f425cb44aa09bca53e708a35a - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PATH=/opt/venv/bin:$PATH - -RUN addgroup -S -g 10001 approval \ - && adduser -S -u 10001 -G approval -H approval \ - && rm -rf /usr/local/bin/pip /usr/local/bin/pip3 /usr/local/bin/pip3.12 \ - /usr/local/lib/python3.12/site-packages/pip \ - /usr/local/lib/python3.12/site-packages/pip-*.dist-info \ - /usr/local/lib/python3.12/site-packages/setuptools \ - /usr/local/lib/python3.12/site-packages/setuptools-*.dist-info \ - /usr/local/lib/python3.12/site-packages/pkg_resources - -COPY --from=build /opt/venv /opt/venv - -WORKDIR /app -USER 10001:10001 -EXPOSE 8080 -ENTRYPOINT ["approval-engine"] -CMD ["serve", "--help"] diff --git a/Makefile b/Makefile index aa61807..192ec65 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,26 @@ SHELL := /usr/bin/env bash .DEFAULT_GOAL := test +IMAGE ?= forgejo.coulomb.social/coulomb/approval-engine +VERSION ?= 0.1.0 +# trivy is the sanctioned scanner (operator decision, 2026-09-06). +TRIVY ?= aquasec/trivy:latest + test: ## Run unit tests python3 -m pytest -q image-build: ## Build the production image locally - docker build -f Containerfile -t approval-engine:local . + docker build -f Containerfile -t approval-engine:local -t $(IMAGE):$(VERSION) . + +image-scan: ## Scan the built image; fails on any CRITICAL or HIGH finding + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock $(TRIVY) \ + image --scanners vuln --severity CRITICAL,HIGH \ + --exit-code 1 $(IMAGE):$(VERSION) + +image-release: image-build image-scan ## Build, scan, then push. Push only runs if the scan passes. + docker push $(IMAGE):$(VERSION) + @echo "Pin this digest in deploy/approval-engine.yaml (never the tag):" + @docker inspect --format '{{index .RepoDigests 0}}' $(IMAGE):$(VERSION) deploy-dry-run: ## Validate Kubernetes manifests without applying them kubectl apply --dry-run=client -f deploy/approval-engine.yaml -f deploy/networkpolicies.yaml @@ -14,4 +29,4 @@ help: ## Show this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} \ /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) -.PHONY: test image-build deploy-dry-run help +.PHONY: test image-build image-scan image-release deploy-dry-run help diff --git a/docs/image-scan-2026-09-06.md b/docs/image-scan-2026-09-06.md index 77a99b8..38a26b3 100644 --- a/docs/image-scan-2026-09-06.md +++ b/docs/image-scan-2026-09-06.md @@ -1,9 +1,23 @@ # Image scan — 2026-09-06 -Scanner: `aquasec/trivy:latest` (`--scanners vuln`), run against locally built -images. Requested by `glas-harness` under `GLAS-WP-0015` / `APPROVAL-WP-0002-T03`. +Scanner: **trivy** (`aquasec/trivy:latest`, `--scanners vuln`) — sanctioned by +the operator 2026-09-06. Requested by `glas-harness` under `GLAS-WP-0015` / +`APPROVAL-WP-0002-T03`. -**No image was pushed.** The scan is why — see "Why nothing was released". +## Outcome + +**Base decision: Alpine/musl, adopted 2026-09-06** by operator, alongside the +scanner. `Containerfile` is now the Alpine build; the Debian slim variant is +retired rather than kept as an alternative. + +**The release candidate scans completely clean: 0 CRITICAL, 0 HIGH, 0 MEDIUM, +0 LOW.** + +It has **not** been pushed. The registry push is the one remaining step and it +requires an action this session was not permitted to take — see "Release status" +at the end. + +The history below is kept because it records why the base changed. ## Results @@ -91,13 +105,70 @@ call this repo should make silently. So: Local build digests, recorded for traceability and explicitly **not** release digests: `slim-hardened` `sha256:78b87e68e520…`, `alpine` `sha256:736647294706…`. -## Open questions for owners +## Both open questions were answered -1. Is Alpine/musl acceptable as the sanctioned base for this service? If yes, - `Containerfile.alpine` becomes `Containerfile` and the release is scanned - clean of CRITICALs. -2. If glibc is required, is a documented exception for the three unfixable - `perl-base` CVEs acceptable, given the service never invokes perl? A - distroless glibc base is the third option and was not evaluated here. -3. Which scanner is sanctioned for the gate? This used trivy because it needed - no install; the estate may have a different standard. +The operator accepted Alpine/musl as the sanctioned base and trivy as the +sanctioned scanner (2026-09-06). `Containerfile.alpine` was promoted to +`Containerfile`; the Debian variant is retired. The distroless option was never +evaluated and is now moot. + +## Clearing the last findings — libuuid + +Promoting Alpine left 7 HIGH and 1 MEDIUM, all in `libuuid` 2.42.1-r0 as shipped +by the pinned Alpine 3.24.1, and **all with fixes available** in 2.42.3. + +The runtime stage now installs `libuuid>=2.42.3-r1`. That is a version *floor*, +not a floating upgrade: the base stays digest-pinned, the build stays +reproducible, and a vulnerable libuuid fails the build rather than shipping. +After that patch the image scans clean at every severity including LOW. + +## Release candidate + +Tag `forgejo.coulomb.social/coulomb/approval-engine:0.1.0`, local image id +`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`. + +**This is a local image id, not the release digest.** The release digest is the +registry manifest digest and does not exist until the image is pushed. + +Verified on this exact artifact: + +| Gate | Result | +| --- | --- | +| Vulnerability scan (trivy, all severities) | 0 findings | +| Runtime identity | `uid=10001(approval) gid=10001(approval)`, non-root | +| pip present | no — `command -v pip pip3` returns nothing | +| Schema | `LATEST_SCHEMA_VERSION = 3` | +| Store tenant default | `tenant:platform` | +| First install, no prior DB | `migrate` then `verify`: schema 3, `schema_current`, `integrity ["ok"]`, 0 FK violations, persistent | +| Restart persistence | re-`verify` on the same volume: `ok=True`, schema 3, integrity ok | +| Production without persistent DB | refused — "production requires a persistent database" | +| Production without audit config | refused — "production requires authenticated audit delivery" | +| Full test suite on musl | 111 passed | +| Manifest inputs | `kubectl apply --dry-run=client` passes | + +## Release status — blocked on one permitted action + +`docker push forgejo.coulomb.social/coulomb/approval-engine:0.1.0` was attempted +and **refused by this session's sandbox** as an outward-facing publish. It was +not retried or worked around. + +Consequently: + +- No release digest exists yet. +- `deploy/approval-engine.yaml` still carries `REPLACE_WITH_RELEASE_DIGEST` on + both image references. It must be pinned to the registry manifest digest + returned by the push, never to the tag and never to the local image id above. +- `APPROVAL-WP-0002-T03` stays `wait`. + +To finish, an operator runs the push and returns the digest, or re-runs it in a +session permitted to publish: + +``` +docker push forgejo.coulomb.social/coulomb/approval-engine:0.1.0 +docker inspect --format '{{index .RepoDigests 0}}' \ + forgejo.coulomb.social/coulomb/approval-engine:0.1.0 +``` + +Then pin both `image:` references in `deploy/approval-engine.yaml` to that +digest. Rollout, restart and restore evidence remain gated on the KeyCape +registrations and the audit sender credential, which are still unmaterialized. diff --git a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md index c200a2f..becfcfa 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -232,6 +232,36 @@ carries `REPLACE_WITH_RELEASE_DIGEST`. Awaiting an owner answer on the base (Alpine, a documented perl exception on glibc, or distroless — unevaluated) and on which scanner is sanctioned. +2026-09-06 both answered; release candidate scans clean. The operator adopted +Alpine/musl as the sanctioned base and trivy as the sanctioned scanner. +`Containerfile.alpine` is promoted to `Containerfile` and the Debian variant is +retired rather than kept as an option. + +Promoting Alpine left 7 HIGH and 1 MEDIUM, all `libuuid` 2.42.1-r0 and all with +fixes in 2.42.3. The runtime stage now requires `libuuid>=2.42.3-r1` — a version +floor rather than a floating upgrade, so the base stays digest-pinned and a +vulnerable libuuid fails the build instead of shipping. **The candidate now +scans 0 CRITICAL / 0 HIGH / 0 MEDIUM / 0 LOW.** + +Verified on that exact artifact: non-root `uid=10001`, pip absent, schema v3, +tenant default `tenant:platform`, fresh-store `migrate`+`verify` clean, restart +persistence via re-`verify` on the same volume, both production fail-closed +refusals, 111 tests on musl, and `kubectl --dry-run` passing. + +The gate is now reproducible rather than a one-off: `make image-scan` fails on +any CRITICAL or HIGH, and `make image-release` runs build then scan then push so +a failing scan blocks the push by construction. + +Still `wait`, and now for exactly one reason: `docker push` was attempted and +refused by this session's sandbox as an outward-facing publish. It was not +worked around. No release digest exists, so the manifest keeps +`REPLACE_WITH_RELEASE_DIGEST`; an operator must run `make image-release` (or the +push and `docker inspect`) and pin the returned registry manifest digest — never +the tag, never the local image id +`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`. +Rollout, restart and restore evidence remain gated on the KeyCape registrations +and audit sender credential. + ## Wire outbox delivery and reconciliation ```task