软件供应链与 SBOM

本文讲软件供应链安全与 SBOM 的工程化落地,回答如何生成与消费 SBOM、SLSA 等级怎么对照、制品如何签名验证、依赖混淆与投毒怎么防。覆盖 SPDX 与 CycloneDX 的字段差异、purl 与 VEX 的作用、Sigstore 无密钥签名、构建溯源与策略门禁,并给出可执行的分阶段落地路线。

引言

供应链安全要回答的问题和十年前已经完全不同。过去我们担心的是「依赖里有没有已知漏洞」,现在担心的是「我构建出来的那个二进制,到底是不是我以为的那份源码编出来的」。SolarWinds 事件里攻击者没有改任何上游仓库,而是污染了构建系统,让被正常签名的官方更新包带上后门;xz-utils 事件里攻击者用两年时间经营贡献者身份,最后试图在构建脚本里植入后门;event-stream、colors.js 这类 npm 事故则说明,一个下游数量以百万计的包,发布权限可能只握在一个人手里。

工程上的难点有三层。第一层是可见性:一个容器镜像里到底有哪些组件、哪个版本、从哪来、由谁构建,绝大多数团队答不上来,因为依赖是传递引入的,基础镜像、vendored 代码、构建期工具都可能带着组件进来。第二层是完整性:即使你能列出组件,也无法证明「这份制品确实由这份源码、在这条流水线上、由可信的人构建」——这正是 SLSA 与签名要解决的问题。第三层是可消费性:SBOM 生成容易,但生成一份几百兆、没人看的 JSON 毫无价值,必须让机器能拿它做漏洞匹配、许可证判定与策略门禁。

还有一条容易被忽略:供应链风险的成本曲线是非线性的。早期几个依赖靠人工核对可行,一旦依赖数破千、基础镜像每月更新,人工核对必然崩掉。此时再补自动化,历史制品的清单早已丢失,无法回溯「三个月前发布的那个版本里到底有没有被投毒的包」。所以供应链治理要尽早做成流水线的固定环节。

本文按「威胁模型 → SBOM 格式 → 生成与消费 → SLSA 溯源 → 签名验证 → 投毒防范 → 与漏洞响应衔接 → 私有仓库 → 落地路线」展开。需要划清一条边界:SBOM 用于许可证合规的那一面(义务清单、NOTICE、审计存证)另有专文,见开源许可证合规实战 ,本文重心在安全与完整性;漏洞的协调披露与 CVE 流程见开源漏洞响应与 CVE 流程 ,本文只讲「怎么把扫描结果接进流水线」。读者对象是负责 CI/CD 与制品安全的工程师。

目录

  1. 供应链威胁模型
  2. SBOM 格式:SPDX 与 CycloneDX 的安全视角
  3. SBOM 的生成与消费
  4. SLSA 等级与构建溯源
  5. 制品签名与 Sigstore 验证
  6. 依赖混淆与投毒防范
  7. 与漏洞响应的衔接:VEX 与告警收敛
  8. 私有仓库、镜像代理与 vendor 目录
  9. 分阶段落地路线与成熟度模型

1. 供应链威胁模型

先定义攻击面,否则防御会变成「把能买的工具都装上」。供应链攻击按「在链条的哪一环动手」分成四类,每类的检测手段不同。

四类供应链攻击:
  1. 源码投毒   直接往上游仓库/分支塞恶意代码(含社工骗取提交权限)
  2. 构建投毒   污染构建系统或构建脚本,源码干净但产物带后门(SolarWinds)
  3. 分发投毒   劫持发布账号、抢注同名包、篡改镜像仓库中的制品
  4. 依赖混淆   利用内网包名在公共仓库注册同名包,诱导构建拉取外部包

四类里构建投毒最难防,因为它的产物和正常产物在源码层面完全一致,只能靠「构建过程可验证」来识别——这也是 SLSA 存在的理由。分发投毒最容易被忽视,因为大多数团队信任镜像仓库的 tag,而 tag 是可以被覆盖的(latest 指向变了没人知道)。依赖混淆在引入私有包管理器的团队里高发,往往一次配置疏漏就中招。

攻击类型典型手法主要防线检测难度
源码投毒社工贡献者、抢注 abandoned 包双人评审、CODEOWNERS、分支保护中
构建投毒篡改构建脚本、注入 CI 变量SLSA 溯源、隔离构建、签名高
分发投毒劫持发布凭证、覆盖 tag不可变 tag、签名验证、摘要锁定中
依赖混淆公共仓库注册内网包名scope 前缀、registry 优先级、代理白名单低

威胁模型的另一个维度是爆炸半径:一个只有 200 行代码、却拖进 40 个传递依赖的包,供应链暴露面反而比一个大而全的框架更大。评估时要看传递依赖深度,而不只看直接依赖。相关方法(criticality score、下游广度)在开源项目健康度度量里有系统讨论,这里只把它当作选型输入。

从真实事件里提炼的三条教训

复盘近十年的公开供应链事件,能提炼出三条反复出现的教训,它们直接决定了防御投入的优先级。

教训一:攻击者偏好「链条最薄的一环」
  被攻击的往往不是最流行的包,而是它依赖的、只有一两个维护者的小包
  → 防御要覆盖传递依赖,而不是只盯直接依赖

教训二:源码干净不等于产物干净
  SolarWinds 的源码仓库没有任何异常,后门是在构建阶段注入的
  → 只做源码扫描(SAST、code review)无法覆盖这类攻击

教训三:签名普及度不足时,「官方渠道」本身就是攻击面
  官网下载页、CDN、镜像仓库的 tag 都可能被替换
  → 必须做「内容摘要 + 签名 + 身份」三重绑定

这三条对应本文的三条主线:SBOM 解决可见性(覆盖传递依赖),SLSA 溯源解决构建完整性,Sigstore 签名解决分发完整性。理解了这个映射,后面的工具选型就不再是「装哪个」,而是「补哪一层」。

一个常见的认知误区

很多人把「SCA 工具报出零高危」当作供应链安全的达标线,这是错的。SCA 只回答「已知漏洞」,对「未知后门」「构建投毒」「账号接管」完全无感。真正的达标线应当是三条同时成立:能列出全部组件(可见性)、能验证制品来源(完整性)、能在上游被污染后快速定位受影响版本(可追溯性)。把这三条写进团队的安全目标,比订阅更多漏洞库更有效。

2. SBOM 格式:SPDX 与 CycloneDX 的安全视角

SBOM 是供应链治理的单一事实源。格式上主流是 SPDX 与 CycloneDX 两种,许可证场景更偏 SPDX,安全场景更偏 CycloneDX,但两者都能承载安全消费所需的核心字段。选型时真正要盯的不是「哪个更流行」,而是四个字段是否完整。

安全消费必需的四个字段:
  purl        包唯一标识,形如 pkg:npm/lodash@4.17.21,漏洞库靠它匹配
  依赖关系    dependsOn / dependencies,决定传递依赖能否展开
  构建信息    supplier、author、tools、timestamp,用于溯源
  哈希        SHA-256/SHA-512,用于制品与组件的一致性校验

purl(Package URL)是最容易被忽略却最关键的一个。没有 purl 的 SBOM 只能靠包名做模糊匹配,遇到 lodash 与 lodash-es 这类同源不同包、或者私有 registry 与公共 registry 同名的情况,漏洞匹配会大面积误报或漏报。生成 SBOM 后第一件事就是抽查 purl 覆盖率,低于 95% 就要调工具。

字段SPDX 表达CycloneDX 表达安全消费影响
唯一标识externalRefs 中的 purlcomponents[].purl决定漏洞匹配准确率
依赖图relationships 数组dependencies 数组决定能否展开传递依赖
许可证licenseConcluded/Declaredlicenses[]合规侧主档
组件哈希PackageChecksumhashes[]制品一致性校验
漏洞信息不承载vulnerabilities[](1.4+)VEX 与告警收敛

CycloneDX 1.4 起原生支持 vulnerabilities 与 VEX(Vulnerability Exploitability eXchange),这让它能同时充当「清单」和「这个漏洞对我是否可利用」的判定载体。SPDX 3.0 也在补这块,但生态工具跟进较慢。实践上建议:合规以 SPDX 为主档,安全以 CycloneDX 为主档,两者从同一份扫描结果转换生成,成本只有一条命令,没必要二选一。

还要理解 SBOM 的粒度问题。一份「文件级 SBOM」会列出每个文件的哈希,体积巨大但能精确到 vendored 代码;一份「包级 SBOM」只列包管理器认识的组件,体积小但漏掉复制进来的第三方代码。安全场景推荐至少做一次文件级扫描补盲区,日常 CI 用包级扫描保持速度。

3. SBOM 的生成与消费

生成侧的工具矩阵已经很成熟,选型的关键是「从哪生成」。三种来源的可靠性递增:

# 1) 从源码目录生成(最快,但看不到基础镜像里的组件)
syft dir:. -o cyclonedx-json=sbom.src.json --source-name app --source-version "$GIT_SHA"

# 2) 从容器镜像生成(推荐,含基础镜像全部层)
syft registry.example.com/app:1.2.3 -o cyclonedx-json=sbom.img.json \
  --scope all-layers --source-name app --source-version 1.2.3

# 3) 从构建产物/文件系统生成(覆盖 vendored 与生成代码)
syft /path/to/rootfs -o cyclonedx-json=sbom.fs.json

--scope all-layers 不能省:默认只扫镜像最上层,会把基础镜像里打进底层的组件漏掉,而基础镜像恰恰是「带了漏洞的旧库」高发区。多语言项目可以用 cdxgen 统一入口:

cdxgen -t js -t java -t go -o sbom.cdx.json .   # 多类型聚合
npm sbom --sbom-format cyclonedx > sbom.cdx.json # Node 内置(npm 10+)

生成只是第一步,消费才是价值所在。消费有三种典型场景:

# 场景 A:漏洞匹配(把 SBOM 喂给 SCA 或漏洞库)
grype sbom:sbom.img.json -o json > vulns.json
# 场景 B:与基线 diff,只对新增/变更组件做策略评估
cyclonedx-diff baseline.cdx.json sbom.cdx.json -o diff.json
# 场景 C:策略门禁(非零退出即阻断)
conftest test --policy policy/ sbom.cdx.json

diff 是最被低估的能力。对一个有 3000 个组件的制品,全量评估每次都跑一遍既慢又吵;而两次构建之间通常只有十几个组件变化,只对增量做策略判断,既快又能精确定位「是哪个 PR 引入了这个高风险组件」。这也是把门禁接进 GitHub Actions 工作流时的正确姿势:基线冻结在主干,PR 只比对增量。

消费侧还要建立覆盖度自检:SBOM 里的组件数应与包管理器锁文件(package-lock.json / go.sum / 解析后的 pom.xml)的条目数在同一量级。差一个数量级说明扫描方式有漏——这条自检比任何工具报告都可靠,且不依赖任何外部服务。

把 SBOM 当成数据来用

一次性生成、人工打开看一眼的 SBOM 没有价值。要让 SBOM 产生持续收益,必须把它当作可查询的数据存起来。Dependency-Track 是这类平台的代表:接收 SBOM 上传后,持续与漏洞库比对,新增漏洞自动告警,并支持「按项目、按版本、按组件」三种检索维度。

# 上传 SBOM 到 Dependency-Track,之后由平台负责持续监控
curl -X POST "$DT/api/v1/bom" \
  -H "X-Api-Key: $DT_KEY" -H "Content-Type: multipart/form-data" \
  -F "project=$PROJECT_UUID" -F "bom=@sbom.cdx.json"

平台化的价值在于把「扫描」从流水线里解耦出来:流水线只负责产出 SBOM 并上传,漏洞匹配、告警收敛、影响面分析都在平台上持续进行。这样当某个 CVE 在半年后被披露时,平台能立刻告诉你「哪些项目的哪些版本受影响」,而不必重新构建历史版本。自建平台要维护数据库与漏洞同步,成本不低;如果团队规模小,用现成的 SaaS 或直接依赖 CI 定时任务也够用。

另一个常见做法是把 SBOM 作为制品的一部分随镜像一起分发,让下游用户能拿到清单做自己的合规判断。这在大企业采购里越来越常见:客户会要求提供 SBOM 才允许入库。分发时要注意 SBOM 里可能包含内部仓库地址、构建路径等信息,需要做一次脱敏。

4. SLSA 等级与构建溯源

SLSA(Supply-chain Levels for Software Artifacts)是回答「这份制品能不能被信任」的框架,当前稳定版本是 v1.0,把要求拆成「构建级别」和「溯源级别」两条轨道。核心思路是用可验证的构建溯源替代对构建者的信任。

SLSA Build 级别(v1.0):
  L0  无要求
  L1  构建过程有溯源(provenance),但是否可信不保证
  L2  托管构建平台 + 签名的溯源,构建隔离
  L3  加固的构建平台:构建间隔离、凭证不可窃取、溯源不可伪造

SLSA Source 级别(v1.0 引入):L1~L4,关注源码与版本控制的可追溯性

L1 到 L2 的跨越是「有没有用托管构建 + 溯源是否签名」;L2 到 L3 的跨越是「构建平台本身是否加固到攻击者即使拿到 CI 凭证也无法伪造溯源」。绝大多数团队的目标应当是关键制品达到 L3,其余 L2,而不是全量追求 L3——L3 要求自建或使用加固的构建服务,成本不低。

溯源(provenance)是一份符合 in-toto 规范的 JSON,描述「谁、用什么、从哪份源码、执行了什么命令、产出了什么」。GitHub Actions 的官方 action 可以产出 SLSA L3 溯源:

  # .github/workflows/release.yml 片段
- uses: actions/attest-build-provenance@v1
  with:
    subject-path: dist/app_1.2.3_linux_amd64.tar.gz

验证侧用 slsa-verifier 或 gh attestation verify:

slsa-verifier verify-artifact dist/app_1.2.3_linux_amd64.tar.gz \
  --provenance-path provenance.intoto.jsonl \
  --source-uri github.com/org/app --source-tag v1.2.3

验证命令的关键是把期望值写死:--source-uri 与 --source-tag 必须精确匹配,否则攻击者可以用同一构建平台产出一份「溯源合法但源码不是你的」的制品。策略上应当把「允许的 source-uri 列表」固化进准入控制器(如 Kubernetes 的 sigstore policy-controller),让部署时自动拒绝未验证的镜像。

SLSA 与 SBOM 是互补的:SBOM 回答「制品里有什么」,溯源回答「制品是怎么来的」。两者都要有,才能形成完整证据链。只签 SBOM 不签制品,攻击者可以替换制品而保留 SBOM;只签制品不签 SBOM,你无法知道被签的那份制品里有什么。

验证溯源时要盯住哪些字段

溯源文件不是「有就行」,验证时必须逐项核对关键字段,否则一份来自攻击者构建平台的合法溯源同样能骗过校验。

溯源验证的关键字段:
  subject[].name        制品名与哈希,必须与待验证制品完全一致
  subject[].digest      内容摘要,哈希不符直接拒绝
  buildType             构建类型,应匹配你期望的构建方式
  builder.id            构建者身份,如 github.com/actions/runner
  invocation.parameters 构建参数,包含源码仓库与 commit/tag
  metadata.buildStartedOn / finishedOn  构建时间窗口

其中最容易被忽略的是 invocation.parameters:它记录了「这份制品由哪个仓库的哪个 commit 构建」。如果验证时只校验 builder.id(确认来自 GitHub Actions),攻击者只要能用任意一个 GitHub Actions 工作流构建,就能产出「构建者合法但源码不是你的」的制品。因此策略上要把允许的源码仓库与 ref 写成白名单,逐项比对。

校验项只校验它后果
有溯源即可L1 心态无法区分可信与不可信构建
builder.id确认构建平台攻击者用同平台的其他仓库伪造
source-uri + ref钉死源码来源需要维护白名单,是正确做法
全部字段 + 摘要最强配置成本高,关键制品值得

5. 制品签名与 Sigstore 验证

传统签名方案要求维护一套 GPG 密钥、发布公钥、处理密钥轮换与吊销,社区里真正做好的项目很少。Sigstore 用**无密钥签名(keyless)**绕开了这个问题:签名者用 OIDC 身份(如 GitHub Actions 的 workload identity)向 Fulcio 申请一张短时证书,私钥只在内存中存活几分钟,签名记录写入透明的 Rekor 日志,验证时只需校验「签名时身份是谁、是否在证书有效期内、记录是否在透明日志里」。

# 无密钥签名一个制品(在 CI 中运行,自动使用 OIDC 身份)
cosign sign-blob --yes dist/app.tar.gz --output-signature app.tar.gz.sig \
  --output-certificate app.tar.gz.pem

# 验证:绑定到具体身份与颁发者
cosign verify-blob --signature app.tar.gz.sig --certificate app.tar.gz.pem \
  --certificate-identity-regexp 'https://github.com/org/app/.github/workflows/release.yml@refs/tags/.*' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  dist/app.tar.gz

验证命令里 --certificate-identity-regexp 与 --certificate-oidc-issuer 都不能省。只做「签名有效」的校验等于没校验:任何人都能签一个制品,签名本身有效。必须把身份钉死到「来自我这条 release 工作流、来自 GitHub 的 OIDC 颁发者」,才能防住冒名。

容器镜像的签名与验证同理:

cosign sign --yes registry.example.com/app@sha256:<digest>
cosign verify --certificate-identity-regexp '...' --certificate-oidc-issuer '...' \
  registry.example.com/app@sha256:<digest>

注意签名对象应当是镜像摘要(digest)而不是 tag。tag 可以被覆盖,签名一个 tag 意味着签名可以被指向另一份内容;摘要不可变,签名才真正绑定到内容。同理,Kubernetes 部署清单里应当用 digest 引用镜像,imagePullPolicy: Always 配合 tag 也无法防止 tag 被改。

签名体系的最后一环是准入。签名存在但没人校验,等于没有。三条落地路径:CI 里校验(防止发布流程被注入)、镜像仓库 webhook 校验(拒绝未签名的 push)、集群准入控制器校验(拒绝未签名的部署)。三条都做最好,至少要做第三条,因为那才是真正拦住「带毒镜像上线」的地方。

6. 依赖混淆与投毒防范

依赖混淆(dependency confusion)是最容易防、也最容易漏的一类。原理很直接:如果包管理器在解析 @mycorp/utils 时既查私有 registry 又查公共 registry,攻击者在公共 registry 注册同名包并给一个更高的版本号,构建就会拉到攻击者的包。

防范措施(按有效性排序):
  1. scope 前缀:内部包统一加组织 scope(@mycorp/),公共 registry 无法注册同名 scope
  2. registry 优先级:配置 .npmrc / settings.xml 明确「先查私有源,命中即停」
  3. 代理与白名单:所有依赖经私有代理(Nexus/Artifactory)拉取,公共源仅作上游镜像
  4. 锁文件 + 完整性校验:提交 lockfile,启用 npm 的 integrity 与 go 的 GONOSUMCHECK
  5. 命名空间声明:在公共 registry 主动注册内部包名(防御性占位,成本低)

Python 生态的同类问题是 --extra-index-url 的隐式行为:pip 会同时查询所有 index 并选版本最高的,等于给了公共源插队的机会。正确做法是用 --index-url 指向私有源,或改用 uv/poetry 的显式源配置。Go 的 GOPRIVATE / GONOSUMDB 则用于避免私有模块被送到公共 checksum 数据库。

第二类投毒是抢注(typosquatting):reqeusts、lodahs 这类拼写相近的包名,或利用 Unicode 同形字符。防范上,CI 里可以加一条依赖名与「已批准清单」比对的门禁,任何新增依赖名都要人工确认;对高频拼错的包名维护一份黑名单。

# 列出本次构建新增的依赖,人工确认
comm -13 <(sort baseline.deps) <(sort current.deps)

第三类是上游账号接管:维护者账号被钓鱼或 CI 凭证泄漏,攻击者直接发布恶意版本。这类无法从包名层面防,只能靠「新版本发布后不立即自动合并」的策略——对关键依赖设置冷却期(如 72 小时),配合社区告警,能在恶意版本被广泛传播前拦下一部分。npm 的 provenance 标记(发布时绑定 CI 身份)可以作为筛选信号:优先选择带 provenance 的版本。

投毒类型检测信号缓解措施
依赖混淆包来源 registry 与预期不符scope 前缀 + 私有代理
抢注依赖名不在批准清单名称门禁 + 黑名单
账号接管新版本无 provenance、维护者变更冷却期 + 人工确认
构建脚本注入postinstall 脚本、构建期网络访问禁用 postinstall、构建网络隔离

postinstall 脚本是 npm 生态的长期痛点,绝大多数包并不需要它。在 CI 中用 npm ci --ignore-scripts 全局禁用,再对确实需要的包单独放行,能挡掉相当一部分「安装即执行」的投毒。

CI 自身也是供应链的一环

流水线里引用的第三方 action、构建插件、基础镜像同样是供应链。GitHub Actions 的 uses: some/action@v3 是浮动引用,上游把 v3 指向恶意 commit 时你不会收到任何通知。正确做法是把 action 钉到 commit SHA:

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1

同时限制 GITHUB_TOKEN 权限(permissions: contents: read 起步),并优先使用 GitHub 官方或已启用 provenance 的 action。CI 凭证泄漏是构建投毒最常见的入口,把 token 权限收窄到「这次任务真正需要的最小集合」,能把单点失陷的影响面压到最低。

7. 与漏洞响应的衔接:VEX 与告警收敛

SBOM 生成的直接收益是漏洞匹配,但纯匹配会产生大量噪声:一个基础镜像里的 OpenSSL 被报出 20 个 CVE,其中大部分「存在但不可达」(函数没被调用、模块没被编译进来)。不收敛噪声,团队很快就会对告警脱敏,门禁也会被 continue-on-error 绕过。

VEX 是收敛的手段:对每个被报出的漏洞声明状态——not_affected(不可利用,附理由)、affected(受影响)、fixed(已修复)、under_investigation(调查中)。CycloneDX 1.4+ 原生承载 VEX,可以与 SBOM 合并成一份文件:

{
  "vulnerabilities": [{
    "id": "CVE-2026-12345",
    "affects": [{"ref": "pkg:maven/org.example/lib@2.4.1"}],
    "analysis": {
      "state": "not_affected",
      "justification": "code_not_reachable",
      "detail": "vulnerable function is only reachable from test scope"
    }
  }]
}

justification 必须用规范枚举值(code_not_present / code_not_reachable / requires_configuration / requires_dependency / requires_environment / protected_by_compiler / protected_at_runtime / protected_at_perimeter),自由文本只能放 detail。用规范值的好处是下游工具能自动处理,审计时也有统一口径。

告警收敛的策略是分级 + 豁免 + 时限:affected 且组件在运行时路径上 → 立即阻断;affected 但仅在构建期 → 限时整改;not_affected 且理由充分 → 自动放行并归档 VEX。豁免同样要带过期时间,避免「一次判定永久放行」。漏洞从发现到修复的完整流程(分级、协调披露、补丁回溯)见开源漏洞响应与 CVE 流程,本文只负责把「扫出来的东西」变成「可执行的门禁信号」。

还有一条实践建议:把 SBOM 与 VEX 一起归档到不可变存储,每个制品一份。这样当某个 CVE 在半年后被披露时,可以直接查「受影响版本的 SBOM 里有没有这个组件」,而不必重新构建历史版本——历史构建环境通常早已销毁。

8. 私有仓库、镜像代理与 vendor 目录

供应链的物理入口是「包从哪里来」。三种模式的安全性递增:

三种依赖获取模式:
  A. 直连公共源    最快,但受制于公共源的可用性与安全性,且无法审计
  B. 代理/镜像     所有拉取经私有代理,可缓存、可扫描、可封禁,推荐
  C. vendor 入库   依赖源码提交进仓库,完全离线可构建,成本最高

模式 B 是多数团队的甜点:Nexus / Artifactory 作为代理,上游仍是公共源,但所有下载经过代理,可以做「封禁特定版本」「只允许已扫描过的包」「保留副本以防上游删包」。上游删包是真实风险:npm 允许维护者 unpublish,一个被广泛依赖的包被删除后,直连模式的构建会直接失败,而代理模式因为有副本不受影响。

  # Nexus 代理仓库的封禁与隔离策略(示意)
  proxy:
    remote: https://registry.npmjs.org
    negativeCache: true
    blockedVersions:
      - "lodash@4.17.20"      # 已知问题版本
    quarantineHours: 24       # 新版本先隔离观察

模式 C(vendor)适合对构建可复现性要求极高的场景(如安全产品、嵌入式),代价是升级依赖变得笨重、仓库体积膨胀。一个折中是 vendored + 校验:依赖源码入库,同时记录每个组件的哈希与来源 URL,CI 里校验哈希一致。Go 的 vendor/ 目录配合 go mod verify、Rust 的 cargo vendor 都是这个思路。

容器侧还有一个独立话题:基础镜像的选择。优先用官方或厂商维护的最小镜像(如 distroless、alpine),减少自带组件数就等于减少漏洞面;同时按 digest 锁定基础镜像,避免 FROM node:20 在某天悄悄变成另一个内容。相关实践在 Docker 与容器基础 中有更完整的讨论。

9. 分阶段落地路线与成熟度模型

供应链治理不适合一次性铺开,按成熟度分四阶段推进,每阶段都有可交付的成果。

阶段一 可见性(1~2 个月)
  - 所有制品在构建时产出 SBOM(CycloneDX + SPDX 双格式)
  - SBOM 与制品哈希绑定,归档到不可变存储
  - 抽查 purl 覆盖率 ≥ 95%

阶段二 门禁(2~3 个月)
  - SBOM 与基线 diff,只对增量做策略评估
  - 依赖名批准清单 + registry 白名单
  - 关键依赖的漏洞告警接入工单系统

阶段三 完整性(3~6 个月)
  - 制品签名(cosign keyless)+ 构建溯源(SLSA L2/L3)
  - 发布流程与镜像仓库 webhook 校验签名
  - VEX 收敛告警,豁免带过期时间

阶段四 准入(持续)
  - 集群准入控制器拒绝未签名/未验证镜像
  - 依赖健康度纳入选型流程
  - 定期演练「某个上游被投毒,多久能定位并阻断」
阶段核心能力关键交付物典型投入
一看得见全制品 SBOM + 归档1 人月
二管得住增量策略门禁 + 白名单1~2 人月
三验得了来源签名 + 溯源 + 验证2~3 人月
四拦得住上线准入控制 + 演练持续

衡量是否做到位的标准很朴素:被问「三个月前发布的 1.2.0 版本里,有没有那个被投毒的包」时,能不能在半小时内从归档里调出 SBOM 给出答案;被问「线上跑的这个镜像是不是我们构建的那份」时,能不能用一条 cosign verify 给出证据。能,就说明这套体系真的在运转。

权衡取舍

取舍点偏 A偏 B建议
SBOM 粒度文件级,精确但体积大包级,轻量但漏 vendored日常包级 + 定期文件级补盲
生成来源源码目录,快但看不到基础镜像镜像,全但慢以镜像为准,源码作补充
签名方式GPG 长密钥,无需外部依赖Sigstore 无密钥,依赖透明日志新项目用 Sigstore,老项目渐进
门禁强度全量阻断,安全但吵只告警,不阻断增量阻断 + 存量清债
告警处理全量上报,覆盖全但噪声大只报运行时可达,静默但可能漏VEX 分级 + 豁免限期
依赖获取直连公共源,简单私有代理,可控但多一层代理为主,关键场景 vendor
SLSA 目标全量 L3,最强但成本高全量 L1,便宜但价值有限关键制品 L3,其余 L2

核心取舍是成本与可验证性的平衡:签名和溯源每加一层都增加构建复杂度与失败点,但少了任何一层,「制品可信」就只是口头承诺。工程上最优解通常是「SBOM 全覆盖、签名只覆盖发布制品、溯源只覆盖关键路径」,而不是所有制品一视同仁。

常见坑清单

  • SBOM 里没有 purl:漏洞匹配只能靠包名模糊比对,误报漏报同时暴增——生成后必须抽查 purl 覆盖率。
  • 只扫镜像最上层:基础镜像里的旧组件全部漏掉,而它们才是漏洞高发区——--scope all-layers 不能省。
  • 签名 tag 而不是 digest:tag 可被覆盖,签名随之指向另一份内容——一律签 digest。
  • 只验「签名有效」不验身份:任何人都能签一份有效签名——必须钉死 --certificate-identity 与 --certificate-oidc-issuer。
  • 签名了但没人校验:签名形同虚设——至少在集群准入层做一次强制校验。
  • 用 latest 或浮动 tag 引用基础镜像:内容会悄悄变化且不可追溯——按 digest 锁定。
  • npm 未禁用 postinstall:安装即执行,是投毒的主要入口——CI 用 --ignore-scripts。
  • pip 用 --extra-index-url:公共源可插队抢版本,等于敞开依赖混淆——改用单一私有 index。
  • SBOM 不带版本标识:一堆同名 sbom.json 事后无法对账——命名里写入 product/version/commit。
  • 全量阻断存量依赖:团队立刻加 continue-on-error 绕过——冻结基线只卡增量。
  • 漏洞告警不做收敛:噪声导致团队脱敏,真问题被淹没——用 VEX 分级并给豁免设过期。
  • 归档放 CI 制品里:CI 制品有保留期,审计时补不回来——写入不可变存储。

小结

软件供应链安全的工程化本质是三件事:看得见(每个制品都有带 purl 的 SBOM 并归档)、验得了(制品签名 + 构建溯源,验证时钉死身份)、拦得住(增量门禁 + 准入控制 + 告警收敛)。SBOM 负责第一件,SLSA 与 Sigstore 负责第二件,策略与准入负责第三件,缺一不可。把这三件事做成流水线的固定环节之后,供应链治理就不再是出事后的应急排查,而是每次构建都在自动执行的日常。

落地顺序建议从 SBOM 开始:先让每个制品都有带版本标识、带 purl 的 SBOM,再接增量门禁,最后补签名与溯源。跳过第一步直接上签名,通常以「签了一份自己都看不懂的制品」告终;跳过第二步直接上准入,则容易因为误报太多被运维关掉。

如果想继续深入,建议沿两条线读:一条是合规侧,从 开源许可证合规实战 理解同一份 SBOM 如何同时服务于许可证义务与审计存证;另一条是响应侧,从 开源漏洞响应与 CVE 流程 理解漏洞从被发现到补丁回溯的完整链路,把「防注入」与「出事后怎么办」两条判断链接起来。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「开源生态」更多文章

  1. 开源商标与品牌治理
  2. 开源度量与分析
  3. 企业参与开源与 OSPO