TT Lab
はじめる
学ぶ 学習パス コース

Infrastructure as Code

sensitive で隠したのに状態ファイルにパスワードがそのまま残っていた

TT Labで続きを見る

目標

ツールが作ったパスワードが、状態・JSON出力・保存したプランのどこに平文で残るかを自分で見つけ、OpenTofuの状態・プランの暗号化を、既存の平文の状態から移行して強制したうえで、秘密情報を状態の外に置く構成を作ります。

なぜ重要なのか

sensitiveは、人が見る画面で値を隠す表示にすぎず、ツールが保存する記録には、値がそのまま入ります。状態ファイルと、CIがアップロードするプランファイルが、そのままシークレットの保管場所になってしまうので、これを知らないと、「隠したから安全だ」と錯覚したまま、状態をどこにでも置いてしまいます。防ぐ方法は3つの層です。保存時の暗号化(鍵が、そのまま状態の鍵になります)、平文に戻れないようにする強制、そして、そもそも値をツールに渡さず、参照だけを渡す設計です。

ステップ

  1. /root/iac-secret/main.tfに、random_password.db(長さ20、特殊文字なし)、その値をout/db.confにpassword=<값>として書くlocal_sensitive_file.dbconf、そしてsensitive = trueの出力db_passwordを置いてください(プレースホルダーは値です)。init・applyしたあと、tofu outputで値が隠されて表示されることを確認します。
  2. 状態ファイルから、パスワードの値が入っているすべての場所を探して、/root/iac-secret/leak.txtにJSONパスを1行に1つずつ書いてください。パスは、キーと配列の番号をドットでつないだ形式です(例: 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の項目に、パスワードが何回現れるかを数えて、/root/iac-secret/planleak.txtにplanleak=<수>として書いてください(プレースホルダーは回数です)。
  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方式と2つの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. /root/iac-secret/ref/secret/db.keyに、openssl rand -hex 16で秘密情報を作り、権限を600にしてください(ツールの外で作った秘密情報)。/root/iac-secret/ref/main.tfには、random_passwordなしで、out/db.confにpassword_file=<그 경로>を1行書くlocal_file.dbconfだけを置いて、init・applyします(プレースホルダーはそのパスです)。採点ツールは、refの状態のどこにもdb.keyの値がなく、パスだけがあるかを見ます。

参考

パスワードを作り、出力は隠す

/root/iac-secret/main.tfに、random_password.db(長さ20、特殊文字なし)、その値をout/db.confにpassword=<값>として書くlocal_sensitive_file.dbconf、そしてsensitive = trueの出力db_passwordを置いてください(プレースホルダーは値です)。init・applyしたあと、tofu outputで値が隠されて表示されることを確認します。

random_passwordのresultは、プロバイダーが機密の値として表示します。その値を出力として出すには、出力にもsensitiveを付けないと、プランが通りません。local_sensitive_fileは、内容を画面に見えなくするだけで、ファイルは同じように書きます。

隠した秘密情報が状態ファイルのどこにあるかを探す

状態ファイルから、パスワードの値が入っているすべての場所を探して、/root/iac-secret/leak.txtにJSONパスを1行に1つずつ書いてください。パスは、キーと配列の番号をドットでつないだ形式です(例: resources.0.instances.0.attributes.x)。採点ツールは、tofu state pullで取得した状態で同じ計算を行って比較します。

tofu output -raw db_passwordで値を得られます(隠すのは表示方法にすぎないという証拠でもあります)。jqのpaths(scalars)とgetpathで、値が同じ場所をすべて探せます。1か所だけではありません。

隠すのは画面だけだと確認する: JSON出力と保存したプラン

tofu outputの出力を、/root/iac-secret/masked.txtに保存してください。そのあと、tofu plan -out=/root/iac-secret/leak.tfplanでプランを保存し、プランファイル(zip)の中のtfstateの項目に、パスワードが何回現れるかを数えて、/root/iac-secret/planleak.txtにplanleak=<수>として書いてください(プレースホルダーは回数です)。

保存したプランファイルは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方式と2つの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で先頭の2バイトを比較してみてください。zipはPKで始まります。プランの暗号化は、stateとは別に、planブロックが決めます。

秘密情報は状態の外に置き、状態には参照だけを入れる

/root/iac-secret/ref/secret/db.keyに、openssl rand -hex 16で秘密情報を作り、権限を600にしてください(ツールの外で作った秘密情報)。/root/iac-secret/ref/main.tfには、random_passwordなしで、out/db.confにpassword_file=<그 경로>を1行書くlocal_file.dbconfだけを置いて、init・applyします(プレースホルダーはそのパスです)。採点ツールは、refの状態のどこにもdb.keyの値がなく、パスだけがあるかを見ます。

ツールが値を作ったり読んだりすると、その値は状態に入ります。値を使う側(アプリケーション)がファイルやシークレットストアから直接読むようにし、ツールは場所だけを知らせる構成が、最も単純な回避策です。データソースで秘密情報のファイルを読んでも、状態に残るという点に注意してください。