GitHub Actions 安全实践:从 tj-actions 事件学到的 3 个关键防护
2025年3月,一条 GitHub Advisory 刷新了我的认知边界。tj-actions/changed-files——这个我用了三年的 Action,被标记为 CVE-2025-30066。23,000 多个仓库,一夜之间全部暴露在 Secrets 泄露风险之下。
更让我后背发凉的是攻击手法。攻击者先窃取某个项目的 PAT,然后篡改 tj-actions 的版本标签,把原本安全的代码指向恶意脚本。那些 Secrets——AWS 密钥、数据库密码、API Token——就这么悄无声息地流向了公开日志。
说实话,我当时挺崩溃的。自己配置的 CI/CD 流程,居然成了攻击者的跳板。赶紧翻遍所有仓库,检查 Action 版本引用、GITHUB_TOKEN 权限、审计日志设置。折腾了一整天,才发现原来有那么多细节早就被我忽略了。
这篇文章,就是那次”惊魂时刻”后整理出来的防护清单。我们聊聊供应链攻击的套路、Secrets 管理的正确姿势、GITHUB_TOKEN 权限控制的细节,以及 GitHub 2026 安全路线图里那些值得提前准备的新特性。
重点是:这些都是你现在就能配置的东西,不用等什么新功能上线。
从 tj-actions 事件看 CI/CD 供应链攻击
攻击是怎么发生的
先说 tj-actions/changed-files 这个 Action。它在 GitHub 上很火——用于检测哪些文件在 PR 中发生了变化,很多 CI/CD 流程都会用到。我的几个项目也依赖它做增量部署判断。
攻击链条大概是这样的:
攻击者先从 reviewdog/action-setup 项目窃取了一个 PAT(个人访问令牌)。这个 PAT 有仓库写入权限。拿到令牌后,攻击者用它篡改了 tj-actions 仓库的版本标签——把原本指向安全代码的 v45 标签,悄悄指向了植入恶意逻辑的新提交。
那些还在用 uses: tj-actions/changed-files@v45 的项目,毫不知情地拉取了恶意代码。恶意脚本做的事很简单:把 CI/CD 环境里的 Secrets 全部打印到日志里。日志是公开的,Secrets 就这么泄露了。
据 GitHub Advisory 报告,超过 23,000 个仓库受到影响。Coinbase 的安全团队后来披露,这次攻击波及了 70,000 多个客户数据。
我踩过的坑:标签引用 vs SHA 固定
检查自己的仓库时,我发现很多地方都在用标签引用:
# 错误示范:使用可变标签
- name: Check changed files
uses: tj-actions/changed-files@v45
标签是活的。仓库维护者(或者拿到写入权限的攻击者)可以随时把标签指向新的提交。你以为引用的是 v45,实际运行的可能是被篡改的恶意代码。
正确的做法是用完整 SHA 固定:
# 正确做法:使用完整 SHA
- name: Check changed files
uses: tj-actions/changed-files@6cbf527e7a7b6d61c4e7f25e5ce5f7b7c8f3c72a
SHA 是死的。只要你不主动更新引用,运行的就是那一段代码,永远不会变。
我当时一边改一边骂自己:这明明是基础安全常识,怎么就一直没注意?
第三方 Action 的信任成本
tj-actions 事件还暴露了另一个问题:我们对第三方 Action 的信任太廉价了。
随便一个 Action,stars 数多一点、用的人多一些,就敢直接塞进生产环境的 CI/CD 流程。但谁知道维护者的安全意识怎么样?谁知道他们的 PAT 会不会被窃取?
GitHub 2026 安全路线图里提到了一个方案:工作流级依赖锁定。类似 package-lock.json,把所有 Action 的 SHA 固定在一个锁定文件里。这个功能还没上线,但我们可以现在就手动做——把每个 Action 引用改成 SHA,定期审计。
另一个建议是减少第三方 Action 的数量。能用官方 Action 解决的,就别用第三方。比如文件检测,其实可以用 GitHub 官方的 actions/checkout 配合 shell 脚本实现,没必要依赖 tj-actions。
Secrets 管理的 3 个层级与进阶实践
GitHub Secrets 的三级存储
GitHub 内置的 Secrets 分三个层级:组织级、仓库级、环境级。
组织级 Secrets 可以跨多个仓库共享,适合存放 AWS 密钥、云服务 Token 这些通用凭证。仓库级 Secrets 只在当前仓库可用,适合项目专用的数据库密码。环境级 Secrets 更精细,可以配合环境保护规则——比如要求 PR 审批通过后才能访问生产环境的 Secrets。
存储方面,GitHub 用 libsodium sealed box 加密。Secrets 写入后就没法再读出来,只能在工作流运行时通过 secrets 上下文访问:
steps:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
一个常见的错误是把 Secrets 直接插值到 shell 命令里:
# 危险:Secrets 可能被打印到日志
- name: Configure AWS
run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}
如果 Secrets 值里有特殊字符,或者命令执行失败,日志可能会把 Secrets 暴露出来。正确做法是用环境变量传递:
# 安全:通过环境变量传递
- name: Configure AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
run: aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"
动态屏蔽:::add-mask::
有些敏感数据不是预先存好的 Secrets,而是运行时生成的。比如某个脚本输出的临时 Token。这时候可以用 ::add-mask:: 动态屏蔽:
- name: Generate temporary token
run: |
token=$(generate-token.sh)
echo "::add-mask::$token"
echo "TOKEN=$token" >> $GITHUB_ENV
屏蔽后的值在日志里会显示为 ***。但要注意:屏蔽必须发生在值被打印之前。如果先打印了,再屏蔽,日志里还是会暴露。
进阶方案:HashiCorp Vault OIDC 集成
如果你的项目对安全要求比较高,用 GitHub 内置 Secrets 可能不够。长期凭证存在 GitHub,万一仓库被攻破, Secrets 就全完了。
更好的方案是用 HashiCorp Vault,配合 OIDC(OpenID Connect)实现无凭证访问。原理是让 GitHub Actions 向 Vault 证明”我是谁”,Vault 验证后颁发短期 Token。这样 GitHub 上就不存任何长期凭证了。
配置步骤大概是这样的:
第一步:在 Vault 配置 OIDC 角色
Vault 需要信任 GitHub 的 OIDC 提供者。配置一个角色,指定哪些仓库可以获取什么 Secrets:
resource "vault_jwt_auth_backend_role" "github_actions" {
backend = "jwt"
role_name = "github-actions-role"
bound_audiences = ["https://github.com/your-org"]
user_claim = "repository"
role_type = "jwt"
token_policies = ["ci-policy"]
token_ttl = "1h"
}
第二步:在 GitHub Actions 使用 vault-action
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Import Secrets from Vault
uses: hashicorp/vault-action@v2.4.0
id: vault
with:
url: https://vault.example.com:8200
role: github-actions-role
method: jwt
secrets: |
secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
- name: Deploy with AWS credentials
run: |
echo "Accessing AWS with Vault-provided credentials"
aws s3 ls
这个流程里,Vault-action 会自动用 GitHub 提供的 OIDC Token 向 Vault 认证。Vault 验证后返回 Secrets,注入到环境变量里。1 小时后 Token 自动失效,下次运行重新获取。
Azure Key Vault 也支持类似的 OIDC 集成,配置方式差不多,用 azure/login Action 配合 Azure Key Vault Secrets。
GITHUB_TOKEN 权限控制实战
什么是 GITHUB_TOKEN
每个 GitHub Actions 工作流运行时,都会自动获得一个 GITHUB_TOKEN。这是一个临时的 OAuth Token,用于操作当前仓库——比如创建 Release、推送代码、评论 PR。
问题在于:GITHUB_TOKEN 默认权限太大了。
在旧版本的 GitHub Actions 里,GITHUB_TOKEN 读写权限几乎全开。工作流可以随意修改仓库内容、创建分支、推送代码。如果一个工作流被攻击者利用(比如通过 PR 触发的恶意脚本),GITHUB_TOKEN 就成了攻击者的武器。
permissions 键的完整配置
GitHub 在 2021 年 4 月引入了 permissions 键,让你可以精确控制 GITHUB_TOKEN 的权限范围。
基本语法是这样的:
permissions:
actions: read|write|none # 管理 Actions
contents: read|write|none # 仓库内容
issues: read|write|none # Issue 操作
packages: read|write|none # GitHub Packages
pull-requests: read|write|none # PR 操作
security-events: read|write|none # 安全事件上报
deployments: read|write|none # 部署状态
statuses: read|write|none # Commit 状态
一个关键机制:一旦你在工作流里写了 permissions 键,所有未指定的权限自动变成 none。这就是”最小权限”的安全边界。
工作流级 vs 作业级权限
permissions 可以写在两个层级:工作流级(全局)和作业级(局部)。
工作流级权限对所有作业生效:
name: CI Pipeline
permissions:
contents: read # 所有作业默认只读仓库内容
issues: write # 所有作业可以写 Issue
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Lint with read-only access"
test:
runs-on: ubuntu-latest
steps:
- run: echo "Test with read-only access"
作业级权限可以覆盖工作流设置:
name: Release Pipeline
permissions:
contents: read # 默认只读
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read # 继承只读
steps:
- run: echo "Build needs read only"
release:
runs-on: ubuntu-latest
permissions:
contents: write # 覆盖为写入,用于创建 Release
packages: write # 发布到 Packages
steps:
- name: Create Release
uses: actions/create-release@v1
一个实际案例:我有个项目的 Release 工作流,build 作业只需要读取代码,release 作业需要创建 Release 并推送 Docker 镜像。通过作业级权限隔离,build 作业就算被攻击,也没法写入仓库内容。
仓库级权限模式
除了工作流配置,GitHub 仓库设置里也有两种权限模式:
- Permissive(宽松):GITHUB_TOKEN 默认读写所有权限。工作流不指定
permissions时生效。 - Restricted(受限):GITHUB_TOKEN 默认只读 contents 和 packages。工作流需要额外声明才能获得写入权限。
建议把所有仓库都设为 Restricted 模式。在仓库 Settings > Actions > General 里,找到 “Workflow permissions”,选择 “Read repository contents and packages permissions”。
这样一来,就算工作流忘记写 permissions,也不会有过度权限。安全边界在仓库层面就已经锁住了。
审计日志与合规检查
工作流运行事件纳入审计日志
从 2021 年 2 月开始,GitHub 把 Actions 工作流运行事件纳入了组织审计日志。这意味着你可以追踪谁触发了什么工作流、用了什么权限、访问了什么 Secrets。
审计日志入口:组织 Settings > Security > Audit log。
关键字段包括:
action:事件类型,比如workflow_run.create、workflow_run.completeactor:触发者,可能是用户名、App 或github-actions[bot]repo:仓库路径token_scopes:使用的 Token 权限范围request_id:请求追踪 ID,用于排查日志关联
一个实际用途:发现异常工作流运行时,用审计日志溯源。比如某个 Secrets 被异常访问,查 workflow_run 事件找到触发者。
企业审计 API
如果你的组织用的是 GitHub Enterprise,可以用 Audit Log API 查询所有操作:
curl -H "Authorization: Bearer YOUR_TOKEN" \
"https://api.github.com/enterprises/YOUR_ENTERPRISE/audit-log?phrase=workflow_run"
返回 JSON 里包含详细的事件记录。可以导出到 SIEM 系统(比如 Splunk、Datadog)做持续监控。
合规要求简述
如果你的项目需要满足 SOC 2 或 ISO 27001 合规,CI/CD 安全是必须项。审计日志是最直接的合规证据——证明你有能力追踪 CI/CD 活动、发现异常、响应事件。
一个建议配置:把审计日志导出到外部系统,设置关键事件的告警规则。比如:
- 工作流运行失败率突增
- Secrets 异常访问(短时间内大量读取)
- 陌生 IP 触发工作流
GitHub 2026 安全路线图前瞻
GitHub 在 2026 年 3 月发布了 Actions 安全路线图,规划了 6 个重大新特性。有些已经上线,有些还在开发。
工作流级依赖锁定
这是针对 tj-actions 类型攻击的官方解决方案。类似 package-lock.json,把所有 Action 引用锁定到 SHA。工作流里还是写 uses: tj-actions/changed-files@v45,但锁定文件记录了对应的 SHA。维护者更新 Action 时,锁定文件不会自动变化——需要你主动审核并更新。
这个功能还没上线,但我们可以现在就手动做 SHA 固定。
Layer 7 原生出站防火墙
原生支持控制 CI/CD 流程的外部网络访问。比如限制工作流只能访问你的 AWS API,不能访问任意外网。
目前要实现这个,需要自托管 Runner + 自定义网络策略。2026 上线后,GitHub 云 Runner 也能配置出站防火墙了。
Scoped Secrets
更精细的 Secrets 作用域控制。比如某个 Secrets 只能被特定分支或特定作业访问。目前 Secrets 的作用域是仓库级或环境级,粒度不够细。
策略驱动执行控制
定义信任边界、审批和证明门控。比如要求所有来自 fork 的 PR,必须经过人工审批才能触发工作流。或者要求工作流必须通过安全扫描才能运行。
这和环境保护规则类似,但粒度更细、逻辑更复杂。
Actions Data Stream
CI/CD 活动的实时可见性。类似审计日志,但实时推送,不是事后查询。可以接入 SIEM 系统做实时监控。
OIDC 自定义属性声明
增强云提供商身份验证。OIDC Token 里可以携带更多自定义属性,比如仓库标签、分支信息。Vault 或 AWS 可以基于这些属性做更精细的授权判断。
结论
从 tj-actions 事件到现在,我学到的核心教训是三点:
第一,SHA 固定。 别用标签引用第三方 Action,用完整 SHA。这是 23,000 个仓库用教训换来的经验。
第二,最小权限。 GITHUB_TOKEN 默认权限太大,用 permissions 键限制,把仓库设为 Restricted 模式。
第三,审计日志。 Actions 事件已经纳入审计,定期检查异常运行,导出到监控系统。
这三点,你现在就能配置。不用等 GitHub 2026 新特性上线,不用换什么新工具。打开你的仓库,检查 Action 引用、权限设置、审计日志。半小时就能完成基础防护。
说到底,CI/CD 安全不是什么高深技术,而是细节习惯。每多一个 SHA 固定、每少一个过度权限,风险就小一点。tj-actions 事件告诉我们:攻击者不需要多高明的手段,只需要我们疏忽一个小细节。
GitHub Actions 安全防护配置流程
从 SHA 固定到权限控制,完成 CI/CD 安全加固的三个核心步骤。
⏱️ 预计耗时: 30 分钟
- 1
步骤1: SHA 固定 Action 引用
检查所有工作流文件,将 `uses: action@tag` 改为 `uses: action@full-sha`,避免标签被篡改。使用 `pinact` 等工具批量转换现有引用。 - 2
步骤2: 配置 permissions 键
在工作流或作业级别添加 permissions 键,仅授予必需权限。例如:`permissions: contents: read` 用于只读操作,`permissions: contents: write` 用于创建 Release。将仓库设置为 Restricted 模式作为兜底。 - 3
步骤3: 启用审计日志监控
在组织 Settings > Security > Audit log 中查看 Actions 工作流运行事件。配置关键事件告警:工作流运行失败率突增、Secrets 异常访问、陌生 IP 触发工作流。将日志导出到 SIEM 系统做持续监控。
常见问题
为什么要用 SHA 固定而不是标签引用?
GITHUB_TOKEN 的权限怎么控制?
如何检测工作流是否被攻击?
OIDC + Vault 相比内置 Secrets 有什么优势?
第三方 Action 能不能用?
11 分钟阅读 · 发布于: 2026年5月16日 · 修改于: 2026年5月17日
相关文章
GitHub Actions Matrix 矩阵构建:多版本并行测试实战
GitHub Actions Matrix 矩阵构建:多版本并行测试实战
GitHub Actions 入门:YAML 工作流基础与触发器配置
GitHub Actions 入门:YAML 工作流基础与触发器配置
GitHub Actions 入门:YAML 工作流基础与触发器配置
评论
使用 GitHub 账号登录后即可评论