TT Lab
开始
学习 学习路径 课程

基础设施即代码

明明标了 sensitive,状态文件里却留着明文密码

在 TT Lab 中继续学习

目标

亲自找出工具生成的密码在状态、JSON 输出和已保存的计划中的哪些位置以明文残留,把 OpenTofu 的状态与计划加密从现有的明文状态迁移过来并强制执行,然后构建一种把密钥放在状态之外的配置。

为什么重要

sensitive 只是在人看的界面上遮住值的标记,工具保存的记录里仍然原样包含这个值。状态文件和 CI 上传的计划文件,实际上就成了密钥存储,不知道这一点,就会抱着“遮住了所以安全”的错觉,把状态随便放在任何地方。防范的办法有三层:存储时加密(密钥就成了状态的钥匙)、强制禁止退回明文,以及从一开始就不把值交给工具、只传递引用的设计。

步骤

  1. 在 /root/iac-secret/main.tf 中放置 random_password.db(长度 20,无特殊字符)、把它的值以 password=<값>(占位符为密码值)写入 out/db.conf 的 local_sensitive_file.dbconf,以及 sensitive = true 的输出 db_password。执行 init·apply 之后,确认 tofu output 中的值是被遮住显示的。
  2. 找出状态文件中所有包含密码值的位置,把 JSON 路径按每行一个写入 /root/iac-secret/leak.txt。路径的格式是用点把键和数组序号连起来(例如 resources.0.instances.0.attributes.x)。评分器会对通过 tofu state pull 获得的状态做同样的计算并进行比较。
  3. 把 tofu output 的输出保存到 /root/iac-secret/masked.txt。然后用 tofu plan -out=/root/iac-secret/leak.tfplan 保存计划,并统计计划文件(zip)内的 tfstate 条目中密码出现了多少次,用 planleak=<수>(占位符为数量)写入 /root/iac-secret/planleak.txt。
  4. 在 /root/iac-secret/encryption.tf 中声明敏感变量 state_passphrase 以及状态·计划加密:pbkdf2 密钥提供者(口令就是该变量)、aes_gcm 方法,并把用于读取现有明文的 unencrypted 方法设为 state·plan 的 fallback。口令为 labhub-state-passphrase-01,通过环境变量 TF_VAR_state_passphrase 传入。apply 之后,terraform.tfstate 必须是加密的(文件中没有密码,而有 encrypted_data)。
  5. 在 encryption.tf 中删除 unencrypted 方法和两个 fallback,并在 state·plan 中设置 enforced = true。使用正确的口令时,计划必须是干净的。用错误的口令(TF_VAR_state_passphrase=wrong-passphrase-000000)运行 tofu plan,把输出保存到 /root/iac-secret/wrongkey.txt。评分器会在副本中确认用错误的密钥读取确实会失败。
  6. 用正确的口令用 tofu plan -out=/root/iac-secret/safe.tfplan 保存计划。这个文件中不得有明文密码,也不能是 zip。确认有密钥时 tofu show -json safe.tfplan 能够执行,没有密钥时会失败。
  7. 用 openssl rand -hex 16 在 /root/iac-secret/ref/secret/db.key 中生成密钥,并把权限设为 600(在工具之外生成的密钥)。在 /root/iac-secret/ref/main.tf 中不放 random_password,只放向 out/db.conf 写入一行 password_file=<그 경로>(占位符为该路径)的 local_file.dbconf,然后执行 init·apply。评分器会检查 ref 的状态中任何位置都没有 db.key 的值,只有路径。

参考

生成密码,并把输出遮住

在 /root/iac-secret/main.tf 中放置 random_password.db(长度 20,无特殊字符)、把它的值以 password=<값>(占位符为密码值)写入 out/db.conf 的 local_sensitive_file.dbconf,以及 sensitive = true 的输出 db_password。执行 init·apply 之后,确认 tofu output 中的值是被遮住显示的。

random_password 的 result 会被 provider 标记为敏感值。要把这个值作为输出导出,输出上也必须加上 sensitive,计划才能通过。local_sensitive_file 只是不在界面上显示内容,写入文件的方式是完全一样的。

被遮住的密钥在状态文件的哪些位置

找出状态文件中所有包含密码值的位置,把 JSON 路径按每行一个写入 /root/iac-secret/leak.txt。路径的格式是用点把键和数组序号连起来(例如 resources.0.instances.0.attributes.x)。评分器会对通过 tofu state pull 获得的状态做同样的计算并进行比较。

可以用 tofu output -raw db_password 获得该值(这也证明了遮蔽只是显示方式)。用 jq 的 paths(scalars) 和 getpath 可以找出所有值相同的位置。不止一处。

遮蔽只在界面上有效——JSON 输出和已保存的计划

把 tofu output 的输出保存到 /root/iac-secret/masked.txt。然后用 tofu plan -out=/root/iac-secret/leak.tfplan 保存计划,并统计计划文件(zip)内的 tfstate 条目中密码出现了多少次,用 planleak=<수>(占位符为数量)写入 /root/iac-secret/planleak.txt。

已保存的计划文件是 zip,里面包含生成计划时的状态副本。用 unzip -l 查看条目,再用 unzip -p <파일> tfstate(占位符为文件)取出来统计。如果 CI 把计划文件作为制品上传,那个制品也就成了密钥。

把明文状态迁移为加密状态

在 /root/iac-secret/encryption.tf 中声明敏感变量 state_passphrase 以及状态·计划加密:pbkdf2 密钥提供者(口令就是该变量)、aes_gcm 方法,并把用于读取现有明文的 unencrypted 方法设为 state·plan 的 fallback。口令为 labhub-state-passphrase-01,通过环境变量 TF_VAR_state_passphrase 传入。apply 之后,terraform.tfstate 必须是加密的(文件中没有密码,而有 encrypted_data)。

如果只是开启加密,工具会拒绝读取现有的明文状态(因为它可能是被篡改的明文)。所以要先设置 fallback,使它能读取一次明文,然后应用,让它用新方法重新写入。口令必须不少于 16 个字符。

迁移完成后,去掉明文回退并强制执行

在 encryption.tf 中删除 unencrypted 方法和两个 fallback,并在 state·plan 中设置 enforced = true。使用正确的口令时,计划必须是干净的。用错误的口令(TF_VAR_state_passphrase=wrong-passphrase-000000)运行 tofu plan,把输出保存到 /root/iac-secret/wrongkey.txt。评分器会在副本中确认用错误的密钥读取确实会失败。

如果保留 fallback,即使有人塞进一份明文状态也能被读取。enforced 会阻止以明文写入这件事本身。密钥一旦丢失,状态就永远无法读取,所以保管密钥就是保管状态。

加密的计划文件没有密钥就无法读取

用正确的口令用 tofu plan -out=/root/iac-secret/safe.tfplan 保存计划。这个文件中不得有明文密码,也不能是 zip。确认有密钥时 tofu show -json safe.tfplan 能够执行,没有密钥时会失败。

把它与第 3 步的 leak.tfplan 并排,用 head -c 2 比较开头两个字节。zip 以 PK 开头。计划加密由 plan 块决定,与 state 是分开的。

密钥放在状态之外,状态中只放引用

用 openssl rand -hex 16 在 /root/iac-secret/ref/secret/db.key 中生成密钥,并把权限设为 600(在工具之外生成的密钥)。在 /root/iac-secret/ref/main.tf 中不放 random_password,只放向 out/db.conf 写入一行 password_file=<그 경로>(占位符为该路径)的 local_file.dbconf,然后执行 init·apply。评分器会检查 ref 的状态中任何位置都没有 db.key 的值,只有路径。

只要工具生成或读取了值,这个值就会进入状态。让使用该值的一方(应用)直接从文件或密钥存储中读取,工具只告知位置,是最简单的规避办法。注意,即使用数据源读取密钥文件,它也会留在状态中。