Skip to content

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):

CategoryFieldDescription
Data fieldsComponent namePackage name (e.g., fastapi, express)
Data fieldsVersionSpecific version number (e.g., 0.104.0)
Data fieldsSourcePURL or CPE identifier
Data fieldsDependency relationshipRelationship between direct and transitive dependencies
Data fieldsAuthorSBOM generator
Data fieldsTimestampSBOM generation date
AutomationMachine-readable formatSPDX or CycloneDX
PracticeUpdate frequencyUpdated with software version

CycloneDX format is currently the most popular SBOM format (SPDX is also supported):

json
{
  "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:

bash
# 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.json

Syft is an excellent container image SBOM generation tool:

bash
# 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-layers

SBOMs 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 builds

SLSA 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:

json
{
  "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:

yaml
# .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/*.jar

L1'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:

  1. The provenance must be signed by the build platform (using the platform's identity, not a user-provided key)
  2. 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.

yaml
# 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 package

CI 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:

  1. The build process must be tamper-resistant—provenance is signed with hardware-assisted keys (e.g., Sigstore's keyless signing)
  2. The build environment must have complete audit logs
  3. 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:

yaml
# 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.jar

Key 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
LevelProvenanceSigning MethodBuild IsolationAuditPractical Difficulty
L0NoneNoneNoneNoneZero
L1Yes (declarative)Platform keyOptionalNoneLow
L2Yes (linked)Platform signingIsolatedNoneMedium
L3Yes (verified)Keyless / HSMIsolatedAudit trailHigh
L4ReproducibleMulti-party signingIsolatedFull auditVery 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:

LayerActivityTool/Method
1. Lock versionsEnsure every install gets the same versionsLockfile (package-lock.json, pinned requirements.txt)
2. Verify sourceConfirm the package was published by its maintainer, not MITM-tamperedPackage signature verification (npm audit signatures, PyPI trust settings)
3. Verify integrityConfirm package content hasn't been modifiedHash 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.

json
// 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:

bash
# 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.signatures

Package 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:

ComponentRoleDescription
CosignSigning toolSigns and verifies container images, binaries, blobs
FulcioCertificate AuthorityIssues short-lived certificates based on OIDC tokens
RekorTransparency logPublic 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 deployment

Verification 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 deploy

Practical operations:

bash
# 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.0

If you need to manage your own keys (e.g., on platforms without OIDC—Jenkins, self-hosted CI), Cosign also supports traditional keys:

bash
# 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.0

Verification 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:

bash
# 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 index

Fifth 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:

yaml
# 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:

yaml
# 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 leak

Common 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 audit or mvn versions:display-dependency-updates to 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.

Built with VitePress | Software Systems Atlas