ci: attach SBOM from a least-privilege upload job (LAB-983) - #64
Draft
27Bslash6 wants to merge 3 commits into
Draft
ci: attach SBOM from a least-privilege upload job (LAB-983)#6427Bslash6 wants to merge 3 commits into
27Bslash6 wants to merge 3 commits into
Conversation
The Generate SBOM job had no permissions: block, so it inherited the repo default of read-only and every release: published run died on "Resource not accessible by integration" at the asset upload. cachekit-core-v0.4.0 shipped with zero release assets. Adding contents: write to that job would have been the wrong fix: it runs cargo sbom, which compiles and executes third-party build scripts and proc-macros, and a release-write token must not share a runner with untrusted code. Split instead, matching cachekit-py's sbom job — generation stays credential-less at contents: read, the SBOM crosses as a workflow artifact, and a separate sbom-upload job holds the only write token while running nothing but first-party actions. Also in this path: - Replace actions/upload-release-asset (archived by GitHub 2021-03-03, and running Node 20 under a forced Node 24 runtime) with actions/github-script. gh is not an option: the cachekit ARC runner has no gh CLI (LAB-899). - Delete the cargo install cargo-sbom, rust-toolchain and cache steps. The runner image bakes Rust stable and cargo-sbom, the plain install exits 101 against the runner's persistent CARGO_HOME, and this job compiles nothing. - Guard against an empty SBOM. An empty-but-valid CycloneDX document looks like a verified supply chain and isn't. The guard also fails closed on an unparseable file, which the naive form does not: a failed command substitution leaves count empty, [ "" -lt 1 ] errors instead of comparing, and the step exits 0. - Switch to cyclone_dx_json_1_6 so the asset matches the format release.yml attests, and so the guard can count .components. - Add a release_tag dispatch input. Without it this path could only ever be exercised by cutting a release, which is how v0.4.0 stayed broken. It also backfills v0.4.0. An SBOM dispatch no longer drags Kani onto the ARC pool. - Check out the released tag rather than main: an SBOM describing the wrong tree is worse than a missing one.
Three findings from the panel: - The workflow concurrency group did not distinguish backfill dispatches, so a second dispatch (or a scheduled run on the same ref) could cancel one mid-flight. Because sbom-upload deletes a pre-existing asset before re-uploading, a cancel landing in that window would leave a release with its SBOM deleted and never replaced — worse than the missing asset this fixes, and hidden behind a "cancelled" status. release_tag is now part of the key. Uses github.event.inputs rather than inputs: the group is evaluated on every event and the inputs context is only documented for dispatch/reusable calls. - The target tag was not checked against a real release until the upload job, so `cargo sbom` would compile and execute third-party build scripts for any ref a dispatch named, and a typo cost a full generation run before failing. Resolution moved ahead of the checkout, while the workspace is still empty. - The comment justifying the deleted install steps cited release.yml's persistent-CARGO_HOME rationale, which is itself stale there — that job moved to ubuntu-latest. Reworded to stand on the runner image instead. release.yml is out of scope for this ticket, so its stale comment is left alone. Resolving the release id in the generate job and passing it to the upload job also drops a redundant getReleaseByTag and closes the window where a repointed tag could put this SBOM on a different release than it was generated from. Rejected: extending the backfill exclusion to deep-fuzz. run_deep_fuzz is already an explicit opt-in, and silently ignoring an input the operator ticked is worse than honouring it. The kani exclusion is not the same case — kani runs on any dispatch with no opt-in at all.
Live verification of the split found the wall behind the permissions bug: immutable releases are enabled on this repository, and GitHub applies immutability at publish. `release: published` therefore fires only ever against a sealed release, and the upload returns "Cannot upload assets to an immutable release". Verified in run 30359945625 - Generate SBOM green in 19s, Attach SBOM to Release red on that error. Every release in this repo reports immutable: true and none has ever carried an asset; cachekit-rs is the same, cachekit-py and cachekit-ts are not. That is a policy decision, not a retry: publishing an SBOM asset needs either immutable releases turned off for this repo, or releases created as drafts with assets attached before publish (which lives in release.yml, out of scope here). So the target release is now checked before generation runs, and the job fails with that choice spelled out instead of burning a generation run and dying in 48KB of octokit output. SECURITY.md documents what the repo actually ships: the attested SBOM, which is real and verifiable today, plus an explicit statement that releases carry no SBOM asset and why. The earlier wording in this branch promised an asset that cannot exist - an unverifiable claim in a security document is a trust bug, and the expert panel flagged it before this run confirmed it.
Contributor
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes LAB-983 — draft: needs a policy decision before this can merge. See "The wall" below.
What was broken
The
sbomjob insecurity.ymlhad nopermissions:block, so it inherited the repository's read-only default token. Everyrelease: publishedrun died at the asset upload withResource not accessible by integration, andcachekit-core-v0.4.0shipped with zero release assets.Adding
contents: writeto that job would have been the wrong fix. It runscargo sbom, which compiles and executes third-party build scripts and proc-macros; a release-write token must not share a runner with untrusted code.cachekit-pyalready rejected exactly this shape.What this does
sbomgenerates atpermissions: contents: readwithpersist-credentials: false; the SBOM crosses as a workflow artifact;sbom-uploadholds the onlycontents: writetoken and runs nothing but first-party actions.upload-release-assetwas archived by GitHub on 2021-03-03. Replaced withactions/github-script—ghis not an option, thecachekitARC runner has noghCLI (LAB-899).cargo-sbom, and this job compiles nothing, socargo install/rust-toolchain/actions/cacheall went. Generation now takes 19s.countempty,[ "" -lt 1 ]errors rather than compares, and the step exits 0. Unit-tested against 8 malformed inputs.cyclone_dx_json_1_6, matching the formatrelease.ymlattests.release_tagdispatch input so the path is testable without cutting a release, and excludes Kani from a backfill dispatch so it does not drag the ARC pool along.The wall
Live verification found the real blocker behind the permissions bug. Immutable releases are enabled on this repository, and GitHub applies immutability at publish.
release: publishedtherefore only ever fires against an already-sealed release:Run 30359945625 —
Generate SBOMgreen in 19s,Attach SBOM to Releasered on that error. Every release in this repo reportsimmutable: trueand none has ever carried an asset.cachekit-rsis the same;cachekit-pyandcachekit-tshave immutability off.So no amount of fixing
security.ymlcan attach an SBOM asset, andv0.4.0cannot be backfilled — a sealed release refuses assets permanently. This needs a decision, not a retry:release.yml, which LAB-983 puts out of scope. The two jobs here are reusable unchanged.actions/attest-sbominrelease.yml, which is the cryptographically verifiable artifact. ← my recommendationPending that call, the workflow now checks immutability before generating and fails with the choice spelled out, rather than burning a generation run and dying in 48KB of octokit output:
Review
Expert panel (bug-hunter-supreme, security-specialist, code-craftsman, catchphrase-agent) at high stakes. Three findings applied:
cancel-in-progress: true; since upload deletes a pre-existing asset before re-uploading, a cancel in that window would leave a release with its SBOM deleted and never replaced — worse than the bug being fixed.release_tagis now part of the key.cargo sbomwould execute third-party build scripts for any ref a dispatch named before anything checked the tag was a real release. Resolution moved ahead of the checkout, while the workspace is still empty. Also resolves the release id there and passes it downstream, closing the window where a repointed tag could put this SBOM on a different release.release.yml(its persistent-CARGO_HOMErationale predates that job's move toubuntu-latest). Reworded.release.ymlis out of scope so its own stale comment is left alone.Rejected: extending the backfill exclusion to
deep-fuzz.run_deep_fuzzis already an explicit opt-in, and silently ignoring an input the operator ticked is worse than honouring it.Filed separately: the credential-less job and token-holding jobs share a persistent cargo volume on the ARC pool, so build-script code could poison it for a later privileged job. Pre-existing across the estate and not made worse here, but the job-level
permissions:split alone does not isolate a shared filesystem.Docs
SECURITY.mdgains a Software Bill of Materials section documenting the attested SBOM, which is real and verifiable today, plus an explicit statement that releases carry no SBOM asset and why. An earlier revision of this branch promised the asset; the panel flagged it as unverifiable and this run proved it impossible.