pin the base image to a digest, with TTL refresh and audit labels
A tag is a moving pointer, and three things conspired to make the base immutable-in-practice while mutable-in-theory: image_hash() hashed the string "ubuntu:24.04" rather than what it resolves to, ensure_image() returns early when the tag exists, and podman build's pull policy is "missing" -- so even 'build --rebuild' rebuilt on the cached base. A Canonical patch simply never arrived, and nothing said so. Now the tag is resolved to a registry digest (skopeo, falling back to a pull unless AISB_BASE_PULL_FALLBACK=0), recorded per-ref under $AISB_CACHE_DIR/base/ at 0600, and used in FROM *and* in image_hash(): refreshing the base produces a new image instead of mutating one a sandbox may be running. Freshness is a TTL (AISB_BASE_TTL_DAYS=7, 0 every build, -1 never) plus an explicit 'update' / 'update --check'. Two rules keep it usable rather than merely strict. A stale pin beats a mutable tag: offline or with AISB_BASE_AUTO_UPDATE=0 the recorded digest is still used, and only a build with no digest at all is allowed -- announced as UNPINNED, labelled that way, warned about by doctor. And preview modes stay deterministic: --print-* never reach the network, inspect is the surface that asks the machine now. Both images carry aisb.base-digest / .node / .pi-package / .pi-version labels plus /etc/aisb/manifest.txt of resolved apt versions, which answers "which squid was enforcing this?" after the build log is gone. Tags are named after the base instead of the hardcoded u24- prefix. Bug found on the way: POL[base_digest]="$(base_pin ...)" silently produced unpinned labels, because an assignment inside a command substitution is discarded with the subshell. Helpers return text; callers assign. Also fixed: the manifest step used jq on npm's dependency objects as if they were strings, and a missing Node checksum used to skip verification quietly -- it now warns. 311 assertions pass (unit + e2e), shellcheck clean. LLM-Generated: true
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Execution plan for `ai-sandbox`, with the state each phase leaves behind.
|
||||
Every phase ends with a gate that must pass before the next one starts.
|
||||
Status as of this writing: **phases 1-7 complete**, follow-ups listed at the end.
|
||||
Status as of this writing: **phases 1-8 complete**, follow-ups listed at the end.
|
||||
|
||||
| # | Phase | Gate | Status |
|
||||
|---|-------|------|--------|
|
||||
@@ -14,6 +14,7 @@ Status as of this writing: **phases 1-7 complete**, follow-ups listed at the end
|
||||
| 5 | Environment passing | deny-by-default, `0600` delivery, redaction, fail-closed on unset | ✅ done (`tests/unit/04_env.sh`, `tests/e2e/12_sandbox.sh`) |
|
||||
| 6 | Limits, lifecycle, observability | exit codes, cleanup, caps, session log, `--keep` | ✅ done (`tests/e2e/12_sandbox.sh`) |
|
||||
| 7 | Harness defaults, docs, portability sweep | alias UX, README/threat model, shellcheck clean, no personal paths | ✅ done |
|
||||
| 8 | Supply chain: pinned base, TTL refresh, audit trail | digest in `FROM` and in the content hash; `update`/`--check`; labels + in-image package manifest | ✅ done (`tests/unit/07_base.sh`, `tests/e2e/10_image.sh`) |
|
||||
|
||||
## Phase detail
|
||||
|
||||
@@ -66,6 +67,30 @@ change the image's home layout and existing volumes keep the old files, so
|
||||
`ensure_home_volume()` creates it labelled and `clean --volumes` is the documented way
|
||||
to drop it.
|
||||
|
||||
### 8 — Supply chain
|
||||
The hash made *our* changes traceable but said nothing about the base: `FROM
|
||||
ubuntu:24.04` is a moving pointer, `podman build`'s pull policy is *missing*, and
|
||||
`ensure_image()` returns early when the tag exists — so even `build --rebuild` reused
|
||||
the cached base. Now the tag is resolved to a digest (`skopeo`, falling back to
|
||||
`podman pull` unless `AISB_BASE_PULL_FALLBACK=0`), recorded per-ref under
|
||||
`$AISB_CACHE_DIR/base/`, and used in `FROM` *and* in `image_hash()`: a refreshed base
|
||||
yields a new image rather than mutating one that may be running.
|
||||
|
||||
Two rules make it survivable rather than merely strict. **A stale pin beats a mutable
|
||||
tag** — offline, or with `AISB_BASE_AUTO_UPDATE=0`, the recorded digest is used, and
|
||||
only a build with no digest at all is allowed, loudly marked `UNPINNED` in the output,
|
||||
in the image labels and in `doctor`. **Freshness is a TTL** (`AISB_BASE_TTL_DAYS=7`,
|
||||
`0` every build, `-1` never) rather than a background job, so nothing surprises you
|
||||
mid-session and `update` stays an explicit, dated event. Both images carry
|
||||
`aisb.base-digest`/`aisb.node`/`aisb.pi-version` labels and
|
||||
`/etc/aisb/manifest.txt` with resolved apt versions, which is how you answer "which
|
||||
squid was enforcing this?" a month later. `--print-*` stays cache-only and
|
||||
deterministic; `inspect` is the surface that asks the machine now.
|
||||
|
||||
One bug worth remembering: `POL[base_digest]="$(base_pin …)"` looked right and
|
||||
silently produced `unpinned` labels, because the subshell's assignment was discarded.
|
||||
Helpers return text; callers assign.
|
||||
|
||||
## Follow-ups (not yet done)
|
||||
|
||||
* **`--net unix` mode**: `--network=none` plus a unix-socket proxy and an in-container
|
||||
@@ -81,3 +106,9 @@ to drop it.
|
||||
* **`--record`**: optional pcap/CONNECT-log capture of a session for review.
|
||||
* **`--mount` for agent skills/plugins**: first-class flag instead of a generic mount.
|
||||
* Packaging: a `quadlet`/systemd unit for a long-lived named gate.
|
||||
* Signature verification for the base where the registry signs it (cosign/sigstore),
|
||||
so a pin is authenticated rather than merely immutable; and an optional local
|
||||
mirror/`AISB_SKOPEO`-compatible offline source for air-gapped hosts.
|
||||
* `update --auto` as a timer unit, if TTL-on-next-run turns out to be too lazy for
|
||||
security response — deliberately not done yet, since a background rebuild that
|
||||
surprises a running session is its own hazard.
|
||||
|
||||
Reference in New Issue
Block a user