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
| Level | What It Means | Requirements |
|---|---|---|
| SLSA 0 | No guarantees | Most projects today |
| SLSA 1 | Documented build process | Build script exists and is version controlled |
| SLSA 2 | Tamper-resistant build | CI/CD with audit logs, version-controlled build |
| SLSA 3 | Hardened build platform | Isolated builds, signed provenance |
| SLSA 4 | Highest assurance | Two-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
| Format | Spec Body | Use Case |
|---|---|---|
| SPDX | Linux Foundation | Comprehensive, license-focused |
| CycloneDX | OWASP | Security-focused, VEX support |
| SWID | ISO/IEC | Enterprise/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
| Level | Capability | Status |
|---|---|---|
| Basic | Dependencies locked | ☐ |
| Basic | Dependabot enabled | ☐ |
| Basic | Image scanning in CI | ☐ |
| Intermediate | Images signed | ☐ |
| Intermediate | SBOMs generated | ☐ |
| Intermediate | Admission policy (non-enforcing) | ☐ |
| Advanced | Admission policy (enforcing) | ☐ |
| Advanced | SLSA 2+ provenance | ☐ |
| Advanced | Verified reproducible builds | ☐ |
Conclusion
Supply chain security isn’t a product you buy - it’s a set of practices you adopt. Start with the basics:
- Lock dependencies - Know exactly what you’re running
- Scan everything - Vulnerabilities, secrets, misconfigurations
- Sign artifacts - Prove who built what
- Generate SBOMs - Know what’s inside
- Enforce policies - Don’t just detect, prevent
The next supply chain attack is coming. The question is whether you’ll be ready.