通过将所有GitHub Actions纳入单一内部镜像并固定到特定SHA提交来防御软件供应链攻击。
AI translation, not an official translation. Refer to the original for technical details.
Adapted from @skwp# 针对Shai-Hulud级攻击的供应链安全加固 供应链安全很难!将本指南接入你的智能体,抢先一步做好防护。 在弗兰克·赫伯特的《沙丘》中,Shai-Hulud——巨大的沙虫——是一种体量庞大、深植于沙漠之中的力量,等你感受到震动时,一切已经太晚。在25年末至26年初,我们看到了针对软件供应链的加速威胁,包括TeamPCP发起的Shai Hulud蠕虫攻击——它入侵开发者机器,然后利用被盗凭据向更多恶意包发布,进而入侵更多开发者机器。(https://www.wiz.io/blog/mini-shai-hulud-teampcp-hits-antv-supply-chain) 蠕虫爆发时,Swan已有所准备。我们的系统未受感染或影响。然而,我们借助其他受影响公司发布的经验教训,进一步强化了自身系统。本文介绍了我们在CI/CD流水线中构建的若干安全控制措施,以抵御此类攻击。尽管这并非对我们整体安全方案的全面审视,但我们认为这些是任何公司将供应链安全作为一等关切时应当采取的基础步骤。 ## 统一控制平面 一切的基础是一个用于GitHub Actions的内部仓库。我们不允许我们的仓库引入外部Action;每个关键仓库中的每一条CI/CD工作流都继承自这同一个地方。外部Action非常危险,因为那些公开仓库的性质使其容易遭受诸如Megaldon攻击以及TeamPCP缓存投毒攻击之类的威胁。(https://www.csoonline.com/article/4177124/github-actions-abused-by-megalodon-attack-to-slip-malicious-commits-into-5500-repos.html) (https://www.wired.com/story/teampcp-software-supply-chain-attack-spree-github/) 当我们引入一个GitHub Action时,我们会将其fork至内部镜像,对其进行审计,并将其固定到特定的提交SHA。每个工作流文件中的每一条 `uses:` 指令都指向我们自己的副本——永远不直接指向上游。我们不指向标签,而是指向特定的SHA——即软件的哈希指纹,这与比特币区块链所采用的思路相同,确保对数据的任何篡改都无法在不破坏其指纹的情况下进行。 为什么这很重要?2025年3月,tj-actions/changed-files Action遭到入侵,攻击者注入了恶意代码,将CI密钥泄露至构建日志中。每一个引用了上游Action的组织——数以万计的仓库——都受到了波及。如果你指向的是 `tj-actions/changed-files@v44`,你得到的就是攻击者推送的任意代码。Swan的工作流永远不会拉取那段恶意代码,因为我们不引用上游,而是引用我们自己经过审查、固定了SHA的副本。 镜像工具会替你处理那些危险的环节。一个名为 `versions.txt` 的单一文件列出了我们信任的每个外部Action及其受信任的精确版本。一个 `vendor-action` 脚本用于添加新条目。随后,一个 `sync-actions` 脚本会执行三件人工操作时容易遗漏的事情: - 重写引入的Action内部所有的 `uses:` 引用。当我们镜像 `oven-sh/setup-bun` 时,其 `action.yml` 内部可能调用了 `actions/checkout@v4`。该同步脚本会自动将其重写为指向我们内部镜像的引用。在运行时,引入的Action不存在任何途径可以调用公开的GitHub Actions注册表——每一层引用都被重写为内部引用。 - 检测缺失的镜像。如果一个引入的Action依赖于另一个我们尚未镜像的Action,脚本会在我们发布之前告知我们。不存在任何静默逃逸。 - 在供应商处理时将内部引用固定到某个 SHA。即使是我们自己的 swan-bitcoin/actions master 也会被解析为一个提交哈希并内嵌进去,这样未来对我们 actions 仓库的任何更改都无法悄无声息地传播到已经过供应商处理的 action 中。 ## 供应链检查 我们 actions 流水线的第一道关卡是我们自己构建的静态扫描器,名为 supply-chain-lint。它的职责看似简单:找出每一处供应商处理过的 GitHub Action 可能在运行时悄悄拉取不受信任代码的地方。 你可以从头到尾审计一个 GitHub Action 的源代码,通过 SHA 将其固定,并感到安心。但如果该 action 的 entrypoint.sh 在执行时运行了 npm install 或 curl | bash,你的审计便毫无意义。实际在你 CI 运行器上执行的代码,并不是你审查过的代码——而是构建时互联网上提供的任何内容。这个 action 的目标是防范类似针对 Datadog action 的攻击,该攻击利用了其流程中未固定版本的包安装操作 https://github.com/DataDog/junit-upload-github-action/issues/49 Supply-chain-lint 会捕获以下模式: - 没有版本约束的 npm install、npx 或 pip install - curl | bash 及类似的"获取并执行"模式 - 会解析为最新版本的 @latest 标签 - 绕过完整性校验的 -no-lockfile 标志 当扫描器确实标记了某个合理的情况——比如某个供应商处理过的 action 确实需要安装一个我们自己控制的特定工具——我们不会简单地压制警告。允许列表要求提供精确的(文件,模式)对、书面说明以及日期。这是一份可审计的记录。六个月后,任何人都可以查看允许列表,清楚地了解每项例外存在的原因以及谁批准了它。 该扫描器的范围被刻意限制得很窄。它标记的是获取机制——即不受信任的代码可能进入流水线的那一刻——而将更深层的包检查留给下一层处理。如果 supply-chain-lint 通过,则意味着我们供应商处理过的 actions 中没有任何内容会在运行时向互联网索取未经审查的代码。 ## AI 驱动的依赖安全门控 第二道关卡 analyze-dependencies 是事情变得有趣的地方。这是一个多层分析系统,用于评估拉取请求中引入的每一个新依赖项。 第一层:锁文件差异对比。当开发者提交一个添加或更新依赖的 PR 时,系统会解析基础分支的锁文件和 PR 的锁文件,然后精确隔离出哪些 package@version 组合是新增的。这适用于 yarn v1、yarn berry、pnpm 以及 monorepo 设置。 第二层:年龄隔离检疫。近期发布的包版本在其 npm 发布日期之后的一段时间窗口内会被自动阻止。这是针对供应链攻击最简单也最有效的防御手段之一——绝大多数恶意包会在发布后不久被 npm 安全团队发现并下架,因此要求包在公开环境中"陈化"一段时间,可以在大量此类攻击到达我们的代码之前将其过滤掉。我们不公开具体的时间阈值。 第三层:GuardDog 静态扫描。Trail of Bits 的开源工具 GuardDog 对每个新包执行一轮静态分析。它会检查生命周期钩子滥用(执行任意代码的安装脚本)、域名抢注(包名看起来像流行库但实际上并非)以及混淆的有效载荷。这一层捕获的是低技术含量的攻击——那些依赖开发者不仔细审查所安装内容的攻击。 第四层:AI 源代码审查。这一层捕获的是基础静态规则无法发现的问题。系统会下载每个新包的实际 tarball,遍历源码树,并将其发送给 LLM,依据多类别供应链威胁评估标准进行分析,该标准大致涵盖: - 主动规避阅读的代码——混淆、动态执行、隐藏在二进制资产或非代码文件中的有效载荷 - 越权访问的代码——意外的网络活动、文件系统访问、命令执行或持久化机制 - 安装时行为——获取或运行远程代码的生命周期脚本、多阶段有效载荷投递、基于时间或环境条件的触发激活 - 身份与元数据信号——域名仿冒、元数据字段不匹配、在决定行为之前对运行环境进行指纹识别的包 静态规则捕获已知的恶意模式;AI 则捕获新颖的模式——那些人工审查员会标记但正则表达式会遗漏的问题。每条发现结果都附带严重程度、文件位置和建议处置措施。 ## 定制化 Claude 安全扫描器 第三道关卡是 Anthropic 开源 Claude 代码安全审查 Action 的定制化分支版本。我们的分支进行了若干改动,任何在生产环境中运行此工具的团队都应加以考虑: - 对每个有实质内容的 PR 执行 STRIDE 威胁建模。除内联扫描外,我们还会单独执行一次 STRIDE 分析(欺骗、篡改、抵赖、信息泄露、拒绝服务、权限提升),并将结果作为顶级 PR 评论发布。在此之前,系统会先用低成本的 Haiku 进行预筛,跳过仅涉及文档、测试、格式、依赖升级和重命名的 PR,避免在不可能引入风险的变更上消耗 token。 - 在 PR 生命周期内进行模型分级。Opus 负责执行首次扫描——此时整个 diff 都是新内容,推理深度最为关键。Sonnet 负责处理后续提交,此时增量 diff 较小。这正是后续变更成本可控的原因所在。 - 对每次提交重新扫描,并按发现结果去重。上游版本只扫描一次,此后不再重扫。我们对每次提交都进行扫描,确保反馈反映当前 diff 的状态,同时评论层会与已有的内联评论进行去重,避免审查者看到同一问题被重复发布。 - 误报过滤结果的显式呈现。上游版本会静默丢弃过滤器判定为误报的发现结果。我们仍会将其从阻断列表中移除,但会以折叠的审计区块形式发布在 PR 评论中,同时附上排除原因和置信度评分——以便审查者对过滤器进行审计,并在过滤器出错时提出异议。 - 扩展漏洞分类体系。审计提示词新增了 TypeScript/Node.js 特有模式(原型污染、不安全的 require、Express 中间件顺序问题),并将 IDOR 作为一级分类加以处理。该分类体系统一应用于内联扫描、STRIDE 分析和误报过滤器的提示词中。 - 对 Action 自身的供应链加固。通过 `npm ci` 对已提交的锁文件执行安装(杜绝意外的 CLI 升级),对内部自引用使用 SHA 固定,并对 PR 评论渲染器进行 Markdown 注入加固,防止恶意 diff 将内容注入审查评论。 以上所有改动贯穿一个核心主题:生产级安全门控必须可调试、可追责,并在出现问题时大声报警。每项改动都堵上了一条扫描器可能悄然表现欠佳的路径。 ## 基础控制措施 上述关卡在 PR 阶段拦截威胁,但我们同样在技术栈的每一层强制执行控制措施。以下示例以 JavaScript/npm 为主,因为最大份额的攻击都集中于此,但等效的控制措施(注册表固定、锁文件不可变性、漏洞扫描、在生态系统支持的情况下进行版本年龄限制)同样适用于 Go、Python 和 Rust 工具链。 **不可变锁文件,无处不在。** CI 中、Docker 构建中以及生产环境中的每一次 `yarn install` 都带有 `--frozen-lockfile` 或 `--immutable` 参数运行。若锁文件与 `package.json` 不完全匹配,构建即告失败。这意味着不会有幽灵依赖更新,开发者本地测试的内容与生产环境运行的内容之间也不会出现解析漂移。 **下游仓库保持浮动,actions 仓库保持锁定。** actions 仓库本身完全采用 SHA 固定(外部和内部引用均如此),但下游消费方仓库有意固定到 actions 仓库的 `@master`。这种不对称设计是刻意为之:它让我们能够将安全修复推送到经过严格审计、并由多重审批和代码所有者保护的 actions 仓库,让每个消费方在下一次 CI 运行时自动获取更新,而无需追着数十个仓库逐一提交固定版本升级的 PR。 **安装时的版本年龄限制。** 除 CI 关卡之外,我们还在包管理器层面本身强制执行版本年龄限制。`.yarnrc.yml` 中的最小年龄设置(`npmMinimalAgeGate`)意味着 yarn 拒绝安装任何比我们阈值更新的版本,甚至在 CI 运行之前就已拦截。这与 CI 关卡的隔离机制相同,但在每位开发者的本地机器上强制执行。 **默认禁用构建脚本。** 包安装脚本——即在执行 `npm install` 时运行任意代码的 `postinstall` 钩子——通过我们 yarn 配置中的 `enableScripts: false` 在全局范围内禁用。少数确实需要原生编译的包(例如用于加密模块的 `node-gyp`)通过 `dependenciesMeta` 单独启用。其余一切均保持静默。 **阻断已被攻陷的版本。** 当某个特定版本的包被确认遭到入侵时,我们不会只是寄希望于开发者主动更新。我们在包配置中使用 `resolutions` 和 `overrides`,强制将整个依赖树——包括传递依赖——从问题版本迁移走。当 `axios 1.14.1` 被发现含有恶意代码时,跨仓库的一处配置变更便确保了任何 Swan 项目都无法解析到该版本,即便它是三层之深的传递依赖也不例外。 ## Egress Hardening CI runner 默认可以在任意端口连接互联网上的任何地址。一旦某个恶意依赖或被攻陷的工具决定携带你的部署密钥向外回传,这就成了一个隐患。我们的 egress-lock GitHub Action 关上了这扇门:它强制所有 HTTP/HTTPS 流量通过带有主机名白名单的本地代理,并使用防火墙规则拒绝其他一切流量。大多数工作流只需添加一两行列出项目特定域名的配置;GitHub、npm 和 AWS 等常见目标默认已在允许列表中。 底层实现如下: - **Tinyproxy 作为守门员**——以默认拒绝模式在本地运行,对每个连接的主机名与白名单进行比对。 - **防火墙作为后备**——`iptables` 规则仅允许 DNS、回环地址、已建立的连接以及代理自身的出站流量。其他一切均被快速拒绝,使构建迅速失败并给出清晰的错误信息,而非无限挂起。 - **工具接入自动完成**——自动设置 `HTTP_PROXY`/`HTTPS_PROXY`,以及针对 git、npm、Yarn 和 Node.js 的工具专项配置。 - 轻松调试——仅报告模式开放通道,但持续记录每个目标地址,配套的出口报告操作在失败时转储代理日志,让你准确看到哪些内容被拦截。 ## 变更控制:守卫守卫者 以上所有控制措施均为代码或配置——这意味着它们可以被修改,而一个自身配置可被编辑的安全系统,其强度取决于审查最宽松的那一环。 每一个定义安全策略的文件——动作镜像的 versions.txt、yarn 配置、供应链检查的允许列表、依赖解析覆盖项,以及将每道关卡接入 CI 的工作流文件——均由 GitHub CODEOWNERS 保护,需要经过多位指定安全维护者批准后才能合并。拥有提交权限的开发者无法单方面放宽年龄门控、允许某个包运行安装脚本,或移除已知受损版本的拦截规则。每一次对控制措施的放宽,都会与新增依赖项一样受到严格审查。任何违反此策略的行为都将被标记并告警。 这消除了纯技术控制无法覆盖的一类内部风险:这些关卡只有在无法被悄悄关闭时才真正有意义。 ## 可执行审计技能 安全控制的价值在于其在整个组织中的执行力度。为此,我们构建了一个 Claude Code 技能,将完整的操作手册编码为可执行审计。将其指向任意 Swan 代码库,它便会读取实际的配置文件。它检查是否启用了冻结安装、年龄门控设置、GuardDog 集成、AI 依赖项审查接入、通报扫描、操作检查配置、内部镜像使用情况、针对已知有害版本的解析覆盖项、脚本阻止设置,以及合并覆盖流程。 它不是在合规电子表格上打勾,而是读取 .yarnrc.yml,告诉你 enableScripts 是否真正被设置为 false;读取工作流文件,告诉你 supply-chain-lint 是否真正被接入为必要检查项。"我们有政策"与"政策得到执行"之间的差距,正是安全剧场与安全工程之间的差距。 ## 纵深防御:每项控制的触发位置 没有任何单一控制能捕获所有威胁。该系统的意义在于:恶意包若要进入生产环境,必须突破所有这些层级,而这些层级分布于流水线的不同节点。以下是完整全貌: 在拉取请求阶段: - dependency-review-action 拦截任何引入已知高危 CVE 版本的依赖变更(GitHub 通报数据库) - supply-chain-lint 拒绝在运行时获取不可信代码的托管操作(无锁的 npm install、curl | bash、@latest 标签) - analyze-dependencies 隔离近期发布的包,运行 GuardDog 静态分析,并根据我们的供应链评审标准以 AI 审查源代码 - claude-code-security-review 对 PR 差异本身运行 STRIDE 威胁模型;由 Haiku 预筛决定该 PR 是否值得扫描,Opus 负责首次深度扫描,Sonnet 处理后续提交 - check-linear-link 确保每个 PR 都可追溯至已追踪的 Linear 工单——杜绝匿名合并 在安装阶段(CI 及开发者机器): - 冻结锁文件——若 package.json 与锁文件不一致,yarn install --immutable 将使构建失败 - npmMinimalAgeGate——yarn 本身拒绝安装任何比我们最小年龄阈值更新的版本,甚至在 CI 运行之前即生效 - enableScripts: false — 软件包生命周期钩子默认关闭,仅可通过 dependenciesMeta 逐项启用 - resolutions / overrides — 已知受损版本会被强制从整个依赖树中移除,包括传递依赖 在 CI 运行时: - egress-lock 将所有 HTTP/HTTPS 流量路由至一个默认拒绝的代理,并配置主机名白名单;iptables 拒绝其余一切流量(非代理 HTTP 收到 TCP RST,其他所有端口收到 ICMP 端口不可达,以便工具快速报错) - egress-report 在失败时转储代理日志,使被拦截的流量可见且可调试 持续执行: - 可执行审计技能读取每个关键仓库中的实际配置文件,并验证上述每一项均已正确接入——不是"我们有一项策略",而是"该策略正在被强制执行"。 攻击者若想将恶意软件包植入生产环境,需要:在未被检测到的情况下使其度过隔离窗口期,规避 GuardDog 和 AI 审查,绕过 STRIDE 差异扫描,躲开所有 CVE 数据库,突破冻结的锁文件,在脚本被禁用的情况下设法执行,还需使一个受 CODEOWNERS 保护的配置变更获得合并以放宽上述任一限制,最后还要在不建立任何非白名单出口连接的情况下完成数据渗出。这就是攻击者必须越过的门槛。 ## 为何这至关重要 供应链攻击并非纸上谈兵——它是当今针对软件公司最主要的攻击向量。npm 生态系统拥有超过两百万个软件包,其中任何一个,或其任何传递依赖,都可能成为入口点。GitHub Actions 在运行时可访问你的源代码、密钥和部署凭证,一个遭到入侵的 action 便可将一切悉数泄露。 大多数公司的应对方式是加一个漏洞扫描器,然后寄希望于运气。扫描器只能捕获已知的威胁;2025 年的蠕虫级攻击的传播速度远超任何公告数据库的更新速度。上述系统的核心理念是:将未知软件包视为有罪直至证明无罪,并让链条中的每一个环节——action 镜像、依赖关卡、PR 扫描器、安装配置、网络出口——各自独立地执行这一安全态势。即便某一层被绕过,攻击者仍需攻克下一层。这才是攻击者在触及你的密钥之前必须越过的门槛。 将本文输入你的智能体,即可对你的系统展开审计!