feat(ci): cosign-sign sc.sh itself before publishing to dist.simple-container.com#290
Merged
Conversation
…ontainer.com Phase 2c verifies the *tarball* sc.sh downloads, but a bucket-write or CDN-swap attacker can replace sc.sh with a version whose verify_sc_tarball is stripped or hard-coded to succeed. The in-script protection regresses silently for every consumer running `bash <(curl ... sc.sh)`. This wires push.yaml's docker-finalize job to sign the post-template sc.sh (the bytes that actually land at dist.simple-container.com) under the same keyless cosign identity already used for the tarballs: push.yaml@refs/heads/main. The resulting sc.sh.cosign-bundle is dropped next to sc.sh in the dist bundle dir and shipped by the existing `welder deploy -e prod` step. Hard-fail by design: if `cosign sign-blob` fails, the deploy step never runs, so consumers never observe an sc.sh whose bundle is missing or points at a previous release. Atomic w.r.t. the script bytes. docs/SECURITY.md gets a parallel "Verifying sc.sh itself" section mirroring "Verifying tarballs", with a copy-pastable curl + verify-blob + bash snippet against the same regex. Welder.sh (welder.simple-container.com) is out of scope: welder has no GitHub Actions today and ships from a local mage flow, so giving it a real trust root needs its own scoped PR. Signing welder.sh from api/push.yaml would smear the trust root (api would attest to bytes it didn't build). Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
Semgrep Scan ResultsRepository:
Scanned at 2026-05-24 15:37 UTC |
Security Scan ResultsRepository:
Scanned at 2026-05-24 15:38 UTC |
smecsia
approved these changes
May 24, 2026
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.
Summary
Phase 2c (PR #186/#212-era) shipped cosign keyless verification for the
tarball that
sc.shdownloads. Every release tarball atdist.simple-container.com publishes a sibling
.cosign-bundleandverify_sc_tarball()aborts extraction on verification failure.The remaining gap:
sc.shitself is not signed. A bucket-write orCDN-swap attacker can replace
sc.shwith a version whoseverify_sc_tarballis stripped or hard-coded to return 0 — downstreamconsumers running
bash <(curl ... sc.sh)then execute an unverifiedtarball, silently regressing the Phase 2c protection.
This wires
push.yaml'sdocker-finalizejob to sign thepost-template
sc.sh(the bytes that actually land atdist.simple-container.com) under the same keyless cosign identity
already used for the tarballs:
^https://github\.com/simple-container-com/api/\.github/workflows/push\.yaml@refs/heads/main$The resulting
sc.sh.cosign-bundleis dropped next tosc.shin thedist bundle dir and shipped by the existing
welder deploy -e prodstep. One regex, one identity, one verify pattern for both tarballs
and the bootstrap script.
Why hard-fail (no
continue-on-error)The existing tarball signing uses
continue-on-error: trueduring thePhase 2 14-day bake-in. This step deliberately does not — if signing
fails the deploy step never runs, so consumers never observe an
sc.shwhose
.cosign-bundleis missing or stale. Atomic w.r.t. the scriptbytes: both publish or neither. The cosign primitive is already proven
in production (same step shape on line 220-224 has signed tarballs since
Phase 2c), so the residual flake risk is sigstore-public being down —
that should block the release, not silently ship an unsignable script.
What's in the diff
.github/workflows/push.yaml—docker-finalizejob:id-token: write+attestations: writeso keyless cosign works.install-attest-toolsstep (same composite action already used inbuild-platforms).finalize dist bundle setup(the step that runs the sed templating ofVERSION), signs.sc/stacks/dist/bundle/sc.shand writes.sc/stacks/dist/bundle/sc.sh.cosign-bundle.welder deploy -e prodstep (which uploads the wholebundleDir).docs/SECURITY.md— new "Verifying sc.sh itself" section parallelto "Verifying tarballs", with a copy-pastable curl + cosign verify-blob
that the script does NOT self-verify (an attacker would tamper with the
verifier too) — verification must happen out-of-band in the consumer's
runner.
Pre-merge testing
pre-existing shellcheck noise on untouched steps). New lines clean.
copy of the actual sc.sh:
cosign sign-blob --bundle … sc.sh→cosign verify-blob --bundle … sc.shreturnsVerified OK; flippingone byte (appending
exit 0) →cosign verify-blobexits 1 withinvalid signature when validating ASN.1 encoded signature. Thebundle serialisation works.
production for tarballs (push.yaml:220-224), so the only delta is
which file gets signed — mechanical.
Post-merge smoke test
On the first
push.yamlrun after merge:Welder follow-up (out of scope)
welder.shat welder.simple-container.com has the same gap and isNOT addressed here. Welder has no GitHub Actions today — its releases
run from somebody's laptop via mage +
sc deploy. Signing welder.shfrom
api/push.yamlwould smear the trust root (api would attest tobytes it didn't build) and create a snapshot-vs-live race the moment
welder ships a new release out-of-band. The proper fix is a real
release workflow in the welder repo, which is materially more work
than this PR. Captured as a follow-up.
Out of scope
verify_sc_tarball()logic in sc.sh — Phase 2c is fine, this ispublishing-side only.
contract the in-script verifier already uses.
Test plan
https://dist.simple-container.com/sc.sh.cosign-bundle(HTTP 200).Verified OKon clean and exits non-zero after a tamper byte.