1.4.9.1. 流水线的安全问题

在治理大规模 Terraform 模块的过程中,我们通常会在流水线中执行一系列自动化测试,比如验证模块代码的有效性、运行 terraform plan 检查示例代码是否能成功执行。这些测试的关键之处在于:它们必须连接到真实的云环境,例如 Azure、AWS 或 GCP,因为只有在真正的云平台上执行,才能验证模块的完整功能。

然而,这里就出现了一个和传统软件组件开发截然不同的风险点:云资源的访问凭据。要让流水线正常工作,我们必须在流水线中配置可以访问云账号的凭据。这些凭据一旦泄露或被滥用,后果极其严重,可能导致云环境中资源被恶意创建、修改、甚至删除,带来直接的经济损失和安全风险。

这个问题在开源项目中尤为突出。开源意味着我们的代码仓库是公开的,任何人都可以查看代码,甚至提交 Pull Request。攻击者完全可以提交一段精心构造的恶意代码,一旦与我们的流水线上的敏感凭据结合起来执行,就会借助我们的流水线权限,对云资源进行恶意操作。

这就是流水线安全的最大挑战所在:如何在保障自动化测试顺利运行的同时,防止凭据被滥用?相比传统软件项目,Terraform 模块的流水线更像是在“带着云环境钥匙”进行测试,而一旦钥匙落入不怀好意的人手中,损失将是灾难性的。因此,我们必须格外小心地设计流水线安全机制,确保即便是最普通的开源贡献者提交代码,也绝不会有机会接触到我们的敏感凭据。

1.4.9.1.1. 潜在的攻击面

在开源 Terraform 模块的流水线中,攻击者可能利用以下攻击面来获取或滥用云平台的访问凭据:

  • 恶意 Pull Request 注入:攻击者提交包含恶意 Terraform 配置或脚本的 Pull Request,试图在未经审查的流水线中执行,从而访问敏感凭据。
  • 供应链攻击:利用被篡改的第三方 GitHub Action 插件或依赖项,在流水线中执行恶意代码。
  • 工作流配置篡改:攻击者可能尝试修改 GitHub Actions 的工作流文件(如 .github/workflows/*.yml),以绕过现有的安全防护措施。例如,他们可能更改工作流的触发条件、权限设置或引用的第三方 Action,从而在未经授权的情况下执行敏感操作。
  • 未受保护的环境变量:在流水线中使用未受保护的环境变量存储敏感信息,攻击者可以通过日志或其他方式获取这些信息。
  • 缺乏审批机制的敏感操作:在流水线中未对 terraform planterraform apply 等敏感操作设置手动审批,导致未经审查的代码可以直接访问云平台资源。
  • 尝试在测试环境中创建并维持资源,用以进一步攻击使用:Terraform 模块的测试天然地就是要在云端创建一些资源,关键是创建哪些资源,测试完成后是否会销毁。潜在的攻击者可能会尝试直接创建恶意资源,例如用来挖矿的服务器,或是创建高权限的账户和相应的凭据,以便在测试结束后可以直接控制我方的测试账号。

1.4.9.1.2. 两种自动检查

我们在持续集成流水线中执行的检查大致可以分为两类:

  • 无需凭据的自动检查:例如代码格式化(terraform fmt)、静态分析工具(如 tflinttfsec)等,这些任务不依赖任何云平台的访问凭据,因此可以在 Pull Request(PR)创建时自动执行,无需人工干预。

  • 需要凭据的敏感操作:例如运行 terraform planterraform apply,这些操作需要访问真实的云环境,必须使用有效的云平台凭据。在开源项目中,若在未经审查的 PR 中自动执行这些操作,可能导致凭据泄露或被恶意利用,造成严重后果。

1.4.9.1.2.1. 为什么仅靠一个私密的,非公开可见的流水线是无法解决问题的

我们看到有些开源项目的策略是:在开源仓库维护一个公开可见的,提供了无需凭据的自动检查的流水线的同时,在一个私有环境中维护了一个非公开可见的,用以执行需要凭据的敏感操作的测试流水线,例如用来执行 terraform apply 来测试 examples。这种方法并不能彻底解决风险,因为攻击者仍可能利用各种隐蔽手段将恶意代码注入测试流程,从而窃取我们的凭据,有时甚至不需要提交代码变更即可完成攻击,例如对供应链进行投毒,等待我们在后续的自动化测试中自动执行到被篡改的依赖库代码,这里就有一个 StepSecurity 报告的有关 tj-actions/changed-files 的案例。

我们必须假设攻击者的代码已经渗透进了测试环境,并且已经接触到了测试所使用的所有机密。

1.4.9.1.3. GitHub Actions Environments

为了解决这一问题,GitHub Actions 提供了 Environments(环境) 功能,允许我们为特定的任务配置部署保护规则。通过设置环境的保护规则,可以实现以下目标:

  • 手动审批:在执行敏感操作前,要求指定的审查者手动批准。
  • 限制机密:仅允许在特定环境中访问指定机密,防止未经审查的变更接触到凭据。

例如,我们可以为 terraform apply 操作配置一个名为 test 的环境,并要求至少一位审查者批准。这样,即使有人提交了恶意的 PR,也无法在未经审批的情况下执行敏感操作,从而保护了云环境的安全。

测试任务在没有得到至少一位管理员批准之前不会自动运行
图 1.4.9/1 - 测试任务在没有得到至少一位管理员批准之前不会自动运行
切记要在设置中将环境配置为必须由审核者批准才可部署
图 1.4.9/2 - 切记要在设置中将环境配置为必须由审核者批准才可部署

1.4.9.1.4. Terraform 模块测试应该使用哪种身份认证方法?

Terraform 在和公有云打交道时往往需要提供代表身份的凭据,通过身份认证后方可正常调用 API。以 Azure 为例,它支持以下几种身份认证方法:

必须强调的是,以上所有身份认证方式都是错误的,请不要在你的开源项目中使用。

1.4.9.1.5. OpenID-Connect(OIDC) 认证

目前为止唯一一种已知安全的用来在 Terraform 模块测试中进行身份认证的方法是使用 OpenID-Connect,AWSAzure 都支持这种认证方法。在配置好了 OIDC 后,只有在指定的 GitHub 仓库中指定的环境中才可以成功获取一个凭据,并且该凭据无法在其他仓库或者非该环境下工作。

1.4.9.1.5.1. 针对来自复刻(Fork)仓库的 Pull Request 的测试流程

由于 GitHub 的 OIDC 机制不支持在 Fork 仓库中调用云端身份认证,这意味着外部贡献者提交的 Pull Request(PR)无法直接在流水线上使用 OIDC 凭据连接云平台,从而无法完成完整的端到端测试(例如运行 terraform plan 检查示例代码是否能真正部署到云上)。

为了解决这个问题,我们采用了一个分支策略:

  • 贡献者提交 PR:外部贡献者提交代码时,只会触发无需凭据的检查,比如 terraform fmttflint 等静态检查,保证基础代码质量。
  • 模块维护者合入 Release 分支:一旦初步审核通过,模块的维护者会将 PR 合并到主仓库的一个专门的 release 分支。因为这个分支位于主仓库,所以它的流水线拥有完整的 OIDC 权限。
  • 执行端到端测试:在 release 分支中,流水线会运行需要真实云凭据的端到端测试,例如 terraform plan,确保模块可以在真实环境中无误执行。
  • 确认后合入主分支:测试通过后,代码才会被最终合并到 main 分支,完成发布。
┌──────────────────────────────┐
│ 贡献者提交 Pull Request (PR)  │
└────────────┬─────────────────┘
             │
             ▼
┌──────────────────────────────┐
│ 模块维护者审查 PR,检查是否有  │
│ 恶意代码或工作流文件的更改     │
└────────────┬─────────────────┘
             │
             ▼
┌─────────────────────────────┐
│ 创建 release 分支(建议命名: │
│ release/<变更描述>)         │
└────────────┬────────────────┘
             │
             ▼
┌──────────────────────────────┐
│ 编辑 PR,将目标分支更改为新建  │
│ 的 release 分支              │
└────────────┬─────────────────┘
             │
             ▼
┌────────────────────────────┐
│ 等待 PR 检查完成,验证代码无 │
│ 误后,合并到 release 分支    │
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│ 从 release 分支创建新的 PR, │
│ 目标为主仓库的 main 分支     │
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│ 触发端到端测试,维护者批准测  │
│ 试运行                      │
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│ 端到端测试通过?            │
└───────┬────────────┬───────┘
        │            │
        ▼            ▼
  是,合并到 main   否,调查失败原因
        │            │
        ▼            ▼
  流程结束         ┌───────────────────────┐
                  │ 与贡献者协作解决问题    │
                  │ 或在 release 分支中修复 │
                  └───────────────────────┘

最常见的攻击手法就是通过在 Pull Request 中篡改 Actions 工作流定义,绕过我们设置的各种审查流程,直接在敏感环境中运行恶意代码,从而窃取凭据,而这种两次 Pull Request 流程确保了所有测试在执行前都必须经过维护者的严格审批,既有自动检查,也有人工复核,最大限度降低风险。。有兴趣的读者可以在这里阅读 AVM 规范中的相关内容。

1.4.9.1.6. 双层云账号体系

潜在的攻击者可能会尝试在测试时创建一些不会被 terraform destroy 自动销毁的资源,以便在测试结束后继续滥用这些资源。这类攻击的典型方式是利用 null_resource + local-exec 执行云平台的原生命令(例如 Azure CLI),在 Terraform 状态文件之外创建资源。这些“逃逸”资源既不会在 terraform destroy 时被销毁,也很难通过正常的 Terraform 状态检查发现。例如,攻击者可能会:

  • 创建隐藏的高权限账户或访问密钥;
  • 部署挖矿程序的虚拟机;
  • 留下后门程序或持续运行的任务,以在将来再次入侵。

即便我们在测试环境中配置了定期清理任务,自动销毁超时未使用的资源,这些清理逻辑本身也有可能被恶意代码破坏或绕过,尤其当清理逻辑运行在同一个测试账号内时,攻击者可以利用现有权限去禁用或反制清理任务。这就引出了一个更深层的安全问题:如何有效防止测试账号被“持久化攻陷”?

1.4.9.1.6.1. “控制面-数据面”思路

为应对这种威胁,我们必须引入云安全领域的经典分层理念,即将“控制面”和“数据面”隔离。其设计要点包括:

  • 控制面账号(高权限):该账号主要用于运维和监管,拥有对测试环境的全面权限,包括监控、资源审计、异常检测和强制清理等。它不直接参与测试流水线的执行,因此不会暴露在流水线的凭据中,也不受流水线恶意代码的直接威胁。

  • 数据面账号(低权限):该账号是流水线在执行测试时所使用的账号,权限被严格限制:

    • 仅允许在指定资源组/项目/订阅中操作;
    • 禁止创建高权限身份(如新的 Service Principal、用户或角色分配);
    • 对资源配额做出限制(如最大 VM 数量、禁止 GPU 实例等)。

1.4.9.1.6.2. 运行机制示意

在这种架构下,流水线的测试操作全部发生在“数据面账号”下,即便攻击者通过恶意代码创建了残留资源,这些资源也完全暴露在“控制面账号”的视野中。控制面账号定期执行:

  • 资源盘点:对数据面账号的资源进行全量扫描,识别超时未清理或异常资源;
  • 越权检测:检查是否存在超出白名单范围的高风险操作,如 IAM 变更;
  • 强制销毁:清理所有违反安全策略的资源,确保测试环境“无污染”。

这种双层体系实现了“最坏场景下的兜底”:即便流水线被攻破,也只会在低权限范围内产生影响,攻击者无法真正实现持久性占领或大范围破坏。

1.4.9.1.6.3. 关键注意事项

  • 账号独立:控制面账号的凭据必须严格保密,禁止出现在流水线或公开环境中,最好仅供安全团队持有和操作。
  • 防御纵深:数据面账号即便被攻陷,攻击者也无法横向渗透到其他账号或提升权限。
  • 资源审计:尽可能启用云厂商提供的自动审计工具(如 Azure Policy + Defender),自动触发违规警报。

1.4.9.1.7. 另外的思考:流水线安全的其他部分

除了对云平台凭据的保护,我们还必须关注流水线本身的安全性,尤其是在使用第三方组件和模块时。以下是一些关键的安全实践建议:

1.4.9.1.7.1. 仅使用可信的第三方 GitHub Actions

在工作流中引入第三方 GitHub Actions 时,务必确保其来源可信。优先选择以下类型的 Actions:

  • 由 GitHub 官方维护的 Actions。
  • 拥有 GitHub Verified Publisher(蓝色认证徽章)的 Actions。
  • 由知名组织或活跃社区维护的 Actions。

对于不熟悉的 Actions,建议先进行代码审查,确保其不包含恶意代码或不安全的操作。

1.4.9.1.7.2. 使用 Git 提交哈希锁定第三方 Action 的版本

为了防止第三方 Action 被恶意更新或其标签被篡改,建议在工作流中使用 Git 提交哈希(commit SHA)来锁定特定版本。例如:

- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2

这种做法可以确保每次工作流运行时使用的都是经过验证的代码版本,降低供应链攻击的风险。

1.4.9.1.7.3. 最小化依赖,仅引用可信的 Terraform 模块

在模块开发中,应遵循最小依赖原则,避免引入不必要的外部模块。对于必须使用的模块,确保其来源可信,并定期审查其更新内容。

此外,建议对模块进行版本控制,使用语义化版本(Semantic Versioning),并在 versions.tf 文件中明确指定所依赖模块和提供者的版本范围,以防止因不兼容的更新导致的问题。

在 AVM 规范中规定了,AVM 模块代码仅允许引用其他 AVM 模块。

1.4.9.1.7.4. 定期审计和更新依赖项

定期使用工具(如 dependabot)检查和更新项目中的依赖项,包括 GitHub Actions、Terraform 模块和 Provider 等,及时应用安全补丁,修复已知漏洞,保持项目的安全性和稳定性。

通过实施上述安全实践,可以进一步强化流水线的安全防护,降低潜在的风险,确保在大规模治理 Terraform 模块时的稳定性和安全性。

results matching ""

    No results matching ""