Skip to content
Back to Quality Assurance

Artifact Signing & Supply-Chain Provenance (SLSA)

Sign build artifacts and attach provenance metadata so consumers can verify that an artifact was built by the expected pipeline from the expected source — not injected by an attacker.

Why

A registry full of unsigned artifacts is a trust boundary with no enforcement — any artifact that looks right (same name, same version) is trusted, and an attacker who can push to the registry (via a compromised token, a typosquatted package, or a dependency confusion attack) can inject a malicious artifact that consumers pull and run. Artifact signing closes this gap: the build pipeline signs the artifact with a key the developer never holds (keyless signing via OIDC), and the consumer verifies the signature before pulling. Provenance goes further: it attaches a signed attestation that records exactly which source commit, which workflow, and which build inputs produced the artifact — so the consumer can verify not just 'is this signed' but 'was this built from the source I expect, by the pipeline I expect'. This is the SLSA framework (Supply-chain Levels for Software Artifact Verification): each level adds a control (L1: provenance exists, L2: hosted build, L3: non-falsifiable provenance). The cost is a few minutes of pipeline setup; the benefit is eliminating an entire class of supply-chain attacks that have become the most common vector for compromising open-source ecosystems.

When

When publishing artifacts (containers, npm packages, binaries) that other people or other systems will consume — internal-only artifacts can start at L1, public artifacts should target L3.

How

1. Enable OIDC in the CI workflow
   - GitHub Actions issues an OIDC token to every job; no long-lived secret to manage
   - Add permissions: id-token: write to the workflow

2. Sign the artifact with keyless signing (Sigstore / cosign)
   - cosign sign --keyless <image> — signs using an ephemeral key, cert tied to the OIDC identity
   - The signature is stored in the registry alongside the artifact

3. Generate provenance with a build action
   - Use slsa-framework/slsa-github-generator to produce a provenance attestation
   - The attestation records: source repo, commit SHA, workflow path, build inputs
   - Attach it to the artifact as a cosign attestation

4. Verify on the consumer side
   - cosign verify --certificate-identity <workflow-ref> <image>
   - cosign verify-attestation --predicate-type slsaprovenance <image>
   - In a deploy pipeline, verification is a gate: if verify fails, the deploy aborts

5. Progress through the SLSA levels
   - L1: provenance exists (the generator produces it)
   - L2: build is hosted (GitHub Actions is a hosted builder)
   - L3: provenance is non-falsifiable (the builder is isolated and cannot be tampered with)
   - Each level is a property of the build platform, not extra code to write

Example

# .github/workflows/release.yml — keyless sign + provenance
name: release
on:
  push:
    tags: ["v*"]
permissions:
  contents: read
  id-token: write   # required for keyless signing
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t ghcr.io/$ORG/app:$GITHUB_REF_NAME .
      - run: docker push ghcr.io/$ORG/app:$GITHUB_REF_NAME
      # keyless sign (no secret to manage)
      - uses: sigstore/cosign-installer@v3
      - run: cosign sign --yes ghcr.io/$ORG/app:$GITHUB_REF_NAME
  provenance:
    needs: build
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
    with:
      image: ghcr.io/$ORG/app:$GITHUB_REF_NAME

# Consumer (deploy pipeline) verifies before pulling:
# cosign verify --certificate-identity-regexp 'github.com/$ORG/.github/workflows/release.yml' \
#   ghcr.io/$ORG/app:v1.2.3

Common Problems

  • ⚠Signing with a long-lived key stored as a CI secret — the secret is a single point of failure: if it leaks (and CI secrets leak regularly), an attacker can sign arbitrary artifacts as your project, and the signature provides no trust.
  • ⚠Signing the artifact but not verifying it on the consumer side — the signature exists in the registry but every deploy pulls without verifying, so a malicious artifact with no signature is pulled and run, and the signing was wasted effort.
  • ⚠Generating provenance but not checking the source commit — the consumer verifies 'is there a provenance attestation' but not 'does the provenance match the source I expect', so an attacker who can trigger the workflow from a malicious fork produces valid provenance for a malicious artifact.
  • ⚠Treating SLSA level as a checkbox rather than a property — claiming L3 without using an isolated, non-falsifiable builder, so the provenance is technically present but falsifiable, and the supply-chain guarantee is illusory.

Best Practices

  • ✓Use keyless signing (cosign sign --keyless via OIDC) so there is no long-lived key to leak — the signing certificate is tied to the workflow identity and is valid only for that run.
  • ✓Make verification a deploy gate: the deploy pipeline runs cosign verify and aborts if it fails — this turns signing from a side-effect into an enforced control.
  • ✓Verify the full provenance (source repo, commit SHA, workflow ref) on the consumer side, not just the signature presence — this prevents an attacker from substituting a malicious source that produces a validly-signed malicious artifact.
  • ✓Use the SLSA generator action for your build platform (it produces non-falsifiable L3 provenance for free) rather than hand-rolling provenance — the framework has already solved the hard problems.