🔐

tfstateに平文を残さずに秘密情報を管理する

に公開

はじめに

クラシル社でSREをしているKaitoです。

弊社では開発環境を含む全ての環境でIaC (Terraform) を採用しており、秘密情報 (Secrets Manager, SSM Parameter Store など) については SOPS × KMS で管理しています。

従来の手法では、以下のようにリソースを設定することになります。

data "sops_file" "app" {
  source_file = "secrets/app.sops.yaml"
}

resource "aws_secretsmanager_secret_version" "app" {
  secret_id     = aws_secretsmanager_secret.app.id
  secret_string = data.sops_file.app.data["private_key"]
}

SOPS × KMS を利用することで、秘密情報を暗号化された状態で Git 管理できます。しかし、Terraform は通常の data source で読み取った値を state file (以下 tfstate) に保存するため、data.sops_file.app.data["private_key"] のように参照すると、SOPS で復号した平文がそのまま tfstate に書き出されてしまいます。variable / output に sensitive = true を付けたり、provider が sensitive 扱いする属性を経由したとしても、これは CLI 出力で値を伏せるだけで、tfstate には平文のまま書かれます。

Terraform stores values with the sensitive argument in both state and plan files, and anyone who can access those files can access your sensitive values.

https://developer.hashicorp.com/terraform/language/state/sensitive-data

同じことが terraform plan -out=tfplan で生成する saved plan file にも当てはまります。

If your plan includes any sort of sensitive data, even if obscured in Terraform's terminal output, it will be saved in cleartext in the plan file.

https://developer.hashicorp.com/terraform/cli/commands/plan

CodePipelineなどでapplyワークフローを組み、planファイルをS3アーティファクトに保存するような構成を取る場合、そのS3には平文の秘密情報が載ることになります。

なぜ問題か

tfstate / plan file に秘密情報が含まれることの何が問題かというと、state backend (S3 bucket) を読める権限だけで、そこに含まれる全ての秘密情報を閲覧可能になる からです。本来は「tfstate を読める」と「Secrets Manager の値を読める」は別の権限境界に置きたいはずですが、tfstate に平文があるとこの2つが1つにまとまってしまいます。

加えて、Secrets Manager から直接値を取得すれば CloudTrail に GetSecretValue のログが残るのに対し、tfstate 経由で読み取った場合は Secrets Manager の GetSecretValue としては追跡できません。state backend (S3) 側も CloudTrail data events を別途有効化していなければ GetObject 単位の読み取り監査ログは残らないため、「誰がいつどの secret を読んだか」を追う手段がほぼ失われます。

解決策の全体像

本記事では、ephemeral resource と write-only attribute (secret_string_wo) を組み合わせてこの問題を解決します。前者で SOPS 復号値が tfstate に乗らないようにし、後者で Secrets Manager への投入値も tfstate に乗らないようにします。

Before / After

既存の手法と対比すると以下のようになります。

- data "sops_file" "app" {
+ ephemeral "sops_file" "app" {
    source_file = "secrets/app.sops.yaml"
  }

  resource "aws_secretsmanager_secret_version" "app" {
-   secret_id     = aws_secretsmanager_secret.app.id
-   secret_string = data.sops_file.app.data["private_key"]
+   secret_id                = aws_secretsmanager_secret.app.id
+   secret_string_wo         = ephemeral.sops_file.app.data["private_key"]
+   secret_string_wo_version = 1
  }
  • data "sops_file"ephemeral "sops_file": SOPS 復号値を tfstate / plan file に書かない
  • secret_stringsecret_string_wo + _version: Secrets Manager への投入値を tfstate / plan file に書かない

必要なバージョン

この構成を利用するには Terraform 1.11+ / carlpett/sops 1.3+ / AWS Provider 5.88+ が必要です。ephemeral resource 自体は Terraform 1.10 から使えますが、write-only attribute の利用には Terraform 1.11 が必要なので、本記事の構成では 1.11+ が要件になります。

機能 提供元 最低バージョン 出典
Ephemeral resource Terraform core 1.10.0 (2024-11) v1.10.0 release notes
Ephemeral input variable / output Terraform core 1.10.0 (2024-11) 同上
Write-only attribute (core 側の対応) Terraform core 1.11.0 (2025-02) v1.11.0 release notes
ephemeral "sops_file" / ephemeral "sops_external" carlpett/sops provider 1.3.0 (2025-10) v1.3.0 release / PR #140
aws_secretsmanager_secret_version.secret_string_wo (write-only attribute) hashicorp/aws provider 5.88.0 (2025-02) v5.88.0 release notes

ephemeral resource とは

Terraform 1.10 で導入された ephemeral resource は、「Terraform実行中だけ存在し、tfstate や plan file に値を永続化しない特殊なリソース」です。

通常の resourcedata source は provider が返した attribute を tfstate に保存し、次回 plan 時の差分検出に使います。一方 ephemeral resource は state に書き込まない代わりに、plan / apply の各Terraform実行フェーズで再評価されます。

ここで注意したいのは、terraform plan -out=tfplan で plan file を保存して後段で terraform apply tfplan するワークフローを取っている場合でも、ephemeral 値は plan file に保存されず apply 時に再取得される という点です。CodePipeline で plan / apply を別ジョブに分けるような構成だと、plan 時点と apply 時点の間で SOPS ファイルや KMS の復号権限が変わると、保存済み plan が「plan 時点の秘密値」を固定してくれません。plan と apply の間で SOPS や KMS 権限が変動しないことを前提に運用設計する必要があります。

ephemeral 値には次の制約があります。

  • plan / apply の Terraform 実行中だけ メモリ上に存在し、終わると破棄される
  • tfstate にも plan file にも値が永続化されない
  • 値を参照できる場所が限定されている (下記)
ephemeral 値を扱える場所・宣言の例 (8パターン)
# (1) 別の ephemeral resource の argument
ephemeral "some_provider_thing" "x" {
  token = ephemeral.sops_file.app.data["api_token"]
}
# (2) resource の write-only attribute
resource "aws_secretsmanager_secret_version" "v" {
  secret_id                = aws_secretsmanager_secret.app.id
  secret_string_wo         = ephemeral.sops_file.app.data["private_key"]
  secret_string_wo_version = 1
}
# (3) locals ブロック (この local 自身も ephemeral 扱いになる)
locals {
  pem = ephemeral.sops_file.app.data["private_key"]
}
# (4) provider configuration ブロック
provider "aws" {
  assume_role {
    external_id = ephemeral.sops_file.app.data["external_id"]
  }
}
# (5) ephemeral = true を付けた input variable
variable "pem" {
  type      = string
  ephemeral = true
}
# (6) child module の ephemeral = true な output
output "pem" {
  value     = ephemeral.sops_file.app.data["private_key"]
  ephemeral = true
}
# (7) provisioner ブロック
resource "null_resource" "x" {
  provisioner "local-exec" {
    command = "deploy.sh"
    environment = {
      API_TOKEN = ephemeral.sops_file.app.data["api_token"]
    }
  }
}
# (8) connection ブロック
resource "null_resource" "y" {
  connection {
    type     = "ssh"
    host     = "..."
    password = ephemeral.sops_file.app.data["ssh_password"]
  }
  provisioner "remote-exec" {
    inline = ["echo deploy"]
  }
}

https://developer.hashicorp.com/terraform/language/block/ephemeral

ephemeralsensitive の違い

ephemeral と一緒によく使う sensitive ですが、両者は責務が分かれた直交した属性です。

属性 効果 効かない箇所
ephemeral = true tfstate / plan file に値を書かない CLI 出力 / error message / TF_LOG=debug
sensitive = true CLI 出力 / HCP Terraform UI で値を伏せる tfstate / plan file (平文で残る)

ephemeral だけだと CLI 出力やエラーメッセージで値が漏れる可能性があり、sensitive だけだと tfstate に平文が残ります。秘匿情報を扱う variable には両方付けるのが正解です。

# modules/aws/terraform_pr_plan/variables.tf
variable "github_app_private_key" {
  description = "GitHub App private key (PEM)"
  type        = string
  ephemeral   = true  # tfstate / plan file に値を残さない
  sensitive   = true  # CLI 出力 / エラーメッセージで値を伏せる
}

公式docs でも両者を組み合わせるのが推奨されています。

You can add the sensitive argument to variables ... that have the ephemeral argument to combine their benefits.

https://developer.hashicorp.com/terraform/language/manage-sensitive-data

write-only attribute とは

ephemeral 値は通常の resource の永続 attribute (例: secret_string) には渡せないため、その値を AWS 側に投入するには受け口がwrite-only attribute (例: secret_string_wo) である必要があります。AWS Provider では 5.88 から aws_secretsmanager_secret_version で write-only attribute (secret_string_wo) が使えるようになりました。同じく 5.88 で aws_db_instance.password_wo など他リソースの write-only 版も多数追加されています。

write-only attribute の特徴は次のとおりです。

  • AWS API へは値を 送信する (= Secrets Manager に書き込まれる)
  • tfstate / plan file には 値を書かない
  • そのため Terraform は「現状の値と希望の値が一致しているか」を判別できない

3つ目の特徴は重要な制約で、Terraform は write-only 値そのものを state に保持しないため、値だけを変更しても plan diff として表現できないことを意味します。これを補うために、AWS Provider の多くの write-only attribute は *_version という相棒の attribute を併せて提供しています (provider 実装側の差分トリガー設計です)。Secrets Manager の場合は secret_string_wo_version で、ここをインクリメントすることで「値が更新されたので投入してね」と Terraform に伝えます。

移行時の注意点

secretsmanagerに限らず、data "sops_file" で運用していた環境から ephemeral / write-only 構成に切り替える際、いくつか注意点があります。

コードの書き換えで過去の平文は消えない

ephemeral resource / write-only attribute は「これから書く値を tfstate / plan file に載せない」だけで、過去に書き込まれた平文は S3 上にそのまま残り続けます。具体的には以下のような場所に残ります。

  • state backend の S3 versioning で残る旧バージョン
  • backup bucket / ローカルの *.tfstate.backup

これらを完全に消し切るのが難しい場合、該当secretをrotateするアプローチと併用するのがおすすめです。

secret_stringsecret_string_wo 切替で replace になることがある

AWS Provider 側の挙動として、既存の aws_secretsmanager_secret_versionsecret_stringsecret_string_wo に切り替えると、in-place update ではなく delete + create の replace になるケースがあります。意図しない secret version の作り直しが発生しうるので、移行 PR では必ず terraform plan で replace が出ていないかを確認し、出ているなら「ローテーション (revoke + 新規発行) と合わせて意図的に切り替える」運用に寄せるのが安全です。

https://github.com/hashicorp/terraform-provider-aws/issues/41635

まとめ

Terraform の tfstate に秘密情報が平文で載ってしまうことは、これまでも課題だと感じていました。
個人的には、tfstate / plan file 自体を暗号化できる OpenTofu に移行することも選択肢に入ると考えていました。

https://opentofu.org/docs/language/state/encryption/

しかし、各種 provider の対応が進んでいることで、Terraform のままでも tfstate / plan file に平文を載せずにセキュアな運用が組めるようになりました。

write-only attribute は AWS Provider 全体でも対応リソースが増えており、Secrets Manager 以外 (RDS の master password 等) でも同じパターンが使えます。「provider が write-only 版 attribute を生やしているかどうか」がカギになるので、秘匿情報を扱うリソースを書くときは provider docs の *_wo attribute を一度チェックしてみることをおすすめします。

参考

Kurashiru Tech Blog

Discussion