Terraform State 与 IaC 凭证防御:加密远端状态、零密钥输出与暴露阻断
1. 威胁模型与 IaC 暴露根因
Terraform 在运行期会将资源属性、output 以及部分 Provider 元数据写入状态文件(.tfstate)和计划文件(tfplan)。一旦发生以下任一疏漏,攻击者无需攻破云控制台即可获得高价值凭据与完整资产拓扑:
- 静态/Web 路径暴露:
terraform.tfstate、terraform.tfstate.backup、*.tfvars、.terraform/或crash.log被打包进镜像、静态站点或遗留在 Git 历史中。 - 远端 Backend 权限与加密缺失:S3/GCS Backend Bucket 未启用公开访问阻止(Public Access Block)、缺少 KMS 静态加密及最小 IAM Policy,或 CI 产物公开下载包含了
terraform plan/output明文日志。 - 明文密钥持久化入 State:通过
random_password或资源属性直接生成凭证并写入state,即使标记sensitive = true,CLI 仅隐藏终端回显而 state JSON 内部仍保存明文。
2. 分层防御与 IaC 安全工程规范
| 防线层级 | 控制措施 | 具体要求 | 防御效果 |
|---|---|---|---|
| Git 与构建排除 | .gitignore、.dockerignore 与 pre-commit 钩子强制拦截 *.tfstate*、*.tfvars、*.tfplan | 接入 gitleaks/trufflehog 门禁 | 阻止状态文件与变量文件进入仓库或容器镜像 |
| 远端 Backend 加密隔离 | 启用 S3/GCS 私有访问阻止、encrypt = true(SSE-KMS CMK)及版本控制/锁表 | 仅专用部署 Role 可读写特定环境 prefix | 消除状态文件被越权枚举或未加密存储 |
| State 零密钥设计 | Terraform >= 1.10/1.11 优先使用 ephemeral 资源与 write-only(*_wo)属性,或通过 AWS Secrets Manager 外部生成轮换 | 禁止将数据库密码、Token 直接作为 output 导出 | 让状态文件本身不落地持久明文秘密 |
| CI Plan 与日志隔离 | PR 阶段将 tfplan 加密保存于受限存储,日志禁止打印 terraform show -json 明文细节 | 应用与部署分离,仅受保护分支允许 apply | 防止 CI 日志与公开 Artifact 泄露敏感属性 |
| Web 边界兜底 | 反向代理统一拒绝访问 *.tfstate*、*.tfvars、*.hcl、.terraform/* | 返回 404/403 | 阻断自动化路径探测直接下载状态文件 |
安全远端 Backend 与 Ephemeral 凭据示例(HCL)
hcl
terraform {
required_version = ">= 1.11.0"
backend "s3" {
bucket = "org-prod-tfstate-private"
key = "prod/core/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:111122223333:key/example-cmk-id"
use_lockfile = true
}
}
# 使用 ephemeral 资源生成密码并配合 write-only 属性,不写入 tfstate
ephemeral "random_password" "db_admin" {
length = 32
override_special = "!#$%&*()-_=+[]{}<>:?"
}Nginx 兜底拦截规则
nginx
location ~* \.(tfstate|tfstate\.backup|tfvars|tfplan|hcl)$ {
return 404;
}
location ^~ /.terraform {
return 404;
}防御工作流
- 暴露面排查与紧急轮换:核查 Web 根目录、Git 历史、CI Artifact 与对象存储是否存在公开可达的
tfstate/tfvars/crash.log;一旦发现历史暴露,假定其中所有数据库密码、API Key 与角色均已失密并立即轮换。 - Backend 迁入加密私有存储:将本地 state 迁移至启用 Public Access Block、SSE-KMS 独立密钥与访问日志的远端 Backend,删除本地遗留
.tfstate和.backup文件。 - 代码与 CI 改造:升级 Terraform 至支持
ephemeral/write-only的版本,逐步移除明文output和静态密钥变量,将.gitignore/.dockerignore与扫描门禁写入仓库基线。 - 持续验证:定期对产物目录、公开域名及仓库运行扫描脚本与
http_probe检查,确认零状态文件泄露。
3. Python IaC 状态暴露与敏感字段检测脚本
python
#!/usr/bin/env python3
"""Terraform 状态文件暴露与敏感属性留存检查工具。"""
import json
from pathlib import Path
SENSITIVE_ATTR_HINTS = ("password", "secret", "token", "private_key", "connection_string", "access_key")
FORBIDDEN_PUBLISH_FILES = {"terraform.tfstate", "terraform.tfstate.backup", "terraform.tfvars", "crash.log"}
def scan_tfstate_for_plaintext_secrets(state_obj: dict) -> list[str]:
findings: list[str] = []
for out_name, out_val in state_obj.get("outputs", {}).items():
if any(h in out_name.lower() for h in SENSITIVE_ATTR_HINTS) or out_val.get("sensitive"):
findings.append(f"output '{out_name}' stored in state (avoid sensitive outputs in tfstate)")
for res in state_obj.get("resources", []):
rtype = res.get("type", "unknown")
rname = res.get("name", "unknown")
for idx, inst in enumerate(res.get("instances", [])):
attrs = inst.get("attributes", {})
hits = [k for k, v in attrs.items() if v and any(h in k.lower() for h in SENSITIVE_ATTR_HINTS)]
if hits:
findings.append(f"resource {rtype}.{rname}[{idx}] retains sensitive attrs: {hits}")
return findings
def check_gitignore_coverage(gitignore_text: str) -> list[str]:
required_patterns = ("*.tfstate", "*.tfvars", ".terraform")
return [pat for pat in required_patterns if pat not in gitignore_text]
if __name__ == "__main__":
sample_state = {
"outputs": {"db_password": {"value": "placeholder", "sensitive": True}},
"resources": [
{"type": "aws_db_instance", "name": "main", "instances": [{"attributes": {"password": "example-value"}}]}
],
}
hits = scan_tfstate_for_plaintext_secrets(sample_state)
missing_ignore = check_gitignore_coverage("*.tfstate\n*.tfvars\n.terraform/\n")
print("state hits:", hits)
print("missing gitignore:", missing_ignore)
assert len(hits) == 2
assert missing_ignore == []验证与判定标准
| 检查项 | 判定为已加固(PASS) | 判定为存在风险(FAIL) | 验证方式 |
|---|---|---|---|
| Web/仓库暴露面 | 所有 .tfstate*、.tfvars、crash.log 均未入库且 Web 端返回 404 | 任意暴露路径返回 200 或存在于 Git 历史 | http_probe + Git 历史密钥与文件扫描 |
| 远端 Backend 安全 | 强制 encrypt = true(CMK KMS)、开启 Public Access Block 与版本控制 | 使用本地 state 或 S3 Backend 无加密/权限过宽 | 检查 backend HCL 配置与 Bucket 策略 |
| State 敏感字段控制 | 新建密钥采用 ephemeral/write-only 且无明文秘密 output | tfstate 明文留存大量高权 DB/API 密码 | scan_tfstate_for_plaintext_secrets 核查 |
| CI 日志与产物 | Plan 产物加密且仅限受保护分支,日志不回显敏感输出 | PR 日志或公开 Artifact 可下载完整 Plan/State | 检查 CI Workflow 与 Artifact 保留策略 |
MCP 工具映射
| 防御步骤 | MCP 工具 | 说明 |
|---|---|---|
| 检索 IaC 防护策略 | kb_router | 按 Terraform / tfstate / secrets 搜索相关防护文档 |
| 核对泄露路径清单 | kb_read_file | 读取 ctf-website/10-cloud/terraform-state-secrets 对照常见暴露面 |
| Web 暴露面复核 | http_probe | 验证公开域名对 /terraform.tfstate 等路径返回 404 |