在 CI 流水线里被 Terraform 模板的合规扫描卡住估计不少做基础设施测试的同行都有过这种经历。IaC 把云上资源定义写成.tf文件之后测试的粒度一下子细到了每一段 HCL 语法上以前我们只能靠人眼 review 安全组是否裸奔、S3 是不是公共可写现在完全可以把这些检查变成自动化审计规则在 merge 前就把高风险项挡在外面。这篇实践指南面向测试工程师、DevOps以及刚接手 IaC 治理的人讲的是如何围绕 Terraform 模板搭建一套安全合规性的自动化审计体系从工具体系选型、规则基线设计到 CI 门禁落地和排坑经验照着做就能跑起来。先说清楚一件事这里的“模板”指的是 Terraform 的.tf配置代码不是 Web 开发里常说的页面模板更不是模板注入攻击里的模板。Terraform 模板本质上就是声明式的基础设施说明书写着我要哪几台云主机、要什么样的网络策略、要哪些存储桶。既然它变成了代码测试人员就能用在代码测试里积累的那套方法去管它——静态扫描、自动化断言、回归门禁全部可以复用。这就是为什么安全合规审计这件事在 IaC 时代特别适合由测试从业者来牵头。1. 为什么测试从业者要盯上 Terraform 模板安全合规性1.1 基础设施代码化之后测试的边界变了传统功能测试解决的是“系统能不能按预期工作”这个预期通常来自产品需求。但 Terraform 模板一旦落地它直接影响的就是云账号里的真实资源边界从应用代码扩展到了整个基础设施层。你不需要等到环境部署完再拿扫描器去打靶场因为在terraform apply之前你的main.tf里可能已经埋了能让攻击者直接拿到一台机器的漏洞。一个很常见的例子安全组入站规则把0.0.0.0/0配在 22 端口上这条规则在传统测试阶段很难被发现。功能测试只关心这台机器能不能 SSH 上去不会管这条安全组规则是不是太过宽松。但如果你在 CI 里对 Terraform 模板做自动化审计这种问题会在构建阶段直接暴露出来根本不会进入到生产环境。测试从业者做这件事还有一个天然优势我们习惯从反例出发。功能测试要想的是这个输入会不会导致崩溃基础设施测试要想的是这段配置会不会导致越权、暴露、数据泄漏。前者是验证逻辑后者是验证信任边界思维方式非常接近。所以我一直觉得IaC 安全合规审计不是纯安全团队的事测试团队完全有能力也很有必要接手一部分。1.2 Terraform 模板里的典型风险和合规断点Terraform 模板的风险通常集中在几类资源上安全组/防火墙规则入站来源过宽、端口全开、协议过泛。存储桶对象存储ACL 设置为 public-read 或 public-read-write未启用服务端加密未开启版本控制。IAM 策略Action 使用通配符*Resource 也是*等于把整个账号的管理员权限授予一个普通角色。密钥管理数据库密码、访问密钥直接以明文写在模板或变量默认值里或者没有用 Vault/Secrets Manager 引用。数据库与计算资源数据库开启了 public accessibility计算镜像没有指定版本数据卷没有加密。网络结构VPC 流量日志没开子网直接关联了互联网网关却没有经过堡垒机。这些断点不只是安全问题也是合规断点。合规框架很多最常见的是 CIS Benchmark针对 AWS 有 CIS AWS Foundations BenchmarkAzure 和 GCP 也有对应的版本。支付行业要碰 PCI DSS医疗行业要碰 HIPAA在欧洲还要考虑 GDPR。这些框架要求的东西很多在 Terraform 模板里都有直接对应物比如必须启用加密、必须限制管理端口、必须记录网络流量。用生活里的例子讲安全和合规的关系类似楼宇管理安全是问“这门锁坏了没有窗户关没关”合规是问“按消防规定这里必须有几个疏散出口疏散通道宽度够不够”。Terraform 模板审计要两件事一起做既要防住安全隐患也要把配置对齐到外部要求。1.3 审计到底审计什么安全扫描与合规基线很多人刚开始做自动化审计时会混为一谈觉得只要是扫描出来的问题就该一样对待。实际不是。安全扫描更偏向风险合规基线更偏向标准。安全扫描回答的问题是这段配置有没有暴露面攻击者利用它能不能造成实际的破坏。比如 S3 桶开放公共读这是一个具体的安全风险IAM 策略给了*:*这也是。这类问题优先级往往很高因为被利用的成本低、影响面大。合规基线回答的问题是这段配置符不符合某套外部标准。CIS 要求 S3 桶启用默认加密你就算没有实际风险只要桶是明文存储合规检查就会失败。再比如某些标签规范要求所有资源必须带 Environment 和 Owner 标签这不威胁安全但会被合规审计标记出来。理解了这两层差异再去选工具就会清晰很多。工具输出的 FAILED 不等于全部要按同一优先级处理只有把安全风险和合规约束分开看才不会被误报和无效告警淹没。2. 自动化审计链路的设计与工具取舍2.1 先定策略用“测试金字塔”来分层如果你做过自动化测试一定听过测试金字塔底层是单元测试中间是接口测试顶层是端到端测试越往上成本越高、数量越少。Terraform 模板审计也可以按这个思路分层。最底层是静态语法与最佳实践检查对应 TFLint。它检查.tf文件本身有没有语法错误、provider 参数是否正确、有没有用过时的参数写法。这层跑得最快适合放在每次提交或每次 PR 打开时。中间层是配置安全与合规扫描对应 Checkov、Trivy 这类工具。它们读取模板内容和 plan 输出匹配内置规则库输出安全风险和合规基线失败项。这层是审计体系的核心成本可控覆盖面大。最顶层是策略即代码和业务级断言对应 OPA/Conftest、Sentinel。这层解决的是通用工具覆盖不到的定制化需求比如“关键业务环境的数据库必须开启备份”“所有公网负载均衡必须接入 WAF”。这些规则不是 CIS 基线直接给的但业务上必须有所以放到最高层。分层的好处是回报快、排查容易。底层失败先拦住避免无效的顶层扫描。顶层规则更稳定不会因为工具规则库升级而频繁误报。这种设计思路是从测试金字塔迁移过来的落地时团队接受度很高。2.2 工具选型对比Checkov、Trivy、TFLint、OPA目前社区里常用的几款工具各有侧重我用一张表把它们的关键差异列出来工具定位规则语言/扩展方式适用场景Checkov合规与安全静态扫描基于 Python 的规则内置大量 CIS、PCI、HIPAA 等基线多框架、多云平台按合规框架做批量检查Trivy配置错误扫描 容器/依赖漏洞内置策略规则随版本更新单文件二进制统一扫描容器镜像、文件系统、IaC 配置tfsec曾经的社区主流 IaC 安全扫描YAML 自定义规则老项目存量流水线新项目建议迁移到 TrivyTFLintTerraform 语法与最佳实践插件机制支持 provider 规则检查语法、废弃参数、实例类型有效性OPA/Conftest策略即代码Rego 语言编写自定义策略业务级合规约束、定制门禁SentinelHashiCorp 商业策略引擎HashiCorp Sentinel 语言Terraform Cloud/Enterprise 深度集成这里提一句 tfsec。它前几年很好用但后来仓库进入维护模式官方把 IaC 扫描能力合并到了 Trivy。我的建议是老项目如果流水线已经接了 tfsec暂时不用推翻但要关注规则库不再更新的问题新项目就不要再用它起步了直接用 Trivy一条命令同时管容器和 IaC运维成本低得多。2.3 为什么我坚持“组合拳”很多人会问能不能只用一个工具搞定所有事情答案是能跑通但跑不稳。Checkov 的合规基线很全但自定义策略不如 OPA 顺手Trivy 的配置扫描很稳但部分 provider 版本相关的语法问题不如 TFLint 精准TFLint 可以抓语法错误但它不做安全风险判断。用组合拳还有另一层原因安全扫描引擎的规则库会定期做大规模更新你今天依赖的某条规则明天可能被拆分、合并或者调整严重级别。如果你把所有门禁都依赖在单一工具上一次规则库更新就可能让你的全部流水线变红。分层之后某一层工具升级导致的结果波动不会影响其他层。我自己经历的教训是一开始图省事只接了 Checkov结果规则库升级之后几十个存量资源全部 FAILED团队花了两周去逐条处理。后来加上 Trivy、TFLint把规则按层级分开每个工具只负责它最擅长的部分整体稳定性反而上来了。这不是工具越多越好的问题而是每层职责要单一、边界要清楚。3. 实操搭建一套可落地的 Terraform 模板审计流水线3.1 准备一个“带病”的示例模板为了讲清楚检测过程我准备了一个故意埋了多个问题的infra/main.tf你可以直接拿来做演示或者跑一遍。terraform { required_version 1.0 } resource aws_security_group public { name allow-all ingress { description allow everything from_port 0 to_port 65535 protocol -1 cidr_blocks [0.0.0.0/0] } } resource aws_s3_bucket data { bucket sales-data-2024 acl public-read tags { Environment dev } } resource aws_iam_policy admin { name full-admin policy jsonencode({ Version 2012-10-17 Statement [ { Action * Effect Allow Resource * } ] }) }这段模板有三个典型问题安全组对所有来源开放了全部端口S3 桶公共可读IAM 策略给了全量权限。如果你把这套模板 apply 到真实云账号问题会非常直观。注意我用了旧版 provider 风格的 S3 ACL 写法新版本的 AWS provider 更推荐用aws_s3_bucket_public_access_block但扫描原理是一样的不影响演示。3.2 第一次扫描Checkov 跑合规基线先安装 Checkov可以直接用 pippip install checkov然后扫描整个目录checkov -d infra --framework terraform --download-external-modules true-d指定目录--framework terraform告诉它只扫 Terraform 配置--download-external-modules true表示遇到远程 module 时下载到本地一起扫。实际输出会分成 PASSED 和 FAILED 两段。拿上面的模板来跑你会看到类似这样的信息aws_security_group.publicFAILED因为入站规则来源是0.0.0.0/0端口范围 0-65535 全开。aws_s3_bucket.dataFAILED因为 ACL 是public-read同时可能还会报未启用加密、未启用版本控制。aws_iam_policy.adminFAILED因为 Action 和 Resource 都是通配符。Checkov 的规则 ID 在输出里会直接给出比如CKV_AWS_开头的代码。这些 ID 是后续做跳过和基线管理的依据建议在报告里保留不要只留下人类可读的摘要。这里要理解的一个点是Checkov 默认会扫所有它懂的合规基线所以有时候同一个资源会一下子报出十几条。这不代表每个都必须马上改但至少它把问题暴露出来了。合规审计的第一价值是“可见性”先让你知道存量问题有哪些再谈怎么治理。3.3 安全漏洞扫描Trivy 的完整用法Trivy 安装方式很灵活macOS 上可以用 HomebrewLinux 上可以直接下载二进制brew install trivy # 或者 curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin扫描 IaC 配置用的是config子命令trivy config --severity HIGH,CRITICAL --exit-code 1 infra--severity用来控制只关心高和严重两个级别--exit-code 1表示如果存在这些级别的问题就让命令返回非零状态这样 CI 里可以直接通过命令退出码门禁。输出会比 Checkov 更偏安全视角它会在aws_security_group.public上提示公网暴露在aws_s3_bucket.data上提示公网可读。用 Trivy 还有一个好处如果你们仓库里除了 Terraform 还有 Kubernetes YAML、Dockerfile它一条命令全能扫。基础设施建设很少是单一文件单一生效的很多云资源最终还要配合容器和编排配置Trivy 能把这条链上的配置错误统一管理起来。3.4 计划文件审计把检测动作前移到 plan 阶段静态扫描.tf源码有一定限制。比如变量值是var.allowed_cidr静态扫描不知道它最终会解析成什么又比如count和for_each会生成多少实例静态扫描也看不全。这时候可以把检测动作前移到terraform plan阶段让 Terraform 自己先把配置渲染成具体计划再对这个计划做扫描。具体流程terraform init terraform plan -outtfplan.binary terraform show -json tfplan.binary tfplan.json然后让 Checkov 扫描这份计划文件checkov -f tfplan.json --framework terraform_plan这个方式的优势是扫描结果和最终将要 apply 的内容是一致的。变量被解析了动态资源被展开了模块输出的真实路径也出现了。缺点是要多跑两个 Terraform 命令会占用一点 CI 时间而且要求 CI 里有有效的云凭证或至少能正常完成 plan 的环境。如果你们项目规模不大、变量简单静态扫描就够如果基础设施复杂我强烈建议把 plan 文件审计作为门禁标配。3.5 自定义合规策略用 OPA 管住业务条款通用工具覆盖不了所有业务规则比如公司规定“生产环境的数据库必须开启删除保护”这个要求并不在 CIS 基线里。这时候就轮到 OPA 出场。OPA 的哲学是策略即代码你写一份 Rego 策略文件把它放在仓库里CI 每次跑 plan 之后用这份策略去审计划文件。给一个简化版的 Rego 示例目标是禁止安全组对所有来源开放package main default allow false allow { count(deny) 0 } deny[msg] { resource : input.resource_changes[_] resource.type aws_security_group ingress : resource.change.after.ingress[_] ingress.cidr_blocks[_] 0.0.0.0/0 msg : sprintf(resource %s: 安全组对全网开放违反业务安全策略, [resource.address]) }用 OPA 去评估这个策略opa eval -f json -d opa/policy.rego -i tfplan.json data.main.deny如果能输出 deny 列表就说明计划里有违规项。把命令接到 CI 里用jq判断返回结果是否为空即可。Rego 第一次写会有学习成本但它带来的收益是任何业务规则都能落成断言不再依赖某个扫描工具内置规则是否支持。这也是整个审计体系里最灵活的一层适合测试团队自己维护。不过我的建议是不要一开始就铺开写大量 Rego先把公共基线跑稳再挑三五条业务最高优先级的条款做自定义策略慢慢迭代。3.6 接进 CIGitHub Actions 实现合并前强制门禁上面的命令单跑没问题之后就可以把整套流程接进 CI。以 GitHub Actions 为例我通常会把它做成一个单独的 job放在每个 PR 的必检清单里。一个可直接参考的.github/workflows/terraform-compliance.ymlname: terraform-compliance-check on: [pull_request] jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Setup Terraform uses: hashicorp/setup-terraformv3 with: terraform_version: 1.7.0 - name: Setup tools run: | pip install checkov brew install trivy brew install opa brew install tflint - name: Terraform plan run: | terraform init terraform plan -outtfplan.binary terraform show -json tfplan.binary tfplan.json - name: Checkov compliance baseline run: | checkov -f tfplan.json --framework terraform_plan --compact - name: Trivy config scan run: | trivy config --severity HIGH,CRITICAL --exit-code 1 infra - name: OPA custom policy run: | opa eval -d opa -i tfplan.json data.main.deny | jq -e .result [] - name: TFLint run: | tflint --init tflint --chdirinfra这里有几个细节。brew在 Ubuntu runner 上要先安装 Homebrew实际生产环境更推荐下载二进制或者用官方 action比如 Trivy 有aquasecurity/trivy-action。我写 Homebrew 只是为了演示方便避免把 YAML 拉得太长。真正落地时建议给每个工具固定版本锁在仓库的版本文件里避免工具升级导致的结果波动。门禁策略也要分层Checkov 建议先--soft-fail只出报告不阻断等基线稳定后再开硬门禁Trivy 的高危和严重问题建议直接阻断TFLint 作为语法层失败也应该阻断因为语法错了后面什么都跑不了。4. 踩坑记录从误报到门禁策略的常见问题速查4.1 误报太多怎么办severity 降级、skip 与基线管理工具跑起来后第一波冲击一定是误报。误报不全是工具的问题很多时候是规则和业务语境不匹配。比如安全组在测试环境里允许内网段全端口互访扫描器看到from_port 0、to_port 65535可能直接告警公网暴露但如果来源 cidr 是内网网段这其实是合理的。处理误报有几个层次。第一层是看严重级别可以用--skip-check跳过特定规则或者在代码里加注释声明原因。Checkov 支持在资源上方写# checkov:skipCKV_AWS_xxx:允许内网互访Trivy 也有类似的 ignore 注释。第二层是用基线文件第一次全量扫描产生的结果可以记录为 baseline后续只扫新增和变动的违规项这样存量问题不会一直砸在门禁上。第三层是规则治理定期复盘被 skip 的规则如果是大量资源都命中同一条规则那大概率是组织级配置准则的问题应该从源头改样例模板而不是逐条 skip。我自己比较反感的做法是直接--soft-fail一把梭。软失败适合前期观察但长期开软失败告警会被团队当成背景噪音彻底失去效力。正确顺序应该是先软失败观察两周清理存量问题再分级别逐步开启硬门禁。4.2 动态 count 和 for_each 导致的结果失真静态扫描.tf源码时遇到count和for_each很容易给出错误判断。比如一个资源用了count length(var.cidrs)扫描器常常直接取到 null 或者空数组就会漏报真实的开放规则。这个问题的解法就是前面说的 plan 文件审计因为terraform plan之后count会被展开成确定的资源实例plan JSON 里能看到最终每个实例的值。如果你的流程暂时没法跑 plan只能静态扫描那至少要把这类动态值列成已知盲区避免团队误以为扫过就安全了。我给不少团队审过 IaC 流水线不少事故恰恰发生在“工具没报错”上根因是工具根本没有看到解析后的值。所以做审计体系时一定要把“扫描范围”写清楚哪些场景依赖 plan 文件审哪些场景只做静态参考。4.3 多模块、Remote State 与外部模块的扫描姿势项目大了之后Terraform 通常会拆成模块有时模块还放在 Git 或 HTTP 上。Checkov 默认会下载远程模块所以你要确保 CI 环境能访问这些源地址私有的 Git 模块还要配好访问密钥。如果模块下游是第三方提供的扫描外部模块时要注意工具可能报告很多问题但它们并不在你的直接控制范围内这时候要区分“我们自己写的模板”和“引用的模块”后者的治理手段是升级模块版本而不是在应用层改源码。Remote State 本身不直接影响扫描但会影响 plan 阶段。如果 CI 里没有读 state 的权限terraform plan就失败后面的 plan 文件审计全都会中断。这个问题最常见的表象是本地扫描一切正常一到 CI 就挂在 init 或 plan。我的经验是给 CI 单独建一个只读 launch role让它能拉 state 和 provider 数据但绝不能给写权限。审计流水线的凭证权限最小化是底线。4.4 从“扫描报告”到“测试报告”的转换经验安全审计工具的输出默认是给安全人员看的直接丢给测试团队或管理层并不合适。我建议做一层报告转换最轻量的做法是让 Checkov 和 Trivy 输出 SARIF 格式然后跟 GitHub 或者 GitLab 的 code scanning 对接让问题直接显示在代码行上。这样测试人员处理一条告警就像处理一条代码评审意见不需要专门去打开安全平台。如果你们没有现成的 code scanning 平台也可以让工具输出 JUnit XML把检查结果伪装成测试用例。每条 FAILED 就是一条失败的测试每个资源就是测试套件里的一个用例最后合并进测试报告里。这样做的好处是团队已经习惯了看测试报告安全合规检查不会变成孤岛。对于测试从业者来说这个思路特别顺手。4.5 常见问题速查表问题现象可能原因处理建议Checkov 报了一堆规则但团队认为不需要处理规则与业务语境不匹配分层看严重级别用 skip 注释记录原因定期复盘 skip 规则工具扫了但没发现真实漏洞动态 count/for_each 导致静态扫描失真改用terraform plan输出 plan JSON再对 plan 文件扫描CI 跑到 plan 阶段就失败缺少读取 remote state 的权限为 CI 配置最小权限只读凭证确保能正常terraform plan外部模块扫描结果不稳定依赖的第三方模块版本变动固定模块版本升级需要走变更流程高频升级把流水线全部打红工具未锁版本在 CI 里固定工具版本升级后先本地跑一遍再合入告警被当成背景噪音长期 soft-fail 无人跟进存量问题先做基线新变更开硬门禁逐步收紧这几条是我们在实际落地过程中踩得比较多的坑尤其是前两条几乎每个新接手的项目都会遇到。把它们列成速查表可以让排查路径清晰很多省得每次从零开始查日志。落到我自己的实践我会反复提醒团队一句话不要试图第一天就把所有规则打开。不管是 Checkov 还是 Trivy第一次全量扫描一定是几十上百条 FAILED这时候直接把全部规则接到 CI 里只会得到一个整天红着的流水线然后大家慢慢失去信任。我们当时是先跑一次扫描把看门人打开再做一次基线让老代码先通过新改动从第一天就必须干净。后面规则再按月渐进式开放每开一条都要有解释。这个节奏看起来慢实际是让 Terraform 模板安全合规性审计能长期活下来的关键。