feat(ci): sign + SBOM + SLSA L3 provenance for images and tarballs#257
Draft
Cre-eD wants to merge 1 commit into
Draft
feat(ci): sign + SBOM + SLSA L3 provenance for images and tarballs#257Cre-eD wants to merge 1 commit into
Cre-eD wants to merge 1 commit into
Conversation
Phase 2 of the supply-chain hardening pass: every published artifact
(Docker images + sc CLI tarballs) gets a cosign keyless signature, a
CycloneDX SBOM attestation, and a SLSA build provenance attestation.
A post-publish verification job re-fetches the artifacts and runs
cosign verify + slsa-verifier against them before the workflow
reports release-success.
What changes:
* New composite action `.github/actions/install-attest-tools` installs
cosign, syft, and slsa-verifier from a single SHA-pinned source —
one place to bump, three workflows consume it.
* `push.yaml` `docker-build` matrix:
- Per-job `id-token: write` + `attestations: write` (root stays read).
- `docker buildx build` loop replaced with `docker/build-push-action`
so we capture the pushed digest as a step output.
- Soft-fail (`continue-on-error: true`) sign + attest steps after
publish: SBOM via syft, keyless sign + SBOM-attest via cosign,
SLSA L3 provenance via `actions/attest-build-provenance`.
- Trailing aggregator emits `::warning::` if any attestation step
failed without blocking the run.
* `push.yaml` `build-platforms` matrix:
- Same per-job permissions.
- Per-tarball sidecars generated in `.sc/stacks/dist/bundle/`:
`.sha256`, `.sbom.cdx.json`, `.cosign-bundle`, `.intoto.jsonl`.
- `upload-artifact` path widened to capture all sidecars.
* `push.yaml` `docker-finalize`:
- Artifact copy step enumerates every sidecar kind explicitly
instead of `*.tar.gz` only.
- New sidecar-count assertion records partial Welder uploads as
warnings (Phase 2 bake-in policy).
* `push.yaml` new `verify-attestations` job (`needs: docker-finalize`):
- Re-fetches every published image + tarball with sidecars from
`dist.simple-container.com` / Docker Hub.
- Runs `cosign verify` + `cosign verify-blob` + `slsa-verifier
verify-image` / `verify-artifact` against the *production*
identity regex (push.yaml@refs/heads/main) only.
- Aggregator job fails on missing/invalid attestations so the
Telegram notifier surfaces it; per-step `continue-on-error: true`
keeps individual sigstore-public flakes from gating releases.
* `build-staging.yml` `build-staging` job:
- Same attestation pipeline applied to `:staging` images.
- Staging images sign with their *own* OIDC identity
(`build-staging.yml@refs/heads/staging`), separate from prod.
* `SECURITY.md`:
- Split identity-regex contract: production trust root vs staging
trust root, documented for consumer-side verification.
- Image + tarball verification command examples.
- Composite-action consumer guidance: SHA-pin the underlying
Docker image, not just the action ref (mutable-tag pull is the
V5 residual risk).
- CDN-rollback residual risk + the sc.sh manifest-pinning
mitigation landing in the follow-up PR (Phase 2 PR 2c).
Soft-fail policy: publish is the gating job; sign + attest run after
publish with `continue-on-error: true` so a sigstore-public outage
does not block releases. After 14 days of clean post-publish
verification, flip per-step `continue-on-error` to `false`.
Identity-regex contract is the consumer-facing API; see SECURITY.md
for the canonical regex. Renames to either `push.yaml` or
`build-staging.yml` must bump the regex in the same PR.
Refs: HARDENING.md "Phase 2 — Plan" + SECURITY-CONTROLS.md §3
control matrix. Follow-up PR (2c) rewrites `sc.sh` to verify the
cosign bundle before extracting the tarball.
Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
Semgrep Scan ResultsRepository:
Scanned at 2026-05-13 15:25 UTC |
Security Scan ResultsRepository:
Scanned at 2026-05-13 15:26 UTC |
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 2 of the supply-chain hardening pass. Every published artifact — Docker images (
simplecontainer/{kubectl,caddy,github-actions,cloud-helpers}) andscCLI tarballs ondist.simple-container.com— now ships with:cosign attest --type cyclonedx) for every image; per-tarball.sbom.cdx.jsonsidecar.actions/attest-build-provenance@v4.1.0(GitHub-native, non-falsifiable).cosign verify+slsa-verifieragainst the documented production identity regex, on every release.Plan reviewed by codex + gemini before implementation; reviewer findings applied:
SC_VERIFY=warndesign hardened (cosign sig failure with cosign present is always hard-fail) — lands in the follow-upsc.shPR.Tracker entries in HARDENING.md (Phase 2 — Plan section) and SECURITY-CONTROLS.md §3 reflect the locked scope.
What's in this PR
.github/actions/install-attest-tools/action.yml(new)cosign+syft+slsa-verifier. Single SHA-pinned source of truth; bump once, three workflows pick it up..github/workflows/push.yamldocker-buildid-token: write+attestations: write.docker buildx buildloop replaced withdocker/build-push-action@v7.1.0(captures digest). Soft-fail sign + SBOM + SLSA provenance steps + aggregator..github/workflows/push.yamlbuild-platforms.sha256,.sbom.cdx.json,.cosign-bundle,.intoto.jsonl.upload-artifactpath widened..github/workflows/push.yamldocker-finalize.github/workflows/push.yamlverify-attestations(new job)cosign verify+slsa-verifier verify-*against the prod identity regex.finalizejob updated toneeds:it..github/workflows/build-staging.yml:stagingimages, signed with the staging OIDC identity (separate trust root).SECURITY.mdThreat model (per
HARDENING.mdPhase 2 plan)sc.shmanifest-pinning in PR 2c, fully closed by TUF in Phase 6.Soft-fail policy
Publish is the gating job. Sign + attest steps run after publish with
continue-on-error: true, so a sigstore-public outage does not block a release. A trailing aggregator emits::warning::annotations and the post-publishverify-attestationsjob aggregates the failure for the Telegram notifier.After 14 days of clean post-publish verification, flip the per-step
continue-on-errortofalse(separate one-line PR; tracker entry inHARDENING.md).Test plan
Cannot exercise the full workflow locally — sigstore + GitHub OIDC + Docker Hub push require the actual CI environment. The validation that can run pre-merge:
python3 -c 'import yaml; yaml.safe_load(open(...))'passes on all three touched files.actionlint 1.7.7— all findings on this PR's added lines areinfo/styleshellcheck nags (SC2012 fixed; remaining are pre-existing on unchanged code, including the known custom-runner-label warning forblacksmith-8vcpu-ubuntu-2204).go build ./... && go vet ./... && go test -short ./pkg/security/...— green (PR touches no Go).stagingfirst (small blast radius) to validate the staging-side attestation flow before merging.cosign verifyagainst the new prod identity regex succeeds on the freshly-pushed images and tarballs; confirmslsa-verifier verify-image+verify-artifactsucceed; confirmverify-attestationsjob is green.Out of scope (deliberate)
sc.shverify-before-extract — separate PR (2c), opens after this merges + at least one signed release exists on the CDN. Consumer-side change has different rollback semantics; bundling would force a synchronous revert.branch.yamlPPE split — accepted-risk, see existingnosemgrepannotation.