SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

Ansible VaultでgitのSecretを暗号化する方法

Ansible Vaultでgit内のパスワードやAPIトークンを保護する方法を解説します。ファイル全体と単一文字列の暗号化、stagingとproductionの分離、再暗号化の手順を紹介します。

Ansible Vault が保護するものと、保護しないもの

Ansible Vault は playbook のリポジトリ内にある Secret を暗号化します。そのため、git に保存されるのは平文のパスワードではなく暗号文です。ansible-vault コマンドは、パスワードから導出した共通鍵を使用して、ファイル全体またはファイル内の単一の値を暗号化します。play の実行時、Ansible はその内容をメモリ上で復号するため、変数は他の変数と同じように扱われます。

この仕組みには明確な境界があります。Vault が保護するのは、リポジトリ内で保存されている Secret だけです。それ以上は保護しません。タスクが実行されると、その値は、処理中のテンプレート、モジュール引数、実行結果に平文で現れます。実行結果への表示を抑制しない限り、これらを防ぐことはできません。playbook を実行できる全員が vault のパスワードを保持するため、Vault が提供するのはチーム外の人に対する秘匿性であり、チーム内の個人単位のアクセス制御ではありません。

まだ playbook を作成していない場合は、VPS に対する最初の Ansible playbookから始めてください。その playbook でパスワードが必要になったら、ここに戻ってきてください。

ファイル全体を暗号化するか、単一の文字列を暗号化するか

ansible-vault encryptはファイルを暗号文で置き換えます。ファイルは、$ANSIBLE_VAULTで始まるヘッダー行の下に、1 つの base64 テキストブロックとして保存されます。ファイルの内容が秘密情報だけの場合に使用します。

ansible-vault encrypt_stringは 1 つの値を暗号化し、通常の vars ファイルに貼り付ける YAML スニペットを出力します。変数名は判読可能なままで、値だけが暗号文になります。秘密情報が平文の設定と同じファイルにある場合に使用します。

日常の作業で重要な違いは、diff です。vault ファイルは保存するたびに新しいランダムな salt で再暗号化されるため、暗号文のすべてのバイトが変わります。そのため、git diffを実行すると、判読できないブロックが別の判読できないブロックに置き換わって表示されます。レビュー担当者は、パスワードを 1 つローテーションしたのか、ファイル全体を書き換えたのかを判断できません。encrypt_stringでは、各秘密情報が平文ファイル内の独立したブロックになります。そのため、diff にはどの変数が変更されたかが正確に表示され、ファイルの残りの部分はそのまま残ります。

inline 形式には代償があり、それはローテーション時に現れます。ansible-vault rekeyは inline ブロックを変更しません。秘密情報の一覧が長く、変更頻度が低い場合は、ファイル形式を選択してください。ファイルに秘密情報と通常の変数が混在し、コードレビューを有効に機能させたい場合は、inline 形式を選択してください。

保護対象が分かる group_vars の構成

Ansible は group_vars/<group>.yml を読み込み、group_vars/<group>/ ディレクトリ内のすべてのファイルも読み込みます。ディレクトリ形式を使用してください。1 つのグループに、平文ファイルと暗号化ファイルを並べて配置できるためです。

inventory/
  hosts.ini
group_vars/
  all/
    vars.yml
    vault.yml
  web/
    vars.yml
    vault.yml
host_vars/
  db01/
    vars.yml
    vault.yml
playbooks/
  site.yml

すべての vault.yml は暗号化されています。すべての vars.yml は平文です。ファイル名から判断できるため、内容を開かなくても、どの値が保護されているか分かります。

このパターンの後半は間接参照です。暗号化ファイル内では、すべての変数に vault_ を付けます。

vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"

次に、平文ファイルから、その隣にある暗号化ファイル内の名前を参照します。

db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"

ロールとテンプレートでは db_password を使用するだけで、値の取得元を意識する必要がありません。これにより、playbook とロールの分離を明確に保てます。平文の vars.yml は検索可能な索引としても機能します。grep -r vault_ group_vars/ に、リポジトリが必要とするすべての Secret が、復号せずに一覧表示されます。代わりに、Secret ごとに名前が 1 つ増えます。また、vault_ の名前を誤記すると、構文エラーではなく、実行時の未定義変数として検出されます。

encrypt_string で 1 つの変数を暗号化する

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  --stdin-name 'vault_db_password'

Secret を入力し、Ctrl-D を押します。--stdin-name は標準入力から値を読み取るため、値が shell の履歴ファイルに記録されません。もう一方の形式では値をコマンドラインに記述するため、shell に記録されます。

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  'a real password' --name 'vault_db_password'

どちらの方法でも、コマンドは YAML ブロックを出力します。!vault タグの下のインデントも値の一部であるため、出力されたとおりに vars ファイルへ貼り付けてください。

vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          6638643965323633646262656665306333616466396630323136393465356136396436383331
          3131303163306665326539353837343663313762616561306534373963383531613664393332

!vault タグは、YAML ローダーに対して、そのスカラーがテキストではなく暗号文であることを示します。ヘッダーには、フォーマットのバージョン、暗号方式、暗号化に使用した vault ID のラベルが含まれます。vault ID を指定せずに暗号化した値には、ラベルのない 1.1 ヘッダーが付加されます。これは引き続き使用できますが、パスワードの出所について得られる情報は少なくなります。

ボールトのパスワードはどこに保存しますか?

リポジトリの外部です。これが唯一の例外のないルールです。

--ask-vault-pass は実行ごとに 1 回だけプロンプトを表示し、何も保存しません。ラップトップには適していますが、cron ジョブや CI runner には適していません。

パスワードファイルは、1 行目にパスワードを記録したプレーンテキストファイルです。まず厳格な権限で空のファイルを作成し、その後エディターで内容を入力します。これにより、パスワードがシェルの履歴に残りません。

mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt

--vault-password-file を指定すると、任意のコマンドからそのファイルを使用できます。

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --vault-password-file ~/.ansible/vault-prod.txt

このフラグをすべてのコマンドで毎回指定するのは忘れやすいため、リポジトリのルートにある ansible.cfg で 1 回だけ設定します。

[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt

同じ設定は環境変数 ANSIBLE_VAULT_PASSWORD_FILE からも読み取れます。CI ジョブでは通常、この方法で指定します。ジョブは自身の認証情報ストアからパスワードを一時ディレクトリ内のファイルに書き込み、変数を export し、実行終了時にファイルを削除します。.gitignore にファイル名のパターンも追加してください。ansible.cfg のパスはコミットされるため、遅かれ早かれ誰かが checkout 内に実際のファイルを作成する可能性があります。

パスワードファイルに実行権限がある場合、Ansible はそのファイルを実行し、ファイルをテキストとして読み取る代わりに、標準出力からパスワードを読み取ります。これにより、パスワードをディスクに一切書き込まずに、システムの keyring やクラウドの secret manager からボールトのパスワードを取得できます。--vault-id で使用するスクリプトには追加要件があります。名前の末尾は -client、または -client と拡張子の組み合わせでなければなりません。実行可能である必要があり、--vault-id オプションを受け付け、パスワードを標準出力に出力する必要があります。

ステージングと本番用の2つの vault ID

vault ID は、label@source として記述する vault パスワードに付けるラベルです。値には prompt、パスワードファイルのパス、またはクライアントスクリプトのパスを指定します。ラベルを使うと、1つのリポジトリで複数のパスワードによって保護された Secret を管理できます。そのため、ステージング用パスワードで本番用ファイルを開くことはできません。

ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
  group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
  group_vars/prod/vault.yml

実行時に必要になるすべての ID を渡します。

ansible-playbook playbooks/site.yml \
  --vault-id staging@~/.ansible/vault-staging.txt \
  --vault-id prod@~/.ansible/vault-prod.txt

または、ansible.cfg に1回だけ記述します。

[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt

注意が必要な動作があります。デフォルトでは、ラベルは制約ではなくヒントです。Ansible は、現在保持しているすべての Secret をファイルに順番に試し、いずれかで復号できるまで処理します。そのため、staging というラベルのファイルでも、本番用パスワードが正しい鍵であれば開けてしまいます。[defaults] の下に vault_id_match = True を設定するか、環境変数 ANSIBLE_VAULT_ID_MATCH を設定すると、Ansible はファイルヘッダーと一致するラベルの Secret だけを使用します。この確認には 1.2 ヘッダーが必要です。そのため、最初から vault ID を使って暗号化されたコンテンツにだけ適用されます。

複数の ID を読み込んでいる場合、ansible-vault encrypt はどのパスワードで暗号化すればよいか判断できなくなります。--encrypt-vault-id prod で指定するか、ansible.cfgvault_encrypt_identity を設定して、リポジトリのデフォルトを定義します。

利点は、デプロイ範囲を分離できることです。ステージングをデプロイする CI ジョブには、ステージング用パスワードだけを渡します。そのため、runner が侵害されても本番用認証情報を読み取られることはありません。1台の制御マシンから Linux サーバー群 に対して play を実行するようになると、この分離が小規模なインシデントで済むか、大規模なインシデントになるかを分けます。

退職者が出た場合の vault の再暗号化

再暗号化では、vault のパスワードを変更し、新しいパスワードで内容を再暗号化します。ただし、過去の状態を取り消すことはできません。以前のパスワードを知っていた人は、保管していたリポジトリのコピーを引き続き復号できます。そのコピーに含まれるすべての古いコミットも対象です。そのため、保有者が退職した時点で vault のパスワードは漏えいしたものとして扱い、次の順序でローテーションします。

  1. サーバーとサードパーティサービスで実際の認証情報を変更します。この手順によって、実際にアクセスを無効化できます。
  2. ansible-vault edit を使用して、新しい値を vault ファイルに書き込みます。
  3. すべての暗号化ファイルを、新しい vault パスワードで再暗号化します。
  4. 引き続き必要とする人に、リポジトリとは別の経路で新しい vault パスワードを渡します。
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
  --new-vault-id prod@prompt \
  group_vars/prod/vault.yml host_vars/db01/vault.yml

rekey は 1 つのコマンドで複数のファイルを受け付けます。また、--new-vault-id prod@prompt を使用すると、ディスクからパスワードを読み取らず、新しいパスワードを 1 回だけ入力できます。変更する理由がない限り、同じラベルを使用してください。ラベルは、コマンドによって書き換えられるすべてのファイルのヘッダーに書き込まれるためです。

ここで、インライン形式のコストが発生します。ansible-vault rekey は完全に暗号化されたファイルを処理するため、プレーンテキストの vars ファイル内にある !vault ブロックは変更されません。まずそれらを見つけ、次に新しいパスワードで encrypt_string を使用してそれぞれを再生成します。

grep -rl '!vault' group_vars/ host_vars/

これが全体のトレードオフです。インラインブロックでは差分を読みやすくできますが、ローテーション時に手作業で確認する必要があります。完全に暗号化されたファイルは 1 つのコマンドでローテーションできますが、レビューで有用な情報は得られません。

復号後もシークレットが出力に現れる理由

Vault は値を復号した時点で処理を完了します。Ansible はタスクの結果を報告するため、引数を出力するモジュールを使うと、認証情報がその結果に含まれます。詳細出力、テンプレートタスクでの --diff、引数を出力する失敗タスク、または出力をファイルに書き込むコールバックプラグインのいずれでも、平文が残ります。ファイルを暗号化しても、これらは防げません。

no_log: true がそのためのスイッチです。認証情報を受け取るすべてのタスクで設定してください。

- name: Write the application environment file
  ansible.builtin.template:
    src: app.env.j2
    dest: /etc/myapp/app.env
    owner: myapp
    group: myapp
    mode: "0600"
  no_log: true

Ansible はそのタスクの結果を出力しなくなるため、ログにはタスクが実行されたことだけが記録され、処理した内容は記録されません。特にループでは必ず設定してください。ループは項目ごとに 1 件の結果を報告するため、認証情報のリストをループすると、リスト全体が出力される可能性があります。

復号されたシークレットが漏れる場所は、ほかにも 4 つあります。これらは no_log では防げません。

  • テンプレートから生成したファイルには、指定した modeowner が引き継がれます。認証情報を含むものには mode: "0600" と明示的な所有者を設定してください。設定しないと、対象ホスト上でシークレットが全ユーザーから読み取り可能になります。
  • ansible.builtin.command または ansible.builtin.shell に渡したシークレットは、コマンドの実行中、対象ホストのプロセス一覧に表示されます。対象ホスト上のローカルユーザーは誰でも読み取れます。代わりに、ファイルまたは環境変数を介して渡してください。
  • Fact キャッシュは収集した Fact を control machine のディスクに書き込むため、シークレットを保持する登録変数が、機密情報として扱われていないキャッシュファイルに保存されることがあります。
  • 同じシークレットが、コンテナから読み込まれる環境変数ファイルなど、別の場所にも保存されていることが一般的です。そこでは別のルールが適用されます。その側の対策については、Compose の env ファイルから認証情報を除外するで説明しています。

no_log を設定するとデバッグが難しくなりますが、それが目的です。タスクが正常に動作しない場合は、テストホストで一時的に削除してください。変更を本番環境へ反映する前に、必ず元に戻してください。

暗号化ファイルを平文を残さずに読み取り、編集する

ansible-vault view group_vars/prod/vault.ymlはページャーに復号結果を渡すだけで、ディスクには何も書き込みません。ansible-vault editは一時ファイルに復号し、$EDITORを開き、閉じると再暗号化します。復号済みファイルを作業ツリーに残すansible-vault decryptよりも、これらを優先してください。誤ってステージングされた復号済みの Vault ファイルは、実際の認証情報が公開リポジトリに流出する最も一般的な経路です。

Git は、処理中に復号することで、完全に暗号化されたファイルの読み取り可能な差分を表示できます。

git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributes

有効にする前に、その動作を理解してください。git diffを設定すると、本番環境の Secret が端末に表示されるため、スクロールバックや画面共有にも残ります。これは、1 台のマシンを使用する 1 人のためのローカルな利便性機能です。そのため、git configはローカルに保ち、同じ設定を行わない限り、他のユーザーのチェックアウトでは動作が異なると考えてください。

Vault が適切なツールではなくなる場合

Vault は、ラベルごとに 1 つのパスワードを持つファイル形式です。この構造には適用できる範囲の限界があります。次のいずれかに該当する場合は、専用の Secret ストアへ移行してください。

  • 個人単位のアクセス制御が必要な場合。playbook を実行する全員が同じパスワードを保持します。vault ID で環境ごとにアクセスを分けることはできますが、個人単位には分けられません。
  • 監査証跡が必要な場合。Vault には、誰が何を復号したか、いつ復号したかを記録する機能がありません。
  • 定期的なローテーションが必要な場合。Vault には有効期限もバージョン管理もありません。そのため、認証情報が 2 年間変更されていないことを知らせる仕組みもありません。
  • アプリケーション自体が実行時に Secret を必要とする場合。起動時にデータベースのパスワードを読み込むサービスは、そのパスワードをデプロイ用リポジトリから読み込むべきではありません。

この場合は構成を逆にします。Ansible は Secret を保存せず、lookup plugin を使って実行時に取得します。取得先には HashiCorp Vault(紛らわしい類似名を持つ別製品)、クラウドプロバイダーの Secret manager、または control machine 上の keyring を利用できます。リポジトリにはパスを保存し、値はストアに保存します。アクセスログもストアが保持します。小規模なチームであれば、Vaultwarden サーバーのような API 対応の self-hosted password manager で、より小規模に同じ用途を満たせます。

この仕組みの対象外となる認証情報も 1 つあります。control machine がサーバーへの接続に使用する SSH key は、Vault の問題ではありません。Ansible は play を実行する前に SSH key を必要とするためです。SSH agent と passphrase を使って管理してください。詳しくは SSH key 管理の基本を参照してください。

FAQ

vars ファイル全体を暗号化すべきですか。それとも Secret 文字列だけでよいですか?

Secret だけを含む場合はファイル全体を暗号化してください。1 つのコマンドで全体をローテーションでき、構成も単純になるためです。Secret と通常の変数が同じファイルにある場合は ansible-vault encrypt_string を使用してください。差分には暗号化された値の変更だけが現れ、どの変数に変更があったかをレビュー担当者が確認できます。トレードオフはローテーションです。ansible-vault rekey はファイル全体を対象とし、インラインの !vault ブロックはそのまま残します。そのため、新しいパスワードで手動生成する必要があります。

Ansible Vault のパスワードファイルはどこに保存すべきですか?

リポジトリの外部に、モード 0600 を設定して、~/.ansible/vault-prod.txt のようなパスに保存してください。--vault-password-file で指定するか、ansible.cfg[defaults] の下に vault_password_file を設定するか、環境変数に ANSIBLE_VAULT_PASSWORD_FILE を設定します。CI では、ジョブ自身の認証情報ストアからパスワードを一時ファイルに書き込み、変数をエクスポートして、ジョブ終了時にファイルを削除してください。ファイルが実行可能な場合、Ansible はそれを実行し、標準出力からパスワードを読み取ります。これにより、パスワードをディスクに保存せず、keyring から取得できます。

staging と production で異なる Vault パスワードを使用するにはどうすればよいですか?

--vault-id staging@/path/to/file--vault-id prod@/path/to/file で各パスワードにラベルを付け、各環境のファイルをそれぞれのラベルで暗号化してください。実行時に両方の ID を渡すか、[defaults] の下に vault_identity_list を列挙します。デフォルトでは、Ansible は保持しているすべての Secret を順に試し、ファイルを復号できるものを使用します。ファイルヘッダーのラベルと一致する Secret だけを試す場合は vault_id_match = True を設定してください。複数の ID を読み込んでいる場合は、--encrypt-vault-id で暗号化に使用する ID を選択します。

Ansible Vault を使用すると、パスワードが実行出力に表示されなくなりますか?

いいえ。Vault が保護するのは、リポジトリに保存された Secret だけです。タスクの実行時には値が平文になり、詳細ログやタスクの失敗によってログに出力される可能性があります。認証情報を扱うすべてのタスクに no_log: true を追加してください。テンプレートから生成するファイルには、制限の厳しい modeowner を設定してください。また、Secret をコマンド引数として渡さないでください。コマンドの実行中、対象ホストのプロセス一覧から参照できるためです。