When you download a library, pull a container image, or install a binary, how do you know it came from the source you trust and was not modified in transit? Artefact signing and provenance records solve this. This lesson covers Sigstore, SLSA, and the practical mechanics of proving your software is trustworthy.
The problem
Supply chain attacks modify artefacts after they are built but before consumers install them. Examples:
- A compromised package registry serves a backdoored version of a popular library
- A CI pipeline is modified to inject malicious code into the build output
- A man-in-the-middle replaces a binary during download
Without signing and provenance, consumers have no way to verify that an artefact is:
- Authentic — produced by the claimed author or organisation
- Unmodified — identical to what was built
- Traceable — linked to specific source code, build configuration, and infrastructure
Signing fundamentals
How signing works
- Build produces an artefact (container image, npm package, binary)
- A cryptographic hash (digest) of the artefact is computed
- The hash is signed with a private key
- The signature is published alongside the artefact (or in a transparency log)
- Consumers verify the signature against the public key before using the artefact
Traditional signing vs keyless signing
| Aspect | Traditional (GPG / PGP) | Keyless (Sigstore) |
|---|---|---|
| Key management | You generate, store, rotate, and protect long-lived keys | Ephemeral key pair generated per signing event |
| Identity | Key fingerprint (opaque) | Verified OIDC identity (email, CI workload) |
| Revocation | Complex (key revocation lists) | Short-lived certificates — no revocation needed |
| Trust root | Web of trust or manual key exchange | Certificate transparency log (Rekor) |
| Adoption barrier | High (key ceremonies, secure storage) | Low (works with existing CI OIDC) |
Traditional signing is still used (Debian packages, GPG-signed Git tags), but keyless signing via Sigstore is becoming the standard for CI/CD artefacts.
Sigstore
Sigstore is an open-source project that provides free, keyless signing for software artefacts. Its components:
Cosign — container image signing
# Sign a container image (keyless, uses OIDC identity)
cosign sign ghcr.io/myorg/myapp@sha256:abc123...
# Verify a signed image
cosign verify ghcr.io/myorg/myapp@sha256:abc123... \
--certificate-identity=https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
When you run cosign sign in CI, Sigstore:
- Requests an OIDC token from the CI platform (e.g., GitHub Actions)
- Exchanges the OIDC token for a short-lived signing certificate from Fulcio (Sigstore's CA)
- Signs the image digest
- Records the signature and certificate in Rekor (the transparency log)
- The ephemeral private key is discarded
No long-lived keys to store or rotate.
Fulcio — certificate authority
Fulcio issues short-lived certificates (typically valid for 10 minutes). The certificate binds a signing key to an OIDC identity — for example:
subject: https://github.com/myorg/myapp/.github/workflows/release.yml@refs/tags/v1.2.0issuer: https://token.actions.githubusercontent.com
This means: the artefact was signed by the release.yml workflow running on the v1.2.0 tag.
Rekor — transparency log
Rekor is an append-only, tamper-evident log of all signing events. Even after the short-lived certificate expires, the Rekor entry provides a permanent, verifiable record that the signature was valid at signing time.
Signing in CI/CD
GitHub Actions — container image signing
name: Build and sign
on:
push:
tags: ['v*']
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
npm package signing
npm supports Sigstore-based provenance attestation:
steps:
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
The --provenance flag generates a signed SLSA provenance statement and publishes it to the npm registry. Users can verify it:
npm audit signatures
SLSA — Supply-chain Levels for Software Artefacts
SLSA (pronounced "salsa") is a framework that defines increasing levels of supply chain integrity guarantees.
SLSA levels
| Level | Requirement | What it prevents |
|---|---|---|
| SLSA 1 | Build process documented, provenance generated | Untracked builds |
| SLSA 2 | Provenance generated by a hosted build service (not the developer's laptop) | Source tampering after commit |
| SLSA 3 | Hardened build platform, non-falsifiable provenance | Compromised build process |
| SLSA 4 | Two-party review, hermetic builds | Insider threats |
Generating SLSA provenance
The SLSA GitHub generator creates provenance attestations:
jobs:
build:
outputs:
digest: ${{ steps.hash.outputs.digest }}
steps:
- uses: actions/checkout@v4
- run: go build -o myapp
- id: hash
run: echo "digest=$(sha256sum myapp | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v4
with:
name: myapp
path: myapp
provenance:
needs: build
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
base64-subjects: ${{ needs.build.outputs.digest }}
Verifying SLSA provenance
slsa-verifier verify-artifact myapp \
--provenance-path myapp.sigstore.jsonl \
--source-uri github.com/myorg/myapp \
--source-tag v1.2.0
This checks:
- The artefact matches the digest in the provenance
- The provenance was signed by the expected CI platform
- The build was triggered from the expected source and tag
Verification at deployment
Signing is useless if nothing verifies. Enforce verification at the point of consumption.
Kubernetes admission control
# Kyverno policy — reject unsigned images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: check-cosign-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["ghcr.io/myorg/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/myorg/*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
Other admission controllers that support image verification:
- Sigstore Policy Controller — native Sigstore integration
- OPA Gatekeeper with signature verification
- Docker Content Trust (Notary v2)
Dependency lock file verification
For language packages, verify digests match what was published:
# npm — verify signatures of installed packages
npm audit signatures
# pip — verify hashes
pip install --require-hashes -r requirements.txt
What to sign
| Artefact | Tool | Registry / Log |
|---|---|---|
| Container images | Cosign | OCI registry + Rekor |
| npm packages | npm publish --provenance | npm registry |
| Python packages | sigstore-python | PyPI |
| Go binaries | SLSA GitHub generator | Rekor |
| Helm charts | Cosign | OCI registry |
| SBOMs | Cosign attest | Rekor |
Sign everything that crosses a trust boundary — anything that is built in one environment and consumed in another.
Summary
Artefact signing and provenance close the gap between "code was committed" and "software was deployed." Use Sigstore's keyless signing in CI (Cosign + Fulcio + Rekor) — no long-lived keys to manage. Adopt SLSA provenance to document exactly how artefacts were built and from which source. Most importantly, enforce verification at deployment using admission controllers or package audit tools. Signing only matters when verification is mandatory.
