104 lines
4.8 KiB
Markdown
104 lines
4.8 KiB
Markdown
|
|
# 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.
|