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 <noreply@anthropic.com>
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
This commit is contained in:
tegwick 2026-09-06 23:01:41 +02:00
parent f88a92fb37
commit d7a9fe53db
5 changed files with 154 additions and 75 deletions

View file

@ -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

View file

@ -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"]

View file

@ -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<target>\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

View file

@ -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.

View file

@ -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