How to Harden Your CI/CD Pipeline with Sigstore, SLSA, and SBOMs
Before you begin
- A CI/CD platform such as GitHub Actions, GitLab CI, Jenkins, or a comparable system
- Permission to edit build and release pipelines
- An artifact registry or release destination for the software you publish
- A basic dependency inventory or lockfile for the project you want to harden
- A staging or pre-production deploy gate where you can add verification checks
What you'll learn
- Map the parts of your pipeline that actually create supply-chain risk
- Add artifact signing with Sigstore and verify the right identity at the right point
- Generate provenance and understand where SLSA fits into build trust
- Produce an SBOM in a standard format and store it with the artifact
- Add verification gates before deploy instead of stopping at generation
- Reduce pipeline identity risk with shorter-lived credentials and tighter permissions
- Define a minimum policy baseline that small teams can actually enforce
On this page
CI/CD is a software supply-chain risk surface because the pipeline can fetch dependencies, compile code, create artifacts, publish them, and hand them to production with very little human inspection. OpenSSF still treats supply-chain security as a central theme, and its current project set continues to include both Sigstore and SLSA as core building blocks for securing how software is built and distributed.
Sigstore, SLSA, and SBOMs solve different parts of the problem. Sigstore gives you signing, verification, transparency, and keyless identity-based workflows; SLSA gives you a framework for build integrity plus provenance that describes where, when, and how an artifact was produced; and SBOM standards such as SPDX and CycloneDX give you a machine-readable inventory of the components inside the software you ship. None of these replaces the others. They work best as a stack.
This tutorial uses GitHub Actions for the concrete examples because the current GitHub flow maps cleanly to OIDC-based signing, build provenance attestations, and signed SBOM attestations. The same baseline still applies on other CI systems: sign what you ship, generate provenance from the builder, generate an SBOM at build time, and verify all three before deployment. For broader context on how AI-driven development changes the supply-chain risk picture, see Software Supply Chain Security in the AI Era.
Step 1: Map your current pipeline
Before you add controls, map the actual software path from source to production. SLSA’s provenance model exists because artifacts are not trustworthy just because they came from “the pipeline”; you need verifiable information about the builder, the inputs, the invocation, and the produced subject. If you cannot describe those moving parts, you cannot defend them.
Inventory the pipeline stages
For each application or image you publish, capture:
- source repository and protected branches
- build workflow or runner
- dependency resolution step
- packaging step
- artifact registry or release store
- deployment path
- third-party actions, plugins, and base images
A lightweight inventory file is enough to start.
File: pipeline-inventory.yaml
application: payments-api
source:
repo: github.com/acme/payments-api
protected_branches:
- main
build:
ci_platform: github-actions
workflow: .github/workflows/release.yml
runner_type: github-hosted
dependencies:
lockfiles:
- package-lock.json
- Dockerfile
third_party_actions:
- actions/checkout
- docker/build-push-action
artifact:
type: container-image
registry: ghcr.io/acme/payments-api
deploy:
environments:
- staging
- production
gate: deploy-verify.sh
Identify what is actually deployable
Sign and verify the thing your runtime consumes, not just an intermediate file. GitHub’s artifact attestation docs make the same point in practice: sign software people actually run, such as binaries, packages, and container images, rather than every incidental file in the repo.
For most teams, that means one or more of:
- container image digests
- release binaries
- package artifacts
- deployment bundles or manifests that pin digests
Identify your third-party trust points
SLSA’s threat model focuses heavily on tampering in the build path. In real pipelines, the most common trust points are not just the source repo. They are also:
- reusable actions or plugins
- base container images
- package registries
- self-hosted runners
- registry credentials
- deploy credentials
Create a short checklist and force yourself to name them.
File: third-party-trust-points.md
# Third-Party Trust Points
- Docker base images
- npm registry packages
- GitHub Actions used by the release workflow
- Container registry credentials
- Cloud deploy role assumed by the workflow
The first win is visibility. Most teams discover they trust more pipeline components than they thought.
You should now have a pipeline map that tells you where artifacts are built, where they are published, and which external systems influence the result.
Step 2: Add artifact signing
Signing is the first enforceable control because it answers a simple question: did the artifact come from an expected identity? Sigstore’s keyless flow is especially useful in CI because it uses an OIDC identity token and short-lived certificates instead of forcing you to keep a long-lived private signing key in the pipeline. In GitHub Actions, Sigstore’s Fulcio integration can bind the certificate identity to the workflow and repository that performed the signing.
Decide what to sign
For OCI workloads, sign the immutable image digest, not a floating tag. For release binaries, sign the final artifact that users download. If you ship both a container and a release tarball, sign both separately and verify them where each is consumed.
Add keyless signing to the build
The example below builds a container image, pushes it, and signs the image digest with Cosign using GitHub’s OIDC token.
File: .github/workflows/release.yml
name: release
on:
push:
branches:
- main
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ghcr.io/acme/payments-api
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Log in to GHCR
run: echo "${{ github.token }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Build and push image
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ env.IMAGE_NAME }}:${{ github.sha }}
# Check https://github.com/sigstore/cosign/releases for latest version
- name: Install Cosign
run: |
COSIGN_VERSION="v3.0.5"
curl -fsSL -o /tmp/cosign.deb \
"https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign_${COSIGN_VERSION#v}_amd64.deb"
sudo dpkg -i /tmp/cosign.deb
- name: Sign image digest with Sigstore keyless
env:
IMAGE_URI: ${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE_URI}"
Verify signatures where they matter
Do not stop at cosign sign. Verification belongs where trust decisions happen:
- before deployment
- at admission time in Kubernetes
- during release promotion
- on the consumer side if other teams pull your artifacts
Sigstore’s policy-controller exists specifically to enforce image signature and attestation policy in Kubernetes admission, and it can validate signatures and attestations with namespace-scoped enforcement.
A simple pre-deploy verification script looks like this:
File: deploy-verify-signature.sh
#!/usr/bin/env bash
set -euo pipefail
IMAGE_URI="$1"
cosign verify \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity-regexp "https://github.com/acme/payments-api/.github/workflows/release.yml@refs/heads/main" \
"${IMAGE_URI}"
Signing without verification is record-keeping, not protection.
You should now have artifact signing in the pipeline and a verification point before deployment or admission.
Step 3: Generate provenance
Provenance is an attestation that describes how an artifact was produced. In SLSA terms, provenance should let a verifier check the builder identity, the artifact subject and digest, and the build details that connect the output back to source and build instructions. Provenance only becomes useful once you verify it against an expected trust root and expected build identity.
What build metadata matters
At a minimum, provenance is valuable when it lets you answer:
- which builder produced the artifact
- which source repo and revision were used
- which build instructions ran
- which artifact digest was produced
GitHub’s current artifact attestation model includes repository, workflow, environment, commit SHA, event, and other OIDC-derived build information. GitHub also explicitly notes that artifact attestations on their own map to SLSA v1.0 Build Level 2, while reusable workflows can help move toward Build Level 3 by isolating build instructions.
Baseline: add build provenance in GitHub Actions
For a practical first baseline, add a build provenance attestation right after the image is built. GitHub’s official attest-build-provenance action supports binaries and container images and can push the attestation to the registry.
Add this step to the build-sign job after the image build. The job-level permissions block from the workflow above already includes the required attestations: write scope; add it if your workflow does not have it yet.
- name: Generate build provenance attestation
uses: actions/attest-build-provenance@v3
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Stronger path: move toward reusable, isolated build workflows
If you need a stronger SLSA story on GitHub Actions, use reusable workflows or the SLSA GitHub Generator instead of leaving all build logic inline in one mutable release workflow. The SLSA GitHub Generator project is the official SLSA framework tooling for GitHub-native projects and is designed to generate SLSA Build Level 3 provenance for supported workflows.
A simplified reusable provenance job for file artifacts looks like this:
jobs:
build:
runs-on: ubuntu-latest
outputs:
hashes: ${{ steps.hashes.outputs.subjects }}
steps:
- uses: actions/checkout@v4
- name: Build artifact
run: |
mkdir -p dist
tar -czf dist/payments-api.tar.gz src package.json package-lock.json
- name: Compute artifact digest file
run: sha256sum dist/payments-api.tar.gz > subject-digests.txt
- name: Encode subjects for SLSA generator
id: hashes
uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@v2.1.0
with:
path: subject-digests.txt
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.build.outputs.hashes }}
upload-assets: true
Verify provenance before promotion
GitHub’s gh attestation verify verifies build provenance attestations, and SLSA’s own verification guidance says the verifier should check the signature, ensure the subject matches the artifact digest, confirm the predicate type, and compare the builder identity to a preconfigured root of trust.
A simple verification step for a container image looks like this:
gh attestation verify \
"oci://ghcr.io/acme/payments-api:${GITHUB_SHA}" \
-R acme/payments-api
Provenance is not just metadata for auditors. It is the machine-readable link between an artifact and the builder identity that produced it.
You should now have build provenance generated and a concrete place to verify it before promotion or deploy.
Step 4: Produce an SBOM
An SBOM answers a different question from provenance. Provenance tells you how the artifact was built. An SBOM tells you what is inside it. GitHub’s SBOM attestation flow supports signed SBOM attestations, and Sigstore/Cosign support both SPDX and CycloneDX SBOM-related workflows. SPDX and CycloneDX are both active, official standards, so the important operational choice is not “which one is real,” but which one you will use as your canonical internal format.
Pick one canonical format
A good default rule is:
- choose SPDX JSON if your org already leans heavily on license/compliance tooling or wants the ISO-backed SPDX ecosystem
- choose CycloneDX JSON if your security tooling is more application-security and dependency-analysis focused
Both are valid. The bigger mistake is generating multiple formats ad hoc and having no clear source of truth.
Generate the SBOM at build time
For GitHub Actions, GitHub’s SBOM attestation docs explicitly point to creating the SBOM first and then attesting it. The example below uses Syft to generate an SPDX JSON file for the built image. Syft is a widely used SBOM generator for images and filesystems.
Add these steps to the build-sign job after the image build:
# Check https://github.com/anchore/syft/releases for latest version
- name: Install Syft
run: |
SYFT_VERSION="v1.42.1"
curl -fsSL -o /tmp/syft.tar.gz \
"https://github.com/anchore/syft/releases/download/${SYFT_VERSION}/syft_${SYFT_VERSION#v}_linux_amd64.tar.gz"
tar -xzf /tmp/syft.tar.gz -C /tmp
sudo mv /tmp/syft /usr/local/bin/syft
- name: Generate SPDX SBOM for image
env:
IMAGE_URI: ${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
run: |
syft "${IMAGE_URI}" -o spdx-json=sbom.spdx.json
Store the SBOM with the artifact
GitHub’s current SBOM attestation flow signs the SBOM as an attestation associated with the artifact. Sigstore’s Cosign also supports attaching or attesting SBOMs for OCI artifacts. For a practical CI baseline on GitHub, using the official attest-sbom action keeps the artifact-to-SBOM link explicit and verifiable.
Add this step after SBOM generation. The job-level permissions block must include attestations: write:
- name: Generate signed SBOM attestation
uses: actions/attest-sbom@v4
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
sbom-path: sbom.spdx.json
push-to-registry: true
Verify the SBOM attestation
GitHub’s verification flow supports SBOM attestations and lets you verify a specific predicate type. For SPDX, the documented predicate type is https://spdx.dev/Document/v2.3.
gh attestation verify \
"oci://ghcr.io/acme/payments-api:${GITHUB_SHA}" \
-R acme/payments-api \
--predicate-type https://spdx.dev/Document/v2.3
Treat the SBOM as part of the release record, not as an optional side file someone may or may not upload later.
You should now have a build-time SBOM, a signed attestation that binds it to the artifact, and a verification path before deployment.
Step 5: Add verification gates
This is where the baseline becomes real. SLSA’s verification guidance is blunt about this: provenance does nothing unless someone inspects it. The same logic applies to signatures and SBOMs. Generation without gates is a dashboard feature, not a control.
Block unsigned artifacts
A deploy or promotion job should fail if the artifact signature is missing or the signing identity is not what you expect. In GitHub/Sigstore terms, that usually means verifying the OIDC issuer and matching the certificate identity to the release workflow or trusted regex pattern.
Block untrusted provenance
At minimum, require that the artifact has a valid provenance attestation from your expected repo and builder. If you use GitHub artifact attestations, verify them with gh attestation verify. If you use SLSA-native builders, also validate the builder identity and expected source or ref.
Flag unknown dependencies before deploy
An SBOM is most useful when you actually compare it to policy. Good first checks are:
- no packages from unapproved registries
- no empty or obviously incomplete SBOM
- no critical unknown licenses if your org tracks that
- no newly introduced components outside allowed dependency policy
A simple gate script can enforce the minimum baseline:
File: deploy-verify.sh
#!/usr/bin/env bash
set -euo pipefail
IMAGE_URI="$1"
REPO="acme/payments-api"
echo "1) Verify Sigstore signature"
cosign verify \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--certificate-identity-regexp "https://github.com/acme/payments-api/.github/workflows/release.yml@refs/heads/main" \
"${IMAGE_URI}" >/dev/null
echo "2) Verify build provenance"
gh attestation verify \
"oci://${IMAGE_URI}" \
-R "${REPO}" >/dev/null
echo "3) Verify SBOM attestation"
gh attestation verify \
"oci://${IMAGE_URI}" \
-R "${REPO}" \
--predicate-type "https://spdx.dev/Document/v2.3" >/dev/null
echo "4) Pull SBOM payload for policy checks"
gh attestation verify \
"oci://${IMAGE_URI}" \
-R "${REPO}" \
--predicate-type "https://spdx.dev/Document/v2.3" \
--format json \
--jq '.[].verificationResult.statement.predicate' > verified-sbom.json
# Check that the SBOM contains package data (SPDX uses .packages, CycloneDX uses .components)
jq -e '.packages // .components | length > 0' verified-sbom.json >/dev/null
echo "All supply-chain checks passed"
Enforce at deployment or admission time
If your deploy target is Kubernetes, admission is the cleanest place to enforce “no unsigned image” or “no image without required attestation.” Sigstore policy-controller is built for exactly this use case and can evaluate signatures and attestation policies on admission.
The most common pipeline mistake is putting all the effort into generation and none into deploy-time rejection.
You should now have a gate that blocks artifacts missing a valid signature, valid provenance, or a valid SBOM attestation.
Step 6: Secure pipeline identities
A hardened supply-chain pipeline still fails if the runner can impersonate too much or if secrets live too long. GitHub’s OIDC model exists precisely so workflows can authenticate to cloud providers and other services without storing long-lived static credentials in secrets. GitHub also documents tightening GITHUB_TOKEN permissions explicitly rather than leaving broad defaults in place.
Prefer OIDC over long-lived secrets
For cloud deploys and registry access, prefer short-lived, federated credentials where your platform supports them. This reduces the blast radius of leaked tokens and ties the trust decision back to the workflow identity. Sigstore’s keyless model follows the same principle for signing.
Reduce default workflow permissions
Do not run release workflows with broad repository permissions unless the job truly needs them. A release workflow that only reads source, writes packages, signs artifacts, and writes attestations can usually say so directly.
permissions:
contents: read
packages: write
id-token: write
attestations: write
Protect branches and environments
SLSA higher-level guidance emphasizes trustworthy builders and protected control planes, not just signed outputs. In practical CI terms, that means:
- protect release branches
- protect tags
- use environment approvals for production deploys
- restrict who can modify release workflows
- isolate self-hosted runners carefully if you use them
Those controls are not separate from supply-chain security. They are how you stop a human or workflow from bypassing the builder trust you are trying to establish.
Keep token lifetime short and secrets scoped
Good defaults include:
- no long-lived registry passwords in CI if OIDC or short-lived tokens are available
- environment-scoped secrets
- deploy credentials separate from build credentials
- no reuse of broad personal tokens for automation
Pipeline identity hardening is part of provenance quality. A well-signed artifact from an overprivileged runner is still a weak supply-chain story.
You should now have a tighter identity model for runners, tokens, and protected workflows.
Step 7: Build a minimum policy baseline
A baseline is what keeps supply-chain hardening from becoming aspirational. GitHub’s artifact attestation docs are clear that attestations are not a guarantee an artifact is secure; they let you define and enforce policy using the build and origin information they expose. SLSA’s verification model says the same thing in different words: trust comes from checking the attestation against your expectations.
Start with a small, enforced policy
A practical baseline for a small or mid-size team is:
- every deployable artifact must be signed
- every deployable artifact must have build provenance
- every deployable artifact must have an SBOM
- deploys fail closed if verification fails
- exceptions require manual approval and expiry
File: supply-chain-policy.yaml
required:
signature: true
provenance: true
sbom: true
trusted_signers:
- issuer: https://token.actions.githubusercontent.com
identity_regex: https://github.com/acme/payments-api/.github/workflows/release.yml@refs/heads/main
provenance:
trusted_repo: acme/payments-api
trusted_branch: refs/heads/main
sbom:
predicate_type: https://spdx.dev/Document/v2.3
must_include_dependencies: true
exceptions:
max_duration_hours: 24
approvers:
- platform-team
- security-team
Add a simple exception workflow
Exceptions are not failure. Permanent exceptions are. Keep them explicit:
File: exception-request.md
# Supply-Chain Policy Exception
- Artifact:
- Reason:
- Missing control: signature / provenance / SBOM
- Requested by:
- Approved by:
- Expiration time:
- Remediation issue:
What good maturity looks like over time
A sensible maturity path is:
- generate signatures, provenance, and SBOMs
- verify them before deploy
- narrow builder identities and workflow permissions
- enforce at admission time or promotion time
- standardize reusable build workflows and stricter provenance trust
That progression aligns with OpenSSF’s ecosystem direction around supply-chain security and with SLSA’s own emphasis on moving from “metadata exists” to “metadata is trustworthy and verified.”
Common Setup Problems
Generating SBOMs but not using them
Teams often produce SBOMs because the tooling is easy, then never verify the attestation, never attach the SBOM to the artifact, and never compare its contents to policy. GitHub’s SBOM attestation workflow and verification commands exist specifically so the SBOM can become part of a verifiable release record rather than a loose file in build artifacts.
Signing artifacts without verification
This is the single most common failure mode. Sigstore, GitHub artifact attestations, and SLSA verification guidance all assume that someone verifies the result against a trusted identity or root of trust. If your deploy step never checks, the control has no teeth.
Treating SLSA as compliance only
SLSA is not just a reporting label. Its provenance and verification model is about whether a verifier can trust what builder produced the artifact and what source it came from. If you stop at “we have a provenance file,” you are missing the entire point.
Ignoring human access to the pipeline
A strong signing story does not help much if too many people can edit the release workflow, bypass branch protection, or run deploys from unreviewed branches. SLSA’s builder-trust model and GitHub’s guidance around reusable workflows and protected identities both point to the same conclusion: human access to the control plane matters.
Wrap-Up
A practical CI/CD supply-chain baseline is not huge. Start by mapping the pipeline, sign the deployable artifact with Sigstore, emit provenance that your deploy system can verify, generate an SBOM in one canonical format, and then block deployment when those checks fail. That already gets you much farther than “we generated some metadata in CI.”
For small teams, the quick wins are straightforward: keyless signing, build provenance attestation, a single SBOM format, minimal workflow permissions, and one pre-deploy verification script. Over time, maturity looks like reusable hardened build workflows, stricter provenance trust, admission-time enforcement, and fewer long-lived credentials anywhere in the path from source to production.