Skip to main content

Software Supply Chain Security — SBOM, Sigstore, and SLSA

· 8 min read
Goel Academy
DevOps & Cloud Learning Hub

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:

AttackYearVectorImpact
SolarWinds2020Build system compromise18,000 orgs, US government agencies
Codecov2021CI script tamperedThousands of repos' secrets exposed
Log4Shell2021Vulnerable dependencyMillions of Java applications
ua-parser-js2021NPM account hijack8M weekly downloads compromised
colors/faker2022Maintainer sabotageInfinite loop in millions of apps
xz Utils2024Long-term social engineeringBackdoor 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:

LevelNameRequirementsWhat It Prevents
SLSA 1Provenance existsBuild process documented; provenance generatedUnknown build process
SLSA 2Hosted buildBuild runs on hosted service; signed provenanceDeveloper machine compromise
SLSA 3Hardened buildsIsolated, ephemeral build environment; non-falsifiable provenanceBuild service compromise
SLSA 4ReproducibleTwo independent builds produce identical outputSophisticated 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.