一、供应链威胁模型
1.1 四类攻击面
1) 依赖投毒
抢注相似包名、劫持已废弃的包、在合法包的新版本中植入后门。
影响:构建产物中混入恶意代码,且看起来一切正常。
2) 构建环境劫持
CI runner 上执行了被污染的脚本,或第三方 action 被改写。
典型:pull_request_target 下 checkout 了 fork 的代码,
导致 fork 中的脚本在持有 secret 的环境中执行。
3) 制品篡改
镜像推送到 registry 之后被替换,或 tag 被重新指向另一个镜像。
4) 凭证泄露
workflow 日志、artifact、缓存中意外带出 token。
1.2 信任链的断裂点
源码 ──► 依赖安装 ──► 构建 ──► 制品 ──► 分发 ──► 部署
▲ ▲ ▲ ▲ ▲ ▲
分支保护 锁文件 runner 签名 registry 准入控制
CODEOWNERS lockfile 隔离 证明 不可变 admission
每一段都需要证据。SLSA 与构建证明要解决的核心问题就是:如何让下游在不信任上游的前提下,验证制品确实由某段源码、在某次构建中、用某个构建配置产出。
1.3 一个真实的攻击链
步骤 1 攻击者给某开源项目提了一个 PR,修改测试脚本
步骤 2 维护者用了 pull_request_target 触发 CI 并 checkout 了 PR 代码
步骤 3 测试脚本读取环境变量,把 GITHUB_TOKEN 外发
步骤 4 攻击者用 token 推了一个带后门的新版本 tag
步骤 5 下游项目自动拉取该 tag,后门进入生产环境
对应防线
步骤 2 → 第四章:事件选择与权限最小化
步骤 3 → 第三章:GITHUB_TOKEN 权限与 OIDC
步骤 4 → 第二章:分支保护与 SLSA 等级要求
步骤 5 → 第七、八章:验证侧消费证明
二、SLSA 等级逐级解读
2.1 四个等级的要求
SLSA Build L1 构建过程有文档化的证明
要求:构建产出 provenance,记录源码、构建器、参数
防住:无。L1 只是「有记录」,provenance 可以伪造
SLSA Build L2 构建服务提供已签名的证明
要求:由托管构建服务生成并签名 provenance,防止构建后被篡改
防住:构建完成后的制品替换
SLSA Build L3 构建环境强隔离且防篡改
要求:构建在隔离环境中执行,构建平台自身难以被篡改
防住:构建过程中的环境劫持、跨构建污染
SLSA Build L4 双人审查与可复现构建
要求:所有构建步骤需要两人审查,构建过程完全可复现
防住:内部人员作恶
现状:业界实践极少,多数团队的目标是 L3
2.2 对照 GitHub Actions 的现状
能力 等级贡献
actions/attest-build-provenance L2(签名 provenance)
GitHub 托管 runner(非自托管) L3(隔离的托管构建环境)
environment 必需审批人 L4 的部分能力(双人审查)
reusable workflow + CODEOWNERS L4 的部分能力
可复现构建(固定时间戳、锁依赖版本) L4 的必要条件
用 GitHub 托管 runner 加 attest-build-provenance,基本可以声称达到 SLSA Build L3(视具体审计口径而定)。自托管 runner 会拉低等级,因为构建环境不由平台保证隔离。
2.3 常见误解
误解 1:用了 SLSA 工具链就等于达到某个等级
实际:等级是审计结论,工具只是提供证据
误解 2:SLSA 保证代码没有漏洞
实际:SLSA 只保证制品与源码的对应关系未被破坏
误解 3:L1 没意义
实际:L1 的 provenance 已经能回答「这个镜像对应哪个 commit」
误解 4:只要签名就够了
实际:签名只证明是谁签的,provenance 才证明是怎么来的
三、GitHub 原生构建证明
3.1 前置权限
permissions:
contents: read
id-token: write # 必需,用于 OIDC 换取签名身份
attestations: write # 必需,用于写入证明
packages: write # 推送镜像时需要
3.2 为镜像生成构建证明
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/my-org/app:${{ github.ref_name }}
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/my-org/app
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
subject-digest 必须是 sha256: 开头的摘要,不能用 tag。这是整个证明体系的基石:tag 可变,digest 不可变。
为二进制文件生成证明时改用 subject-path:
- name: Build and attest binary
run: |
go build -trimpath -ldflags="-s -w" -o dist/app ./cmd/app
sha256sum dist/app > dist/app.sha256
- uses: actions/attest-build-provenance@v2
with:
subject-path: dist/app
3.3 SBOM 证明
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ghcr.io/my-org/app@${{ steps.push.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cdx.json
artifact-name: sbom.cdx.json
- name: Attest SBOM
uses: actions/attest-sbom@v2
with:
subject-name: ghcr.io/my-org/app
subject-digest: ${{ steps.push.outputs.digest }}
sbom-path: sbom.cdx.json
push-to-registry: true
3.4 查看与验证
gh attestation verify oci://ghcr.io/my-org/app:sha-abc1234 --owner my-org
gh attestation verify oci://ghcr.io/my-org/app:sha-abc1234 --owner my-org --format json > attestation.json
gh attestation download oci://ghcr.io/my-org/app:sha-abc1234 --owner my-org
验证输出中的关键字段
predicateType https://slsa.dev/provenance/v1 或 sbom 类型
subject 制品名称与 sha256 摘要
buildDefinition 构建类型、外部参数、依赖的源码引用
runDetails 构建器 ID、构建时间、调用来源
四、事件选择与权限最小化
4.1 pull_request 与 pull_request_target
# 安全:默认的 pull_request 不向 fork 下发任何 secret
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
# 危险:pull_request_target 在基础仓库上下文中执行且持有 secret
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # 灾难:checkout 了 fork 代码
- run: npm ci && npm run build # 恶意脚本在此执行
正确用法(确实需要 pull_request_target 时)
1) 绝不 checkout PR 的 head
2) 只做打标签、评论、状态回写等不执行代码的动作
3) 必须显式声明 permissions,默认收紧到 contents: read
4) 若确需执行 PR 代码,用 workflow_run 两段式
4.2 GITHUB_TOKEN 最小权限
permissions:
contents: read # 顶层默认只读
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # 仅该 job 需要写
id-token: write
steps:
- uses: actions/checkout@v4
推荐做法
1) 组织级 Settings 把默认权限设为 read-only
2) 每个 workflow 顶层声明 permissions: contents: read
3) job 级按需提权,绝不写 permissions: write-all
4) 关闭 Allow GitHub Actions to create and approve pull requests
4.3 fork PR 的两段式隔离
# 第一段:在无 secret 的环境中构建
name: Build from PR
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: pr-build
path: dist/
# 第二段:workflow_run 触发,有 secret 但不执行 PR 的代码
name: Deploy after build
on:
workflow_run:
workflows: ["Build from PR"]
types: [completed]
jobs:
deploy:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: pr-build
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: ./scripts/deploy.sh # 只消费制品,不执行 PR 脚本
两段式的关键约束
1) workflow_run 触发的 job 拿不到 fork 的 GITHUB_TOKEN,
需要显式传 github-token 才能下载制品
2) 下载的制品必须做完整性校验(摘要比对)
3) 部署脚本必须来自基础仓库,不能来自制品
五、第三方 action 的固定策略
5.1 固定到 commit SHA
# 不安全:tag 可被移动,@master 可被随时改写
- uses: actions/checkout@v4
- uses: some-org/some-action@master
# 安全:固定到完整 commit SHA,tag 只作为注释
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
# 获取某个 tag 对应的 commit SHA
gh api repos/actions/checkout/git/ref/tags/v4.2.2 --jq '.object.sha'
# 若返回的是 tag 对象(type 为 tag),再解引用一次
gh api repos/actions/checkout/git/tags/<sha> --jq '.object.sha'
5.2 用 Dependabot 维护固定值
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
groups:
actions:
patterns: ["*"]
commit-message:
prefix: "chore(actions)"
没有这一步,固定 SHA 会迅速变成「永不升级」,长期停留在有漏洞的旧版本。
5.3 组织级白名单与自动检查
Settings → Actions → General → Actions permissions
● Allow select actions and reusable workflows
[x] Allow actions created by GitHub
[x] Allow Marketplace actions by verified creators
[x] Allow specified actions and reusable workflows
actions/*
docker/*
aws-actions/*
my-org/shared-workflows/.github/workflows/*@*
白名单粒度建议用 owner/* 而非 */*,把信任边界限定到具体的组织;对涉及发布、部署、密钥的高危 action 单独逐个固定到 SHA。
# 用脚本断言所有 uses 都带 40 位 SHA
set -euo pipefail
bad=$(grep -rEn '^\s*-?\s*uses:\s*[^#]+@(v[0-9]+|main|master)\s*$' .github/workflows/ || true)
if [ -n "$bad" ]; then
echo "::error::以下 action 未固定到 commit SHA"
echo "$bad"
exit 1
fi
六、SBOM 与依赖审查
6.1 生成与解析 SBOM
- name: Generate SBOM with Syft
uses: anchore/sbom-action@v0
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
artifact-name: sbom.spdx.json
upload-artifact: true
syft . -o cyclonedx-json > sbom.cdx.json
syft ghcr.io/my-org/app:sha-abc1234 -o spdx-json > image.spdx.json
jq -r '.components[] | "\(.name) \(.version)"' sbom.cdx.json | sort -u
SBOM 格式选择
SPDX Linux 基金会主导,许可证信息表达力强
CycloneDX OWASP 主导,漏洞与 VEX 支持好,工具链更活跃
两者都支持 JSON 与 XML,选一个统一即可,不必都生成
6.2 dependency-review-action
name: Dependency review
on:
pull_request:
permissions:
contents: read
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0
comment-summary-in-pr: on-failure
能做什么
1) 对比 PR 前后依赖清单,列出新增与升级
2) 阻断引入已知高危漏洞的版本
3) 按许可证白名单或黑名单阻断
4) 在 PR 中直接评论结论
不能做什么
1) 不检查已存在的历史依赖(只在 PR 增量上生效)
2) 不替代 SCA 工具对全量依赖的扫描
6.3 锁文件与禁用安装脚本
语言 锁文件 安装命令
Node package-lock.json npm ci
Python poetry.lock poetry install --no-root
Go go.sum go mod verify
Rust Cargo.lock cargo build --locked
Ruby Gemfile.lock bundle install --frozen
npm ci --ignore-scripts
npm 的 postinstall 脚本是供应链攻击的高频入口。对不信任的依赖,先禁用脚本安装,再按需显式执行必要的构建步骤。
七、cosign 签名与证明验证
7.1 用 cosign 做密钥无关签名
- uses: sigstore/cosign-installer@v3
with:
cosign-release: v2.4.1
- name: Sign image with keyless
run: |
cosign sign --yes \
ghcr.io/my-org/app@${{ steps.push.outputs.digest }}
keyless 签名的原理
1) cosign 通过 OIDC 向 Fulcio 申请一张短期证书
2) 证书把签名身份绑定到 workflow 的 sub 声明
3) 签名记录写入 Rekor 透明日志,公开可审计
4) 没有长期私钥需要保管,也就没有私钥泄露风险
7.2 验证证明
# 验证构建证明,限定签发者与来源仓库
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp '^https://github.com/my-org/my-repo/.github/workflows/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/my-org/app@sha256:9f2c1a...e3b
验证时必须限定的两项
--certificate-identity-regexp 谁签的(必须是自己的仓库与 workflow)
--certificate-oidc-issuer 由哪个 OIDC 提供者签发
漏掉这两项,任何人都能用自己的 GitHub 账号签一个包冒充,
因为 cosign 默认不校验身份来源。
7.3 把验证放进部署流水线
verify-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: sigstore/cosign-installer@v3
- name: Verify provenance before deploy
run: |
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "^https://github.com/${{ github.repository }}/" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/my-org/app@${{ inputs.digest }} > /dev/null
- name: Deploy only after verification
run: ./scripts/deploy.sh ghcr.io/my-org/app@${{ inputs.digest }}
八、验证侧消费证明
8.1 Kubernetes 准入控制
用 Sigstore Policy Controller 在准入阶段拒绝未签名的镜像:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-gha-provenance
spec:
images:
- glob: "ghcr.io/my-org/**"
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "^https://github.com/my-org/my-repo/.github/workflows/.*$"
ctlog:
url: https://rekor.sigstore.dev
attestations:
- name: require-provenance
predicateType: https://slsa.dev/provenance/v1
启用后的效果
1) 任何没有有效构建证明的镜像,Pod 创建直接被拒绝
2) 证明中的源码仓库与 workflow 必须匹配策略
3) 攻击者即便拿到 registry 写权限,也无法让未签名镜像上线
8.2 用 Kyverno 校验
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-provenance
spec:
validationFailureAction: Enforce
rules:
- name: check-provenance
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/my-org/*"
attestations:
- predicateType: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
subject: "https://github.com/my-org/my-repo/.github/workflows/release.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
准入控制落地建议
1) 先用 Audit 模式跑一周,观察会拦下哪些镜像
2) 确认业务镜像全部有证明后,再切到 Enforce
3) 给基础镜像配置例外规则
4) 把 policy 本身也纳入 GitOps 管理
8.3 消费证明的其他场景
1) 制品仓库网关
在 registry 前挂一层代理,拉取时校验证明,未签名直接 403
2) 部署流水线
部署前用 cosign verify-attestation 校验,失败则中止
3) 合规审计
定期导出全部证明与 SBOM,形成「这个版本由谁在何时构建」的证据链
4) 事故响应
用 provenance 中的源码引用反查 commit,快速定位线上镜像对应哪次提交
九、常见踩坑与最小清单
9.1 常见踩坑
1) id-token: write 忘记声明
报错:无法获取 OIDC token。attest 与 cosign keyless 都需要它。
2) attestations: write 缺失
attest 步骤 403,但错误信息不直观,先检查权限块。
3) 用 tag 而非 digest 作为 subject
attest-build-provenance 会直接报错,subject 必须是摘要。
4) cosign 验证未限定 identity
任何人都能签一个同名包通过验证,等于没验证。
5) 固定 SHA 之后不再升级
缺少 Dependabot 会导致长期停留在有漏洞的旧版本。
6) 自托管 runner 上做 attest
构建环境不由平台保证隔离,SLSA 等级会受影响。
7) SBOM 只生成不使用
生成后不归档、不比对、不做漏洞扫描,等于多花了几十秒。
8) workflow_run 两段式忘记校验制品
第一段的产物可被 PR 作者影响,第二段必须比对摘要。
9.2 最小可用清单
必做
[ ] GITHUB_TOKEN 默认只读,job 级提权
[ ] 第三方 action 固定到 commit SHA 并用 Dependabot 维护
[ ] 镜像用 digest 引用,不用可变 tag
[ ] attest-build-provenance 生成证明并推到 registry
[ ] 部署前 cosign verify-attestation 且限定 identity 与 issuer
[ ] 禁止 pull_request_target 与 fork 代码 checkout 的组合
建议
[ ] SBOM 归档并接入漏洞扫描
[ ] dependency-review-action 阻断高危新增依赖
[ ] 准入控制器校验证明,先 Audit 后 Enforce
[ ] 证明与 SBOM 定期导出留存,形成审计证据链
[ ] 组织级 Actions 白名单,禁止 Allow all
总结
供应链安全的核心不是「相信上游」,而是「让下游可以验证上游」。SLSA 用 L1 到 L4 把「有多少证据」分成了可度量的等级,GitHub 托管 runner 加上 actions/attest-build-provenance 已经能覆盖到 L3 的绝大部分要求。工程上要抓住三条主线:一是让制品不可变,镜像一律用 digest 引用,subject 绝不使用 tag;二是让来源可验证,id-token: write 换取签名身份,cosign verify-attestation 时严格限定 certificate-identity-regexp 与 OIDC issuer,否则验证形同虚设;三是让执行环境最小化,pull_request_target 与 fork 代码的组合是最高频的翻车点,GITHUB_TOKEN 默认只读、第三方 action 固定到 commit SHA 并用 Dependabot 维持更新。最后把证明真正消费起来,在部署流水线里校验、在 Kubernetes 准入阶段强制,供应链才从「有文档」变成「有防线」。
延伸阅读:
- GitHub Actions 安全加固 — 权限最小化与工作流注入防护
- GitHub Actions 依赖与安全更新 — Dependabot 与依赖治理
- GitHub Actions 密钥管理 — Secrets 与凭证使用规范
- GitHub Actions 与 Kubernetes GitOps 部署 — 部署链路与准入控制
- GitHub Actions 容器化 CI/CD — 镜像构建与 registry 推送
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。