Cilium CI/CD 依赖安全实践

概述

在持续集成与持续部署(CI/CD)流水线中,控制“谁”可以触发构建固然重要,但构建过程所拉取的“什么”代码同样关键。即使访问控制严格,一条工作流若引用了被篡改的依赖项,其本身依然是被污染的。依赖项构成了软件供应链的基石,其安全性直接关系到最终交付物的可靠性、完整性,以及整个 SRE 流程的可信度。本文以 Cilium 项目为实例,深入探讨如何通过一系列工程实践锁定 CI/CD 依赖,防范供应链攻击。

核心概念

  1. SHA 固定:放弃使用可变标签(如 v1.0.0)引用 GitHub Actions 或容器镜像,改为使用不可变的 Git 提交 SHA 或镜像摘要(sha256 摘要)进行精确引用。这确保了引用的代码始终是特定的、经过审核的版本,不会因上游标签被恶意修改而受影响。
  2. 工作流依赖锁定:一个新兴的概念,旨在解决直接依赖通过 SHA 固定后,其传递依赖(间接依赖)仍然可能在运行时动态解析、存在风险的问题。类似于 go.mod 文件,它将锁定所有直接和传递的动作依赖。
  3. Go 模块 Vendor 化:将所有 Go 语言项目的第三方依赖下载到代码仓库的 vendor/ 目录中并提交。构建时直接使用本地依赖,而非实时从模块代理拉取,确保了构建的确定性,并允许在代码审查阶段对依赖变更进行人工审计。
  4. 依赖冷却期:在自动化更新工具中设置一个最小发布时间,只接受发布了一定时间(如 5 天)以上的依赖版本。此策略旨在避免自动集成那些可能被快速发现并回滚的、新发布的被污染包。
  5. 表达式注入:GitHub Actions 工作流中的一种漏洞,攻击者通过控制可注入 ${{ }} 语法的外部输入(如 PR 标题、分支名),在 run: 步骤中插入恶意 shell 命令。通过先将变量赋值给环境变量再引用,可以有效缓解此风险。

实践

1. GitHub Actions 的 SHA 固定

Cilium 在所有工作流文件的 uses: 指令中,均使用完整的 40 位 Git 提交 SHA 来引用 Actions,并在注释中保留人类可读的版本标签。

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

此方法同样应用于工作流步骤中直接使用的容器镜像,使用 @sha256: 摘要进行固定。这构成了第一道坚固的防线。

2. 自动化更新与信任边界 (Renovate)

手动维护数百个 SHA 引用不切实际。Cilium 使用 Renovate 自动化更新依赖。其核心配置包括:

  • 全局启用摘要固定 (pinDigests: true),确保新版本 PR 始终更新 SHA。
  • 设置依赖冷却期 (minimumReleaseAge: “5 days”),延迟集成最新版本,等待社区审查。
  • 建立受信任包列表并启用自动合并:对于来自 actions/, k8s.io/ 等可信组织的更新,允许 Renovate 自动创建 PR 并在 CI 通过后自动合并。其他更新则需人工审核。
  • 实施双重验证的自动批准:自动批准工作流不仅检查 PR 是否由 cilium-renovate[bot] 创建,还验证触发该 PR 的 Actor 确实是该机器人,防止伪造。

3. Go 模块管理策略

  • 强制 Vendor 化:所有 Go 依赖被 vendor 化并提交到仓库。CI 流水线会验证 go.mod, go.sum 与 vendor/ 目录的一致性,确保无漂移。构建过程完全离线,不依赖外部代理。
  • 精细化管理:对需要特殊处理(如需同步测试的 gateway-api)、有项目特定补丁(如 cilium/dns)或自身维护(如 cilium/ebpf)的依赖,禁用 Renovate 自动更新,改为手动管理。这些 vendor/ 变更由专门的 @cilium/vendor 团队通过 CODEOWNERS 机制审核。
  • 定期审计与简化:遵循“少依赖多拷贝”原则,定期审计第三方库,用少量内联代码替代仅提供小功能的依赖,减少攻击面。

4. 静态分析防护

  • 权限检查:使用 CodeQL 规则 (actions/missing-workflow-permissions) 扫描工作流,强制要求所有修改的文件明确声明所需的权限(如 permissions: read-all 或具体的 id-token: write),遵循最小权限原则。
  • 工作流语法与规范检查:使用 actionlint 静态检查工具,检测语法错误、不安全模式和不规范配置,例如:确保所有作业有名称、固定运行器版本(如 ubuntu-24.04)而非使用浮动标签。
  • 漏洞模式检测:actionlint 和 CodeQL 均能检测工作流中 ${{ }} 语法在 run: 块中对不可信输入的直接使用,从而发现潜在的表达式注入漏洞。

SRE 要点

  • 与可靠性关联:锁定依赖直接提升了构建的确定性和可重复性,减少了因依赖源不可用或突变导致的构建失败(未计划工作)。它从源头减少了因供应链问题引入的缺陷,提升了发布质量。
  • 与可观测性关联:通过 vendor/ 化和明确的变更审查,所有第三方代码的引入都留下了清晰、可审计的 diff 记录。这为安全事件调查和依赖链分析提供了可靠的“事实来源”。
  • 与事件响应关联:当某个流行依赖被曝出漏洞或恶意更新时,基于 SHA 固定的项目可以立即确认自身是否受影响(检查依赖的哈希值),并快速、精准地通过自动化工具更新到已修复的版本,显著缩短了应急响应窗口。
  • 最佳实践:
    1. 默认不可变:在 CI/CD 中,始终优先使用不可变引用(SHA、摘要)而非可变标签。
    2. 自动化与人工审查结合:用自动化工具(Renovate)处理高频、低风险的更新,用人工审查处理高风险、特殊的变更。
    3. 分层防御:结合使用 SHA 固定、依赖冷却、静态分析、人工审核、CODEOWNERS 等多种控制措施,构建深度防御。
    4. 最小攻击面:主动审计并移除不必要的依赖。
  • 常见反模式:
    1. 依赖泛滥:为微小功能引入庞大的第三方库。
    2. 信任可变引用:工作流中使用 @v1、@latest 等浮动标签。
    3. 忽视传递依赖:仅固定直接依赖,忽视其可能引入的风险。
    4. 权限过度授予:工作流使用默认的宽泛权限,而非最小所需权限。
  • 相关指标:
    • 构建成功率/确定性:依赖锁定有助于提升此指标。
    • 依赖更新延迟:从漏洞公告到应用修复的平均时间。
    • 供应链安全事件数:因依赖问题引发的安全事件数量。
    • 代码审查覆盖率:对 vendor/ 目录和工作流文件变更的审查是否严格执行。