diff --git a/Containerfile b/Containerfile index fa05b1f..27ee80f 100644 --- a/Containerfile +++ b/Containerfile @@ -1,18 +1,48 @@ -FROM python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9 +# Build stage: pip exists here and is never copied into the runtime image. +FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS build ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PATH=/opt/venv/bin:$PATH -RUN python -m venv /opt/venv \ - && addgroup --system --gid 10001 approval \ - && adduser --system --uid 10001 --ingroup approval --no-create-home approval +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]' +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 +# Runtime stage. +FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea + +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 \ + && 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 + +COPY --from=build /opt/venv /opt/venv + +WORKDIR /app USER 10001:10001 EXPOSE 8080 ENTRYPOINT ["approval-engine"] diff --git a/Containerfile.alpine b/Containerfile.alpine new file mode 100644 index 0000000..bb1d0b8 --- /dev/null +++ b/Containerfile.alpine @@ -0,0 +1,47 @@ +# 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/docs/image-scan-2026-09-06.md b/docs/image-scan-2026-09-06.md new file mode 100644 index 0000000..77a99b8 --- /dev/null +++ b/docs/image-scan-2026-09-06.md @@ -0,0 +1,103 @@ +# 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`. + +**No image was pushed.** The scan is why — see "Why nothing was released". + +## Results + +| Image | CRITICAL | HIGH | MEDIUM | +| --- | --- | --- | --- | +| As pinned before this session (`python:3.12-slim@sha256:d764629c…`, Debian 13.5) | 3 | 81 | 103 | +| `slim-hardened` — base bumped to `@sha256:78387bc3…` (Debian 13.6), pip removed | 3 | 51 | 56 | +| `alpine` — `python:3.12-alpine@sha256:b64631e0…` (Alpine 3.24.1), pip removed | **0** | **7** | **1** | + +Every finding is inherited from the base image or its distro packages. None is in +`approval_engine` code, and after removing pip the Python dependency layer +(`cryptography` 50.0.1, `PyJWT` 2.13.0, `waitress` 3.0.2, `cffi`, `pycparser`) +reports zero findings at MEDIUM and above in all three. + +## Two fixes applied + +**The base pin was stale.** `Containerfile` pinned a Debian 13.5 build of +`python:3.12-slim`; current upstream is Debian 13.6. Bumping the pin removes 30 +HIGH and 47 MEDIUM findings on its own. The pin is still a digest, not a tag — +the fix is a newer immutable pin, not a floating one. + +**pip is gone from the runtime image.** All 10 MEDIUM Python findings were in +pip itself, which is a build-time tool with no business in a running approval +service. The build is now two-stage: pip installs the venv in the build stage +and is deleted from both the venv and the base's `/usr/local` in the runtime +stage. `command -v pip pip3 pip3.12` returns nothing in both variants. + +## The three CRITICALs, and why the base choice is now open + +The remaining CRITICALs on Debian are all `perl-base` 5.40.1-6: + +- `CVE-2026-13221` +- `CVE-2026-42496` +- `CVE-2026-8376` + +**All three have no upstream fix** (trivy reports no fixed version), so no base +bump or package upgrade clears them. `perl-base` is `Essential: yes` on Debian +and is not safely removable. This service does not use perl at any point. + +Alpine carries no perl at all, which is why it reports 0 CRITICAL. That makes +the base a real decision rather than a preference: + +- **Alpine** clears all three CRITICALs and drops HIGH from 51 to 7, but changes + the C library from glibc to musl. +- **Debian slim** keeps glibc and ships three unfixable CRITICALs in a package + the service never calls. + +### Evidence that Alpine is viable + +- Image builds with no compiler toolchain: `cryptography` 50.0.1 installs from + musllinux wheels. +- **The full test suite passes on musl: 111 passed**, the same count as the + workstation. (An earlier run showed 102 passed / 1 skipped; the 9-test gap was + `tests/test_examples.py` skipping on a missing `jsonschema` test dependency in + the throwaway container, not a musl failure.) +- Runtime identity `uid=10001(approval) gid=10001(approval)`, non-root. +- Carries `LATEST_SCHEMA_VERSION = 3` and `Engine` tenant default + `tenant:platform`. +- First-install migration on a fresh volume: `migrate` then `verify` both report + `schema_version: 3`, `schema_current: true`, `integrity: ["ok"]`, + `foreign_key_violations: 0`, `persistent: true`. +- Fail-closed gates hold in the image: `serve --production --db :memory:` + refuses with "production requires a persistent database"; production without + audit configuration refuses with "production requires authenticated audit + delivery". + +`Containerfile.alpine` holds this variant. It is a **candidate**, not the +sanctioned base — the sanctioned base is still `Containerfile`. + +## Why nothing was released + +`glas-harness` asked for a scanned immutable image and said to deploy only when +identity and audit requirements pass. The scan gate is not met by the Debian +variant, and pushing it would bake three unfixable CRITICALs into a release +digest that the manifest then pins by hash. + +Choosing the runtime C library for a production approval service is also not a +call this repo should make silently. So: + +- No push to `forgejo.coulomb.social`. +- `deploy/approval-engine.yaml` still carries `REPLACE_WITH_RELEASE_DIGEST` on + both image references. +- `APPROVAL-WP-0002-T03` stays `wait`. + +Local build digests, recorded for traceability and explicitly **not** release +digests: `slim-hardened` `sha256:78b87e68e520…`, `alpine` `sha256:736647294706…`. + +## Open questions for owners + +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. 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 795d6c7..c200a2f 100644 --- a/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md +++ b/workplans/APPROVAL-WP-0002-production-readiness-and-consumer-adoption.md @@ -203,6 +203,35 @@ inventory a scanner needs is the pinned base above plus T03 stays `wait`: still no release digest, no KeyCape/audit credentials, no rollout, and no restart/restore evidence. Nothing here is a deploy. +2026-09-06 scan gate — run, and it failed on the sanctioned base. Full record in +`docs/image-scan-2026-09-06.md`. Scanned with trivy (no scanner was installed; +this one needed none). Findings are all inherited from the base image, none in +`approval_engine` code. + +Two fixes applied. The base pin was stale at Debian 13.5 while upstream is +13.6 — bumping the digest removed 30 HIGH and 47 MEDIUM. And pip, which held all +10 MEDIUM Python findings, is now absent from the runtime image via a two-stage +build; it is a build-time tool and had no business in a running approval +service. + +What remains is a decision, not a task. Three CRITICALs persist on Debian, all +`perl-base` (`CVE-2026-13221`, `CVE-2026-42496`, `CVE-2026-8376`), **none with +an upstream fix**, in a package this service never invokes and which Debian +marks `Essential: yes`. An Alpine variant carries no perl and scans 0 CRITICAL / +7 HIGH / 1 MEDIUM against Debian's 3 / 51 / 56. It is proven viable — musl +wheels resolve without a toolchain, the full suite passes on musl at 111, and +non-root identity, schema v3, `tenant:platform`, fresh-store migrate/verify and +both production fail-closed gates all hold in the image. It is parked in +`Containerfile.alpine` as a candidate; `Containerfile` remains the sanctioned +base. + +Nothing was pushed. Pushing the Debian variant would pin three unfixable +CRITICALs into a release digest, and choosing the runtime C library for this +service is not a call to make silently. `deploy/approval-engine.yaml` still +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. + ## Wire outbox delivery and reconciliation ```task