Skip to main content

SBOM & Supply-Chain Signing

TL;DR

A Software Bill of Materials (SBOM) is a complete list of all components and dependencies in a software artifact. Supply-chain security ensures that artifacts are built by trusted builders and haven't been tampered with. Cosign (from sigstore) enables keyless cryptographic signing of artifacts using OIDC identity. SLSA (Supply-chain Levels for Software Artifacts) is a framework defining maturity levels for supply-chain security. Verify artifact provenance (who built it, from what code) before deployment. Require SBOM, signatures, and attestations at the image registry admission layer.

Learning Objectives

  • Generate and understand Software Bills of Materials (SBOM) for containers
  • Implement image signing using cosign and keyless identity (OIDC)
  • Verify attestations and provenance in CI/CD and admission controllers
  • Evaluate and implement SLSA supply-chain security levels
  • Detect and prevent compromised artifacts from reaching production
  • Build verifiable provenance chains from source code to running container

Motivating Scenario

A DevOps team pulls a container image from a public registry: node:18.0.0-alpine. They assume it's the official Node image, but it's actually a compromise: an attacker created a near-identical image with a cryptominer injected. No one noticed. The cryptominer silently runs on every server. Months later, performance drops and a security audit discovers it.

With supply-chain security: Image must be signed by Node.js maintainers' key. SBOM must list all dependencies. Before deployment, Kubernetes admission controller verifies: "Is this image signed? Does SBOM match expected dependencies?" Compromised image is rejected immediately.

Core Concepts

SBOM (Software Bill of Materials)

An SBOM lists all components in an artifact:

  • Direct dependencies (packages explicitly used)
  • Transitive dependencies (dependencies of dependencies)
  • Versions and build info
  • License information
  • Known vulnerabilities

Example SBOM entry: pkg:npm/lodash@4.17.21 (NPM package, lodash, version 4.17.21)

Formats: SPDX, CycloneDX, SWID

Artifact Signing & Attestation

Signature: Cryptographic proof that an artifact came from a specific key/identity.

Attestation: Structured metadata signed alongside the artifact. Examples:

  • "Built by cosign version 1.12.0"
  • "Built from Git commit abc123def456"
  • "Passed security scan with 0 vulnerabilities"
  • "SBOM generated by syft"

Cosign & Keyless Signing

Cosign uses OIDC (OpenID Connect) to sign artifacts without managing keys:

  1. Build system proves identity via OIDC (e.g., GitHub Actions, GitLab CI)
  2. Cosign generates a short-lived certificate from OIDC token
  3. Image is signed with that certificate
  4. Certificate is verifiable against OIDC issuer's public keys
  5. No private key storage needed (keyless = simpler + safer)

SLSA Framework

SLSA defines 4 levels of supply-chain security maturity:

LevelRequirementsEffort
L0MinimalScripted build, no attestation
L1Versioned + provenanceAutomated build, signed provenance
L2Hermetic + hardenedIsolated build, no external network, script reviewed
L3Hardened + locked-downReproducible builds, source integrity verified
L4Maximum securityFull audit trail, offline signing keys, 2FA

Most organizations target L2-L3.

Practical Example

#!/bin/bash
# Generate SBOM for container image using syft

# Install syft (https://github.com/anchore/syft)
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# Generate SBOM in SPDX format
syft myregistry.azurecr.io/myapp:1.2.3 -o spdx > sbom.spdx.json

# Or CycloneDX format
syft myregistry.azurecr.io/myapp:1.2.3 -o cyclonedx > sbom.cyclonedx.xml

# View SBOM
jq . sbom.spdx.json | head -50
# Output sample:
# {
# "spdxVersion": "SPDX-2.3",
# "creationInfo": {
# "created": "2025-02-14T10:00:00Z",
# "creators": ["Tool: syft-0.68.3"]
# },
# "name": "myapp:1.2.3",
# "packages": [
# {
# "SPDXID": "SPDXRef-Package-node-modules-lodash",
# "name": "lodash",
# "versionInfo": "4.17.21",
# "downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
# "filesAnalyzed": false
# },
# {
# "SPDXID": "SPDXRef-Package-npm-express",
# "name": "express",
# "versionInfo": "4.18.2",
# "downloadLocation": "https://registry.npmjs.org/express/-/express-4.18.2.tgz"
# }
# ]
# }

# Publish SBOM to registry (attach to image)
cosign attach sbom --sbom sbom.spdx.json myregistry.azurecr.io/myapp:1.2.3

Usage:

  • Generate during build
  • Publish alongside image
  • Query at deployment time: "What's in this image?"

When to Use / When NOT to Use

Supply-Chain Security: Best Practices vs Shortcuts
Best Practices
  1. DO: Sign All Images Built by Your Organization: CI/CD signs every image with OIDC (GitHub Actions, GitLab CI). Signature verifiable at deployment. No key management burden.
  2. DO: Require Verification at Admission Time: Kubernetes admission controller verifies every image: signature present, signed by trusted builder. Rejected if unsigned.
  3. DO: Generate and Publish SBOM: Build generates SBOM (syft). Publish alongside image. At deployment, verify SBOM lists expected dependencies.
  4. DO: Use OIDC (Keyless Signing): Cosign + OIDC = no private keys to manage. GitHub Actions proves identity. Certificate valid for seconds. Simpler + safer.
  5. DO: Define Trusted Builders: Admission policy: only accept images signed by GitHub Actions from your org repos. Prevents supply-chain compromise.
  6. DO: Audit Supply-Chain Regularly: Monthly: scan all deployed images for signatures + SBOM. Report unsigned images. Drive adoption.
Anti-Patterns
  1. DO: Sign All Images Built by Your Organization: Skip signing because 'it's extra work.' Or manually sign images (error-prone, slow). Or rely on registry username/password only.
  2. DO: Require Verification at Admission Time: Trust that teams will use only 'good' images. Someone will inevitably deploy an unsigned image. Compromise slides through.
  3. DO: Generate and Publish SBOM: No SBOM. Can't answer: what's in this image? Can't detect dependency changes. Vulnerable package sneaks in undetected.
  4. DO: Use OIDC (Keyless Signing): Manage private keys manually (risk of exposure). Or use hardcoded signing keys (single point of failure).
  5. DO: Define Trusted Builders: Accept images signed by anyone. Or trust images without signatures. Attacker can push signed malicious image.
  6. DO: Audit Supply-Chain Regularly: Set policy once, never check compliance. Unsigned images creep in. Policy becomes unenforceable myth.

Patterns & Pitfalls

Images built without signing. Anyone could push a malicious image with same tag. No way to verify origin. Admission controller would catch this, but no admission control deployed.
Build completes, no SBOM generated. Can't answer: what dependencies are in this image? Vulnerability in transitive dependency discovered, but you don't know which images are affected.
Private signing key stored on CI/CD server. Single compromise = all images signed by that key are now untrusted. Key rotation painful.
GitHub Actions proves identity. Cosign signs with short-lived certificate. No private key management. Certificate verifiable against GitHub's public keys.
Webhook rejects any pod with unsigned image (or not signed by trusted builder). Prevents circumvention. Policy is enforced automatically.
Sign not just the image, but also attestations: vulnerability scan results, SBOM, build metadata. Deployment can verify: 'No critical vulns AND signed.'
Build is manual, no provenance, no signing. Risk of supply-chain compromise never addressed.
Month 1: Automated build (L1). Month 2: Add signatures (L2). Month 3: Add provenance attestations (L2). Month 6: Hardened build with reproducibility (L3).

Design Review Checklist

  • Are all container images built by your organization signed?
  • Is keyless signing (OIDC) enabled to avoid key management?
  • Are SBOMs generated for every image build?
  • Are SBOMs published to the registry (attached to image)?
  • Is there a Kubernetes admission controller verifying signatures?
  • Does admission policy define trusted builders (OIDC issuers)?
  • Are third-party images (Redis, Postgres, etc.) trusted and pinned to specific versions?
  • Are attestations generated beyond signatures (scan results, provenance)?
  • Is supply-chain compliance audited monthly (% signed images)?
  • Are unsigned images detected and teams notified?
  • Is SLSA level documented and progressive improvement planned?
  • Are SBOMs queryable (can you list all images with lodash@4.17.21)?
  • Is image tampering detectable (signature verification before runtime)?
  • Are compromised artifacts recoverable (can you identify deployed images)?
  • Is supply-chain security integrated into CI/CD (not manual)?

Self-Check

  1. Right now, do all your images have cosign signatures? Check with: cosign verify myimage:tag
  2. Which images are unsigned? Run audit script. If count > 0, that's risk.
  3. Can you list all dependencies in your images? Query SBOM. If not, generate them.
  4. If a dependency is vulnerable, can you identify which images are affected? SBOM makes this quick.
  5. What would you do if your signing key was compromised? Have a rotation plan.

Next Steps

  1. Enable OIDC signing — Add cosign to your CI/CD. Sign images with OIDC.
  2. Generate SBOMs — Add syft to build. Attach SBOM to every image.
  3. Implement admission control — Deploy webhook to verify signatures.
  4. Define trust policy — What builders are trusted? Which images allowed?
  5. Audit compliance — Monthly report: % of images signed and with SBOM.
  6. Add attestations — Beyond signatures, attest to: vulnerability scans, SLSA level, build provenance.
  7. Plan SLSA progression — Define path from L1 → L2 → L3.

References

  1. Sigstore: Keyless Container Signing ↗️
  2. Cosign: GitHub Repository ↗️
  3. SLSA: Supply-Chain Security Framework ↗️
  4. Syft: SBOM Generator ↗️
  5. CycloneDX: SBOM Format Standard ↗️
  6. SPDX: Software Package Data Exchange ↗️