跳到内容

元数据卡

  • 前置知识:第9章(DevSecOps 基础)、第7章(SCA 依赖扫描和 SBOM 概念)、容器镜像和 CI/CD 基本概念
  • 预计时间:55 分钟
  • 完成标志:能解释 SBOM 的产生和使用场景,能说明 SLSA 四个等级的核心要求,能用 cosign 签名和验证容器镜像

你的进度

上一章中,你在 CI/CD 流水线的每一段路上都设置了安全哨点。SAST 检查代码本身的问题,SCA 检查依赖中的已知漏洞,DAST 从外部测试运行中的应用。

但这些检查有一个共同的盲区:你只是在"检查依赖是不是有已知漏洞",而不是"验证依赖是不是经过可信构建的"。一个攻击者可以攻破一个开源项目的构建服务器、篡改源代码,然后发布一个"加了料"的新版本。你扫描 CVE 数据库,发现这个包没有已知漏洞——但它有一个后门。

你的安全防御系统无法区分"正常依赖"和"被篡改的依赖"。

你的任务

理解软件供应链安全的完整图景:从 SBOM(软件物料清单)到 SLSA(供应链完整性框架),从依赖治理到容器签名。你将学会如何跟踪软件中的每一个组件的来源、签名和完整性,确保你部署的每一个字节都经过了验证。

本章分层

  • 必读:SolarWinds 攻击的启示、SBOM 的结构和使用、SLSA 四个等级的核心区别
  • 选读:Sigstore/cosign 详细操作
  • 进阶:构建完整性证明(in-toto)、凭证校验和构建策略(SLSA Build L3-L4)

破局 · 溯源

问题:2020 年末,美国网络安全公司 FireEye 发现了一场极其隐蔽的攻击。攻击的源头不是某个防火墙漏洞,也不是 SQL 注入——攻击者攻破了 IT 管理公司 SolarWinds 的构建系统,在 Orion 软件的更新包中插入了恶意代码。

这个被篡改的更新包通过正常的自动更新渠道,分发到了 18,000 多个政府机构和企业的内部网络。其中包括美国国务院、国防部、国土安全部和多家财富 500 强公司。

关键细节:SolarWinds 的软件本身没有漏洞。SolarWinds 的构建流程被篡改了——攻击者获得了构建服务器权限,在编译阶段插入了恶意代码。这个代码通过了 SolarWinds 的全部内部测试和 QA 流程,因为它就是"SolarWinds 自己写的代码"——只不过代码不是 SolarWinds 的开发人员写的。

你的防御体系防御得了"代码的漏洞",但防御不了"构建流程被攻破"。

第一块:SBOM——软件物料清单

SolarWinds 事件揭开了供应链安全最残酷的一面:你信任的依赖可能在构建阶段被植入后门。

想象你在要塞的军械库里有一百种不同来源的装备——你要知道每件装备从哪里来、谁造的、当前状态如何。软件物料清单(SBOM)就是这张清单,列明所有组件的名称、版本、来源和哈希值。

SBOM 的 NTIA / CISA 最小元素(2021年定义):

类别字段说明
数据字段组件名称包名(如 fastapi, express)
数据字段版本具体版本号(如 0.104.0)
数据字段来源PURL 或 CPE 标识符
数据字段依赖关系直接依赖和传递依赖之间的关系
数据字段作者SBOM 的生成者
数据字段时间戳SBOM 生成日期
自动化机器可读格式SPDX 或 CycloneDX
实践操作频率随软件版本更新而更新

CycloneDX 格式是目前最流行的 SBOM 格式(也支持 SPDX):

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"
     ]}
  ]
}

生成 SBOM 的方式因语言而异:

bash
# Maven/Java 项目
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

# 容器镜像
syft registry.fortress.local/app:1.3.0 -o cyclonedx -o bom.json

Syft 是一款出色的容器镜像 SBOM 生成工具:

bash
# 安装 Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin

# 扫描 Docker 镜像并导出 SBOM
syft registry.fortress.local/app:1.3.0 \
  --scope all-layers \
  -o cyclonedx-json=app-sbom.json

# 查看摘要信息
syft registry.fortress.local/app:1.3.0 --scope all-layers

SBOM 不是为了生成而生成,它的核心价值是在漏洞出现时能快速回答一个问题:"这个 CVE 影响我们的服务吗?"

Log4Shell(CVE-2021-44228)爆出时,有 SBOM 的组织可以在 30 分钟内回答"哪些服务受影响";没有 SBOM 的组织花了几周甚至几个月来逐个项目排查。

第二块:SLSA——供应链的安全等级

SLSA(Supply-chain Levels for Software Artifacts)是 Google 发起、OpenSSF 维护的框架,定义了软件供应链安全从低到高的四个等级。就像要塞的军械管理制度——从"有个仓库"到"每件装备的铸造记录都有防伪签名":

Build L0: 没有保障(默认)
Build L1: 构建过程可溯源——至少知道"这个二进制从哪里来"
Build L2: 构建过程受控——生成来源证明,构建环境是隔离的
Build L3: 构建过程可信——构建过程抗篡改,来源证明经过硬签名
Build L4(草案): 完整的可信供应链——双人审核、可复现构建

SLSA Build L1——可溯源

最基本的要求:你的构建过程必须生成一份来源证明(Provenance),说明"这个东西是怎么构建出来的"。

来源证明至少包含:

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"
    }
  }
}

这份证明让你能回答:谁构建的、从哪段代码构建的、用什么命令构建的。

在 GitHub Actions 中生成 SLSA 来源证明:

yaml
# .github/workflows/build.yml
name: Build with SLSA Provenance
on:
  push:
    branches: [main]

jobs:
  build:
    permissions:
      id-token: write  # 需要 OIDC token 来签名来源证明
      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 的核心价值:如果有人告诉你有问题的二进制来源不明,你可以追溯到构建记录。即使构建服务器被攻击,至少你知道问题出在哪。

SLSA Build L2——受控构建

L2 在 L1 的基础上增加了两项要求:

  1. 来源证明必须由构建平台签名(用平台的身份,不是用户手动提供的密钥)
  2. 构建环境必须隔离——每次构建使用干净的、预定义的环境

这意味着你的 CI 构建必须和开发者本机构建的结果可区分——如果你能在你笔记本上"复现"CI 构建出的二进制并表示 SHA256 一致,CI 构建的身份验证才有意义。

yaml
# 隔离构建:使用临时、干净的运行环境
jobs:
  build:
    runs-on: ubuntu-latest  # 每次都从干净的镜像启动
    container:
      image: maven:3.9-eclipse-temurin-21  # 固定版本的构建环境,不是 latest
    steps:
      - uses: actions/checkout@v4
      - run: mvn package

CI 平台(GitHub Actions、GitLab CI)本身就提供了隔离构建环境。L2 要求你确保构建是隔离的、可重复的,并且来源证明是由 CI 平台的身份签名的。

SLSA Build L3——可信构建

L3 在 L2 基础上还要求:

  1. 构建过程必须抗篡改——来源证明由硬件辅助密钥签名(如 Sigstore 的密钥less 签名)
  2. 构建环境必须有完整审计日志
  3. 构建脚本本身必须来自版本控制

L3 的核心区别在于:构建平台不再只是"宣称"构建过程是安全的,而是有第三方签名的证明可以验证。实现 L3 通常需要 Sigstore 或类似服务:

yaml
# L3 级别的构建:Keyless 签名验证
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

关键区别——L3 的签名用的是 Sigstore 的 Keyless 模式,不需要你管理私钥。签名者的身份来自 GitHub OIDC token,由 Sigstore 的公钥基础设施(Rekor 透明日志)记录。任何第三方都可以独立验证这个签名。

SLSA Build L4(草案)

L4 正在制定中,预计增加的要求包括:

  • 双人审核——每次构建需要两个人批准
  • 可复现构建——给定同样的源码和构建环境,总是产生相同的二进制
  • 构建平台本身的完整性证明——构建平台的配置也经过了签名
等级来源证明签名方式构建隔离审计实际难度
L0
L1有(声明式)平台密钥不一定
L2有(链路式)平台签名隔离的
L3有(已验证)Keyless / HSM隔离的有审计
L4可复现多方签名隔离的完整审计极高

大多数成熟项目的目标是 L2 到 L3。L1 是快速启用的基线;L3 需要 Sigstore 或类似基础设施,适合关键基础设施的软件。L4 目前仅适用于安全关键级系统(如内核引导链)。

第三块:依赖治理——Lockfile、来源验证和完整性

依赖治理不是"扫描完 CVE 就完事了"。它包括三个层次:

层次活动工具/方法
1. 锁住版本确保每次安装依赖得到同样的版本lockfile(package-lock.json、requirements.txt 锁定)
2. 验证来源确认包确实是主人发布的,不是中间人篡改的包签名验证(npm audit signatures、PyPI 信任设置)
3. 验证完整性确认包内容没有被修改哈希校验、slsa-verifier

Lockfile(锁定文件) 是一个最基本但最常被忽略的实践。没有 lockfile,你开发时的依赖版本和生产环境跑的版本可能不同。

json
// package-lock.json(自动生成的依赖版本锁定)
{
  "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=="
    }
  }
}

验证包签名: npm 从 2022 年开始支持签名验证。你可以在安装时要求每个包都有维护者的 GPG 签名:

bash
# 要求 npm 验证包的签名
npm config set sign-git-tag true
npm audit signatures

# 查看包是否经过签名
npm view express dist.tarball
npm view express dist.signatures

包注册表级别也有验证机制。GitHub 的 npm 注册表要求新发布的包使用 OIDC 证明发布者身份,PyPI 从 2023 年开始支持可信发布者(Trusted Publishers),避免使用 API token。

第四块:Sigstore 和 Cosign——无密钥签名

回到最基础的问题:你要确认一个容器镜像是你团队构建的、没有被篡改过的。传统做法是用 GPG 或 PGP 加密签名,但这要求你管理密钥,包括分发公钥、密钥轮换、密钥泄露后吊销。

Sigstore 改变了这个流程。它的核心设计是"Keyless Signing"——不需要你管理加密密钥。

Sigstore 的组成:

组件角色说明
Cosign签名工具签名和验证容器镜像、二进制文件、blob
Fulcio证书颁发机构根据 OIDC token 签发短期证书
Rekor透明日志记录所有签名的公开账本,提供审计追踪

Sigstore 的签名流程:

开发者触发 CI 构建

    ├── CI 平台提供 OIDC token(证明"我是 GitHub Actions,仓库是 fortress/app")

    ├── Fulcio 接收 OIDC token,签发短期(通常 10 分钟)签名证书

    ├── Cosign 用这个证书对容器镜像签名

    ├── 签名 + 证书写入 Rekor 透明日志(公开、不可篡改)

    └── 签名完成,镜像可以部署

验证流程:

部署时

    ├── cosign verify 从镜像获取签名
    ├── 检查 Rekor 日志中是否有对应的签名记录
    ├── 验证证书是否来自 Fulcio(EKU 检查、SAN 检查)
    └── 如果一切通过 → 镜像可信,可以部署

实际操作:

bash
# 在 CI 中用 Keyless 模式签名镜像
cosign sign --yes \
  registry.fortress.local/app:1.3.0

# 在部署时验证
cosign verify --yes \
  registry.fortress.local/app:1.3.0

# 也可以验证特定的身份(不只是"有没有签名")
cosign verify --certificate-identity-regexp ".*github\.com/fortress/.*" \
  --certificate-oidc-issuer-regexp ".*token\.actions\.githubusercontent\.com" \
  registry.fortress.local/app:1.3.0

如果你需要管理自己的密钥(例如在没有 OIDC 的平台——Jenkins、自托管 CI),Cosign 也支持传统密钥:

bash
# 生成密钥对
cosign generate-key-pair

# 用私钥签名
cosign sign --key cosign.key registry.fortress.local/app:1.3.0

# 用公钥验证
cosign verify --key cosign.pub registry.fortress.local/app:1.3.0

验证轮次证明(Verification Provenance): 部署不仅仅是"签名验证通过"就够了。你还需要验证签名是在什么条件下生成的——谁签名的、什么时间、什么身份。Sigstore 把所有这些记录在 Rekor 中:

bash
# 从 Rekor 获取签名的审计记录
rekor-cli search --artifact digest:sha256:a1b2c3d4e5f6...

# 返回一个 UUID,然后获取完整记录
rekor-cli get --uuid 123abc456def

# 返回的结构包含:签名时间、签名者身份、证书内容、透明日志索引

第五块:构建平台完整性

说完了组件签名,最后看构建平台本身的完整性。

如果你的 CI 平台的 runner 被攻击了,上面所有签名和 SBOM 都变成了"恶意的签名和恶意的 SBOM"。所以,构建平台本身必须是可信的。

完整性的三个要素:

1. 构建溯源(Provenance)
   → SLSA 来源证明记录了"谁在什么时候从什么代码构建了什么"

2. 构建脚本审计
   → CI 配置文件(.github/workflows/*.yml 或 .gitlab-ci.yml)必须版本控制
   → 任何修改 CI 配置的操作都要经过代码审查

3. 隔离的构建环境
   → 每次构建使用瞬时的、隔离的环境(ephemeral runner)
   → 构建环境不会被前一次构建污染
   → 构建环境不包含持久化凭据(临时 token 通过 OIDC 获取)

检查你的 CI 是否满足基本安全要求:

yaml
# 安全的 CI 配置——最小权限、隔离环境、版本控制
name: Build and Sign

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    # 使用 ephemeral runner(每次构建都是临时环境)
    runs-on: ubuntu-latest

    # 最小权限——只给必要的权限
    permissions:
      contents: read
      id-token: write  # 仅用于签名
      attestations: write

    steps:
      - uses: actions/checkout@v4

      # 构建步骤直接使用预先定义的 action,没有自定义脚本来源
      - 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 }}

相比之下,不安全配置的常见特征:

yaml
# 不安全——命令注入风险
- name: Run user-provided script
  run: |
    # PR 中的 contributor 可以修改这个脚本
    # 如果脚本来源于 issue comment 或 PR body,这是命令注入
    eval "${{ github.event.pull_request.body }}"

# 不安全——runner 暴露敏感信息
- name: Debug
  run: |
    env  # 可能暴露 secrets
    cat /etc/passwd  # 可能的容器逃逸信息泄露

常见陷阱

  • 只生成 SBOM,不消耗 SBOM。 很多团队在 CI 中自动生成 SBOM 文件,然后把它存储在构建产物中,再也不看。SBOM 的价值在使用——在 CVE 发布时快速定位受影响的组件。定期对 SBOM 做漏洞关联分析(VEX,Vulnerability Exploitability eXchange)。
  • 认为 SLSA L3 是所有项目的目标。 SLSA 安全等级和安全强度不是线性对应的。对内部工具,L1 就够了;对直接面向外部用户的 API 服务,L2-L3 合适;L4 目前仅适用于安全关键系统。从 L1 逐步提升,每升一级都对应成本增加。
  • 签名了,但没有验证。 容器签名只有当你部署时验证它才有意义。如果 CI 中签了名,但部署流水线中没有验证步骤,签名只是一串无用的元数据。
  • 只检查直接依赖,忽略传递依赖。 你的 pom.xml 或 package.json 中引入了 20 个依赖,但 npm install 后实际安装了 200 个包。攻击者经常把恶意代码藏在深层的传递依赖中(依赖混淆、typosquatting)。
  • 把锁文件当成"不需要更新"。 Lockfile 锁住版本,但它不是免更新的。定期(至少每月一次)运行 npm auditmvn versions:display-dependency-updates 来检查依赖是否有可用的安全修复版本。

通关挑战

  • 热身:用 Syft 扫描一个本地 Docker 镜像,生成 CycloneDX 格式的 SBOM。然后用手工找到其中一个依赖,查询它是否有已知 CVE(用 NVD API)。
  • 挑战:配置一个完整的 SLSA L2 构建流水线。使用 GitHub Actions 构建一个 Java 或 Node.js 项目,生成 SLSA 来源证明,使用 Cosign 签名构建产物。
  • 观察:部署验证——从 Docker Hub 拉取一个经过签名的公开镜像(如 cgr.dev/chainguard/nginx),用 Cosign 验证它的签名。然后对比一个没有签名的镜像,观察验证结果的区别。
  • 排障:你的 CI 流水线在签名步骤报错:error: no matching credentials。你使用的是 Keyless 模式,但 CI 平台(Jenkins)没有 OIDC 支持。如何解决?(提示:使用 Cosign 的传统密钥模式,或迁移到支持 OIDC 的 CI 平台)

旅人笔记

  • SBOM 是软件成分清单,包含所有组件名称、版本、来源和哈希值,在漏洞应急中快速定位受影响系统
  • SLSA 四个等级(L0-L4)定义了软件构建过程的安全级别,从"可溯源"到"抗篡改"
  • 依赖治理需要 lockfile(锁定版本)+ 来源验证(签名)+ 完整性验证(哈希)
  • Sigstore/Cosign 的 Keyless 签名解决了传统签名方案的密钥管理痛点,通过 OIDC 和透明日志实现无需管理密钥的签名
  • 构建平台本身的完整性比组件签名更底层——如果构建平台被攻破,所有签名和 SBOM 都失去意义
  • 从 SLSA L1 开始逐步提升,不是所有项目都需要 L3/L4

下一站预告

软件供应链安全的知识到这里就结束了。你从密码学出发,经过了 PKI、Web 安全、系统安全、网络防御、SDL 流程、隐私保护、DevSecOps 再到供应链安全——你已经建立了从代码编写到部署运行的完整防御知识体系。下一卷,我们将进入语言的世界,去看看不同语言的灵魂。

Built with VitePress | Software Systems Atlas