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
174 lines
7.7 KiB
Markdown
174 lines
7.7 KiB
Markdown
# Image scan — 2026-09-06
|
|
|
|
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`.
|
|
|
|
## 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
|
|
|
|
| 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…`.
|
|
|
|
## Both open questions were answered
|
|
|
|
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.
|