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:
parent
f88a92fb37
commit
d7a9fe53db
5 changed files with 154 additions and 75 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
19
Makefile
19
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<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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue