Terraformでデータベースのパスワードを扱うとき、sensitive = true を付けておけば安全だと思っていないでしょうか。実は sensitive は画面への表示を隠すだけで、値そのものはStateファイルに平文で保存されます。
この記事では、TerraformでパスワードやAPIキーなどの機密情報を扱うときに、どこに値が残るのかを整理し、Stateに残さないための方法を紹介します。
機密情報はどこに残るのか
Terraformで機密情報を扱うと、次の場所に値が残る可能性があります。
- コード(.tf、.tfvars):値を直接書くと、Gitの履歴に残る
- Stateファイル:リソースの属性として、値が平文で保存される
- planの出力ファイル:terraform plan -out で保存したファイルにも値が含まれる
- CIのログ:sensitiveを付けていない値は、plan結果としてログに表示される
特に見落とされやすいのがStateファイルです。次のようなコードは、どれもパスワードがStateに残ります。
# パスワードを変数で渡す
resource "aws_db_instance" "main" {
# ...
password = var.db_password
}
# ランダムに生成する
resource "random_password" "db" {
length = 32
}
# Secrets Managerから読み込む
data "aws_secretsmanager_secret_version" "db" {
secret_id = "myapp/db-password"
}
random_passwordの生成結果も、データソースで読み込んだシークレットの値も、Stateに保存されます。Stateを読める人は、パスワードも読めると考える必要があります。
sensitive = trueでできること・できないこと
variable "db_password" {
type = string
sensitive = true
}
- できること:planやapplyの画面表示で、値を「(sensitive value)」と伏せる。CIのログに値が出るのを防げる
- できないこと:Stateファイルへの保存を止めることはできない
sensitiveは「うっかり画面に出さない」ための機能であり、値を守る機能ではありません。
方法1:RDSのパスワードはAWSに管理させる
RDSであれば、パスワードの生成と保管をAWSに任せるのが最も確実です。
resource "aws_db_instance" "main" {
identifier = "myapp-db"
engine = "postgres"
instance_class = "db.t4g.micro"
allocated_storage = 20
username = "app_admin"
manage_master_user_password = true
skip_final_snapshot = true
}
manage_master_user_password = true を指定すると、AWSがパスワードを生成してSecrets Managerに保存します。**Terraformはパスワードの値を一度も扱わないので、Stateにも残りません。**アプリケーションは、Secrets Managerからパスワードを取得します。
方法2:シークレットの「入れ物」だけをTerraformで作る
APIキーなど、外部サービスから発行された値は、Terraformで生成できません。この場合は、Secrets Managerのシークレット(入れ物)だけをTerraformで作り、値はTerraformの外から登録します。
resource "aws_secretsmanager_secret" "external_api_key" {
name = "myapp/external-api-key"
}
値はAWS CLIなどで登録します。
aws secretsmanager put-secret-value \
--secret-id myapp/external-api-key \
--secret-string "xxxxxxxxxxxxxxxx"
ECSなどのアプリケーションには、値ではなくシークレットのARNだけを渡します。ECSのタスク定義であれば、secretsのvalueFromにARNを指定すると、起動時に値が注入されます。Terraformのコードにも、Stateにも、値は出てきません。
方法3:ephemeralとwrite-only引数を使う
Terraform 1.10で「ephemeral(一時的な)リソース」、1.11で「write-only引数」が追加されました。この2つを組み合わせると、Terraformの中で値を生成・登録しつつ、Stateには残さないことができます。
ephemeral "random_password" "db" {
length = 32
special = true
}
resource "aws_secretsmanager_secret" "db" {
name = "myapp/db-password"
}
resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string_wo = ephemeral.random_password.db.result
secret_string_wo_version = 1
}
- ephemeral "random_password":実行中だけ値を生成し、Stateやplanファイルには保存しない
- secret_string_wo:write-only(書き込み専用)の引数。AWSには値を送るが、Stateには保存しない
- secret_string_wo_version:write-only引数は前回の値をTerraformが覚えていないため、値を更新したいときはこの番号を上げて変更を知らせる
使うには、Terraform 1.11以降と、ephemeralやwrite-only引数に対応したバージョンのAWSプロバイダー・randomプロバイダーが必要です。比較的新しい機能なので、利用するプロバイダーのドキュメントで対応状況を確認してください。
それでもStateは守る
上の方法でパスワードをStateから減らせても、Stateには他にもリソースの構成情報が含まれます。Stateそのものの保護も欠かせません。
- S3バックエンドで暗号化する:S3のバケットでSSE-KMSを有効にし、KMSキーの利用権限も絞る
- バケットへのアクセスを絞る:Stateを読める人をIAMで最小限にする
- バージョニングを有効にする:誤って壊したときに戻せるようにする
- Stateをローカルに残さない:terraform.tfstateをGitにコミットしない。.gitignoreに入れておく
まとめ
- sensitive = true は表示を隠すだけで、値はStateに平文で残る
- RDSのパスワードは manage_master_user_password でAWSに管理させる
- 外部のAPIキーなどは、シークレットの入れ物だけをTerraformで作り、値は外から登録する
- Terraform 1.11以降なら、ephemeralとwrite-only引数でStateに残さずに値を扱える
- それでもStateは機密情報として、暗号化とアクセス制御で守る
「パスワードをどこに書くか」ではなく、「パスワードがどこに残るか」で考えると、安全な設計を選びやすくなります。