feat(security): restore SBOM ingestion, add OpenVEX + CVE budget gate #70

Closed
ryangr0 wants to merge 2 commits from feat/harden-supply-chain into main
Owner
No description provided.
feat(security): restore SBOM ingestion, add OpenVEX + CVE budget gate
All checks were successful
[Workflow] On Source Change / Guard: per-image .releaserc.cjs present (push) Successful in 1m24s
[Workflow] On Source Change / Determine Changed Directories (push) Successful in 2m40s
[Workflow] On Source Change / Determine changed images (push) Successful in 0s
[Workflow] On Source Change / Release (act-runner, ops/docker/act-runner) (push) Has been skipped
[Workflow] On Source Change / Release (agent-runner, ops/docker/agent-runner) (push) Has been skipped
[Workflow] On Source Change / Release (ci-runner, ops/docker/ci-runner) (push) Has been skipped
[Workflow] On Source Change / Release (mkdocs-runner, ops/docker/mkdocs-runner) (push) Has been skipped
[Workflow] On Source Change / Release (helm-deploy, ops/docker/helm-deploy) (push) Has been skipped
[Workflow] On Source Change / Release (php-ci-runner, ops/docker/php-ci-runner) (push) Has been skipped
[Workflow] On Source Change / Release (playwright-runner, ops/docker/playwright-runner) (push) Has been skipped
[Workflow] On Source Change / Release (node-ci-runner, ops/docker/node-ci-runner) (push) Has been skipped
[Workflow] On Source Change / Release (rust-ci-runner, ops/docker/rust-ci-runner) (push) Has been skipped
[Workflow] On Source Change / Release (rust-releaser, ops/docker/rust-releaser) (push) Has been skipped
[Workflow] On Source Change / Release (semantic-release, ops/docker/semantic-release) (push) Has been skipped
[Workflow] On Source Change / Release (semantic-release-monorepo, ops/docker/semantic-release-monorepo) (push) Has been skipped
[Workflow] On Source Change / Release (semantic-release-rust, ops/docker/semantic-release-rust) (push) Has been skipped
[Workflow] On Source Change / Release (tauri-ci-runner, ops/docker/tauri-ci-runner) (push) Has been skipped
[Workflow] On Source Change / Release (techdocs-builder, ops/docker/techdocs-builder) (push) Has been skipped
[Workflow] On Source Change / Release (techdocs-runner, ops/docker/techdocs-runner) (push) Has been skipped
[Workflow] On Source Change / Release (vikunja-mcp, ops/docker/vikunja-mcp) (push) Has been skipped
7ab628eddd
The supply-chain machinery in this repo was partly not working, in ways that
were invisible *because* the machinery existed.

Dependency-Track had ingested nothing since run 133 (~50 releases). Syft
v1.21.0 -> v1.48.0 in 5da16be crossed the CycloneDX 1.6/1.7 boundary; DT
validates against its bundled schemas, tops out at 1.6, and returned 400 every
time. The upload step is fail-soft, so it emitted :⚠️: and exited 0.
Pin the spec to @1.6 and split the fail-soft semantics: 5xx/000 (outage) stays
a warning, 4xx (we sent something bad) is now ::error:: — loud, still non-fatal.

The same document is what cosign attests, so the pin also keeps the attestation
predicate readable by 1.6-capable consumers, including Kyverno's components
length condition in homelab-cluster's image-verify-harbor-audit.

Add OpenVEX (ops/vex/) as the only sanctioned suppression mechanism: hand
authored, PR-reviewed, justified from the closed vocabulary, stamped with the
built digest and attested with the same OpenBao Transit key as the SBOM. Harbor's
project-wide cve_allowlist stays empty — it has no product scope, no
justification, no author and no expiry.

Add per-image CVE budgets (ops/security/cve-budgets.yaml) that ratchet: a new
image starts in warn, the pipeline measures it, the budget is set at the observed
number, then it only goes down. ci-runner (8C/136H) and agent-runner (1C/51H) are
seeded from measured Trivy Operator data.

The gate runs BEFORE signing. An over-budget image is still pushed but never
signed, and unsigned is what admission refuses — so a webgrip signature comes to
mean "built by CI AND within budget" rather than "built by CI".

Also:
- delete ops/kyverno/ — it targeted ghcr.io/webgrip/* with a Fulcio identity, a
  registry we don't publish to verified by a mechanism we don't use. The live
  policy in homelab-cluster is correct; this copy was fiction that read as a
  guarantee.
- repoint OCI source/url/documentation labels at Forgejo across all 17 images
- ADR-0004 supersedes ADR-0002 (keyless/GitHub -> OpenBao Transit), stating the
  regressions plainly: no SLSA provenance, no transparency log
- ADR-0005 (VEX + budgets), ADR-0006 (hardened base images, proposed)
- remove six phantom ADRs from the techdocs index that were never written

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(cve-gate): hardened image to run the CVE budget gate in
All checks were successful
[Workflow] On Source Change / Guard: per-image .releaserc.cjs present (push) Successful in 13s
[Workflow] On Source Change / Determine Changed Directories (push) Successful in 31s
[Workflow] On Source Change / Determine changed images (push) Successful in 0s
[Workflow] On Source Change / Release (cve-gate, ops/docker/cve-gate) (push) Has been skipped
0579bfc489
The gate's own toolchain was the weakest link in it. The first version ran

    curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh

at release time, in the job that decides whether an image is fit to sign: an
unpinned script from a mutable branch, fetched over the internet, executed with
the workspace and registry credentials in scope. A supply-chain hole inside a
supply-chain control.

Replaces it with ops/docker/cve-gate — which is both the gate and the worked
example for ADR-0006, so every other image migration has something concrete to
copy:

- DHI alpine-base; the -dev variant is a BUILD STAGE ONLY. apk exists there and
  nowhere else, so nothing can be installed into a running gate container.
- grype copied from dhi.io/grype (signed, has its own SBOM) rather than curl|sh.
  Located by find rather than an assumed path, so a layout change fails with a
  sentence instead of a cryptic COPY error.
- jq + yq via apk in the build stage, installed into a single /out tree.
- Runtime: no package manager, no curl, uid 65532, /tmp the only writable path
  (grype's DB cache) so it runs --read-only --cap-drop ALL --security-opt
  no-new-privileges.
- Build-time assertions that grype/jq/yq execute, a shell exists, and the
  entrypoint reaches its own validation — all AS THE NONROOT USER. A gate that
  ships broken is worse than no gate: it reports "budget met" for an image
  nobody scanned.

The gate logic moves out of the composite action into a POSIX sh entrypoint
(the runtime has ash, not bash) with a three-way exit contract: 0 pass, 1 over
budget in enforce mode, 2 could-not-run. 2 is deliberately not collapsed into
either — a gate that failed to execute is not a gate that passed.

cve-gate is the only image starting at mode: enforce, 0/0. If the image that
decides whether everything else is fit to sign cannot meet the standard it
enforces, the standard is not real.

Two things this surfaced that gate all of ADR-0006's Wave 3:
- dhi.io is free (Apache 2.0, Community tier) but NOT anonymous — /v2/ returns
  401. Needs docker login, or preferably a `dhi` pull-through proxy in Harbor to
  match how REGISTRY_DOCKERHUB/GHCR/MCR are already routed.
- Because of that, DHI digests cannot be resolved yet, so these bases are
  tag-pinned where every other base here is digest-pinned. Pin on first build.

Also fixes two bugs found while wiring it up:
- `docker start -a | tee` masked the exit code, so the gate would have always
  passed. Now reads PIPESTATUS[0].
- No bind mount: DOCKER_HOST targets the shared per-node dind DaemonSet over
  TCP, so -v ${GITHUB_WORKSPACE} would resolve on the DAEMON's filesystem.
  Workspace is copied in with docker cp instead.

Includes a loud, self-limiting bootstrap escape (ALLOW_MISSING_GATE) for the
window before cve-gate has ever been released — skipping a gate must never look
like passing one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Author
Owner

Closing in favour of #74.

Everything here except the OCI-label repoint is superseded by #74, which carries newer versions of the same files — that is exactly what the conflict list shows (cosign-sign-attest/action.yml, ADR-0005/0006, cve-gate.md, hardening-roadmap.md, cve-gate/Dockerfile). Merging this would revert three build fixes found in runs 188/192/194.

The only unique content is the org.opencontainers.image.{source,url,documentation} repoint across 17 Dockerfiles — verified as a pure three-line label change per file, nothing else. That is being re-cut as small batches, because a single PR touching all 17 produces 17 concurrent matrix builds (Forgejo ignores strategy.max-parallel) on fringe-workstation.

No content is lost.

Closing in favour of #74. Everything here except the OCI-label repoint is superseded by #74, which carries newer versions of the same files — that is exactly what the conflict list shows (`cosign-sign-attest/action.yml`, ADR-0005/0006, `cve-gate.md`, `hardening-roadmap.md`, `cve-gate/Dockerfile`). Merging this would revert three build fixes found in runs 188/192/194. The only unique content is the `org.opencontainers.image.{source,url,documentation}` repoint across 17 Dockerfiles — verified as a pure three-line label change per file, nothing else. That is being re-cut as small batches, because a single PR touching all 17 produces 17 concurrent matrix builds (Forgejo ignores `strategy.max-parallel`) on fringe-workstation. No content is lost.
ryangr0 closed this pull request 2026-07-31 16:54:37 +00:00
All checks were successful
[Workflow] On Source Change / Guard: per-image .releaserc.cjs present (push) Successful in 13s
[Workflow] On Source Change / Determine Changed Directories (push) Successful in 31s
[Workflow] On Source Change / Determine changed images (push) Successful in 0s
[Workflow] On Source Change / Release (cve-gate, ops/docker/cve-gate) (push) Has been skipped

Pull request closed

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!70
No description provided.