切换语言
切换主题

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/[email protected]
        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.createworkflow_run.complete
  • actor:触发者,可能是用户名、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

    步骤1: SHA 固定 Action 引用

    检查所有工作流文件,将 `uses: action@tag` 改为 `uses: action@full-sha`,避免标签被篡改。使用 `pinact` 等工具批量转换现有引用。
  2. 2

    步骤2: 配置 permissions 键

    在工作流或作业级别添加 permissions 键,仅授予必需权限。例如:`permissions: contents: read` 用于只读操作,`permissions: contents: write` 用于创建 Release。将仓库设置为 Restricted 模式作为兜底。
  3. 3

    步骤3: 启用审计日志监控

    在组织 Settings > Security > Audit log 中查看 Actions 工作流运行事件。配置关键事件告警:工作流运行失败率突增、Secrets 异常访问、陌生 IP 触发工作流。将日志导出到 SIEM 系统做持续监控。

常见问题

为什么要用 SHA 固定而不是标签引用?
标签是可变的,仓库维护者或攻击者可以随时将标签指向新提交。SHA 是固定的,只要你不主动更新引用,运行的就是那段代码。tj-actions 事件证明了标签篡改的风险。
GITHUB_TOKEN 的权限怎么控制?
使用 `permissions` 键精确声明所需权限。一旦写了 permissions,未指定的权限自动变为 none。建议在仓库 Settings > Actions > General 中将权限模式设为 Restricted。
如何检测工作流是否被攻击?
查看组织审计日志中的 workflow_run 事件,关注异常访问、陌生 IP、高频失败等迹象。将日志导出到 SIEM 系统配置自动告警。
OIDC + Vault 相比内置 Secrets 有什么优势?
GitHub 内置 Secrets 是长期凭证,一旦仓库被攻破就全泄露。OIDC + Vault 实现无凭证访问,GitHub 向 Vault 证明身份后获取短期 Token,1 小时后自动失效,攻击窗口大大缩小。
第三方 Action 能不能用?
可以用,但要谨慎。优先选择官方 Action,使用 SHA 固定引用,定期审计依赖。GitHub 2026 路线图会推出工作流级依赖锁定功能,届时可以更安全地管理第三方 Action。

11 分钟阅读 · 发布于: 2026年5月16日 · 修改于: 2026年5月17日

当前属于系列阅读 第 9 / 9 篇

GitHub Actions 完全指南

如果你是从搜索进入这篇文章,建议顺手补上上一篇或继续下一篇,这样更容易把同一主题读完整。

查看系列总览

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

使用 GitHub 账号登录后即可评论