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.3Common 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.