Metadata Card
- Prerequisites: Chapter 9 (DevSecOps basics), Chapter 7 (SCA dependency scanning and SBOM concepts), container image and CI/CD basic concepts
- Estimated time: 55 minutes
- Core difficulty: Advanced
- Completion mark: Can explain the generation and use cases of SBOM, can describe the core requirements of SLSA's four levels, can sign and verify container images with cosign
Your Progress
In the last chapter, you set up security checkpoints at every stage of the CI/CD pipeline. SAST checks the code itself, SCA checks dependencies for known vulnerabilities, DAST tests the running application from outside.
But these checks share a common blind spot: you're only "checking if dependencies have known vulnerabilities," not "verifying if dependencies were built through a trusted process." An attacker can compromise an open-source project's build server, tamper with source code, and publish a "spiked" new version. You scan the CVE database and find no known vulnerability for this package—but it has a backdoor.
Your security defense system can't distinguish between "legitimate dependencies" and "tampered dependencies."
Your Task
Understand the complete picture of software supply chain security: from SBOM (Software Bill of Materials) to SLSA (Supply-chain Levels for Software Artifacts), from dependency governance to container signing. You'll learn how to track the origin, signature, and integrity of every component in your software, ensuring every byte you deploy has been verified.
Chapter Layers
- Required: Lessons from the SolarWinds attack, SBOM structure and usage, SLSA four levels' core differences
- Optional: Sigstore/cosign detailed operations
- Advanced: Build integrity attestation (in-toto), credential verification and build strategy (SLSA Build L3-L4)
Breaking Ground · Tracing the Origin
Problem: In late 2020, the American cybersecurity firm FireEye discovered an extremely stealthy attack. The attack's origin wasn't a firewall vulnerability or SQL injection—attackers compromised the build system of IT management company SolarWinds, injecting malicious code into the Orion software update package.
This tampered update package was distributed through normal auto-update channels to over 18,000 government agencies and corporate internal networks, including the U.S. State Department, Department of Defense, Department of Homeland Security, and numerous Fortune 500 companies.
Key detail: SolarWinds' software itself had no vulnerabilities. SolarWinds' build process was compromised—attackers gained build server access and inserted malicious code during compilation. This code passed all of SolarWinds' internal testing and QA processes because it was "SolarWinds' own code"—just not written by SolarWinds' developers.
Your defense system can defend against "code vulnerabilities," but not against "a compromised build process."
First Piece: SBOM—Software Bill of Materials
The SolarWinds incident exposed the cruelest side of supply chain security: the dependencies you trust may have backdoors implanted during the build phase.
Imagine your fortress armory has equipment from a hundred different sources—you need to know where each piece came from, who made it, and its current status. The Software Bill of Materials (SBOM) is that list, itemizing every component's name, version, source, and hash.
SBOM NTIA / CISA Minimum Elements (defined 2021):
| Category | Field | Description |
|---|---|---|
| Data fields | Component name | Package name (e.g., fastapi, express) |
| Data fields | Version | Specific version number (e.g., 0.104.0) |
| Data fields | Source | PURL or CPE identifier |
| Data fields | Dependency relationship | Relationship between direct and transitive dependencies |
| Data fields | Author | SBOM generator |
| Data fields | Timestamp | SBOM generation date |
| Automation | Machine-readable format | SPDX or CycloneDX |
| Practice | Update frequency | Updated with software version |
CycloneDX format is currently the most popular SBOM format (SPDX is also supported):
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"timestamp": "2026-06-25T09:00:00Z",
"tools": [
{
"vendor": "CycloneDX",
"name": "cyclonedx-maven-plugin",
"version": "2.8.0"
}
],
"component": {
"type": "application",
"name": "fortress-scan-service",
"version": "1.3.0",
"purl": "pkg:maven/com.fortress/fortress-scan-service@1.3.0"
}
},
"components": [
{
"type": "library",
"name": "spring-boot-starter-web",
"version": "3.2.0",
"purl": "pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0",
"hashes": [{"alg": "SHA-256", "content": "a1b2c3d4..."}],
"licenses": [{"license": {"id": "Apache-2.0"}}],
"evidence": {
"identity": {
"field": "purl",
"confidence": 1.0,
"methods": [{"technique": "manifest-analysis"}]
}
}
},
{
"type": "library",
"name": "log4j-core",
"version": "2.20.0",
"purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.20.0",
"hashes": [{"alg": "SHA-256", "content": "e5f6g7h8..."}]
}
],
"dependencies": [
{"ref": "pkg:maven/com.fortress/fortress-scan-service@1.3.0",
"dependsOn": [
"pkg:maven/org.springframework.boot/spring-boot-starter-web@3.2.0",
"pkg:maven/org.apache.logging.log4j/log4j-core@2.20.0"
]}
]
}Generating SBOMs varies by language:
# Maven/Java project
mvn org.cyclonedx:cyclonedx-maven-plugin:2.8.0:makeBom
# npm/Node.js
npm install -g @cyclonedx/cyclonedx-npm
cyclonedx-npm --output-file bom.json
# Python
pip install cyclonedx-bom
cyclonedx-py requirements.txt --output bom.json
# Container image
syft registry.fortress.local/app:1.3.0 -o cyclonedx -o bom.jsonSyft is an excellent container image SBOM generation tool:
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Scan Docker image and export SBOM
syft registry.fortress.local/app:1.3.0 \
--scope all-layers \
-o cyclonedx-json=app-sbom.json
# View summary
syft registry.fortress.local/app:1.3.0 --scope all-layersSBOMs aren't generated for their own sake. Their core value is quickly answering one question when a vulnerability is disclosed: "Does this CVE affect our services?"
When Log4Shell (CVE-2021-44228) broke, organizations with SBOMs could answer "which services are affected" within 30 minutes; organizations without SBOMs spent weeks or months investigating project by project.
Second Piece: SLSA—Supply Chain Security Levels
SLSA (Supply-chain Levels for Software Artifacts) is a framework initiated by Google and maintained by OpenSSF, defining four levels of software supply chain security from low to high. Like a fortress armory management system—from "there's a warehouse" to "every piece of equipment has an anti-forgery signature on its forging record":
Build L0: No guarantees (default)
Build L1: Build process traceable—at least know "where this binary came from"
Build L2: Build process controlled—generate provenance attestation, isolated build environment
Build L3: Build process trusted—build process tamper-resistant, provenance hard-signed
Build L4 (draft): Complete trusted supply chain—two-person review, reproducible buildsSLSA Build L1—Traceable
The most basic requirement: your build process must generate a Provenance attestation explaining "how this thing was built."
The provenance includes at minimum:
{
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [
{
"name": "fortress-scan-service",
"digest": {"sha256": "a1b2c3d4e5f6..."}
}
],
"predicate": {
"builder": {"id": "https://github.com/fortress/scan-service/.github/workflows/build.yml@refs/heads/main"},
"buildType": "https://github.com/actions/build/WorkflowRun",
"invocation": {
"configSource": {
"uri": "git+https://github.com/fortress/scan-service@refs/heads/main",
"digest": {"sha1": "6f7g8h9i..."}
}
},
"buildConfig": {
"steps": [
{"command": "mvn package"},
{"command": "docker build -t app:latest ."}
]
},
"metadata": {
"buildStartedOn": "2026-06-25T08:30:00Z",
"buildFinishedOn": "2026-06-25T08:32:15Z"
}
}
}This attestation lets you answer: who built it, from what code, with what commands.
Generating SLSA provenance in GitHub Actions:
# .github/workflows/build.yml
name: Build with SLSA Provenance
on:
push:
branches: [main]
jobs:
build:
permissions:
id-token: write # Needs OIDC token to sign provenance
contents: read
attestations: write
steps:
- uses: actions/checkout@v4
- name: Build Artifact
run: mvn package -DskipTests
- name: Generate SLSA Provenance
uses: slsa-framework/slsa-github-generator/actions/generate@v2.0.0
with:
base64-subjects: "${{ steps.hash.outputs.digest }}"
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: fortress-scan-service.jar
path: target/*.jarL1's core value: if someone tells you a problematic binary's origin is unknown, you can trace it back to the build record. Even if the build server is compromised, at least you know where the problem is.
SLSA Build L2—Controlled Build
L2 adds two requirements on top of L1:
- The provenance must be signed by the build platform (using the platform's identity, not a user-provided key)
- The build environment must be isolated—each build uses a clean, pre-defined environment
This means your CI build must be distinguishable from what a developer builds locally—if you can "reproduce" the CI binary on your laptop and get the same SHA256, the CI build's identity verification has meaning.
# Isolated build: using temporary, clean runtime environment
jobs:
build:
runs-on: ubuntu-latest # Always starts from a clean image each time
container:
image: maven:3.9-eclipse-temurin-21 # Fixed version build environment, not latest
steps:
- uses: actions/checkout@v4
- run: mvn packageCI platforms (GitHub Actions, GitLab CI) inherently provide isolated build environments. L2 requires you to ensure builds are isolated, repeatable, and provenance is signed by the CI platform's identity.
SLSA Build L3—Trusted Build
L3 adds further requirements on top of L2:
- The build process must be tamper-resistant—provenance is signed with hardware-assisted keys (e.g., Sigstore's keyless signing)
- The build environment must have complete audit logs
- Build scripts themselves must come from version control
The core difference of L3: the build platform doesn't just "claim" the build process is secure—there's a third-party signed attestation that can be verified. Achieving L3 typically requires Sigstore or similar services:
# L3-level build: Keyless signature verification
build-l3:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3.5.0
- name: Build
run: mvn package
- name: Sign with Sigstore (Keyless)
run: |
cosign sign-blob --yes \
--oidc-issuer https://token.actions.githubusercontent.com \
--output-signature target/app.jar.sig \
--output-certificate target/app.jar.cert \
target/app.jarKey distinction—L3 signing uses Sigstore's Keyless mode, requiring no private key management. The signer's identity comes from the GitHub OIDC token, recorded in Sigstore's public key infrastructure (Rekor transparency log). Any third party can independently verify this signature.
SLSA Build L4 (Draft)
L4 is still being drafted, expected requirements include:
- Two-person review—each build requires two approvals
- Reproducible builds—given the same source and build environment, always produces the same binary
- Build platform integrity attestation—the build platform's own configuration is also signed
| Level | Provenance | Signing Method | Build Isolation | Audit | Practical Difficulty |
|---|---|---|---|---|---|
| L0 | None | None | None | None | Zero |
| L1 | Yes (declarative) | Platform key | Optional | None | Low |
| L2 | Yes (linked) | Platform signing | Isolated | None | Medium |
| L3 | Yes (verified) | Keyless / HSM | Isolated | Audit trail | High |
| L4 | Reproducible | Multi-party signing | Isolated | Full audit | Very High |
Most mature projects target L2 to L3. L1 is a fast baseline to enable; L3 needs Sigstore or similar infrastructure, suitable for critical infrastructure software. L4 is currently only feasible for security-critical systems (like kernel boot chains).
Third Piece: Dependency Governance—Lockfile, Source Verification, and Integrity
Dependency governance isn't just "scan for CVEs and you're done." It includes three layers:
| Layer | Activity | Tool/Method |
|---|---|---|
| 1. Lock versions | Ensure every install gets the same versions | Lockfile (package-lock.json, pinned requirements.txt) |
| 2. Verify source | Confirm the package was published by its maintainer, not MITM-tampered | Package signature verification (npm audit signatures, PyPI trust settings) |
| 3. Verify integrity | Confirm package content hasn't been modified | Hash checksums, slsa-verifier |
Lockfile is the most basic but most commonly overlooked practice. Without a lockfile, the dependency versions you develop with may differ from what's running in production.
// package-lock.json (auto-generated dependency version locking)
{
"name": "fortress-api",
"lockfileVersion": 3,
"packages": {
"node_modules/express": {
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vF0Vg==",
"dev": false
},
"node_modules/body-parser": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOyt+w=="
}
}
}Verify package signatures: npm has supported signature verification since 2022. You can require every package to have the maintainer's GPG signature during installation:
# Require npm to verify package signatures
npm config set sign-git-tag true
npm audit signatures
# Check if a package has been signed
npm view express dist.tarball
npm view express dist.signaturesPackage registries also have verification mechanisms. GitHub's npm registry requires new packages to use OIDC to prove publisher identity; PyPI has supported Trusted Publishers since 2023, avoiding API token usage.
Fourth Piece: Sigstore and Cosign—Keyless Signing
Back to the most basic question: you need to confirm a container image was built by your team and hasn't been tampered with. The traditional approach uses GPG or PGP encryption signing, but that requires you to manage keys, including distributing public keys, key rotation, and revocation when keys are compromised.
Sigstore changes this process. Its core design is "Keyless Signing"—no encryption key management needed.
Sigstore components:
| Component | Role | Description |
|---|---|---|
| Cosign | Signing tool | Signs and verifies container images, binaries, blobs |
| Fulcio | Certificate Authority | Issues short-lived certificates based on OIDC tokens |
| Rekor | Transparency log | Public ledger recording all signatures, providing audit trail |
Sigstore signing flow:
Developer triggers CI build
│
├── CI platform provides OIDC token (proving "I'm GitHub Actions, repo is fortress/app")
│
├── Fulcio receives OIDC token, issues short-lived (usually 10 minutes) signing certificate
│
├── Cosign signs the container image with this certificate
│
├── Signature + certificate recorded in Rekor transparency log (public, immutable)
│
└── Signing complete, image ready for deploymentVerification flow:
During deployment
│
├── cosign verify retrieves signature from image
├── Checks Rekor log for corresponding signature record
├── Verifies certificate came from Fulcio (EKU check, SAN check)
└── If all passes → image is trusted, can deployPractical operations:
# Sign image with Keyless mode in CI
cosign sign --yes \
registry.fortress.local/app:1.3.0
# Verify during deployment
cosign verify --yes \
registry.fortress.local/app:1.3.0
# Can also verify specific identity (not just "whether it has a signature")
cosign verify --certificate-identity-regexp ".*github\.com/fortress/.*" \
--certificate-oidc-issuer-regexp ".*token\.actions\.githubusercontent\.com" \
registry.fortress.local/app:1.3.0If you need to manage your own keys (e.g., on platforms without OIDC—Jenkins, self-hosted CI), Cosign also supports traditional keys:
# Generate key pair
cosign generate-key-pair
# Sign with private key
cosign sign --key cosign.key registry.fortress.local/app:1.3.0
# Verify with public key
cosign verify --key cosign.pub registry.fortress.local/app:1.3.0Verification Provenance: Deployment isn't just "signature verification passed." You also need to verify under what conditions the signature was generated—who signed, when, with what identity. Sigstore records all this in Rekor:
# Get signature audit record from Rekor
rekor-cli search --artifact digest:sha256:a1b2c3d4e5f6...
# Returns a UUID, then get full record
rekor-cli get --uuid 123abc456def
# Returned structure includes: signing time, signer identity, certificate content, transparency log indexFifth Piece: Build Platform Integrity
After covering component signing, let's look at the build platform's own integrity.
If your CI platform's runner is compromised, all the signing and SBOMs above become "malicious signatures and malicious SBOMs." Therefore, the build platform itself must be trustworthy.
Three elements of integrity:
1. Build Provenance
→ SLSA provenance records "who built what from what code at what time"
2. Build Script Audit
→ CI configuration files (.github/workflows/*.yml or .gitlab-ci.yml) must be version-controlled
→ Any modification to CI configuration must pass code review
3. Isolated Build Environment
→ Each build uses an ephemeral, isolated environment (ephemeral runner)
→ Build environment is not contaminated by previous builds
→ Build environment doesn't contain persistent credentials (temporary tokens via OIDC)Check if your CI meets basic security requirements:
# Secure CI configuration—least privilege, isolated environment, version control
name: Build and Sign
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
# Use ephemeral runner (temporary environment per build)
runs-on: ubuntu-latest
# Least privilege—only necessary permissions
permissions:
contents: read
id-token: write # Only for signing
attestations: write
steps:
- uses: actions/checkout@v4
# Build steps use predefined actions, no custom script sources
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: registry.fortress.local
- name: Build and Push
uses: docker/build-push-action@v5
with:
push: true
tags: registry.fortress.local/app:${{ github.sha }}
- name: Sign Image
uses: sigstore/cosign-installer@v3.5.0
run: |
cosign sign --yes registry.fortress.local/app:${{ github.sha }}By contrast, common traits of insecure configurations:
# Insecure—command injection risk
- name: Run user-provided script
run: |
# A PR contributor can modify this script
# If the script comes from an issue comment or PR body, this is command injection
eval "${{ github.event.pull_request.body }}"
# Insecure—runner exposes sensitive info
- name: Debug
run: |
env # May expose secrets
cat /etc/passwd # Potential container escape info leakCommon Pitfalls
- Only generating SBOMs, not consuming them. Many teams auto-generate SBOM files in CI, then store them in build artifacts and never look at them again. The value of an SBOM is in its use—quickly finding affected components when a CVE is published. Regularly do vulnerability correlation analysis on SBOMs (VEX—Vulnerability Exploitability eXchange).
- Thinking SLSA L3 is the goal for every project. SLSA security levels don't linearly correspond to security strength. For internal tools, L1 is enough; for API services facing external users, L2-L3 is appropriate; L4 is currently only suitable for security-critical systems. Start from L1 and gradually improve; each level increase comes with cost increase.
- Signing but not verifying. Container signing only matters if you verify it during deployment. If you sign in CI but don't verify in the deployment pipeline, the signature is just useless metadata.
- Only checking direct dependencies, ignoring transitive dependencies. Your pom.xml or package.json may introduce 20 dependencies, but npm install actually installs 200 packages. Attackers often hide malicious code in deep transitive dependencies (dependency confusion, typosquatting).
- Treating lockfiles as "never needing updates." Lockfiles lock versions, but they're not exempt from updates. Regularly (at least monthly) run
npm auditormvn versions:display-dependency-updatesto check if dependencies have available security fix versions.
Pass Challenges
- Warm-up: Use Syft to scan a local Docker image and generate a CycloneDX-format SBOM. Then manually find one of the dependencies and query whether it has a known CVE (using the NVD API).
- Challenge: Configure a complete SLSA L2 build pipeline. Use GitHub Actions to build a Java or Node.js project, generate SLSA provenance, and sign the build artifact with Cosign.
- Observe: Deployment verification—pull a signed public image from Docker Hub (like
cgr.dev/chainguard/nginx), verify its signature with Cosign. Then compare with an unsigned image and observe the difference in verification results. - Troubleshoot: Your CI pipeline errors during the signing step:
error: no matching credentials. You're using Keyless mode, but the CI platform (Jenkins) doesn't support OIDC. How do you resolve this? (Hint: use Cosign's traditional key mode, or migrate to an OIDC-supporting CI platform)
Traveler's Notes
- SBOM is a software component manifest containing all component names, versions, sources, and hashes; quickly locates affected systems during vulnerability emergencies
- SLSA's four levels (L0-L4) define the security level of the software build process, from "traceable" to "tamper-resistant"
- Dependency governance requires lockfile (locking versions) + source verification (signatures) + integrity verification (hashes)
- Sigstore/Cosign's Keyless signing solves the key management pain points of traditional signing schemes, using OIDC and transparency logs for key-free signing
- The build platform's own integrity is more fundamental than component signing—if the build platform is compromised, all signatures and SBOMs become meaningless
- Start from SLSA L1 and gradually improve; not every project needs L3/L4
Next Stop Preview
Your knowledge of software supply chain security ends here. You started from cryptography, went through PKI, Web security, system security, network defense, SDL process, privacy protection, DevSecOps, and finally supply chain security—you've built a complete defense knowledge system from code writing to deployment operation. In the next volume, we'll enter the world of languages and explore the souls of different programming languages.