Run the scan gate; harden the image and stop short of release
glas-harness asked for a scanned immutable image. The scan ran, and it failed on the base we had sanctioned, so no image was pushed. Every finding is inherited from the base image or its distro packages; none is in approval_engine code. Two fixes applied here: The base pin was stale. It named a Debian 13.5 build of python:3.12-slim while upstream is 13.6. Bumping the digest removes 30 HIGH and 47 MEDIUM on its own. Still a digest, not a floating tag. pip is gone from the runtime image. All 10 MEDIUM Python findings were in pip itself, a build-time tool with no business in a running approval service. The build is now two-stage, and pip is removed from both the venv and the base's /usr/local, so command -v pip returns nothing. What remains is a decision rather than 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 that Debian marks Essential. An Alpine variant carries no perl and scans 0 CRITICAL / 7 HIGH / 1 MEDIUM against Debian's 3 / 51 / 56. Alpine is proven viable rather than asserted: musl wheels resolve with no toolchain, the full suite passes on musl at 111 tests, and non-root uid 10001, schema v3, tenant:platform, fresh-store migrate/verify and both production fail-closed gates all hold in the built image. It is parked in Containerfile.alpine as a candidate; Containerfile remains sanctioned. Nothing was released. 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. The manifest still carries REPLACE_WITH_RELEASE_DIGEST. Full record in docs/image-scan-2026-09-06.md. 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
6d18f62a90
commit
f88a92fb37
4 changed files with 214 additions and 5 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
47
Containerfile.alpine
Normal file
47
Containerfile.alpine
Normal file
|
|
@ -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"]
|
||||
103
docs/image-scan-2026-09-06.md
Normal file
103
docs/image-scan-2026-09-06.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue