perf(ci-runner): bake grype + cve-gate so the gate stops being a container #91

Merged
ryangr0 merged 1 commit from perf/ci-runner-bake-gate into main 2026-08-06 19:18:11 +00:00
Owner

Part 1 of 2. This bakes the binaries; #90 then deletes the container that used to run them. Merge this one first.

Why

Five consecutive releases failed in the CVE gate, for five different reasons:

release failure
cve-gate 0.3.0 docker cp into a --read-only rootfs — refused by the daemon, shipped unsigned
run 221 gate-image pin naming a tag that was never built
run 224 seccomp clone rule that denied exactly what its comment said it allowed
run 234 2 GB tmpfs — too small for grype's DB plus an extracted multi-GB image
run 238 cache volumes created root:root, unwritable by uid 65532

Not one is about finding CVEs. Every one comes from running the scan inside a maximally hardened container reached through a remote dind daemon with no shared filesystem — a container that is, by design, extremely hard to get anything into or out of.

That hardening was never required by the gate. It was there because cve-gate doubles as the ADR-0006 worked example of a hardened image, and the two goals were fused into one artifact sitting on the critical path of every release.

What this does

Copies both binaries out of that image. They're static Go, so they run unchanged on this base:

COPY --from=cve-gate-src /usr/local/bin/grype    /usr/local/bin/grype
COPY --from=cve-gate-src /usr/local/bin/cve-gate /usr/local/bin/cve-gate

Pinned tag and digest, following tauri-ci-runner's existing inter-image convention (ARG REGISTRY_WEBGRIP).

cve-gate remains the ADR-0006 example — it's simply no longer in the hot path.

What #90 can then delete

A docker pull per release, the seccomp profile invocation and its verifier, two cache volumes, the ownership helper container, the bootstrap escape hatch, the base64 env marshalling, the stdout marker protocol, and the retry loop. ~150 lines of action become ~20.

Honest caveat

cve-gate releases are currently unsigned — the gate couldn't sign itself while it was broken — so this pins an unsigned artifact. It's one we built, in our own registry, pinned by digest. That resolves the first time cve-gate releases through a working gate, and it's the reason to fix this ordering rather than live with it.

Regression guard, strengthened

The existing command -v cosign && command -v syft check now also runs grype version and asserts cve-gate reaches its own argument validation:

IMAGE_REF= cve-gate 2>&1 | grep -q 'IMAGE_REF is required'

That proves the binaries actually execute on this base — a command -v alone would pass on a binary that segfaults, which is exactly the class of thing that has been biting us.

Verified

Branch build-check is the real test here: it does a full docker buildx build of this Dockerfile, so a wrong digest or an incompatible binary fails on the branch rather than on main.

**Part 1 of 2.** This bakes the binaries; [#90](https://forgejo.webgrip.dev/webgrip/infrastructure/pulls/90) then deletes the container that used to run them. Merge this one first. ## Why Five consecutive releases failed in the CVE gate, for five different reasons: | release | failure | |---|---| | cve-gate 0.3.0 | `docker cp` into a `--read-only` rootfs — refused by the daemon, shipped unsigned | | run 221 | `gate-image` pin naming a tag that was never built | | run 224 | seccomp `clone` rule that denied exactly what its comment said it allowed | | run 234 | 2 GB tmpfs — too small for grype's DB plus an extracted multi-GB image | | run 238 | cache volumes created `root:root`, unwritable by uid 65532 | **Not one is about finding CVEs.** Every one comes from running the scan inside a maximally hardened container reached through a *remote* dind daemon with no shared filesystem — a container that is, by design, extremely hard to get anything into or out of. That hardening was never required by the gate. It was there because `cve-gate` doubles as the **ADR-0006 worked example of a hardened image**, and the two goals were fused into one artifact sitting on the critical path of every release. ## What this does Copies both binaries out of that image. They're static Go, so they run unchanged on this base: ```dockerfile COPY --from=cve-gate-src /usr/local/bin/grype /usr/local/bin/grype COPY --from=cve-gate-src /usr/local/bin/cve-gate /usr/local/bin/cve-gate ``` Pinned tag **and** digest, following `tauri-ci-runner`'s existing inter-image convention (`ARG REGISTRY_WEBGRIP`). `cve-gate` remains the ADR-0006 example — it's simply no longer in the hot path. ## What #90 can then delete A docker pull per release, the seccomp profile invocation and its verifier, two cache volumes, the ownership helper container, the bootstrap escape hatch, the base64 env marshalling, the stdout marker protocol, and the retry loop. **~150 lines of action become ~20.** ## Honest caveat **cve-gate releases are currently unsigned** — the gate couldn't sign itself while it was broken — so this pins an unsigned artifact. It's one we built, in our own registry, pinned by digest. That resolves the first time cve-gate releases through a working gate, and it's the reason to fix this ordering rather than live with it. ## Regression guard, strengthened The existing `command -v cosign && command -v syft` check now also runs `grype version` and asserts `cve-gate` reaches its own argument validation: ```dockerfile IMAGE_REF= cve-gate 2>&1 | grep -q 'IMAGE_REF is required' ``` That proves the binaries actually **execute** on this base — a `command -v` alone would pass on a binary that segfaults, which is exactly the class of thing that has been biting us. ## Verified Branch `build-check` is the real test here: it does a full `docker buildx build` of this Dockerfile, so a wrong digest or an incompatible binary fails on the branch rather than on main.
perf(ci-runner): bake grype + cve-gate so the gate stops being a container
All checks were successful
[Workflow] On Source Change / Guard: per-image .releaserc.cjs present (push) Successful in 2s
[Workflow] On Source Change / Determine Changed Directories (push) Successful in 16s
[Workflow] On Source Change / Determine changed images (push) Successful in 0s
[Workflow] On Source Change / Release (ci-runner, ops/docker/ci-runner) (push) Has been skipped
[Workflow] On Source Change / Build check (ci-runner, ops/docker/ci-runner) (push) Successful in 1m36s
75c77a83bb
Five consecutive releases failed in the CVE gate, for five different reasons:

  - `docker cp` into a --read-only rootfs, refused by the daemon (0.3.0 shipped unsigned)
  - a gate-image pin naming a tag that was never built (run 221)
  - a seccomp clone rule that denied exactly what its comment said it allowed (run 224)
  - a 2g tmpfs, too small for grype's DB plus an extracted multi-GB image (run 234)
  - volumes created root-owned, unwritable by uid 65532 (run 238)

Not one of them is about finding CVEs. Every one comes from running the scan inside a maximally
hardened container reached through a remote dind daemon with no shared filesystem — a container
that is, by design, extremely difficult to get anything into or out of.

That hardening was never required by the gate. It was there because cve-gate doubles as the
ADR-0006 worked example of a hardened image, and the two goals were fused into one artifact on the
critical path of every release.

This unfuses them. Both binaries are copied out of the hardened image — they are static Go
binaries, so they run unchanged on this base — and the gate becomes an ordinary step calling a
binary on PATH. cve-gate remains the ADR-0006 example; it is just no longer in the hot path.

What the follow-up change can then delete: a docker pull per release, the seccomp profile and its
verifier, two cache volumes, the ownership helper container, the bootstrap escape hatch, and the
transient-registry retry loop. About 150 lines of action become about 20.

Two things that only work once the gate runs as a step, both of which were silently lost inside the
container: GITHUB_STEP_SUMMARY is in the environment, so the binary writes its own budget table;
and OUT_DIR is a writable workspace, so it writes cve-budget.json directly instead of the verdict
being awk'd back out of a log.

Pinned by tag AND digest, following tauri-ci-runner's inter-image convention. NOTE: cve-gate
releases are currently UNSIGNED — the gate could not sign itself while it was broken — so this
pins an unsigned artifact we built ourselves. That resolves the first time cve-gate releases
through a working gate.

The regression guard now also runs `grype version` and asserts cve-gate reaches its own argument
validation, which proves the binaries actually execute on this base rather than merely existing.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/infrastructure!91
No description provided.