Skip to content
Back to blog Software Supply Chain Security - Sigstore, SLSA, and Beyond

Software Supply Chain Security - Sigstore, SLSA, and Beyond

SecurityDevOps

Software Supply Chain Security - Sigstore, SLSA, and Beyond

SolarWinds. Log4Shell. Codecov. The biggest security incidents of recent years weren’t direct attacks - they were supply chain compromises. Someone poisoned a dependency, and thousands of companies got owned.

Your application is 90% code you didn’t write. Every npm package, container base image, and GitHub Action is a potential attack vector. If you’re not actively securing your supply chain, you’re trusting thousands of strangers with your production environment.

Let’s fix that.

TL;DR

  • Supply chain attacks exploit trust in dependencies and build systems
  • SLSA framework provides levels of supply chain security maturity
  • Sigstore enables keyless signing and verification of artifacts
  • SBOMs (Software Bill of Materials) track what’s in your software
  • Admission controllers enforce policies at deployment time
  • Start with low-hanging fruit: lock dependencies, verify signatures, scan images

The Attack Surface

Your software supply chain includes:

Source Code
├── Your code (version controlled, reviewed)
├── Dependencies (npm, pip, go modules, etc.)
│   └── Transitive dependencies (you didn't choose these)
├── Base images (who built them? when? with what?)
└── Build scripts (Dockerfiles, Makefiles)

Build System
├── CI/CD platform (GitHub Actions, GitLab CI, Jenkins)
├── Build environment (what's installed? who has access?)
├── Secrets (leaked tokens = supply chain compromise)
└── Build outputs (artifacts, images, binaries)

Distribution
├── Registry (Docker Hub, ECR, Artifactory)
├── Package repositories (npm, PyPI, Maven Central)
└── CDNs and mirrors

Deployment
├── Pull from registry (is this the same image you pushed?)
├── Kubernetes admission (what policies exist?)
└── Runtime (container escape = everything compromised)

Every node in this graph is an attack vector.


SLSA Framework: Levels of Security

SLSA (Supply chain Levels for Software Artifacts) provides a maturity framework. Think of it as a security checklist with levels.

SLSA Levels

LevelWhat It MeansRequirements
SLSA 0No guaranteesMost projects today
SLSA 1Documented build processBuild script exists and is version controlled
SLSA 2Tamper-resistant buildCI/CD with audit logs, version-controlled build
SLSA 3Hardened build platformIsolated builds, signed provenance
SLSA 4Highest assuranceTwo-person review, hermetic builds

SLSA Build Requirements

# SLSA 2+ requirements for your build:

# 1. Version controlled build definition
# Dockerfile, Makefile, CI config in git

# 2. Isolated build environment
# Not your laptop - CI/CD platform

# 3. Provenance generation
# Signed attestation of what was built, by whom, from what source

# 4. Dependency completeness
# All dependencies declared and locked

GitHub Actions with SLSA Provenance

# .github/workflows/build.yml
name: SLSA Build
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write  # For Sigstore
      packages: write
      
    steps:
      - uses: actions/checkout@v4
      
      - name: Build image
        run: |
          docker build -t myapp:${{ github.sha }} .
      
      - name: Install cosign
        uses: sigstore/cosign-installer@v3
      
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Push image
        run: |
          docker tag myapp:${{ github.sha }} ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
      
      - name: Sign image with Sigstore
        run: |
          cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }}
      
      - name: Generate and attach SBOM
        run: |
          syft ghcr.io/${{ github.repository }}:${{ github.sha }} -o spdx-json > sbom.json
          cosign attest --yes --predicate sbom.json \
            --type spdxjson \
            ghcr.io/${{ github.repository }}:${{ github.sha }}

Sigstore: Keyless Signing for the Masses

Traditional code signing requires:

  • Generating keypairs
  • Securely storing private keys
  • Rotating keys
  • Distributing public keys

Sigstore eliminates this with keyless signing using OIDC identity.

How Sigstore Works

1. Developer authenticates (GitHub, Google, etc.)
2. OIDC token proves identity
3. Fulcio (Sigstore CA) issues short-lived certificate
4. Developer signs artifact
5. Signature + cert logged to Rekor (transparency log)
6. Verifier checks Rekor for proof

No keys to manage. Identity tied to your existing accounts.

Signing with Cosign

# Sign a container image (keyless)
cosign sign --yes ghcr.io/myorg/myapp:v1.0.0

# This will:
# 1. Open browser for OIDC authentication
# 2. Get certificate from Fulcio
# 3. Sign the image digest
# 4. Upload signature to registry
# 5. Log to Rekor transparency log

# Verify a signature
cosign verify \
  --certificate-identity=ci@myorg.com \
  --certificate-oidc-issuer=https://token.actions.githubusercontent.com \
  ghcr.io/myorg/myapp:v1.0.0

Cosign in CI (No Browser)

# GitHub Actions with workload identity
- name: Sign image
  env:
    COSIGN_EXPERIMENTAL: 1
  run: |
    cosign sign --yes \
      ghcr.io/${{ github.repository }}:${{ github.sha }}

GitHub’s OIDC token automatically authenticates - no keys needed.


SBOMs: Know What You’re Running

A Software Bill of Materials lists every component in your software. When Log4Shell dropped, teams with SBOMs could answer “are we affected?” in minutes. Teams without SBOMs took weeks.

Generating SBOMs

# Using Syft (from Anchore)
syft myapp:latest -o spdx-json > sbom.spdx.json
syft myapp:latest -o cyclonedx-json > sbom.cdx.json

# For source code
syft dir:./src -o spdx-json > sbom-source.json

# Attach SBOM to image as attestation
cosign attest --yes \
  --predicate sbom.spdx.json \
  --type spdxjson \
  ghcr.io/myorg/myapp:v1.0.0

SBOM Formats

FormatSpec BodyUse Case
SPDXLinux FoundationComprehensive, license-focused
CycloneDXOWASPSecurity-focused, VEX support
SWIDISO/IECEnterprise/government

Most tools support both SPDX and CycloneDX. Pick one and be consistent.

Querying SBOMs

# Using grype to scan SBOM for vulnerabilities
grype sbom:sbom.spdx.json

# Check if a specific package is present
cat sbom.spdx.json | jq '.packages[] | select(.name | contains("log4j"))'

# Count dependencies
cat sbom.spdx.json | jq '.packages | length'

Dependency Security

Lock Everything

# Node.js - use npm ci, not npm install
npm ci  # Installs exactly what's in package-lock.json

# Python - pin with hashes
pip install --require-hashes -r requirements.txt

# Go - vendor dependencies
go mod vendor
go build -mod=vendor

# Terraform - lock providers
terraform init -upgrade
# Creates .terraform.lock.hcl

requirements.txt with hashes

# requirements.txt
requests==2.31.0 \
  --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
  --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1

Dependabot/Renovate Configuration

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      # Group minor/patch updates to reduce noise
      production-deps:
        patterns:
          - "*"
        update-types:
          - "minor"
          - "patch"
    # Security updates are always immediate
    
  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
    
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Admission Control: Enforce at Deploy Time

All the signing and SBOMs are useless if you don’t enforce them at deployment.

Sigstore Policy Controller

# Install policy-controller
helm repo add sigstore https://sigstore.github.io/helm-charts
helm install policy-controller sigstore/policy-controller \
  -n sigstore-system --create-namespace

# Create a ClusterImagePolicy
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-images
spec:
  images:
    - glob: "ghcr.io/myorg/**"
  authorities:
    - keyless:
        identities:
          - issuer: "https://token.actions.githubusercontent.com"
            subject: "https://github.com/myorg/*"

Kyverno Policies

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-signature
      match:
        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

---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-sbom-attestation
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-sbom
      match:
        resources:
          kinds:
            - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/myorg/*"
          attestations:
            - predicateType: "https://spdx.dev/Document"
              conditions:
                - all:
                    - key: "{{ len(packages) }}"
                      operator: GreaterThan
                      value: "0"

OPA/Gatekeeper

# Constraint template
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos
        
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not strings.any_prefix_match(container.image, input.parameters.repos)
          msg := sprintf("image '%v' not from allowed repository", [container.image])
        }

---
# Apply constraint
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: require-internal-registry
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["production"]
  parameters:
    repos:
      - "ghcr.io/myorg/"
      - "myregistry.azurecr.io/"

Vulnerability Scanning Pipeline

# .github/workflows/security.yml
name: Security Scan
on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Dependency scanning
      - name: Scan dependencies
        uses: anchore/scan-action@v3
        with:
          path: "."
          fail-build: true
          severity-cutoff: high
      
      # Build image
      - name: Build
        run: docker build -t myapp:${{ github.sha }} .
      
      # Image vulnerability scan
      - name: Scan image
        uses: anchore/scan-action@v3
        with:
          image: "myapp:${{ github.sha }}"
          fail-build: true
          severity-cutoff: critical
      
      # SBOM generation
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: myapp:${{ github.sha }}
          format: spdx-json
          output-file: sbom.spdx.json
      
      # Secret scanning
      - name: Scan for secrets
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          extra_args: --only-verified

Quick Wins: Where to Start

Week 1: Lock Dependencies

# Commit lock files
git add package-lock.json go.sum requirements.txt
git commit -m "Pin all dependencies"

# Use npm ci in CI
sed -i 's/npm install/npm ci/g' .github/workflows/*.yml

Week 2: Enable Dependabot

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"

Week 3: Add Image Scanning

# In your CI pipeline
- uses: anchore/scan-action@v3
  with:
    image: "myapp:${{ github.sha }}"
    fail-build: true
    severity-cutoff: critical

Week 4: Sign Your Images

- uses: sigstore/cosign-installer@v3
- run: cosign sign --yes $IMAGE

Week 5: Enforce in Cluster

helm install policy-controller sigstore/policy-controller

Maturity Checklist

LevelCapabilityStatus
BasicDependencies locked
BasicDependabot enabled
BasicImage scanning in CI
IntermediateImages signed
IntermediateSBOMs generated
IntermediateAdmission policy (non-enforcing)
AdvancedAdmission policy (enforcing)
AdvancedSLSA 2+ provenance
AdvancedVerified reproducible builds

Conclusion

Supply chain security isn’t a product you buy - it’s a set of practices you adopt. Start with the basics:

  1. Lock dependencies - Know exactly what you’re running
  2. Scan everything - Vulnerabilities, secrets, misconfigurations
  3. Sign artifacts - Prove who built what
  4. Generate SBOMs - Know what’s inside
  5. Enforce policies - Don’t just detect, prevent

The next supply chain attack is coming. The question is whether you’ll be ready.


References

Found this helpful?

Comments