Supply Chain Security: Sigstore, Cosign, and SLSA
Quick answer
Software supply chain attacks compromise build pipelines and dependencies, not just running code. Learn to sign and verify container images with Cosign, generate SBOMs, achieve SLSA provenance, and enforce policies in Kubernetes.
- The Problem: No Artifact Integrity
- Sigstore: The Ecosystem
- Cosign: Installing and Key Concepts
- Key-Based Signing
- Keyless Signing (Recommended)
advanced · 75 min
Before you begin
- Container fundamentals — Docker build, push, registry workflows
- Kubernetes basics — pods, deployments, admission controllers
- GitHub Actions experience helpful
Supply Chain Security: Sigstore, Cosign, and SLSA
The SolarWinds breach (2020), the XZ Utils backdoor (2024), and dozens of npm/PyPI package poisoning incidents share one characteristic: the attack didn't compromise the running application — it compromised the build pipeline or dependencies that produced the artifact. By the time the binary reached production, it was already malicious.
Supply chain security is the practice of verifying that what runs in production is exactly what your developers built, from trusted sources, without tampering.
The Problem: No Artifact Integrity
Without supply chain security:
- Developer pushes code to GitHub
- CI builds an image and pushes to registry as
myimage:latest - Kubernetes pulls
myimage:latest
Nobody verified that step 3's image is the same one built in step 2. A compromised registry credential, a rogue insider, or a man-in-the-middle can replace the image. latest tags are mutable — the same tag can point to different content at different times.
Sigstore: The Ecosystem
Sigstore is a Linux Foundation project providing free, open infrastructure for signing software artifacts:
| Component | Role |
|---|---|
| Cosign | CLI tool for signing and verifying container images and other artifacts |
| Fulcio | Free code signing CA — issues short-lived X.509 certificates via OIDC identity |
| Rekor | Immutable, append-only transparency log — records all signatures |
The key insight: Sigstore enables keyless signing via OIDC. You don't manage long-lived private keys (which can be stolen). Instead, you prove identity via your GitHub Actions OIDC token, Fulcio issues a certificate valid for a few minutes, you sign with it, and the record goes into Rekor.
Cosign: Installing and Key Concepts
1# macOS
2brew install cosign
3
4# Linux
5curl -Lo cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
6chmod +x cosign
7sudo mv cosign /usr/local/bin/
8
9cosign versionKey-Based Signing
Traditional signing: generate a key pair, sign with private key, distribute public key to verifiers.
1# Generate a key pair
2cosign generate-key-pair
3# Creates: cosign.key (private, protect this), cosign.pub (public, share this)
4
5# Sign an image (after building and pushing to registry)
6cosign sign --key cosign.key ghcr.io/myorg/my-api:v1.2.0
7
8# The signature is pushed to the registry as a separate artifact
9# (stored as an OCI artifact in the same repository)
10
11# Verify
12cosign verify --key cosign.pub ghcr.io/myorg/my-api:v1.2.0The signature is stored alongside the image in the registry (as an OCI image with a tag like sha256-abc123.sig). No separate infrastructure needed.
Signing with a KMS key
1# AWS KMS
2cosign sign --key awskms:///arn:aws:kms:us-east-1:123456789:key/key-id \
3 ghcr.io/myorg/my-api:v1.2.0
4
5# GCP KMS
6cosign sign --key gcpkms://projects/PROJECT/locations/LOCATION/keyRings/RING/cryptoKeys/KEY \
7 ghcr.io/myorg/my-api:v1.2.0Keyless Signing (Recommended)
In GitHub Actions, use keyless signing — no key management, identity bound to the OIDC workflow identity.
1# .github/workflows/release.yml
2name: Build, Sign, and Push
3on:
4 push:
5 tags: ['v*']
6
7permissions:
8 contents: read
9 packages: write
10 id-token: write # Required for OIDC keyless signing
11
12jobs:
13 build:
14 runs-on: ubuntu-latest
15 steps:
16 - uses: actions/checkout@v4
17
18 - name: Install Cosign
19 uses: sigstore/cosign-installer@v3
20
21 - name: Log in to GHCR
22 uses: docker/login-action@v3
23 with:
24 registry: ghcr.io
25 username: ${{ github.actor }}
26 password: ${{ secrets.GITHUB_TOKEN }}
27
28 - name: Build and push image
29 id: build
30 uses: docker/build-push-action@v5
31 with:
32 push: true
33 tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
34
35 - name: Sign the image
36 run: |
37 cosign sign --yes \
38 ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
39 # --yes skips the interactive prompt
40 # Sign by digest (immutable), not tag (mutable)The OIDC identity https://github.com/myorg/my-api/.github/workflows/release.yml@refs/tags/v1.2.0 is embedded in the Fulcio certificate and recorded in Rekor.
Verifying a keyless signature
cosign verify \
--certificate-identity-regexp "https://github.com/myorg/my-api/.github/workflows/release.yml@refs/tags/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/my-api:v1.2.0This verifies that:
- The signature is present in Rekor
- The certificate was issued by Fulcio
- The OIDC identity matches the expected workflow
SBOM: Software Bill of Materials
An SBOM lists every component inside your software artifact — like a nutrition label for code.
Generating SBOMs with Syft
Syft scans images and source trees to generate SBOMs:
1# Install Syft
2curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
3
4# Generate SBOM from a container image (CycloneDX format)
5syft ghcr.io/myorg/my-api:v1.2.0 -o cyclonedx-json > sbom.json
6
7# Generate SBOM in SPDX format
8syft ghcr.io/myorg/my-api:v1.2.0 -o spdx-json > sbom.spdx.json
9
10# Scan a local directory
11syft dir:. -o json > sbom.jsonAttaching SBOM to the image with Cosign
1# Attach the SBOM to the image (stored in the registry alongside the image)
2cosign attach sbom --sbom sbom.json ghcr.io/myorg/my-api:v1.2.0
3
4# Sign the SBOM attachment
5cosign sign --key cosign.key \
6 --attachment sbom \
7 ghcr.io/myorg/my-api:v1.2.0Scanning for vulnerabilities with Grype
Grype scans SBOMs and images for CVEs:
1# Install Grype
2curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
3
4# Scan an image
5grype ghcr.io/myorg/my-api:v1.2.0
6
7# Scan from an SBOM
8grype sbom:./sbom.json
9
10# Fail build if any HIGH or CRITICAL CVEs found
11grype ghcr.io/myorg/my-api:v1.2.0 --fail-on highIn CI, add Grype after building the image to block deployment of vulnerable images:
- name: Scan for vulnerabilities
run: grype ghcr.io/${{ github.repository }}:${{ github.ref_name }} --fail-on criticalSLSA: Supply-chain Levels for Software Artifacts
SLSA (pronounced "salsa") is a framework that defines levels of build security:
| Level | Requirements | What it proves |
|---|---|---|
| SLSA 1 | Documented build process | Builds are automated (not manual) |
| SLSA 2 | Versioned build service | Build is reproducible with a hosted service |
| SLSA 3 | Hardened build platform | Build is isolated, provenance is generated by the build platform itself |
Provenance is a signed attestation: "this artifact was built from this source commit, using this build process, at this time."
Generating SLSA provenance with GitHub Actions
The SLSA GitHub Generator produces SLSA 3 provenance for container images:
1# .github/workflows/release.yml
2name: Build and Provenance
3on:
4 push:
5 tags: ['v*']
6
7permissions:
8 contents: read
9 packages: write
10 id-token: write
11 attestations: write
12
13jobs:
14 build:
15 runs-on: ubuntu-latest
16 outputs:
17 digest: ${{ steps.build.outputs.digest }}
18 steps:
19 - uses: actions/checkout@v4
20
21 - uses: docker/login-action@v3
22 with:
23 registry: ghcr.io
24 username: ${{ github.actor }}
25 password: ${{ secrets.GITHUB_TOKEN }}
26
27 - name: Build and push
28 id: build
29 uses: docker/build-push-action@v5
30 with:
31 push: true
32 tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
33
34 provenance:
35 needs: build
36 uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
37 with:
38 image: ghcr.io/${{ github.repository }}
39 digest: ${{ needs.build.outputs.digest }}
40 secrets:
41 registry-username: ${{ github.actor }}
42 registry-password: ${{ secrets.GITHUB_TOKEN }}This produces a signed SLSA provenance attestation attached to the image that contains:
- The source repository and commit SHA
- The GitHub Actions workflow that built it
- The build inputs and environment
Verifying SLSA provenance
# Using cosign
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/my-api:v1.2.0 | jq .payload | base64 -d | jq .Enforcing Signatures in Kubernetes
Option 1: Kyverno
Kyverno is a Kubernetes-native policy engine that validates, mutates, and generates resources. It supports image verification natively.
# Install Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace1# Require that all images in the production namespace are signed
2apiVersion: kyverno.io/v1
3kind: ClusterPolicy
4metadata:
5 name: require-image-signature
6spec:
7 validationFailureAction: Enforce
8 background: false
9 rules:
10 - name: check-image-signature
11 match:
12 any:
13 - resources:
14 kinds: [Pod]
15 namespaces: [production]
16 verifyImages:
17 - imageReferences:
18 - "ghcr.io/myorg/*"
19 attestors:
20 - entries:
21 - keyless:
22 subject: "https://github.com/myorg/my-api/.github/workflows/release.yml@refs/tags/*"
23 issuer: "https://token.actions.githubusercontent.com"
24 rekor:
25 url: https://rekor.sigstore.devAny pod in the production namespace referencing ghcr.io/myorg/* images must have a valid Sigstore signature from the expected workflow. Pods that fail verification are rejected at admission.
Option 2: Sigstore Policy Controller
The Sigstore project provides its own admission controller:
helm repo add sigstore https://sigstore.github.io/helm-charts
helm upgrade --install policy-controller sigstore/policy-controller \
-n cosign-system --create-namespace1apiVersion: policy.sigstore.dev/v1beta1
2kind: ClusterImagePolicy
3metadata:
4 name: image-policy
5spec:
6 images:
7 - glob: "ghcr.io/myorg/**"
8 authorities:
9 - keyless:
10 url: https://fulcio.sigstore.dev
11 identities:
12 - issuer: https://token.actions.githubusercontent.com
13 subject: "https://github.com/myorg/my-api/.github/workflows/release.yml@refs/tags/*"The Rekor Transparency Log
Every keyless signature is automatically recorded in the Rekor transparency log — an append-only ledger similar to Certificate Transparency logs for TLS certificates.
1# Look up a specific image's signing record
2rekor-cli search --artifact ghcr.io/myorg/my-api:v1.2.0
3
4# Or use cosign to retrieve the transparency log entry
5cosign verify \
6 --certificate-identity-regexp "..." \
7 --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
8 ghcr.io/myorg/my-api:v1.2.0 | jq .[0].optional.BundleRekor is public — anyone can verify that your artifacts were signed, and you can detect if a forged signature appears for your artifacts.
Complete CI/CD Pipeline with Supply Chain Security
1# .github/workflows/secure-release.yml
2name: Secure Release
3on:
4 push:
5 tags: ['v*']
6
7permissions:
8 contents: read
9 packages: write
10 id-token: write
11 attestations: write
12
13jobs:
14 security-scan:
15 runs-on: ubuntu-latest
16 steps:
17 - uses: actions/checkout@v4
18
19 - name: Scan source code
20 uses: anchore/scan-action@v3
21 with:
22 path: "."
23 fail-build: true
24 severity-cutoff: critical
25
26 build-sign-push:
27 needs: security-scan
28 runs-on: ubuntu-latest
29 outputs:
30 digest: ${{ steps.build.outputs.digest }}
31 steps:
32 - uses: actions/checkout@v4
33 - uses: sigstore/cosign-installer@v3
34
35 - uses: docker/login-action@v3
36 with:
37 registry: ghcr.io
38 username: ${{ github.actor }}
39 password: ${{ secrets.GITHUB_TOKEN }}
40
41 - name: Build and push
42 id: build
43 uses: docker/build-push-action@v5
44 with:
45 push: true
46 tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
47
48 - name: Generate SBOM
49 run: |
50 syft ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} \
51 -o cyclonedx-json > sbom.json
52
53 - name: Scan image for CVEs
54 run: grype ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }} --fail-on critical
55
56 - name: Sign image
57 run: |
58 cosign sign --yes \
59 ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
60
61 - name: Attach and sign SBOM
62 run: |
63 cosign attach sbom --sbom sbom.json \
64 ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
65
66 provenance:
67 needs: build-sign-push
68 uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
69 with:
70 image: ghcr.io/${{ github.repository }}
71 digest: ${{ needs.build-sign-push.outputs.digest }}
72 secrets:
73 registry-username: ${{ github.actor }}
74 registry-password: ${{ secrets.GITHUB_TOKEN }}Frequently Asked Questions
What does a signature prove, and what does it not?
That an artifact was signed by a particular key or identity — provenance, not safety. A signed image can still contain a vulnerable dependency. Signing lets you require that artifacts came from your pipeline; scanning speaks to what is inside them. You want both.
Why is keyless signing recommended?
There is no private key to store, rotate or leak. The signature binds to a workload identity from your CI provider and is recorded in a transparency log, so verification checks who signed rather than which key. A signing key sitting in a CI secret is exactly the long-lived credential you are trying to eliminate.
What is an SBOM actually for?
Answering "are we affected" quickly when a vulnerability is announced. Without one, that question means rebuilding and rescanning every image; with one, it is a query. Generate it at build time when the dependency graph is known, and store it as an attestation alongside the image.
Does enforcing signatures break third-party images?
It will unless the policy scopes which registries require signatures. Start by requiring them only for your own registry, leave upstream out, and tighten as the projects you depend on adopt signing. A cluster-wide requirement on day one blocks routine pulls.
What's Next
- Docker Multi-Stage Builds and Image Optimisation — build smaller images with fewer attack surface components
- Kubernetes RBAC and Security — the runtime security layer that complements supply chain security
We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.
Struggling with this in production?
We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.