Terraform 敏感信息管理:sensitive 标记、外部密钥源与 state 加密

第一次把 RDS 密码写进 terraform.tfvars 时,我以为 .gitignore 加一行就够了。三个月后翻 CI 日志,plan 输出的 password = "Pr0d-Rds-2019" 明晃晃躺在第 847 行。

💡 示例基于 Terraform 1.5.7 + AWS provider 5.x + Vault provider 3.x,后端是 S3 + DynamoDB。

1. 敏感信息的落点

1.1 五个会留下明文的位置

位置 默认是否明文 处置方式
*.tfvars 提交进仓库 明文 不进仓库,改由外部密钥源提供
terraform.tfstate 明文 后端加密 + 收窄读权限
plan / apply 输出 明文 sensitive = true 遮蔽
CI 日志 明文 日志遮蔽 + OIDC 临时凭据
模块 variables.tf 的 default 明文 敏感项不写 default

state 是最后一道关口,它天生就是明文。后端加密解决的是「磁盘被人拷走」,IAM 解决的是「谁能调 API 读它」。

1.2 泄露的典型路径

密码进仓库之后,清理 git 历史要重写所有分支,还得让每个克隆过的人重新拉。代价远高于一开始就不写进去。

.gitignore 只挡新增文件。已经进过 commit 的 tfvars 会一直留在历史里,git log -p 一翻就能看到。

⚠️ 已经提交过的密钥,从 git 历史里删掉不代表失效。轮转密钥是唯一可靠的补救。

2. sensitive 标记的作用范围

2.1 变量与输出

plain 复制代码
variable "db_password" {
  type      = string
  sensitive = true
}

output "db_endpoint" {
  value = aws_db_instance.this.endpoint
}

标了 sensitive 的变量,在 plan 和 apply 里显示成 (sensitive value)。terraform output 也默认遮住,取值要加 -json 或 -raw。

2.2 标记的传播

敏感值参与运算、拼接、放进 list,结果仍然带标记。

plain 复制代码
output "conn_string" {
  value     = "postgres://app:${var.db_password}@${aws_db_instance.this.endpoint}/app"
  sensitive = true
}

字符串插值会把敏感值带进普通字符串。root 输出里再暴露一次必须显式标 sensitive,漏标就报 Output refers to sensitive values。

确实需要明文输出时用 nonsensitive() 降级,用完记得把这段从输出里摘掉。

2.3 标记管不到的层面

sensitive 只影响显示层,存储层原样写入。

把它当脱敏开关会踩坑:以为标了 sensitive state 就安全,实际 terraform show 一读就出来。

3. 外部密钥源接入

3.1 三种方案对比

方案 成本 轮转 版本管理 适用场景
SSM Parameter Store 标准参数免费 手动或脚本触发 无原生版本回滚 小体量配置密钥
Secrets Manager 按密钥数与 API 调用计费 内置自动轮转 版本 + staging label 数据库凭据
Vault 自建或托管成本 动态凭据、TTL KV v2 带版本 多云、短时凭据

判断依据是轮转责任。交给云厂商托管就选 Secrets Manager,需要动态凭据和多云统一入口就上 Vault。

3.2 SSM Parameter Store

plain 复制代码
data "aws_ssm_parameter" "db_password" {
  name            = "/prod/app/db_password"
  with_decryption = true
}

resource "aws_db_instance" "this" {
  password = data.aws_ssm_parameter.db_password.value
}

with_decryption 漏写会拿到 KMS 密文,apply 能过,应用连不上库。参数存成 SecureString 才有加密。

3.3 Secrets Manager

plain 复制代码
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/app/db"
}

locals {
  db = jsondecode(data.aws_secretsmanager_secret_version.db.secret_string)
}

resource "aws_db_instance" "this" {
  username = local.db.username
  password = local.db.password
}

一次调用取回整个 JSON,用户名和密码在同一个密钥里,轮转时一起换。

3.4 Vault KV v2

plain 复制代码
data "vault_kv_secret_v2" "db" {
  mount = "kv"
  name  = "prod/app/db"
}

resource "aws_db_instance" "this" {
  password = data.vault_kv_secret_v2.db.data["password"]
}

Vault 的认证走 VAULT_TOKEN 或 AppRole,provider 配置里别写死 token。

3.5 接入链路

latex 复制代码
SSM SecureString ──┐
Secrets Manager ───┼──▶ data source ──▶ resource 参数 ──▶ state 明文
Vault KV v2 ───────┘

三个 data source 的取值属性在 provider schema 里都标了 sensitive,plan 里同样显示 (sensitive value)。这一点由 provider 决定,不需要自己加。

多个 root 读同一个密钥时,改一次就全部生效,省掉逐仓库同步的麻烦。

4. state 后端与权限

4.1 S3 后端配置

plain 复制代码
terraform {
  backend "s3" {
    bucket         = "acme-tfstate"
    key            = "prod/app/terraform.tfstate"
    region         = "ap-east-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:ap-east-1:123456789012:key/abcd"
    dynamodb_table = "tfstate-lock"
  }
}

桶开版本化,配一条拒绝非 TLS 请求的桶策略。删错 state 时版本化是唯一的回滚手段。

4.2 读权限等于密钥权限

能读 state 的人等于能读全部密钥。权限收窄到 CI 的 role 和两三个运维账号,别给整个开发组。

桶策略按 role 收窄,前缀限定到环境目录:

json 复制代码
{
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::123456789012:role/gha-terraform"},
  "Action": ["s3:GetObject", "s3:PutObject"],
  "Resource": "arn:aws:s3:::acme-tfstate/prod/*"
}

审计靠 S3 访问日志和 CloudTrail 的 GetObject 事件,别等到出事才想起查。

5. CI 中的凭据注入

5.1 OIDC 换取临时凭据

yaml 复制代码
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/gha-terraform
      aws-region: ap-east-1

长期 AK 写在仓库 Secret 里,一旦泄露就是永久有效。OIDC 换来的凭据默认 1 小时过期,泄露窗口小得多。

Role 的 trust policy 要限定到具体仓库和分支,放开给整个 org 等于谁都能 assume。

5.2 日志遮蔽

yaml 复制代码
- run: echo "::add-mask::${{ steps.secret.outputs.value }}"

外部命令的输出也可能带出密钥。set -x 打开调试时尤其要小心,脚本里对敏感变量加遮蔽。

5.3 环境变量注入

bash 复制代码
export TF_VAR_db_password="$(aws secretsmanager get-secret-value \
  --secret-id prod/app/db --query SecretString --output text | jq -r .password)"

TF_VAR_ 前缀的变量会直接填进对应变量,省掉 tfvars。代价是密码进了进程环境和 shell 历史,只在临时排障时用。

6. 轮转与 Terraform 的配合

6.1 轮转后的刷新路径

latex 复制代码
轮转任务 ─▶ AWSCURRENT 换新版本
                  │
                  ├─▶ 应用下次启动读到新密码
                  └─▶ terraform refresh 读到新值 ─▶ plan 报 password 变更

data source 在 refresh 阶段重新读,轮转一次,下一次 plan 就显示 password 变更。

6.2 谁负责改库密码

策略 写法 适用
Terraform 接管 不写 ignore_changes,apply 时同步改库 密码只由 Terraform 改
轮转器接管 ignore_changes = [password] Secrets Manager 自动轮转
plain 复制代码
resource "aws_db_instance" "this" {
  password = local.db.password

  lifecycle {
    ignore_changes = [password]
  }
}

两边都管会打架:轮转器改了密码,Terraform 下一次 apply 又按 state 里的旧值改回去,应用随即连不上。

7. 常见报错

报错 / 现象 原因 处理
Output refers to sensitive values 输出引用了敏感值没标 加 sensitive = true
Invalid value for "v" parameter 敏感值进了 for 或函数参数 用 nonsensitive() 明确降级
apply 成功但应用连不上库 SSM 漏 with_decryption 补上并重新 plan
error reading SSM Parameter 参数名、区域或 ssm:GetParameter 权限缺失 核对 ARN 与 IAM 策略
plan 每次都报 password 变更 轮转器改了值,state 里是旧的 加 ignore_changes 或交给 Terraform 管
state 里能搜到明文密码 设计如此,sensitive 不管存储 收窄 state 读权限,开 KMS 加密

排查顺序先看 plan 里是 (sensitive value) 还是明文,再确认 data source 读到的版本对不对。

8. 检查清单

  • 仓库里没有 *.tfvars 与带 default 的敏感变量
  • 敏感变量与输出都标了 sensitive
  • 密钥全部来自 SSM / Secrets Manager / Vault,代码里无字面量
  • state 后端开了加密、版本化,读权限只给 CI role
  • CI 用 OIDC 换临时凭据,没有长期 AK
  • 密码的修改责任只有一方:Terraform 或轮转器

9. 关联文章

10. 参考

相关推荐
Z21-X1 小时前
云原生运维学习笔记:揭开虚拟化的硬核面纱,从底层架构到KVM实战
运维·笔记·学习·云原生
mengge.cloud1 小时前
1022小白入门教程(番外筑基)— nerdctl 容器基本功 + K8s 资源编写
linux·网络·云原生·容器·kubernetes·云计算·k8s
FY@-@1 小时前
云计算基础(上):从传统 IT 到 IaaS/PaaS/SaaS
云原生·云计算·paas
AKAMAI1 小时前
PHP 爆出两大高风险漏洞!Akamai 即时部署防护机制
人工智能·云计算
March.s2 小时前
Kubernetes 控制器深度指南:从 ReplicaSet 到 DaemonSet
云原生·容器·kubernetes
你看我名字2 小时前
Kubernetes HPA 自动扩缩容详解:指标、策略、资源与常见故障
云原生·容器·kubernetes
金仓社区的KK2 小时前
数据库主备切换后,业务恢复了吗?五层验收清单
数据库·云计算·软件构建
一级新生2 小时前
Dify 日常使用手册(小白版)
人工智能·docker·云原生·eureka·dify·ai工具
Z21-X2 小时前
云原生运维学习笔记:KVM实战——从环境搭建到热迁移的硬核体验
运维·笔记·学习·云原生