Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
891ae11
feat(ci): sign + SBOM + SLSA L3 provenance for images and tarballs
Cre-eD May 13, 2026
a0a432b
fix(ci): address round-1 review (gh attestation verify, digest TOCTOU…
Cre-eD May 15, 2026
508c6f7
fix(ci): round-2 review — drop staging matrix entry, gh attestation -…
Cre-eD May 15, 2026
581402e
fix(ci): round-3 review — installer best-effort, drop staging dep fro…
Cre-eD May 15, 2026
d387ba6
perf+feat(ci): verify-attestations off critical path; cover branch-pr…
Cre-eD May 15, 2026
9ef3fa8
fix(ci): drop push-to-registry from attest-build-provenance (SBOM was…
Cre-eD May 15, 2026
35bffc9
fix(ci): SBOM sidecar filename → <tarball>.sbom.cdx.json (was missing…
Cre-eD May 15, 2026
9d87017
fix(ci): tighten verify-attestations.yml trust-root + add SBOM verify…
Cre-eD May 15, 2026
f754aa8
fix(docs+ci): thorough-review findings — consumer-facing doc accuracy…
Cre-eD May 15, 2026
71e6f1f
chore(docs): move SECURITY.md to docs/
Cre-eD May 15, 2026
581fea7
feat(test): scripts/verify-preview.sh + negative_tests workflow input
Cre-eD May 15, 2026
4387c79
fix(test): drop unused `require_tool crane` from verify-preview.sh
Cre-eD May 15, 2026
3f867e7
fix(ci+docs): codex round-5 — drop orphan job, ref-guard signing, fix…
Cre-eD May 16, 2026
0b6aa56
chore(ci): drop stale 'dead code / cleanup in follow-up PR' comments
Cre-eD May 16, 2026
c94b869
fix(ci): codex round-6 — branch-preview ref guard + SBOM in verify-at…
Cre-eD May 16, 2026
79c79f4
Merge branch 'main' into feat/phase2-attestation
smecsia May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/actions/install-attest-tools/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'Install attestation tools'
description: 'Install cosign + syft with single-source SHA pins for use across the SC release pipeline. Provenance verification uses the gh CLI (preinstalled on GitHub-hosted runners) against the GitHub-native attestation API, so no separate slsa-verifier install is needed.'

inputs:
cosign-version:
description: 'Cosign CLI release tag to install (e.g. v2.4.1)'
required: false
default: 'v2.4.1'
syft-version:
description: 'Syft CLI release tag to install (e.g. v1.16.0)'
required: false
default: 'v1.16.0'

runs:
using: 'composite'
steps:
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
with:
cosign-release: ${{ inputs.cosign-version }}

- name: Install syft
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
syft-version: ${{ inputs.syft-version }}

- name: Verify gh CLI is available (for attestation verify)
shell: bash
run: |
if ! command -v gh >/dev/null 2>&1; then
echo "gh CLI not found on PATH — required for 'gh attestation verify' provenance checks." >&2
exit 1
fi
gh --version | head -n 1
207 changes: 193 additions & 14 deletions .github/workflows/branch-preview.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ jobs:
outputs:
version: ${{ steps.version.outputs.version }}
steps:
# Trust-root ref guard. Preview signing uses cosign keyless identity
# `branch-preview.yaml@refs/heads/<branch>` and the documented
# preview verification regex pins `@refs/heads/.+`. workflow_dispatch
# ALSO allows dispatching from a tag (`refs/tags/<tag>`), which
# would sign artifacts under an identity that no documented
# verifier accepts — fail fast in that case.
- name: Refuse to run on non-branch ref
if: "!startsWith(github.ref, 'refs/heads/')"
run: |
echo "::error title=branch-preview.yaml requires a branch ref::Got github.ref=$GITHUB_REF; this workflow signs preview artifacts under the preview trust root (refs/heads/* only). Re-dispatch from a branch."
exit 1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
Expand Down Expand Up @@ -122,6 +133,10 @@ jobs:
name: Build sc for ${{ matrix.os }}/${{ matrix.arch }}
runs-on: blacksmith-8vcpu-ubuntu-2204
needs: [prepare, build-setup]
permissions:
contents: read
id-token: write # OIDC for keyless cosign + attest-build-provenance
attestations: write
strategy:
matrix:
include:
Expand Down Expand Up @@ -155,11 +170,73 @@ jobs:
go build -ldflags "-s -w -X=github.com/simple-container-com/api/internal/build.Version=${VERSION}" -o dist/${GOOS}-${GOARCH}/sc${EXT} ./cmd/sc
tar -czf .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}.tar.gz -C dist/${GOOS}-${GOARCH} sc${EXT}
cp .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}.tar.gz .sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz
# Phase 2 attestation (mirrors push.yaml). Soft-fail per step — the
# tarball itself is the publish artifact. Preview builds only publish
# versioned tarballs (no unversioned twin), so sidecars are per-version.
- name: SHA-256 sidecar for ${{ matrix.os }}/${{ matrix.arch }}
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
cd .sc/stacks/dist/bundle
sha256sum "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz" > "sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sha256"
- name: Install attestation tools
id: install_attest_tools
continue-on-error: true
uses: ./.github/actions/install-attest-tools
- name: Generate CycloneDX SBOM for sc-${{ matrix.os }}-${{ matrix.arch }}
id: sbom_tarball
continue-on-error: true
env:
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
cd .sc/stacks/dist/bundle
syft scan "dir:../../../../dist/${GOOS}-${GOARCH}" \
-o "cyclonedx-json=sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sbom.cdx.json"
- name: Cosign sign-blob (keyless, bundle) for sc-${{ matrix.os }}-${{ matrix.arch }}
id: cosign_sign_tarball
continue-on-error: true
env:
COSIGN_EXPERIMENTAL: "1"
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
cd .sc/stacks/dist/bundle
tarball="sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz"
cosign sign-blob --yes --bundle "${tarball}.cosign-bundle" "${tarball}"
- name: SLSA build provenance for sc-${{ matrix.os }}-${{ matrix.arch }}
id: slsa_tarball
continue-on-error: true
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v${{ needs.prepare.outputs.version }}.tar.gz
- name: Materialize SLSA provenance bundle next to tarball
if: steps.slsa_tarball.outcome == 'success'
continue-on-error: true
env:
BUNDLE_PATH: ${{ steps.slsa_tarball.outputs.bundle-path }}
GOOS: ${{ matrix.os }}
GOARCH: ${{ matrix.arch }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
cp "$BUNDLE_PATH" ".sc/stacks/dist/bundle/sc-${GOOS}-${GOARCH}-v${VERSION}.tar.gz.sigstore.json"
- name: upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sc-${{ matrix.os }}-${{ matrix.arch }}
path: .sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz
path: |
.sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz
.sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz.sha256
.sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz.cosign-bundle
.sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz.sigstore.json
.sc/stacks/dist/bundle/sc-${{ matrix.os }}-${{ matrix.arch }}-v*.tar.gz.sbom.cdx.json
retention-days: 1

build-binaries:
Expand Down Expand Up @@ -219,16 +296,22 @@ jobs:
name: Docker build and push preview ${{ matrix.target }} image
runs-on: blacksmith-8vcpu-ubuntu-2204
needs: [prepare, build-setup, build-binaries, test]
permissions:
contents: read
id-token: write # OIDC token for keyless cosign + attest-build-provenance
attestations: write
strategy:
matrix:
include:
- target: github-actions
binary: github-actions
dockerfile: github-actions.Dockerfile
image_repo: simplecontainer/github-actions
tag_prefix: "simplecontainer/github-actions:"
- target: cloud-helpers
binary: cloud-helpers
dockerfile: cloud-helpers.aws.Dockerfile
image_repo: simplecontainer/cloud-helpers
tag_prefix: "simplecontainer/cloud-helpers:aws-"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -260,18 +343,104 @@ jobs:
run: |
sc stack secret-get -s dist dockerhub-cicd-token | docker login --username simplecontainer --password-stdin
- name: Build and push preview ${{ matrix.target }} image
id: build_and_push
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
tags: ${{ matrix.tag_prefix }}${{ needs.prepare.outputs.version }}
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false
# Phase 2 attestation (mirrors push.yaml). Preview builds get the SAME
# security guarantees as production releases so consumers pin-testing a
# preview version (per project_sc_preview memory: PAY-SPACE pins workflows
# to versioned preview builds) can verify the same way they verify prod.
- name: Install attestation tools
id: install_attest_tools
if: steps.build_and_push.outcome == 'success'
continue-on-error: true
uses: ./.github/actions/install-attest-tools
- name: Generate CycloneDX SBOM for ${{ matrix.target }}
id: sbom
if: steps.build_and_push.outcome == 'success'
continue-on-error: true
env:
DOCKER_BUILDKIT: 1
VERSION: ${{ needs.prepare.outputs.version }}
IMAGE_REF: ${{ matrix.image_repo }}@${{ steps.build_and_push.outputs.digest }}
run: |
set -euo pipefail
syft scan "registry:${IMAGE_REF}" -o "cyclonedx-json=sbom-${{ matrix.target }}.cdx.json"
- name: Cosign sign ${{ matrix.target }} (keyless)
id: cosign_sign
if: steps.build_and_push.outcome == 'success'
continue-on-error: true
env:
COSIGN_EXPERIMENTAL: "1"
IMAGE_REF: ${{ matrix.image_repo }}@${{ steps.build_and_push.outputs.digest }}
run: |
cosign sign --yes "${IMAGE_REF}"
- name: Cosign attest SBOM for ${{ matrix.target }}
id: cosign_attest_sbom
if: steps.sbom.outcome == 'success' && steps.cosign_sign.outcome == 'success'
continue-on-error: true
env:
COSIGN_EXPERIMENTAL: "1"
IMAGE_REF: ${{ matrix.image_repo }}@${{ steps.build_and_push.outputs.digest }}
run: |
docker buildx build \
--platform linux/amd64 \
--cache-from type=gha \
--cache-to type=gha,mode=max \
--file ${{ matrix.dockerfile }} \
--tag "${{ matrix.tag_prefix }}${VERSION}" \
--push \
.
cosign attest --yes \
--predicate "sbom-${{ matrix.target }}.cdx.json" \
--type cyclonedx \
"${IMAGE_REF}"
- name: SLSA build provenance for ${{ matrix.target }}
id: slsa_provenance
if: steps.build_and_push.outcome == 'success'
continue-on-error: true
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: index.docker.io/${{ matrix.image_repo }}
subject-digest: ${{ steps.build_and_push.outputs.digest }}
# push-to-registry: false (default). attest-build-provenance@v4 also pushes
# the attestation to the OCI registry under the same .att tag that cosign
# attest --type cyclonedx writes, silently overwriting the SBOM attestation.
# Provenance is published to the GitHub attestation API instead (queryable
# via `gh attestation verify`); SBOM attestation stays in the registry for
# `cosign verify-attestation --type cyclonedx`. Validated 2026-05-15 against
# preview run 25910386532 — cosign verify-attestation now resolves both.
- name: Record image digest for verify-attestations
if: steps.build_and_push.outcome == 'success'
env:
DIGEST: ${{ steps.build_and_push.outputs.digest }}
IMAGE_REPO: ${{ matrix.image_repo }}
run: |
set -euo pipefail
mkdir -p digests
printf '%s@%s\n' "$IMAGE_REPO" "$DIGEST" > "digests/${{ matrix.target }}.txt"
- name: Upload digest artifact for ${{ matrix.target }}
if: steps.build_and_push.outcome == 'success'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: image-digest-${{ matrix.target }}
path: digests/${{ matrix.target }}.txt
retention-days: 1
- name: Soft-fail aggregator for ${{ matrix.target }} attestation
if: always() && steps.build_and_push.outcome == 'success'
env:
SIGN_OUTCOME: ${{ steps.cosign_sign.outcome }}
SBOM_OUTCOME: ${{ steps.sbom.outcome }}
ATTEST_SBOM_OUTCOME: ${{ steps.cosign_attest_sbom.outcome }}
SLSA_OUTCOME: ${{ steps.slsa_provenance.outcome }}
run: |
fail=0
for v in "$SIGN_OUTCOME" "$SBOM_OUTCOME" "$ATTEST_SBOM_OUTCOME" "$SLSA_OUTCOME"; do
if [ "$v" != "success" ]; then fail=1; fi
done
if [ "$fail" -eq 1 ]; then
echo "::warning title=Attestation incomplete::${{ matrix.target }} preview published but attestation steps failed (sign=$SIGN_OUTCOME sbom=$SBOM_OUTCOME attest-sbom=$ATTEST_SBOM_OUTCOME slsa=$SLSA_OUTCOME)."
else
echo "All attestation steps succeeded for ${{ matrix.target }} preview."
fi

publish-sc-preview:
name: Publish preview SC binaries to dist
Expand Down Expand Up @@ -312,11 +481,21 @@ jobs:
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
mkdir -p .sc/stacks/dist/bundle
rm -fR .sc/stacks/dist/bundle/*
# Copy only versioned tarballs — do NOT add sc.sh or the version file.
# This prevents overwriting the latest pointer for users running sc.sh without a version pin.
cp artifacts/sc-*/*-v${VERSION}.tar.gz .sc/stacks/dist/bundle/
# Copy only versioned tarballs + their sidecars (.sha256,
# .cosign-bundle, .sigstore.json, .sbom.cdx.json) — do NOT add sc.sh
# or the version file. Preserves the unversioned-latest pointer for
# users running sc.sh without a version pin.
shopt -s nullglob
for f in artifacts/sc-*/*-v${VERSION}.tar.gz \
artifacts/sc-*/*-v${VERSION}.tar.gz.sha256 \
artifacts/sc-*/*-v${VERSION}.tar.gz.cosign-bundle \
artifacts/sc-*/*-v${VERSION}.tar.gz.sigstore.json \
artifacts/sc-*/*-v${VERSION}.tar.gz.sbom.cdx.json; do
cp "$f" .sc/stacks/dist/bundle/
done
echo "Bundle contents:"
ls -la .sc/stacks/dist/bundle/
- name: publish preview sc binaries
Expand Down
Loading
Loading