← 返回首页

GitHub Actions 供应链踩坑实录:会动的 tag、被烧掉的版本号,与 5 个真实报错的修法

GitHub Actions供应链安全CI/CDDocker

我实际维护一个跑在 GitHub Pages 上的技术博客,部署链是一条再普通不过的 workflow:checkout、configure-pages、upload-pages-artifact、deploy-pages 四步,四个都来自官方 actions 组织。看起来干净得像教科书。

直到上周我把这个文件逐行读了一遍,才发现一件事:**这四个 action,四个都是用标签引用的**。actions/checkout@v4 里的 v4 不是"版本 4",而是一次**运行时的名称查询**——每次跑 CI,runner 都会去问一句"现在 v4 指向哪个 commit",然后拉那份代码来执行。

问题就出在这个"现在"上。标签是可变指针。拿到仓库写权限的人(或者拿到维护者账号的人)可以把它指向另一个 commit,而你的 workflow 文件一个字符都不会变,diff 一片干净,日志看起来一切正常。

先把利益关系说清楚:本文末尾有一个「深夜应急响应工位」小节,其中的 Amazon 链接是联盟链接;你通过这些链接下单,本站可能获得佣金。它和前面的技术结论完全无关,单独成节、方便你直接跳过。

⏳ 太长不看版(TL;DR)

🥇 **一句话结论**:uses: owner/action@v4 不是在锁定版本,而是每次运行都重新问一遍"v4 现在是谁"。真正的锁定只有一种写法——40 位完整 commit SHA。

🔧 今天就能做完的三件事:

1. 扫出所有未固定的引用:grep -rn "uses:" .github/workflows/ | grep -vE "@[0-9a-f]{40}"

2. 取出真实 SHA(以 checkout 为例):git ls-remote https://github.com/actions/checkout "refs/tags/v7.0.1*"

3. 在组织层强制:Settings → Actions → General → Policies 打开 SHA pinning 强制开关,未固定的 workflow 运行期直接失败。

💻 深夜应急响应工位(联盟链接,与本节的结论无关):

👉 https://www.amazon.com/dp/B09TRW57WB?tag=techpassive-20

👉 https://www.amazon.com/dp/B0DK59YKRS?tag=techpassive-20

攻击面到底是什么:你运行的是标签,不是字节

GitHub Actions 解析 uses: owner/repo@ref 的方式是:**在运行那一刻去仓库取这个 Git ref**,再执行它指向的代码。tag 属于 Git ref 里的"可变指针",分支更是。

这就意味着三件事同时成立:

官方文档在 "Security hardening for GitHub Actions" 里把话说得很直:把一个 action 钉到完整 commit SHA,是当前把它当成不可变发布来用的做法;短 SHA 不安全,任何时候都不应该拿来指定 action 的 Git 引用。

事件一:tj-actions/changed-files(CVE-2025-30066)

这是"会动的标签"头一次以大规模事故的形式进入大众视野。

这里有一条对排查非常关键的细节:**被泄露的 secrets 可能以双重 base64 编码的形式出现在日志里**。CISA 的通告里专门点了这一条。这也是我后面踩坑录里那条 base64: invalid input 的来源。

事件二:trivy-action(CVE-2026-33634)——规模更大,还留了两个二次坑

2026 年 3 月这一波,攻击手法完全一样,但破坏力和"善后难度"都上了一个台阶。

**为什么 v0.35.0 幸存?** 因为那个标签受 GitHub 不可变发布(Immutable releases)保护,改不动。同一个仓库里,有不可变保护的那个标签毫发无伤,其余 76 个全线失守——这一条对比,比任何理论都更能说明这个功能的实际价值。

它是怎么在同一个月里被打两次的? 有公开分析指出,2 月底的首次入侵在 3 月 1 日披露后,团队做了凭据轮换,但轮换不是原子的:并不是所有凭据被同时吊销。攻击者在轮换窗口里用仍然有效的旧 token 取走了新一批凭据,于是有了 3 月 19 日的第二波。这也解释了为什么"轮换"本身会被写成一条安全控制——顺序错了,等于没做。

我实际审计自己的仓库:4 个 action,4 个都是标签

我没有停在"知道这件事很危险",而是把那个 pages.yml 拿出来逐行过了一遍。结果不太好看,而且顺带暴露了第二个问题:**版本落后**。

action我当时写的引用当前最新稳定版建议固定的 SHA
actions/checkout`@v4`v7.0.1(2026-07-20)`3d3c42e5aac5ba805825da76410c181273ba90b1`
actions/configure-pages`@v4`v6.0.0(2026-03-25)`45bfe0192ca1faeb007ade9deae92b16b8254a0d`
actions/upload-pages-artifact`@v3`v5.0.0(2026-04-10)`fc324d3547104276b827a68afc52ff2a11cc49c9`
actions/deploy-pages`@v4`v5.0.1(2026-09-01)`368f82528645a54fb793d4d04e342629a3f51346`

表里的 SHA 是我用下面这条命令现取的,你也可以对任意 action 复现:

# 方式一:不带认证,直接取标签指向的 commit
git ls-remote https://github.com/actions/checkout "refs/tags/v7.0.1*"

# 输出里如果同时出现 refs/tags/v7.0.1 和 refs/tags/v7.0.1^{},
# 说明是附注标签(annotated tag),要取带 ^{} 的那一行才是 commit SHA。

# 方式二:用 gh CLI 直接问 commit
gh api repos/actions/checkout/commits/v7.0.1 --jq .sha

顺手说一个我在升级时踩到的点:actions/checkout 的 **v7.0.0** 起,为 pull_request_target 和 workflow_run 这两种事件**默认阻止检出 fork 的 PR**,v7.0.1 继续维护这一行为。这是安全方向的收紧,如果你有依赖"检出外部贡献者 PR 代码"的流程,升级 v4 到 v7 需要专门回归一遍。

踩坑录:5 个真实报错与修法

这一段是本文最有实操价值的部分。前两个报错出在"上游修漏洞"的善后阶段,第三个出在日志排查阶段,后两个出在我自己加固的时候。

报错一:`422 Validation Failed: tag_name was used by an immutable release and cannot be reused`

**场景**:trivy-action 事件善后时,官方把被污染的旧标签删掉,想让 0.34.0 这类名字重新指向正常 commit,结果被 GitHub 直接拒绝。

原因:攻击者的 force-push 让这些标签在 GitHub 侧被登记成了不可变发布。一旦某个标签名被不可变发布占用,这个名字就被永久烧掉:删 Git 标签不释放,删 release 也不释放(已发布的不可变 release 本身也删不掉)。

**修法**:没有修法,只有绕。官方最终的处置是**换名字**——以带 v 前缀的形式重新发布 v0.0.1 到 v0.34.2 指向原始的正常 commit,并在通告里明确写清楚:要引用 0.35.0 以前的版本,请写 aquasecurity/trivy-action@v0.34.0,而不是 @0.34.0。

教训:不可变发布是前向生效的——打开开关不会让已有 release 变不可变;但一旦发布了不可变 release,这个名字就收不回来。所以发布流程要先出 draft,确认无误再 publish。这一条对"我们自己发布 action 或镜像"同样适用。

报错二:`Unable to resolve action 'aquasecurity/trivy-action@0.34.0', unable to find a version`

**场景**:善后告一段落,很多还没改的老流水线沿用 @0.34.0,运行直接挂在这一步。

原因:旧标签在处置过程中被删除,且因为报错一的原因无法以原名重建。

**修法**:三条路,按推荐程度排——① 升级并固定到安全版本的完整 SHA;② 至少升到 v0.35.0;③ 如果业务确实锁在老版本,改用新命名的 @v0.34.0。

教训:上游为了修漏洞做的改名和删标签,对你就是一次破坏性变更。把引用绑在别人的可变标签上,等于把"什么时候 break"的开关也一起交了出去。固定到 SHA 之后,上游改名不影响你,升级变成一次你主动发起的 diff。

报错三:`base64: invalid input`

**场景**:排查阶段。CISA 通告里写明被泄露的 secrets 可能被**双重 base64 编码**。我头一次只解了一层,base64 -d 直接报 invalid input,一度以为日志被截断或者 payload 没落盘。

原因:编码了两层,解码次数不对。

修法:解两次,然后按安全事件流程处理:

# 假设 $PAYLOAD 是从日志里抠出来的那段可疑字符串
echo "$PAYLOAD" | base64 -d | base64 -d

教训:日志里的"乱码"不是没价值,只是还没解。排查供应链事件时,先把 payload 的编码层级摸清楚,再判断"是不是真的没偷到东西"。

报错四:`Not authorized to perform sts:AssumeRoleWithWebIdentity`

场景:把长期 cloud 凭据换成 OIDC 之后,部署 job 起不来。

**原因**:两种,都很常见——① job 没有 id-token: write 权限,拿不到 OIDC token;② 云侧 trust policy 里的 sub 条件和实际运行的工作流上下文对不上(分支名、环境名写歪了)。

**修法**:job 级补权限,并把 sub 收紧到具体分支:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write   # 需要 OIDC token 的 job 才加
"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
  }
}

教训:OIDC 的价值不是"换一种认证方式",而是把凭据绑定到具体构建上下文。长期 key 一旦泄露,在吊销前谁都能用;OIDC token 只有几分钟有效期,而且 trust policy 还能把它钉在某一分支上。顺带一条运维纪律:轮换要原子——新凭据就位、消费方全部切过去、旧凭据同一批吊销,不要拆成几天的"慢慢来"。

报错五:短 SHA 让 workflow 运行期直接失败

**场景**:我把 @v4 换成 SHA 时,随手填了 7 位短 SHA,觉得够用。

原因:官方文档专门警告过:短 SHA 不安全,任何时候都不该用来指定 action 引用。因为任何人都能 fork 仓库推一个与之碰撞的 commit,之后所有在该 SHA 上的 clone 都会因为"commit 不明确"而失败;而 workflows 引用了被缩短的 SHA,会立刻失败。

修法:只写 40 位完整 SHA,后面用注释保留可读版本号,方便下一个人看懂:


教训:安全控制要能被"读懂",否则下一个人升级时会把注释和 SHA 一起删掉。

今天就能做完的 6 步加固

1. **扫描**:grep -rn "uses:" .github/workflows/ | grep -vE "@[0-9a-f]{40}",把所有未固定的引用列出来。

2. 固定:逐条换成完整 SHA,注释保留版本号。

3. **组织级强制**:进入 Settings → Actions → General → Policies。2025-08-15 起,GitHub 的 allowed actions 策略已经支持两件事——用 ! 前缀**显式屏蔽**某个 action 或版本,以及**强制 SHA pinning**(未固定的 workflow 直接运行失败)。把开关打开,记忆的负担就从每个工程师的脑子搬到了一个设置项上。

4. **收权限**:workflow 顶层写 permissions: contents: read,只在需要 OIDC 的 job 上单独加 id-token: write。

5. 换凭据:能上 OIDC 的云访问全部改成 OIDC,把长期 key 清掉。

6. 加观测:在 CI 里加一步出站网络监控。Trivy 那波事件的 C2 是一次异常外连,只要有人盯着出站流量,就能在"日志被公开可读"之前发现问题。

一个必须点出来的局限:固定 SHA 能挡住标签投毒,但挡不住"你固定的那个 SHA 本身就是恶意的"。如果凭据是在你固定它之前就被盗的,SHA 是真的,代码是坏的。所以第 3 步的强制策略和第 6 步的观测不能省。

GitHub 在 2025 到 2026 年补了什么,以及别把方案押在 preview 上

按官方 Changelog 已经落地的两件事:

需要留意的是:不可变发布是**发布者侧**的设置,它保护的是"发布的资产和标签",并不会改变 uses: 的解析方式。作为消费者,你**不能假设对方开了这个开关**——这就是为什么消费侧的控制必须落在 SHA 上。

另外有一些仍在路线图或 preview 阶段的能力值得关注,但**不要拿来当方案**:workflow YAML 里的 dependencies: 依赖锁定段(思路对标 go.mod 加 go.sum,把直接与传递依赖都锁到 SHA 并做哈希校验)、托管 runner 的原生出站防火墙(在 runner VM 之外、L7 生效,所以即使攻击者在 runner 里拿到 root 也改不掉)、按分支/环境/workflow 作用域切分的 secrets。这些功能的公开时间与最终形态,我建议你**以 GitHub 官方 Changelog 为准**。

还有一条值得记下的教训:GitHub 曾规划一种 OCI 形态的 "Immutable Actions",让消费者继续写 uses: owner/action@v1.2.3 但底层不可变;据公开资料,该条目在 2025 年 12 月被从路线图中移除(标记为 not planned),方向改为不可变发布。我不建议把加固计划压在预览中的功能上——**能今天写进 YAML 的 40 位 SHA,比任何 roadmap 都可靠**。

深夜应急响应工位(联盟链接)

排查供应链事件最耗的不是脑力,是**同一时间看多份东西**:workflow 日志、git log -p 的 diff、上游的通告、你自己那份 SHA 清单。这对我这块工位的要求其实很朴素。

👉 https://www.amazon.com/dp/B09TRW57WB?tag=techpassive-20(约 $429–529,截至发稿,请以实际为准)

👉 https://www.amazon.com/dp/B0DK59YKRS?tag=techpassive-20(约 $179–199,截至发稿,请以实际为准)

以上为联盟链接。价格与库存随时变动,下单前请以 Amazon 页面显示为准;这两件装备与本文的技术结论没有关系。

常见问题

Q:我把 action 固定到 SHA,会不会错过安全更新?

会滞后,但可控。用 Dependabot 或 Renovate 的 GitHub Actions 管理器盯着,让升级变成一个个你主动 review 的 PR。另外,考虑给版本加一个"发布冷却期"(例如 7 天后再允许升级)——公开的安全分析普遍认为,大多数恶意版本会在发布后数小时到数天内被发现并撤下,把等待交给流程,比把判断交给当下的注意力可靠。

**Q:官方 actions 组织(actions/*)的标签也要固定吗?**

风险等级更低,但"更低"不等于零。Trivy 事件里失守的 aquasecurity/* 也是官方维护的知名组织。取值方式可以分级,写法上我建议统一固定到 SHA——规则统一了才有人真的执行。

Q:已经固定 SHA 了,还要做别的吗?

要。固定 SHA 只解决"标签被改写"。还需要:权限最小化、OIDC 替代长期凭据、出站网络观测,以及原子化的凭据轮换。

Q:怎么知道自己的流水线曾经跑过被污染的版本?

看时间窗,不看感觉。Trivy 事件的窗口是 2026-03-19 17:43 至 03-20 05:40(UTC,trivy-action),把这段时间跑过的 workflow 日志拉出来,搜可疑输出、搜 base64 串、查组织里有没有多出 tpcp-docs。落窗即视为泄露,凭据先轮换再慢慢查。

结语

标签是名字,SHA 是字节。这场事故的全部教训,最后能压缩成一句可执行的话:**把 @v4 换成 40 位 SHA,并在组织层把这件事变成不能违反的规则。**

如果你也在做运维侧的权限与流程治理,可以顺着看两篇同类的实战复盘:一篇是 WordPress 7.1 + WP-CLI 2.12 实战:600 篇文章内链与 SEO 批量重排,讲的是"批量审计再批量写回"这套思路;另一篇是 WordPress 7.1.2 多人协作内容运营实战:Notes 内联批注与权限审计,讲的是能力体系怎么盘。两篇的底层方法是一样的:**先把权限和引用关系看清楚,再谈效率。**

👉 立即参与 MiniMax Token Plan:AI 编程加速,企业用户专享优惠

👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务

👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速

📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测

🔗 精选推荐工具

使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):

☁️ DigitalOcean 云服务器 ⚡ Vultr 高性能 VPS ⭐ MiniMax Token 套餐 🤖 QoderWork 中国版(推荐有奖) ☁️ 阿里云爆款 AI 产品 📚 WordPress 实用书单 🔍 WordPress SEO 书单 🌐 虚拟主机书单 🐳 Docker 书单 🐧 Linux 书单 🐍 Python 书单 💰 联盟营销书单 💵 被动收入书单 🖥️ 服务器书单 ☁️ 云计算书单 🚀 DevOps 书单 🤖 小米 MiMo 开放平台
GitHub Actions, supply chain security
Langfuse, observability
Langfuse, 可观测性
← 返回首页