Skip to content

feat(ci): sign + SBOM + SLSA L3 provenance for images and tarballs#257

Draft
Cre-eD wants to merge 1 commit into
mainfrom
feat/phase2-attestation
Draft

feat(ci): sign + SBOM + SLSA L3 provenance for images and tarballs#257
Cre-eD wants to merge 1 commit into
mainfrom
feat/phase2-attestation

Conversation

@Cre-eD
Copy link
Copy Markdown
Contributor

@Cre-eD Cre-eD commented May 13, 2026

Summary

Phase 2 of the supply-chain hardening pass. Every published artifact — Docker images (simplecontainer/{kubectl,caddy,github-actions,cloud-helpers}) and sc CLI tarballs on dist.simple-container.com — now ships with:

  • A cosign keyless signature (Fulcio + Rekor, OIDC-bound to this repo's workflow).
  • A CycloneDX SBOM (cosign attest --type cyclonedx) for every image; per-tarball .sbom.cdx.json sidecar.
  • A SLSA Build L3 provenance attestation via actions/attest-build-provenance@v4.1.0 (GitHub-native, non-falsifiable).
  • A post-publish verification job that re-fetches every artifact + sidecar and runs cosign verify + slsa-verifier against the documented production identity regex, on every release.

Plan reviewed by codex + gemini before implementation; reviewer findings applied:

  • VEX cut from Phase 2 (not threat-mitigating; deferred post-Phase-4).
  • Identity regex split prod vs staging (staging is not a production trust root).
  • Continuous verification promoted from weekly cron to required post-publish gate.
  • SC_VERIFY=warn design hardened (cosign sig failure with cosign present is always hard-fail) — lands in the follow-up sc.sh PR.

Tracker entries in HARDENING.md (Phase 2 — Plan section) and SECURITY-CONTROLS.md §3 reflect the locked scope.

What's in this PR

File Change
.github/actions/install-attest-tools/action.yml (new) One composite for cosign + syft + slsa-verifier. Single SHA-pinned source of truth; bump once, three workflows pick it up.
.github/workflows/push.yaml docker-build Per-job id-token: write + attestations: write. docker buildx build loop replaced with docker/build-push-action@v7.1.0 (captures digest). Soft-fail sign + SBOM + SLSA provenance steps + aggregator.
.github/workflows/push.yaml build-platforms Per-tarball sidecars: .sha256, .sbom.cdx.json, .cosign-bundle, .intoto.jsonl. upload-artifact path widened.
.github/workflows/push.yaml docker-finalize Explicit sidecar-kind enumeration in the dist-copy step + count-assertion (Phase 2 bake-in: warn on missing).
.github/workflows/push.yaml verify-attestations (new job) Re-fetches images + tarballs from Docker Hub / dist CDN; runs cosign verify + slsa-verifier verify-* against the prod identity regex. finalize job updated to needs: it.
.github/workflows/build-staging.yml Same attestation pipeline for :staging images, signed with the staging OIDC identity (separate trust root).
SECURITY.md Identity-regex contract (prod + staging), consumer verification command examples, V5 composite-action SHA-pin guidance, CDN-rollback residual-risk note.

Threat model (per HARDENING.md Phase 2 plan)

Vector What this PR does
V1 Registry/CDN artifact swap Closes — signature + provenance bind artifact to this workflow.
V2 MITM at install Sidecars published; sc.sh verify lands in follow-up PR 2c.
V3 Compromised maintainer Rekor transparency entry = post-hoc detection.
V4 CI runner exploit SLSA L3 provenance = non-falsifiable build path.
V5 Composite-action mutable-tag pull Minimum-viable doc: SECURITY.md documents how consumers SHA-pin the underlying image. Native verify-before-launch is a Phase 4/6 followup.
CDN rollback (new) Documented residual risk; closed by sc.sh manifest-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-publish verify-attestations job aggregates the failure for the Telegram notifier.

After 14 days of clean post-publish verification, flip the per-step continue-on-error to false (separate one-line PR; tracker entry in HARDENING.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:

  • YAML syntax — 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 are info/style shellcheck nags (SC2012 fixed; remaining are pre-existing on unchanged code, including the known custom-runner-label warning for blacksmith-8vcpu-ubuntu-2204).
  • go build ./... && go vet ./... && go test -short ./pkg/security/... — green (PR touches no Go).
  • Manual workflow_dispatch dry-run from a fork or scratch branch — needs maintainer to trigger on staging first (small blast radius) to validate the staging-side attestation flow before merging.
  • First-release smoke test post-merge: confirm cosign verify against the new prod identity regex succeeds on the freshly-pushed images and tarballs; confirm slsa-verifier verify-image + verify-artifact succeed; confirm verify-attestations job is green.

Out of scope (deliberate)

  • sc.sh verify-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.
  • OpenVEX — cut from Phase 2 entirely (codex + gemini consensus: noise-reduction, not threat-mitigation). Defer to a standalone PR post-Phase-4.
  • branch.yaml PPE split — accepted-risk, see existing nosemgrep annotation.
  • Branch protection, signed-commits enforcement, MFA, required reviewers — repo-admin UI items, not PRs.

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>
@github-actions
Copy link
Copy Markdown

Semgrep Scan Results

Repository: api | Commit: 26c2b54

Check Status Details
✅ Semgrep Pass 0 total findings (no error/warning)

Scanned at 2026-05-13 15:25 UTC

@github-actions
Copy link
Copy Markdown

Security Scan Results

Repository: api | Commit: 26c2b54

Check Status Details
✅ Secret Scan Pass No secrets detected
⚠️ Dependencies (Trivy) High 1 high, 1 total
⚠️ Dependencies (Grype) High 1 high, 1 total
📦 SBOM Generated 470 components (CycloneDX)

Scanned at 2026-05-13 15:26 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant