Jaybanuan's Blog

調べたことをメモしていくブログ

Ansible Vaultの使い方の調査

はじめに

Ansible Vaultの使い方を調べてみたので、その結果を残しておく。 Ansibleを利用してサーバ構築を行う際には、パスワードなどのセンシティブな情報をhost_varsgroup_varsに配置することがある。 Ansible Vaultを利用すると、このような秘密にしたい情報を暗号化しておくことができる。

検証環境

$ cat /etc/os-release | grep PRETTY_NAME
PRETTY_NAME="Ubuntu 20.04 LTS"

$ ansible --version
ansible 2.9.7
  config file = None
  configured module search path = ['/home/redj/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/redj/.local/lib/python3.8/site-packages/ansible
  executable location = /home/redj/.local/bin/ansible
  python version = 3.8.2 (default, Mar 13 2020, 10:14:16) [GCC 9.3.0]

検証の題材のPlaybook

まずは、Ansible Vaultによる暗号化/復号化の検証の題材にするPlaybookを示す。 ファイル構成は以下。

vault-test
|-- host_vars
|   `-- localhost.yml
`-- playbook.yml

今回の検証ではhost_vars/localhost.ymlの中に秘密情報が入っていると仮定し、その秘密情報をAnsible Vaultで暗号化/復号化してみる。 このファイルの内容を以下に示す。

foo: FOO
bar: BAR

また、Playbook実行のエントリーポイントとなるplaybook.ymlの内容を以下に示す。

- hosts: localhost
  connection: local
  gather_facts: no
  tasks:
  - debug:
      msg: "foo={{ foo }}, bar={{ bar }}"

変数foobarの内容を表示しているだけの単純なもの。 暗号化と復号化の検証さえできればよいので、ローカルコネクションで済ませている。

そして、このPlaybookを実行すると、以下のようになる。

$ ansible-playbook playbook.yml 

[WARNING]: (……インベントリの指定がないという指摘なので、警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果に"msg": "foo=FOO, bar=BAR"とあり、期待通り変数展開されていることが分かる。 今回の検証では、Ansible Vaultによる暗号化/復号化を行っても、同様に期待通りに変数展開されることを確認する。

Ansible Vaultの基本的な仕組み

秘密にしたい情報は、コマンドansible-vaultを利用して事前に暗号化しておく。 暗号化に際して、コマンドansible-vaultはVault Passwordと呼ばれる共通鍵(自分で決めた任意の文字列)を利用する。 図示すると、以下のようになる。

f:id:redj:20200502042603p:plain

そして、その暗号化された情報をhost_varsなどに配置して、コマンドansible-playbookでPlaybookを実行する。 暗号化された情報は、共通鍵であるVault Passwordを利用して、Playbookの実行中に自動的に復号化される。 図示すると、以下のようになる。

f:id:redj:20200502042618p:plain

Vault Passwordとは

Vault Passwordとは、Ansible Vaultが暗号化と復号化で利用する共通鍵で、利用者が自分で決める任意の文字列である。 通常は、Vault Passwordをファイルに書き込んでおいて、コマンドラインパラメータでファイルパスを指定することになる。 例えば、Vault Passwordがhogehogeの場合は、単に以下のようなファイルtest_vault_passwordを準備すればよい。

$ echo -n "hogehoge" > test_vault_password

$ cat test_vault_password 
hogehoge

特定のコマンドラインパラメータを指定することで、実行中に対話的にVault Passwordを入力することもできるが、そういった方法はここでは言及しない。

暗号化の対象

Ansible Vaultの暗号化の対象には、以下の2種類がある。

  • ファイルレベルの暗号化 (File-level encryption)
  • 変数レベルの暗号化 (Variable-level encryption)

変数レベルの暗号化は、YAMLにおける値の暗号化を想定したものである。

ファイルレベルの暗号化

ファイル全体を暗号化する方式を、ファイルレベルの暗号化という。 以下のコマンドを実行することで、ファイルhost_vars/localhost.yml全体が暗号化される。

$ ansible-vault encrypt \
    --vault-id test@test_vault_password \
    host_vars/localhost.yml 

ここで、コマンドライン中の--vault-id test@test_vault_passwordの部分でVault Passwordを指定しているが、詳細は後述する。 ファイルhost_vars/localhost.ymlの内容を確認してみると、以下のように書き換わっている。

$ANSIBLE_VAULT;1.2;AES256;test
34633833356339363463306664666561353663363934646334383338636465323765373235366231
6236613861336239626338383030613863303066636561320a653132643463393735306563653138
33343831653434373862653363373531313264383830313635636135373531373066643363303239
6631663031343063370a663230616362386162643436333036353035613033636531363233666335
35383962663038363431643631646631643934616164626362663366366134656362

これで暗号化は完了。 復号化の確認のために、以下のようにコマンドansible-playbookを実行する。 Vault Passwordは、暗号化した時と同様にコマンドラインパラメータ--vault-idで指定する。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    playbook.yml 

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果を確認すると、自動的にhost_vars/localhost.ymlが復号化されて、期待通りの変数展開が行われていることが確認できる。

変数レベルの暗号化

YAMLファイル中の個々の変数定義を暗号化する方式を、変数レベルの暗号化という。 以下のコマンドを実行することで、FOOという値を暗号化することができる。 Vault Passwordはコマンドラインパラメータ--vault-idで指定する。

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    'FOO'

!vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          32393933393231336263333538323866633037616337663730633339303431313038343935383834
          6237326433333437393366313039653065636630333837350a623339353136343537306132306663
          63306433643336626533383733666439323836363136346635333230653765303965356364333438
          6137656561313130330a346235646633366562343032326166663939343165346364396164393465
          3138
Encryption successful

そして、以下のようにhost_vars/localhost.ymlこの出力結果をコピペする

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          32393933393231336263333538323866633037616337663730633339303431313038343935383834
          6237326433333437393366313039653065636630333837350a623339353136343537306132306663
          63306433643336626533383733666439323836363136346635333230653765303965356364333438
          6137656561313130330a346235646633366562343032326166663939343165346364396164393465
          3138
bar: BAR

これで暗号化は完了。 復号化の確認のために、以下のようにコマンドansible-playbookを、パラメータ--vault-id付きで実行する。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    playbook.yml 

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

出力結果を確認すると、自動的に変数fooの値が復号化されて、期待通りの変数展開が行われていることが確認できる。 もちろん、変数fooに加えて、変数barの値も暗号化しても、同様に期待通りに動作する。

余談だが、以下のようにコマンドラインパラメータ--nameを利用することで、YAMLのキーも出力結果に含まれるようになるので、コピペが少し楽になる

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    --name foo \
    'FOO'

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          34373064336233363430636134383135303233323333633834336337333966346662363964363635
          3430326265333237313664386131363666636264303531370a303939376139313336333961313439
          32356431316231303636623462613232323432346261316561666333323439623934313938666536
          3530613033633963330a623038383834303866653339303331333930653832613431653062663830
          6430
Encryption successful

また、複数の変数を一度に暗号化することもできるが、詳細は割愛する。

Ansibleのベストプラクティス

Ansibleのベストプラクティスでは、実際に利用される変数を含むファイルと、暗号化された変数を含むファイルの、2種類のファイルに分割する方式を提示している。 例えば、以下のようにhost_vars(あるいはgroup_vars)の中にホスト名(あるいはグループ名)と同じ名前のディレクトリを作成し、その中に実際に利用される変数を含むファイルvarsと、暗号化された変数を含むファイルvaultを配置する。

vault-test
|-- host_vars
|   `-- localhost
|       |-- vars
|       `-- vault
`-- playbook.yml

ファイルvarsの内容の例を以下に示す。

foo: "{{ vault_foo }}"
bar: "{{ vault_bar }}"

ここで、変数名にプレフィックスvault_を付与した変数は、ファイルvaultで暗号化された変数である。 ファイルvaultの内容の例を以下に示す。 ただし、説明の都合上ここでは平文で示しているが、本来は暗号化されている。

vault_foo: FOO
vault_bar: BAR

つまり、Playbookなどの変数の利用者は、実際に利用される変数(例えばfoo)を通して、暗号化された変数(例えばvault_foo)を間接参照する方式である。 この方式の動機は、ファイルレベルの暗号化では変数名が判別不能なのだが、これによるPlaybook内の変数の検索性(grep)の低下を防ぐところにある。

ちなみに、ベストプラクティスの例では、ファイル名はvarsvaultになっているが、ファイル名およびその個数は任意との記述がある。

Vault IDとは

ここでようやく--vault-idで指定する、Vault IDを説明する。 なぜ最後の方で説明するかというと、無駄にややこしいから。

Vault IDとは、Playbook中で複数のVault Passwordを区別して利用するためのIDであり、そのフォーマットは以下の通り。

label@source

labelはVault Passwordに付けられた名前であり、sourceはVault Passwordが保存されているファイルのパスである。

使い方の例を示す。 まずhost_vars/localhost.ymlについて、変数レベルの暗号化を利用して、変数fooをVault ID test@test_vault_passwordで、変数barをVault ID other@other_vault_passwordで暗号化したとする。 コマンドラインは以下の通り。

$ ansible-vault encrypt_string \
    --vault-id test@test_vault_password \
    --name foo \
    'FOO'

(出力結果は省略)

$ ansible-vault encrypt_string \
    --vault-id other@other_vault_password \
    --name bar \
    'BAR'

(出力結果は省略)

この出力結果をhost_vars/localhost.ymlにコピペすると、内容は以下のようになる。

foo: !vault |
          $ANSIBLE_VAULT;1.2;AES256;test
          63643064613338386334323730613439313164626338386464623033616138386662653462376133
          (以下略)

bar: !vault |
          $ANSIBLE_VAULT;1.2;AES256;other
          34653962306238643839303634636438396239623838656631333562353138303865633962386564
          (以下略)

ここで、変数fooの暗号文の以下の部分を見てみると、末尾のtestの部分が暗号化時に指定したラベルであり、また復号化でもこのラベルを指定することになる。

          $ANSIBLE_VAULT;1.2;AES256;test

同様に変数barotherというラベルが関連付けられていることが分かる。 このような複数のVault Passwordを利用して暗号化した情報を扱うPlaybookを実行するには、コマンドラインパラメータ--vault-idを複数個指定すればよい。

$ ansible-playbook \
    --vault-id test@test_vault_password \
    --vault-id other@other_vault_password \
    playbook.yml

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

・・・ところが。

Ansible Vaultのドキュメントには以下のようにある。

By default the vault-id label is only a hint, any values encrypted with the password will be decrypted. The config option DEFAULT_VAULT_ID_MATCH can be set to require the vault id to match the vault ID used when the value was encrypted. This can reduce errors when different values are encrypted with different passwords.

以下に、個人的な主観を含めた意訳を示す。

デフォルトではVault IDのラベルはただのヒントであり、Vault Passwordで暗号化された値は手当たり次第に復号化されます。 設定オプションDEFAULT_VAULT_ID_MATCHをセットすることで、暗号化で指定したVault IDと一致するVault IDを要求することができます。 このデフォルトの挙動は、それぞれの値が異なるVault Passwordを利用して暗号化されている場合に、復号化のエラーを減らすことができます。

デフォルトの挙動が緩すぎて、ラベルの意味がないじゃない。。。

試しに、暗号化では指定していない適当なラベルでPlaybookを実行すると、確かに復号化が成功した。 ダメでしょう、この挙動は。

$ ansible-playbook \
    --vault-id xxxxx@test_vault_password \
    --vault-id yyyyy@other_vault_password \
    playbook.yml

[WARNING]: (……警告は無視する……)

PLAY [localhost] **************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [localhost] => {
    "msg": "foo=FOO, bar=BAR"
}

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

付録:ファイルレベルの暗号化のサブコマンド

ファイルレベルの暗号化のサブコマンドは6つ存在していて、ややこしい。

  • ansible-vault create
  • ansible-vault edit
  • ansible-vault rekey
  • ansible-vault encrypt
  • ansible-vault decrypt
  • ansible-vault view

そのため、成果物との関係性を以下に図示してみた。

f:id:redj:20200502042731p:plain

実際に実行するサブコマンドのほとんどは、encryptだと思う。 真面目に運用するなら、rekeydecyptviewも時々利用すると思う。 個人的には、createeditはまず使わない。

参考