Platform Engineering

Supply Chain Security: Sigstore, Cosign, and SLSA

Advanced75 min to complete20 min readJune 1, 2026Updated August 19, 2026

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.

advanced · 75 min

Before you begin

  • Container fundamentals — Docker build, push, registry workflows
  • Kubernetes basics — pods, deployments, admission controllers
  • GitHub Actions experience helpful
Security
Sigstore
Cosign
SLSA
Supply Chain
DevSecOps
Kubernetes

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:

  1. Developer pushes code to GitHub
  2. CI builds an image and pushes to registry as myimage:latest
  3. 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:

ComponentRole
CosignCLI tool for signing and verifying container images and other artifacts
FulcioFree code signing CA — issues short-lived X.509 certificates via OIDC identity
RekorImmutable, 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

bash
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 version

Key-Based Signing

Traditional signing: generate a key pair, sign with private key, distribute public key to verifiers.

bash
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.0

The 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

bash
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.0

In GitHub Actions, use keyless signing — no key management, identity bound to the OIDC workflow identity.

yaml
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

bash
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.0

This verifies that:

  1. The signature is present in Rekor
  2. The certificate was issued by Fulcio
  3. 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:

bash
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.json

Attaching SBOM to the image with Cosign

bash
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.0

Scanning for vulnerabilities with Grype

Grype scans SBOMs and images for CVEs:

bash
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 high

In CI, add Grype after building the image to block deployment of vulnerable images:

yaml
- name: Scan for vulnerabilities
  run: grype ghcr.io/${{ github.repository }}:${{ github.ref_name }} --fail-on critical

SLSA: Supply-chain Levels for Software Artifacts

SLSA (pronounced "salsa") is a framework that defines levels of build security:

LevelRequirementsWhat it proves
SLSA 1Documented build processBuilds are automated (not manual)
SLSA 2Versioned build serviceBuild is reproducible with a hosted service
SLSA 3Hardened build platformBuild 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:

yaml
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

bash
# 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.

bash
# Install Kyverno
helm repo add kyverno https://kyverno.github.io/kyverno/
helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace
yaml
1# 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.dev

Any 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:

bash
helm repo add sigstore https://sigstore.github.io/helm-charts
helm upgrade --install policy-controller sigstore/policy-controller \
    -n cosign-system --create-namespace
yaml
1apiVersion: 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.

bash
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.Bundle

Rekor 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

yaml
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.

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

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.