approval-engine/docs/image-scan-2026-09-06.md

222 lines
10 KiB
Markdown
Raw Normal View History

Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
---
artifact_type: dated status record
status_marker: >-
DATED RECORD. Sections are marked with the date their status was true. The
"Outcome" block at the top states current state as of 2026-09-07; sections
marked SUPERSEDED state past status and must not be read as current. Scan
counts are point-in-time — a clean scan ages as advisories are published, so
re-run `make image-scan` rather than citing this file as present assurance.
last_reconciled: "2026-09-07"
---
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
2026-09-06 22:52:20 +02:00
# Image scan — 2026-09-06
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
2026-09-06 23:01:41 +02:00
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`.
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
2026-09-06 22:52:20 +02:00
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
2026-09-06 23:01:41 +02:00
## 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.**
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
**It is published.** Registry tag `0.1.0` resolves to OCI index digest
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`
(linux/amd64 manifest `sha256:2064d537…`), and both `image:` references in
`deploy/approval-engine.yaml` pin that index digest. See "Registry publication"
at the end for the publication record.
**Not rolled out.** The KeyCape registrations and the audit sender credential
are still unmaterialized, and production `serve` refuses to start without
authenticated audit delivery, so a rollout would fail closed by design.
`APPROVAL-WP-0002-T03` stays `wait` on those gates.
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
2026-09-06 23:01:41 +02:00
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
The history below is kept because it records why the base changed. **Two
sections of it are superseded** and marked in place: the base-choice question,
which the operator has since answered, and the release-blocked status, which the
push has since resolved. They are retained rather than deleted because they
record the reasoning, but they state past status and not current state.
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
2026-09-06 22:52:20 +02:00
## 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.
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
## The three CRITICALs, and why the base choice was open — SUPERSEDED
> **Answered.** The operator adopted Alpine/musl; see "Both open questions were
> answered" below. This section records why the question was posed.
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
2026-09-06 22:52:20 +02:00
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`.
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
## Why nothing was released at the time — SUPERSEDED
> **Superseded.** The image has since been published; see "Registry publication".
> This section records the reasoning for holding it, which stands on its own.
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
2026-09-06 22:52:20 +02:00
`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…`.
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
2026-09-06 23:01:41 +02:00
## 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 |
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
## Release status — SUPERSEDED, states status as of 2026-09-06 before publication
> **Superseded by "Registry publication" below.** Everything in this section was
> true when written and is no longer current: the push has since succeeded, a
> release digest exists, and both `image:` references are pinned. Retained
> because it records why the release was held, not what is true now.
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
2026-09-06 23:01:41 +02:00
`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.
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
Consequently, *as of that moment*:
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
2026-09-06 23:01:41 +02:00
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
- No release digest existed yet.
- `deploy/approval-engine.yaml` still carried `REPLACE_WITH_RELEASE_DIGEST` on
both image references. It had to be pinned to the registry manifest digest
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
2026-09-06 23:01:41 +02:00
returned by the push, never to the tag and never to the local image id above.
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
- `APPROVAL-WP-0002-T03` stayed `wait`.
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
2026-09-06 23:01:41 +02:00
Reconcile the release record; guard the image pin with tests The publication at b51d174 pinned the manifest and appended a note, but left three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It has not been pushed" in its Outcome block at the top while recording the successful push at the bottom; its "Release status" section still said no release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST; and deploy/README.md still instructed an operator to replace a placeholder that is now a real digest -- an instruction to undo the pin. This is the derived-artifact staleness this repo argued for a rule about in v0.8 §12 and then produced in its own release evidence, in the file another repository would read to confirm what shipped. Superseded sections are now marked in place with what replaced them rather than deleted, since they record why the base changed and why the release was held. The scan doc carries a status marker naming which parts state current state, including that a clean scan is point-in-time and ages as advisories are published. The workplan gains the publication entry it never got, and a re-check of T03's full acceptance: a published image reads like a finished task, but the digest was one of five requirements. Nothing is deployed -- the namespace is empty and no secrets exist -- and production serve refuses to start without authenticated audit delivery, so a rollout today would fail closed and prove nothing. T03 stays wait on T01's registrations and the audit credential. Also corrects an earlier caution in the workplan that the digest was "a local image id, not a release digest". That was wrong: with the containerd/OCI store the build id is the manifest digest, and the registry resolves it. tests/test_deploy_manifest.py holds the pin mechanically instead of by review: both image references exist, are digest-pinned rather than tagged, are identical, and no placeholder remains. The identity check matters because the init container migrates the database the server then serves -- two different digests there is a split-brain migration. Verified the guard fails on the drift it targets, not only that it passes today. 116 tests pass. 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
2026-09-07 09:04:34 +02:00
The path recorded at the time, since taken by the operator:
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
2026-09-06 23:01:41 +02:00
```
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
```
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
2026-09-06 22:52:20 +02:00
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
2026-09-06 23:01:41 +02:00
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.
## Registry publication — 2026-09-06, Glas deployment session
The previously blocked push succeeded under the operator's standing production
deployment authorization. Registry tag `0.1.0` resolves to OCI index digest
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`.
The linux/amd64 image manifest is
`sha256:2064d5378e01801ecf9966ccec9c93aadd16d6b404ef4890983d89fa738c164c`.
Both deployment images now pin the registry index digest, verified remotely
following publication. Its equality to the earlier local image identifier is
not the evidence of publication; the successful push and remote index lookup are.
No StatefulSet rollout yet: live KeyCape registrations and audit sender/token
admission remain outstanding. APPROVAL-WP-0002-T03 stays wait for those gates.