Terraform 秘密管理指南

infisical 发布于 2026-07-11 08:08 阅读 20

本文详细介绍了Terraform中秘密管理的核心问题和解决方案。

Blog image

发布于 2026 年 7 月 9 日,星期四

在基础设施即代码中保护秘密安全

Terraform 是一个出色的工具。与其在控制台中点击并运行一次性设置脚本,不如用代码描述整个基础设施:三个应用服务器、一个 Postgres 数据库、一个用于上传的存储桶。Terraform 读取这些描述,检查已有资源,然后创建、更新或删除必要的资源,使实际状态与描述匹配。

为此,它需要访问你的基础设施。因此你向它提供凭据:一个用于创建服务器和存储桶的云密钥,以及一个用于设置 Postgres 数据库的管理员密码。

然后,Terraform 将这些秘密以明文形式写入磁盘上的 Terraform 状态文件中。

这带来了双重问题。状态文件会四处传播:它们被复制到笔记本电脑上,通过远程后端共享,并作为 CI 产物存储,每一次复制都携带着秘密。当秘密需要更改时,所有包含该副本的文件都必须随之更改。

用 Terraform 保护秘密安全意味着解决一个特定问题:在不沿途以明文形式存储任何秘密的前提下,给予 Terraform 所需的访问权限。解决方法是将其正值保存在专用的秘密管理平台中,向 Terraform 提供引用而非实际值,并利用较新的机制,使值在运行过程中传递而不被写入状态。

这需要进行一些设置,但回报丰厚:一旦秘密管理器接入,添加一个秘密只需一次写入,轮换一个秘密只需一次更改,之后无需从 Git 或状态中清理任何内容。

Terraform 秘密的问题是什么?

Terraform 处理的任何秘密,无论是你传入的凭据还是 Terraform 生成的值,都会以明文形式写入其状态文件。任何能读取该文件的人都能读取该秘密。

让我们看看这是如何发生的。以下是一个配置,它不涉及云提供商、数据库或任何真正的秘密。它只是生成一个随机密码并将输出标记为敏感:

terraform {
  required_version = ">= 1.10"
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

resource "random_password" "db" {
  length  = 20
  special = true
}

output "db_password" {
  value     = random_password.db.result
  sensitive = true
}

执行 apply,Terraform 的行为完全符合预期。该值在通常打印的所有地方都被隐藏了:

$ terraform apply -auto-approve
random_password.db: Creating...
random_password.db: Creation complete after 0s [id=none]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

db_password = <sensitive>

到目前为止,一切安全。计划中是 <sensitive>,输出中也是 <sensitive>。但是,如果我们用 grep 搜索 Terraform 刚刚写入当前目录的文件,我们会看到一些令人担忧的东西:

$ grep -n '"result":' terraform.tfstate
35:            "result": "YBG$nq2(UQjIBVSyekYB",

就在这里。Terraform 非常小心地不打印的密码,却以明文形式躺在 terraform.tfstate 中。而且不止一处:

$ grep -n -A1 '"db_password"' terraform.tfstate
7:    "db_password": {
8-      "value": "YBG$nq2(UQjIBVSyekYB",

资源属性被存储了,输出也被存储了,状态还额外携带了同一密码的 bcrypt 哈希值。sensitive = true 标志控制了值的显示方式,但对存储方式毫无影响。

这就是整个问题的缩影。状态默认是明文的 JSON。每个资源的每个属性都写入其中,Terraform 需要这样:状态是它了解已有资源的途径,以便计算下一次差异。这种泄露是 Terraform 正常工作时的副作用。

秘密是如何进入状态文件的?

秘密进入状态的方式有几种:

  • 你传入的值。资源的密码参数,或提供商的 access_key。
  • Terraform 生成的值。上面的 random_password 是最清晰的例子:Terraform 创建了秘密,所以它当然会存储它。
  • 你读取的值。数据源获取信息并将其记录在状态中,以便未来的计划稳定。如果该信息是秘密,它现在也在状态中。
  • 输出。输出也存储在状态中,这就是为什么从秘密派生的输出本身就是一个静态的秘密。

注意 sensitive = true 不在此列表中,因为它不改变任何上述情况。

这到底是不是一个大问题?是的。状态文件会到处出现。它们落在共享的远程后端中,被复制到笔记本电脑上用于快速执行 terraform state 命令,作为 CI 产物出现,或者被某个没有意识到其中内容的人提交。每一个都是你秘密的副本,这就是秘密扩散的开始。

那么,你需要做什么?让真正的秘密值远离状态。这意味着要处理 Terraform 处理的两种秘密:它管理的秘密,比如它在基础设施上设置的数据库密码和 API 密钥;以及它使用的凭据,即让 Terraform 本身能够调用你的云和你的秘密管理器的密钥。

在 plan/apply 时拉取秘密

我们在第一个例子中使用了 random_password,因为它保持了演示的自包含性:Terraform 生成了秘密,因此不需要任何真实的东西。实际项目通常反过来。数据库密码或 API 密钥已经存在,并且被直接写入配置中:

locals {
  db_user     = "app_user"
  db_password = "correct-horse-battery-staple"   # 存在于每个克隆和 Git 历史中
}

这是需要消除的模式。密码位于你的仓库中,因此每个对仓库有读取权限的人都拥有它,并且即使你删除了那一行,它也会在 Git 历史中永久保留。轮换它意味着一次代码更改。

解决方法是将值移到 Infisical(一个开源秘密管理器)中,并在 Terraform 运行时让其获取。Infisical 将秘密存储为键值对,按项目、环境(开发、预发布、生产)和文件夹路径组织,所有读取它们的人或机器都使用自己的身份和权限进行身份验证。对于本次运行,这意味着上述两个值保存在 Infisical 项目中,作为名为 DB_USERDB_PASSWORD 的秘密,位于名为 /database 的文件夹中,并从代码中删除。

你使用机器身份Infisical 提供商进行身份验证,然后使用 infisical_secrets 数据源读取秘密:

terraform {
  required_providers {
    infisical = {
      source  = "infisical/infisical"
      version = "~> 0.16"
    }
  }
}

provider "infisical" {
  # host 默认为 https://app.infisical.com;如果是自托管则设置它。
  auth = {
    universal = {
      # 两个变量默认都为 null,此时提供商会回退
      # 到下面显示的 INFISICAL_UNIVERSAL_AUTH_* 环境变量。
      client_id     = var.infisical_client_id
      client_secret = var.infisical_client_secret
    }
  }
}

data "infisical_secrets" "backend" {
  env_slug     = "dev"
  workspace_id = var.infisical_workspace_id
  folder_path  = "/database"
}

locals {
  db_user     = data.infisical_secrets.backend.secrets["DB_USER"].value
  db_password = data.infisical_secrets.backend.secrets["DB_PASSWORD"].value
}

## 从秘密派生的输出本身必须标记为敏感,否则 apply 会出错。
output "db_user" {
  value     = local.db_user
  sensitive = true
}

你需要设置通用认证。完成这些后,运行过程会以正确的方式平淡无奇:

$ terraform apply -auto-approve -var infisical_workspace_id=<project-id>
data.infisical_secrets.backend: Reading...
data.infisical_secrets.backend: Read complete after 0s

Changes to Outputs:
  + db_user = (sensitive value)

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

db_user = <sensitive>

Reading...Read complete 行表示拉取操作:Terraform 在运行过程中调用了 Infisical 的 API 并拉取了值。没有构建任何东西,因此 0 added, 0 changed, 0 destroyed。它是一个数据源,所以只读取。之前的两个 locals 仍然存在,输出打印为 <sensitive>。改变的是值存在的位置。

以下是这一步带来的收益和未解决的问题:

硬编码 数据源
秘密所在位置 你的仓库 Infisical,背后有访问控制、审计日志和轮换
Git 中有什么 值本身 只有坐标:项目、环境、文件夹路径、键名
如何轮换 编辑代码、提交、重新部署 在 Infisical 中轮换,下一次 plan 会获取
状态中有什么 仍然是值

最后一行是关键。数据源会记录它读取的内容到状态中,因此本次运行后,状态文件将以明文形式保存这两个值,就像第一个例子一样。大约 2 KB 的状态,密码在 resources 下,输出就在其 "sensitive": true 标志旁边。

你解决了秘密在代码中的问题:值存在于一个地方,更改它只需在 Infisical 中更改,无需编辑你的仓库。但你还未解决秘密在状态中的问题。为此,要么加固存储状态的后端,要么使用临时资源将值完全排除在状态之外。

使用临时资源将秘密排除在状态之外

Terraform 1.10 引入了临时资源:在运行期间获取并刻意永不写入状态或计划文件的值。这解决了上一张表格中的最后一行问题。

在 Infisical 中的设置为此改变了形状:一个名为 DB_CREDENTIALS 的单个秘密,保存在项目根目录下,其值是一个包含 hostusernamepassword 键的 JSON 对象。提供商通过 ephemeral 块读取它:

ephemeral "infisical_secret" "db_creds" {
  name         = "DB_CREDENTIALS"
  env_slug     = "dev"
  workspace_id = var.infisical_workspace_id
  folder_path  = "/"
}

locals {
  credentials = jsondecode(ephemeral.infisical_secret.db_creds.value)
}

provider "postgresql" {
  host     = local.credentials["host"]
  username = local.credentials["username"]
  password = local.credentials["password"]
}

将其放在数据源配置旁边,工作上的区别在于块类型:ephemeral 代替了 data,加上单数的 infisical_secret,它读取一个命名的秘密而不是一个文件夹。然后 jsondecode 将 JSON 值解包到本地变量中。

postgresql 提供商就是这些凭据的去向。这个配置中没有创建数据库资源,所以 apply 不会显示任何 postgres 活动。它会显示临时生命周期:

$ terraform apply -var infisical_workspace_id=<project-id>
ephemeral.infisical_secret.db_creds: Opening...
ephemeral.infisical_secret.db_creds: Opening complete after 0s
ephemeral.infisical_secret.db_creds: Closing...
ephemeral.infisical_secret.db_creds: Closing complete after 0s

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

那些 Opening...Closing... 行就是临时生命周期:为运行而获取,在运行结束时显式丢弃。比较数据源运行,它显示 Reading... 并保留了读取的内容。这个获取有一个结束。

然后是最重要的部分。像之前那样 grep 状态文件:

$ grep -i password terraform.tfstate
$ grep -iE 'credential|db_creds' terraform.tfstate
$

什么都没有。整个状态文件只有 181 字节:

$ cat terraform.tfstate
{
  "version": 4,
  "terraform_version": "1.10.5",
  "serial": 1,
  "lineage": "565cc4d1-3241-d9aa-0f87-c89437862f48",
  "outputs": {},
  "resources": [],
  "check_results": null
}

真实的凭据从与数据源运行相同的项目中获取,用 jsondecode 解码,并可用于配置提供商。状态中没有记录任何发生过的痕迹。数据源版本的这段代码在状态中留下了 2 KB 和两个值,但这一个没有留下任何东西。

临时值有意受到限制:它们只能流入其他临时上下文,如提供商配置、其他临时资源或只写参数。这个限制是关键。它让 Terraform 保证值永远不会落在持久化的地方。如果你尝试将临时值路由到普通资源属性或常规输出,Terraform 会阻止你。

这些只写参数也关闭了前面列表中的第二条路径。random 提供商提供了一个 random_password 的临时变体,越来越多的资源接受只写属性,这些属性消费值而不存储它:

ephemeral "random_password" "db" {
  length = 24
}

resource "aws_db_instance" "app" {
  # ...
  password_wo         = ephemeral.random_password.db.result
  password_wo_version = 1
}

密码是临时生成的,传递给只写参数 password_wo,并且永远不会在两端的状态中落地。

在数据源和临时资源之间做出选择时,经验法则很简单:如果一个值只需要在运行期间存在,用于配置提供商或进行身份验证,就让它成为临时的。只有当你确实需要将值具体化时才使用数据源,并且在这种情况下加密你的状态。

将变量和输出标记为敏感

自第一个例子以来,我们一直在使用 sensitive = true,但它到底做了什么?它做一件事:从 Terraform 显示中隐藏该值。它适用于变量和输出:

variable "db_admin_password" {
  type      = string
  sensitive = true
}

## 来自数据源示例
output "db_user" {
  value     = local.db_user
  sensitive = true
}

一个敏感变量在计划中任何出现的地方都会显示为 (sensitive value),敏感输出打印为 <sensitive>。这是一个真正的好处,让秘密远离屏幕和 CI 日志。有时它不是可选的。从敏感值派生的输出在未标记为敏感时无法 apply,这就是数据源示例中标记 db_user 的原因。

这有两个限制:

  • 它不抵抗请求。对第一个例子运行 terraform output db_password,它会按需打印值。
  • 它不影响存储。grep 在 terraform.tfstate 中以明文形式找到了该值,因为 sensitive 既不加密状态,也不阻止任何内容进入状态。

把它当作显示卫生,你应该始终开启它,但它是建立在真正保护之上的,绝不能替代它们。

安全处理提供商凭据

到目前为止,所有内容都涵盖了第一种秘密,即 Terraform 管理的那些。提供商凭据是第二种,并且是设置中价值最高的秘密,因为持有它们的人通常可以访问其他秘密保护的所有内容。

来自硬编码示例的规则同样适用。以下是它读取的两个变量:

export INFISICAL_UNIVERSAL_AUTH_CLIENT_ID=...
export INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET=...

在笔记本电脑上,导出的变量没问题。在 CI 中,它们又成为一个问题:客户端密钥必须存储在某个地方供作业读取,而存储在 CI 平台中的秘密是又一个需要保护的常驻凭据。

解决方法是不拥有一个。GitHub Actions 可以为每个作业生成一个短期的 OIDC Token,而配置了 OIDC 认证 的 Infisical 机器身份接受该Token作为询问者身份的证明。Infisical 的 secrets action 处理交换,这与如何在 CI/CD 流水线中管理秘密中介绍的模式相同:

name: terraform-plan

on:
  pull_request:

permissions:
  id-token: write   # OIDC 必需
  contents: read

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: 从 Infisical 获取秘密
        uses: Infisical/secrets-action@v1.0.9
        with:
          method: "oidc"
          identity-id: "your-machine-identity-id"  # 公共标识符,可安全提交
          project-slug: "your-project-slug"
          env-slug: "prod"

      - run: terraform init
      - run: terraform plan

将工作流作为序列来理解:

  1. 作业启动,GitHub 为其生成一个短期 OIDC Token,由 id-token: write scope 授权。
  2. 操作将该Token呈现给 Infisical。
  3. Infisical 根据身份的绑定检查其声明,绑定主题类似于 repo:your-org/your-repo:ref:refs/heads/main,因此只有该仓库和分支可以认证。
  4. 它返回短期访问权限,并且作业允许的秘密被注入为环境变量,当作业结束时这些变量消失。

两边都没有存储的秘密。工作流中的 identity-id 是一个公共标识符,可以安全提交,因为知道它本身不授予任何权限,除非有匹配绑定的Token。

Terraform 的关联点是 TF_VAR_ 约定。在 Infisical 中存储一个秘密为 TF_VAR_db_admin_password,然后计划步骤将其读取为 var.db_admin_password,而无需在命令行上传递任何内容。

Terraform 在 CI 中通常也需要云凭据,同一个Token可以覆盖。作业的 OIDC Token可以通过 aws-actions/configure-aws-credentials 交换为有范围的 AWS 角色,因此 GitHub 中也没有长期存在的云密钥。

无论在哪里,原则都是一样的:凭据应该从运行环境到达 Terraform,而不是从它运行的代码。

加固状态后端

状态记录了每个资源的每个属性,无论是否是秘密。即使你做得正确,某些秘密值最终也会在那里:你选择读取的数据源值,因为该值需要持久化;或者当 Terraform 创建资源时返回的凭据,例如 IAM 访问密钥。临时资源还不能覆盖所有情况。

当秘密必须存在于文件中时,保护文件。首先从它存储的地方开始。

到目前为止,每次运行都将状态写到了本地的 terraform.tfstate。这就是 grep 读取的文件。团队通常使用远程后端来存储状态,通常是一个存储桶,而这个存储桶就是你需要加固的内容。四个属性很重要:加密、版本控制、锁定、仅由运行 Terraform 的身份可读。

在 AWS 上,这是带有 KMS 密钥的 S3:

terraform {
  backend "s3" {
    bucket       = "org-terraform-state"
    key          = "network/prod.tfstate"
    region       = "us-east-1"
    use_lockfile = true   # 锁定:不能同时运行两个 apply
    encrypt      = true   # 静态加密
    kms_key_id   = "arn:aws:kms:us-east-1:123456789012:key/abcd-1234"  # 使用你自己的密钥,而不是 S3 默认密钥
  }
}

该块处理加密和锁定。另外两个属性位于存储桶上:启用 S3 版本控制 以便覆盖或损坏的状态可以恢复,并限制存储桶的 IAM 范围,以便只有运行 Terraform 的身份才能读取它。

在其他云上,GCS 后端通过 kms_encryption_key 使用 Cloud KMS 密钥,并在存储桶上启用对象版本控制;Azure Blob 通过 use_oidc 使用 Entra ID 进行身份验证。

提供商凭据规则同样适用于后端,而且更严格:后端配置会被复制到 .terraform/ 中并捕获到计划文件中。后端凭据来自环境或上述 CI 流程,绝不应内联或作为 -backend-config 值传入。

完成这些后,数据源交易的权衡就不同了。值仍然会落在状态中,但状态现在是存储在已锁定、版本控制的存储桶中的密文,只有你的运行身份才能打开。

在可重用模块内管理秘密

模块是 Terraform 代码复用的方式:你编写一次数据库设置,然后每个团队和环境用自己的输入调用该模块。复用是重点,但这也正是秘密扩散的原因,因为需要凭据的模块必须从某处获取它。明显的途径是输入变量:

module "service" {
  source      = "./modules/service"
  db_password = var.db_password  # 每个调用者都必须持有并传递原始值
}

这加剧了处理问题。每个使用该模块的配置都必须获取密码并传入,并且一旦模块使用它,该值就会被记录在该配置的状态中,因此一个秘密最终被复制到每个调用该模块的堆栈中。

解决方法是传递坐标,让模块自己获取值:

module "service" {
  source       = "./modules/service"
  workspace_id = var.infisical_workspace_id
  env_slug     = "prod"
  secret_path  = "/services/api"
}

在模块内部,之前相同的 infisical_secrets 数据源会读取该路径下的秘密,或者一个临时块可以在不触及状态的情况下做到。调用者传递一个位置。值只在其使用的地方存在,并且访问由 Infisical 控制,而不是由谁拥有变量来控制。

超越静态秘密:动态秘密和轮换

到目前为止,所有内容都在保护静态秘密:一个在有人更改之前一直有效的单一值。妥善保护它仍然值得在它存活期间被窃取。有两种方法可以缩小这个窗口,在秘密自动化:轮换 vs. 动态秘密中有更深入的介绍。动态秘密是按需创建并自动过期的。轮换保持一个常驻凭据,但按计划替换它。

动态秘密

动态秘密是按需生成并根据你设置的计划过期的。Infisical 不是保持一个单一的数据库密码,而是在每次需要时创建一个新的数据库用户,并在其租约到期时删除它(动态秘密是企业功能)。

每个后端有一个动态秘密资源(例如,SQL 数据库的 infisical_dynamic_secret_sql_database),有 AWS IAM、Kubernetes 和 MongoDB 的变体。

为了让 Infisical 能按需创建和删除数据库用户,它必须用一个可以管理用户的账户登录到数据库本身。数据源提供该登录信息。它从之前使用的 DB_CREDENTIALS 秘密中读取管理员凭据,因此密码永远不会出现在这个文件中。资源包含指令:连接详细信息,创建用户时运行的 SQL,以及删除用户时运行的 SQL:

data "infisical_secrets" "db" {
  env_slug     = "dev"
  workspace_id = var.infisical_workspace_id
  folder_path  = "/"
}

locals {
  creds = jsondecode(data.infisical_secrets.db.secrets["DB_CREDENTIALS"].value)
  # 角色管理 DDL 需要直接端点,而不是连接池。
  db_host = replace(local.creds.host, "-pooler", "")
}

resource "infisical_dynamic_secret_sql_database" "db" {
  name             = "postgres-dynamic"
  project_slug     = var.infisical_project_slug   # 是 slug,不是 ID
  environment_slug = "dev"
  path             = "/"
  default_ttl      = "1h"
  max_ttl          = "4h"

  configuration = {
    client   = "postgres"
    host     = local.db_host
    port     = "5432"
    database = local.creds.database
    username = local.creds.username   # 用于创建用户的特权账户
    password = local.creds.password

    creation_statement = <<-EOT
      CREATE USER "{{username}}" WITH ENCRYPTED PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
      GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{username}}";
    EOT

    revocation_statement = <<-EOT
      REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";
      DROP ROLE "{{username}}";
    EOT
  }
}

应用此配置会将指令存储在 Infisical 中。它不会创建凭据。获取一个凭据是一个单独的步骤,生成一个租约:

  1. 在仪表板中,找到动态秘密并点击生成。
  2. 设置租约的 TTL,可以是配置中 max_ttl 以内的任何值。

Infisical 连接到数据库,运行 creation_statement,并返回一个新的用户名和密码以及一个过期时间。当租约过期或被删除时,运行 revocation_statement,用户消失。你可以在数据库端看到这一点:每一行租约都会在 pg_roles 中出现一行,并在过期时被移除。

应用程序实际使用的凭据是按租约生成并自动撤销的。泄露的凭据在几小时内就失效了,并且存储中没有长期存在的密码供攻击者寻找。

秘密轮换

有些系统无法使用短期用户,需要一个单一的常驻登录。对于这些系统,退路是轮换:凭据保持不变,但其密码按计划被替换,因此被窃取的密码在下一次轮换前就失效了。轮换包含在专业版中。

Infisical 使用两个数据库用户而不是一个来实现这一点。在任何时刻,其中一个是在线登录。当需要轮换时,Infisical 在空闲用户上设置新密码,并将在线秘密切换到该用户。在交换期间不会中断,因为之前的登录在消费者迁移期间保持有效。

在 apply 之前需要两样东西:

  • 两个数据库用户。使用临时密码创建它们,Infisical 会在轮换开始时替换这些密码。
  • 一个应用连接:Infisical 存储的对数据库的访问,与动态秘密所需的访问相同,这次在组织设置下保存一次,并作为 connection_id 传入。

资源命名这两个用户,以及 Infisical 保持更新为当前在线登录的两个秘密键:

resource "infisical_secret_rotation_postgres_credentials" "db" {
  name          = "postgres-rotation"
  project_id    = var.infisical_project_id   # 是 ID,不是 slug
  environment   = "dev"
  secret_path   = "/database"
  connection_id = var.postgres_connection_id # 应用连接,必须已存在

  parameters = {
    username1 = "infisical_user_1"   # 轮换在这两个角色之间交替
    username2 = "infisical_user_2"
  }

  secrets_mapping = {
    username = "POSTGRES_DB_USERNAME"  # 消费者始终读取这些键
    password = "POSTGRES_DB_PASSWORD"
  }

  auto_rotation_enabled = true
  rotation_interval     = 30  # 天
}

应用它,传入项目 ID 和应用连接 ID:

$ terraform apply -auto-approve \
    -var infisical_project_id=<project-id> \
    -var postgres_connection_id=<app-connection-id>

Plan: 1 to add, 0 to change, 0 to destroy.
infisical_secret_rotation_postgres_credentials.db: Creating...
infisical_secret_rotation_postgres_credentials.db: Creation complete after 1s [id=e19d74b0-0be6-432f-bf7d-65b91a571467]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

轮换在创建时立即触发。映射的秘密名称出现在 /database 中,与现有的秘密并列:

DB_PASSWORD
DB_USER
POSTGRES_DB_PASSWORD    <- 新增,由轮换写入
POSTGRES_DB_USERNAME    <- 新增,由轮换写入

为了验证轮换是否工作,我们读取了两个新秘密,并使用它们登录到数据库。登录成功,以 infisical_user_1 身份,密码为 Infisical 生成的 48 个字符。我们为用户创建的临时密码已经被替换。从现在起,Infisical 会设置密码,没有人需要知道它们。

应用程序永远不会看到交换。它们读取 POSTGRES_DB_USERNAME 和 POSTGRES_DB_PASSWORD,获得当前在线的登录信息,并且密码更改时无需重新部署。

审查中值得注意的反模式

大多数 Terraform 秘密泄漏都可以追溯到几个反复出现的习惯,一旦你命名它们,在代码审查中就很容易发现:

反模式 为什么失败 更好的模式
sensitive = true 作为唯一控制 隐藏显示,但值仍然会进入状态和计划 临时资源,或在 Terraform 外部检索
通过数据源读取秘密值 有效负载被写入状态 临时资源,或当你无法读取值时加固后端
静态、长期存在的 CI 凭据 静态爆炸半径和痛苦的轮换 OIDC 和短期身份
未加密、未版本控制或未锁定的状态 静态明文、弱恢复、竞争条件 KMS 加密、版本控制、锁定、最小权限访问

选择 Terraform 秘密管理器

一旦真实值离开你的配置,它们就需要一个地方存放。实际的选择是在 Terraform 运行时与之对话的秘密服务和随仓库移动的加密文件之间。云 KMS 位于两者之下,加密状态和存储本身,但仅凭密钥管理无法提供对单个秘密的访问控制、审计追踪或轮换。

对于 Terraform 具体需要权衡什么:

  • 注册表上有官方提供商,这样秘密平台本身可以和其他东西一样作为代码管理。
  • 临时读取,这样在计划时获取秘密不会写入状态。
  • 动态秘密和轮换,这样一开始就没有那么多常驻凭据。
  • 托管和许可条款,如果你受限于自托管或开源。

以下是解决此问题的工具选项:

选项 它是什么 配合 Terraform 使用 值得了解的
Infisical 开源秘密平台,云或自托管 官方提供商。临时秘密读取、动态秘密、轮换和同步均以代码管理 在一个平台中涵盖静态存储、动态凭据、轮换和向外同步
HashiCorp Vault 长期存在的企业级产品,自管理或 HCP 官方提供商,带有临时资源 自运行操作负担重。自 2023 年起采用 BUSL 许可,现属于 IBM
云秘密管理器 每个云的原生存储(AWS Secrets ManagerAzure Key VaultGoogle Secret Manager 第一方提供商,带有数据源和临时资源 单云设计,覆盖三个云意味着运行三个
SOPS 文件加密,不是服务 社区提供商在计划时解密提交的文件 没有自己的访问控制、审计日志或轮换

这里存在功能重叠。Infisical 和 Vault 都会发布动态秘密,并且都会轮换凭据。最终重要的区别是托管、许可以及你想做多少运维工作;请参阅顶级 HashiCorp Vault 替代品进行更全面的比较。

SOPS 的定位

SOPS 会在这个讨论中出现,但它不是秘密管理器,而是文件加密。没有访问控制、审计日志或轮换。你使用 age、PGP 或云 KMS 密钥加密 YAML 或 JSON 文件中的值,然后提交密文。键保持可读,值不可读,因此文件存在于 Git 中,并且差异仍然显示更改了什么。

SOPS 是补充而非替代。如果你的 GitOps 流程需要在版本控制中有加密文件本身,请使用 SOPS 处理该文件,加固保存状态的后端,并将运行时凭据保存在可以轮换它们的系统中。

需要改变的默认设置

这些技术中没有一项是沉重的。转变主要在于默认设置:

  • 将真实值保存在 Infisical 中,而不是 .tf 文件或 .tfvars 中。
  • 对于任何只需要在运行期间存在的东西,优先使用临时资源,因为它们永远不会触及状态。
  • 当你必须具体化一个值时,使用数据源,并在这样做时加密你的后端。
  • sensitive = true 当作显示卫生,始终开启,但不要作为承重用途。
  • 将提供商凭据保存在环境中,并在 CI 中优先使用短期 OIDC。
  • 加密、版本控制和锁定保存状态的后端。
  • 向模块传递秘密引用,而不是秘密值。
  • 在可能的情况下,使用动态秘密彻底消除常驻秘密,并轮换那些无法消除的。

做到这些,你的状态文件就会回到它本应有的样子:你构建了什么的一份记录,而不是你的凭据列表。

  • 原文链接: infisical.com/blog/terra...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论