Software Supply Chain Security — SBOM, Sigstore, and SLSA
In December 2020, attackers compromised SolarWinds' build pipeline and injected malicious code into a routine software update. 18,000 organizations — including the US Treasury, Department of Homeland Security, and Fortune 500 companies — installed the backdoored update without suspicion. The software was legitimately signed, passed all checks, and came through official channels. This is why supply chain security matters.
The Supply Chain Attack Surface
Your software supply chain includes everything from source code to the running binary in production. Each link is an attack vector:
Supply Chain Attack Surface:
Source Code ──► Dependencies ──► Build System ──► Artifact ──► Deployment
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Compromised Malicious pkg Tampered build Modified Rogue deploy
developer (typosquatting, (SolarWinds- container (stolen
account dependency style inject) image credentials)
confusion)
Notable supply chain attacks:
| Attack | Year | Vector | Impact |
|---|---|---|---|
| SolarWinds | 2020 | Build system compromise | 18,000 orgs, US government agencies |
| Codecov | 2021 | CI script tampered | Thousands of repos' secrets exposed |
| Log4Shell | 2021 | Vulnerable dependency | Millions of Java applications |
| ua-parser-js | 2021 | NPM account hijack | 8M weekly downloads compromised |
| colors/faker | 2022 | Maintainer sabotage | Infinite loop in millions of apps |
| xz Utils | 2024 | Long-term social engineering | Backdoor in Linux SSH (caught before wide release) |
SBOM: Software Bill of Materials
An SBOM is a complete list of every component in your software — like a nutrition label for code. It answers: "What's actually in this binary?"
Generate SBOMs with Syft (by Anchore):
# Generate SBOM for a container image
syft scan myapp:latest -o spdx-json > sbom-spdx.json
# Generate SBOM for a directory (source code)
syft scan dir:./src -o cyclonedx-json > sbom-cyclonedx.json
# Generate SBOM in human-readable table format
syft scan myapp:latest -o table
# Sample Syft output (table format):
NAME VERSION TYPE
express 4.18.2 npm
lodash 4.17.21 npm
axios 1.6.7 npm
jsonwebtoken 9.0.2 npm
pg 8.11.3 npm
openssl 3.0.13 deb
curl 7.88.1 deb
libc6 2.36-9 deb
... (200+ components)
SBOM formats:
- SPDX (Linux Foundation) — ISO standard, widely adopted
- CycloneDX (OWASP) — Security-focused, supports VEX (Vulnerability Exploitability eXchange)
Once you have an SBOM, scan it for known vulnerabilities:
# Scan SBOM for vulnerabilities using Grype
grype sbom:sbom-spdx.json
# Output:
# NAME INSTALLED VULNERABILITY SEVERITY
# openssl 3.0.11 CVE-2024-001 Critical
# curl 7.88.1 CVE-2024-002 High
# lodash 4.17.20 CVE-2021-234 High
Container Image Signing with Cosign
How do you know the container image in production is the same one your CI pipeline built? Cosign (part of Sigstore) lets you cryptographically sign and verify container images:
# Install cosign
brew install cosign
# Generate a key pair (for key-based signing)
cosign generate-key-pair
# Sign a container image
cosign sign --key cosign.key myregistry.io/myapp:v1.2.3
# Verify the signature
cosign verify --key cosign.pub myregistry.io/myapp:v1.2.3
# Keyless signing (uses OIDC identity — no keys to manage!)
# Authenticates via GitHub Actions, Google, Microsoft identity
cosign sign myregistry.io/myapp:v1.2.3
# → Opens browser for OIDC authentication
# → Signs with ephemeral key
# → Signature + certificate stored in Rekor transparency log
Keyless signing with OIDC is the recommended approach — no private keys to rotate, leak, or manage:
# GitHub Actions — sign container image after build
name: Build and Sign
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
jobs:
build-sign:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v5
id: build
with:
push: true
tags: ghcr.io/acme/myapp:${{ github.sha }}
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image (keyless)
run: |
cosign sign --yes \
ghcr.io/acme/myapp@${{ steps.build.outputs.digest }}
- name: Attach SBOM as attestation
run: |
syft scan ghcr.io/acme/myapp@${{ steps.build.outputs.digest }} \
-o spdx-json > sbom.json
cosign attest --yes \
--predicate sbom.json \
--type spdxjson \
ghcr.io/acme/myapp@${{ steps.build.outputs.digest }}
Sigstore Components
Sigstore is a suite of tools for signing, verifying, and storing signatures:
Sigstore Ecosystem:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Cosign │ │ Fulcio │ │ Rekor │
│ │ │ │ │ │
│ Sign & │────►│ Issues │────►│ Stores │
│ verify │ │ short- │ │ entries │
│ artifacts│ │ lived │ │ in │
│ │ │ certs │ │ immutable│
│ │ │ from OIDC│ │ log │
└──────────┘ └──────────┘ └──────────┘
│ │
│ ┌──────────┐ │
└─────────►│ Gitsign │◄───────────┘
│ │
│ Sign git │
│ commits │
└──────────┘
- Cosign: Signs container images and other OCI artifacts
- Fulcio: Certificate authority that issues short-lived certificates based on OIDC identity
- Rekor: Tamper-resistant transparency log (similar to Certificate Transparency for TLS)
- Gitsign: Signs git commits using Sigstore (replacing GPG keys)
SLSA Framework (Supply-chain Levels for Software Artifacts)
SLSA (pronounced "salsa") is a security framework that defines four levels of supply chain integrity:
| Level | Name | Requirements | What It Prevents |
|---|---|---|---|
| SLSA 1 | Provenance exists | Build process documented; provenance generated | Unknown build process |
| SLSA 2 | Hosted build | Build runs on hosted service; signed provenance | Developer machine compromise |
| SLSA 3 | Hardened builds | Isolated, ephemeral build environment; non-falsifiable provenance | Build service compromise |
| SLSA 4 | Reproducible | Two independent builds produce identical output | Sophisticated build tampering |
Generate SLSA provenance in GitHub Actions:
# .github/workflows/slsa-build.yaml
name: SLSA Build
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Build image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/acme/myapp:${{ github.ref_name }}
# Generate SLSA provenance (Level 3)
provenance:
needs: build
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ghcr.io/acme/myapp
digest: ${{ needs.build.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
Dependency Pinning and Lock Files
Unpinned dependencies are a supply chain risk — a compromised version can silently enter your build:
# BAD: Unpinned base image (could change anytime)
FROM node:20
# GOOD: Pinned to digest (immutable reference)
FROM node:20@sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
# BAD: Unpinned GitHub Action
- uses: actions/checkout@main
# GOOD: Pinned to commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# Pin all GitHub Actions in your workflows
# Use step-security/harden-runner
- uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895
with:
egress-policy: audit
# Generate lock files for dependencies
npm ci # Uses package-lock.json (not npm install)
pip install --require-hashes -r requirements.txt
go mod verify # Verify checksums in go.sum
Reproducible Builds
A build is reproducible if the same source code always produces the same binary output. This lets anyone verify that a binary matches its source:
# Reproducible Docker build techniques
FROM golang:1.22@sha256:abc123 AS builder
# Pin build timestamp for reproducibility
ARG SOURCE_DATE_EPOCH=0
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download && go mod verify
COPY . .
# Reproducible Go build
RUN CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-s -w -buildid=" \
-o /app/server ./cmd/server
FROM gcr.io/distroless/static-debian12@sha256:def456
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Key techniques:
- Pin base images by digest
- Sort file operations deterministically
- Zero out timestamps (
SOURCE_DATE_EPOCH=0) - Remove build IDs (
-buildid=,-trimpath) - Use distroless images (minimal, reproducible base)
Implementing Supply Chain Security in CI/CD
Here's a complete pipeline that implements supply chain security best practices:
# .github/workflows/secure-pipeline.yaml
name: Secure Supply Chain Pipeline
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write
security-events: write
jobs:
scan-source:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Detect secrets
uses: gitleaks/gitleaks-action@v2
- name: SAST scan
uses: semgrep/semgrep-action@v1
- name: Dependency scan
run: |
npm ci
npx audit-ci --critical
build-sign-attest:
needs: scan-source
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- uses: docker/build-push-action@v5
id: build
with:
push: true
tags: ghcr.io/acme/myapp:${{ github.sha }}
- name: Scan image for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/acme/myapp@${{ steps.build.outputs.digest }}
exit-code: 1
severity: CRITICAL
- uses: sigstore/cosign-installer@v3
- name: Sign image
run: cosign sign --yes ghcr.io/acme/myapp@${{ steps.build.outputs.digest }}
- name: Generate and attach SBOM
run: |
syft scan ghcr.io/acme/myapp@${{ steps.build.outputs.digest }} -o spdx-json > sbom.json
cosign attest --yes --predicate sbom.json --type spdxjson \
ghcr.io/acme/myapp@${{ steps.build.outputs.digest }}
verify-and-deploy:
needs: build-sign-attest
runs-on: ubuntu-latest
steps:
- name: Verify signature before deploy
run: |
cosign verify \
--certificate-identity-regexp="https://github.com/acme/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/acme/myapp@${{ needs.build-sign-attest.outputs.digest }}
- name: Deploy (only if verified)
run: |
kubectl set image deployment/myapp \
myapp=ghcr.io/acme/myapp@${{ needs.build-sign-attest.outputs.digest }}
In-Toto Framework
in-toto is a framework for securing the entire software supply chain by defining and verifying each step:
# Define the supply chain layout
# layout.json — who can do what
{
"steps": [
{
"name": "clone",
"expected_command": ["git", "clone", "https://github.com/acme/myapp"],
"pubkeys": ["developer-key-id"]
},
{
"name": "build",
"expected_command": ["docker", "build", "-t", "myapp", "."],
"pubkeys": ["ci-system-key-id"]
},
{
"name": "test",
"expected_command": ["npm", "test"],
"pubkeys": ["ci-system-key-id"]
},
{
"name": "scan",
"expected_command": ["trivy", "image", "myapp"],
"pubkeys": ["security-scanner-key-id"]
}
],
"inspect": [
{
"name": "verify-no-extra-files",
"expected_materials": ["match", "sources"],
"expected_products": ["match", "build-output"]
}
]
}
in-toto ensures that each step was performed by the right identity, in the right order, and that no unexpected changes were introduced between steps.
Closing Note
Supply chain security is no longer optional — it's a regulatory requirement in many industries and a top priority after SolarWinds and Log4Shell. Start with the basics: generate SBOMs, pin your dependencies, and sign your container images. Then work toward SLSA Level 3 with automated provenance. The effort compounds: every layer of verification you add makes an attacker's job exponentially harder. This wraps up our DevSecOps and security series — stay tuned for more practical DevOps deep dives in upcoming posts.
