元数据卡
- 前置知识:第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):
{
"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 的方式因语言而异:
# 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.jsonSyft 是一款出色的容器镜像 SBOM 生成工具:
# 安装 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-layersSBOM 不是为了生成而生成,它的核心价值是在漏洞出现时能快速回答一个问题:"这个 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),说明"这个东西是怎么构建出来的"。
来源证明至少包含:
{
"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 来源证明:
# .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/*.jarL1 的核心价值:如果有人告诉你有问题的二进制来源不明,你可以追溯到构建记录。即使构建服务器被攻击,至少你知道问题出在哪。
SLSA Build L2——受控构建
L2 在 L1 的基础上增加了两项要求:
- 来源证明必须由构建平台签名(用平台的身份,不是用户手动提供的密钥)
- 构建环境必须隔离——每次构建使用干净的、预定义的环境
这意味着你的 CI 构建必须和开发者本机构建的结果可区分——如果你能在你笔记本上"复现"CI 构建出的二进制并表示 SHA256 一致,CI 构建的身份验证才有意义。
# 隔离构建:使用临时、干净的运行环境
jobs:
build:
runs-on: ubuntu-latest # 每次都从干净的镜像启动
container:
image: maven:3.9-eclipse-temurin-21 # 固定版本的构建环境,不是 latest
steps:
- uses: actions/checkout@v4
- run: mvn packageCI 平台(GitHub Actions、GitLab CI)本身就提供了隔离构建环境。L2 要求你确保构建是隔离的、可重复的,并且来源证明是由 CI 平台的身份签名的。
SLSA Build L3——可信构建
L3 在 L2 基础上还要求:
- 构建过程必须抗篡改——来源证明由硬件辅助密钥签名(如 Sigstore 的密钥less 签名)
- 构建环境必须有完整审计日志
- 构建脚本本身必须来自版本控制
L3 的核心区别在于:构建平台不再只是"宣称"构建过程是安全的,而是有第三方签名的证明可以验证。实现 L3 通常需要 Sigstore 或类似服务:
# 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,你开发时的依赖版本和生产环境跑的版本可能不同。
// 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 签名:
# 要求 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 检查)
└── 如果一切通过 → 镜像可信,可以部署实际操作:
# 在 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 也支持传统密钥:
# 生成密钥对
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 中:
# 从 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 是否满足基本安全要求:
# 安全的 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 }}相比之下,不安全配置的常见特征:
# 不安全——命令注入风险
- 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 audit或mvn 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 再到供应链安全——你已经建立了从代码编写到部署运行的完整防御知识体系。下一卷,我们将进入语言的世界,去看看不同语言的灵魂。