Gitリポジトリ上でAWSアクセスキーを大公開しないためにAnsible Vaultをフル活用する

こんにちは。Backlog のSite Reliability Engineering (SRE) を担当している吉澤です。

AWS アクセスキーを含むコードを GitHub の公開リポジトリにプッシュしてしまい、そのアクセスキーがビットコインの採掘に使われて AWS から高額請求が来た!という話をたまに目にします。今年の2月に検証された方(GitHub に AWS キーペアを上げると抜かれるってほんと???試してみよー!)によると、git push から13分で不正利用開始されたらしいです。怖いですね……。

Backlog のソースコードは Backlog の提供する Git リポジトリで管理しています。Backlog の Git にはリポジトリの公開機能はないので、AWS アクセスキーをプッシュしたからといって即座に悪用される可能性は低いです。とはいえ、漏洩時の影響が大きいため、AWS アクセスキーは Ansible Vault で暗号化した状態で Git リポジトリ上にプッシュしています。

このあたりのプラクティスは Git 全般で使えるものと思いますので、今回は Backlog で実践している AWS アクセスキーの管理方法をご紹介します。

基礎知識:Ansible Vault とは

まず最初に、Ansible Vault について簡単に紹介します。すでにご存知の方は、次の節まで進んで OK です。

Ansible Vault とは、機密情報(パスワードや API キー)を含む Ansible の変数を、暗号化した状態でファイルに保存するための Ansible の一機能です。この暗号化されたファイルは、安全に Git リポジトリにプッシュすることができます。

ファイルの暗号化は ansible-vault コマンドで行います。例えば、以下のコマンドを実行すると、デフォルトのテキストエディタが開きます。

$ ansible-vault create vault.yml

変数定義を書いて保存すると、暗号化されたファイル vault.yml が作られます。

$ cat vault.yml
$ANSIBLE_VAULT;1.1;AES256
64333632336638663131313233383133333436323861656436656332393265343363336338653765
3365386232633032353037333737346337303937653864610a653639393637353034663463393665
33383664323961643265326161383963306434356536386662663830653235386532356133393130
6563653266333032390a356339613764613235663436646430663432643664376133613763353665
35366161626438643539613531316638623964643631346537313039383031663433

また、Ansible 2.2 以降には特定の変数のみ暗号化する機能 Single Encrypted Variable があります。ansible-vault encrypt_string を以下のように実行すると、指定した変数(この例では test_value)が暗号化されます。あとは、この結果を変数ファイルに埋め込めば OK です。

$ ansible-vault encrypt_string 'test_value' --name 'test_key'
test_key: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          66646233376562386663353736343466343036663561393862303333326565393363313533373463
          3330343262616265303239633263663035316536666337300a313834323136633332633336353930
          63326332383564323536386233656430626130313861333637623035636565633663346365646132
          3731346263623839320a633531396332353339306637363162343937383036653630366235323330
          6561
Encryption successful

復号化は ansible-playbook コマンドの実行時に行われます。ansible-playbook が変数を読み込むパスに、Ansible Vault で暗号化されたファイルがあると、自動的に復号化して読み込まれます。

この暗号化と復号化のパスワード(以下、vault パスワード)は以下のいずれかの方法でコマンドに渡します。

  1. コマンド実行時に表示されるプロンプトで、vault パスワードを入力
  2. コマンドライン引数で、パスワードファイルのパスを指定
  3. ansible.cfg の vault_password_file オプションで、パスワードファイルのパスを指定

パスワードファイルには、vault パスワードが書かれたファイルか、vault パスワードを標準出力するスクリプトを指定することができます。後述する Backlog のプラクティスでは、3番の方法で、パスワードファイルにスクリプトを指定します。

Ansible Vault の更なる詳細については、公式サイトの解説をご参考ください。

プラクティス:Backlog の構成管理システム

Backlog では、デプロイに Ansible、反復的な作業の自動化に Fabric、システム構成の検査に Serverspec を利用しています。

これらのツールに関するファイルを、1個の Git リポジトリ上で管理しています。Git リポジトリ内のディレクトリ構成は以下の通りです。

/ansible ... Ansible および Ansible Vault 関係のファイル
/fabfile ... Fabric 関係のファイル
/serverspec ... Serverspec 関係のファイル

AWS アクセスキーは、/ansible 以下に、Ansible Vault で暗号化された状態で保管されています。AWS アクセスキーを必要とするアプリをデプロイする場合、Ansible がそれを復号化し、設定ファイルなどに埋め込んで、各サーバにデプロイします。

ansible ディレクトリ以下には、AWS アクセスキー以外の機密情報も保存しています。例えば、データベースのパスワード、自社サービス(BacklogCacooTypetalk)の API キー、外部サービスの API キーなど。実際は AWS アクセスキーよりも、これらの機密情報の方が多いです。

また、Backlog では、Fabric や Serverspec の動作にも機密情報の一部を必要としています。そのため、これらを1個の Git リポジトリに入れた上で、fabfile ディレクトリおよび serverspec ディレクトリからも、ansible ディレクトリ以下のファイルを参照させています。

Ansible が使う機密情報

Ansible が使う機密情報については、以下の工夫をしています。

  • vault パスワードは S3 から取得
  • vars ファイルと vault ファイルをペアで用意する
  • Single Encrypted Variable は(まだ)使わない

vault パスワードは S3 から取得

vault パスワードは S3 から取得するように設定します。この方法には以下のメリットがあります。

  • ansible-vault および ansible-playbook の実行時に、パスワード入力を求められない
  • 各ユーザーのマシンに、vault パスワードが書かれたファイルが残らない
  • Ansible の実行権限を、AWS IAM で一元管理できる

まず、ansible.cfg に以下の記載を追加します。前述の通り、vault_password_file オプションには、vault パスワードを標準出力するスクリプトを指定することができます。

[defaults]
vault_password_file = vault_pass.sh

次に、以下の内容で vault_pass.sh を用意し、実行権限を与えます。最後の引数を - にするのがミソです。S3 へのアクセスにデフォルトプロファイルと違うプロファイルを使いたい場合、環境変数 ANSIBLE_AWS_PROFILE に --profile PROFILE_NAME のような値を設定してください。

#!/usr/bin/env sh
aws s3 cp ${ANSIBLE_AWS_PROFILE} s3://EXAMPLE_BUCKET_NAME/vault_pass -

そして、s3://EXAMPLE_BUCKET_NAME/vault_pass に vault パスワードを書いたファイルを置きます(EXAMPLE_BUCKET_NAME の部分は適宜修正してください)。最後に、Ansible の実行権限を持つユーザーに、このバケットにアクセス可能な IAM ユーザー(特定の EC2 インスタンスから実行するなら IAM ロール)を設定します。

これで、ansible-playbook を実行するたびに、S3 から vault パスワードが自動取得されるようになります。

vars ファイルと vault ファイルをペアで用意する

Ansible Vault の説明に書いた通り、変数ファイルを暗号化すると、その中に入っている変数名もわからなくなってしまいます。Single Encrypted Variable を使えば変数名は暗号化されませんが、運用上の課題(後述)があり、Backlog では採用していません。

この問題に関しては、Ansible 公式の Best Practices ページ内でベストプラクティスが示されており、Backlog でもこのプラクティスを採用しています。該当部分の抄訳はこんな感じです。

この問題に対するベストプラクティスの1つは、group_vars ディレクトリ内に、グループの名前を付けたサブディレクトリを作ることである。そのサブディレクトリ内に、vars という名前のファイルと、vault という名前のファイルを作る。vars ファイルには機密情報を入れる変数を含めて、すべての変数を定義する。機密情報は vault ファイル内で定義し、Ansible Vault で暗号化するのだが、このなかで定義する変数名には vault_ というプレフィックスを付ける。そして、vars ファイル内からは、vault_ が付けられた変数を参照する。

このベストプラクティスは、変数や vault ファイルの数、およびその名前を特に制限しない。

具体例を挙げて説明します。

Ansible ではすべてのホストは all グループに属します。そのため、/ansible/group_vars/all というファイルを作って変数を定義すると、その変数は全ホストに適用されます。この機能は使ったことがある人も多いと思います。

このプラクティスでは、all というファイルの代わりにディレクトリを作り、以下のように varsvault ファイルを配置します。

/ansible/group_vars/all/vars
/ansible/group_vars/all/vault

例えば、vault ファイルに以下の内容を書いて暗号化したとします。

---
vault_db_user: root
vault_db_password: pass1234

vars ファイルでは、機密情報以外の変数も含めて、以下のように変数を定義します。

---
app_parameter1: 100
app_parameter2: 200

db_user: "{{ vault_db_user }}"
db_password: "{{ vault_db_password }}"

このときに守るべきルールは、「名前が vault_ から始まる変数は、vars ファイルの中からしか参照しない」ことです。そうすることで、vault ファイルで定義された変数は、vars ファイルのみを見ればわかる状態になります。このルールを守らないと、vault ファイルを ansible-vault view で復号化しないとわからなくなり、管理が面倒になります。

Single Encrypted Variable は(まだ)使わない

Ansible 2.2 から実装された Single Encrypted Variable という機能を使った場合、変数名が暗号化されず、上記のように vars と vault ファイルを分ける必要がなくなります。

私もこの機能を試してみたのですが、一度暗号化した機密情報を、復号化して確認したいときに手間がかかることがわかり、結局採用しませんでした。

例えば、ベストプラクティスに従って作った vault ファイルは、以下のコマンドで復号化して、内容を確認できます。

$ ansible-vault view group_vars/all/vault

しかし、Single Encrypted Variable を使って、暗号化した変数としていない変数が混在したファイルを ansible-vault view に渡すと、以下のように失敗します。

$ ansible-vault view group_vars/all
 ERROR! input is not vault encrypted data for group_vars/all

じゃあ、この YAML から特定の行だけ取り出して ansible-vault decrypt に渡せば復号化できるかと思えば、インデントを解除してから渡さないと失敗します。

$ ansible-vault decrypt
Reading ciphertext input from stdin
          $ANSIBLE_VAULT;1.1;AES256
          66646233376562386663353736343466343036663561393862303333326565393363313533373463
          3330343262616265303239633263663035316536666337300a313834323136633332633336353930
          63326332383564323536386233656430626130313861333637623035636565633663346365646132
          3731346263623839320a633531396332353339306637363162343937383036653630366235323330
          6561
ERROR! input is not vault encrypted data- is not a vault encrypted file for -
$ ansible-vault decrypt
Reading ciphertext input from stdin
$ANSIBLE_VAULT;1.1;AES256
66646233376562386663353736343466343036663561393862303333326565393363313533373463
3330343262616265303239633263663035316536666337300a313834323136633332633336353930
63326332383564323536386233656430626130313861333637623035636565633663346365646132
3731346263623839320a633531396332353339306637363162343937383036653630366235323330
6561
Decryption successful
test_value

これはなかなか面倒だ……と思い、Backlog では採用しませんでした。また、後述する Python や Ruby のライブラリが Single Encrypted Variable をサポートしていなかったことも、不採用の理由の一つでした。将来的に、このあたりの問題を解決できたら、Single Encrypted Variable を採用したいと考えています。

Fabric が使う機密情報

Fabric は、Python 2 で実装された、SSH 接続して行うような作業を自動化するための Python ライブラリおよびコマンドラインツールです。Ansible 自体が Python で実装されているため、Python の ansible モジュールに Ansible Vault の機能も含まれていますので、これを使います。

まず、Fabric が使う機密情報を /fabfile/vault_secrets.yml に格納します(ファイル名は自由)。このファイルは、/ansible ディレクトリで、以下のようにコマンドを実行して編集します。/ansible ディレクトリから実行するのは、ansible.cfg 内の情報を使うためです。

$ ansible-vault edit ../fabfile/vault_secrets.yml

次に、この vault_secrets.yml を読み込む /fabfile/vault.py を追加します。

import os
import re

import yaml
from ansible.parsing.dataloader import DataLoader
from ansible.parsing.vault import get_file_vault_secret, VaultLib


def read_vault_secrets():
    """
    Read vault_secrets.yml by using vault password on S3
    :return: dict created from vault_secrets.yml
    """

    # Find vault password file
    with open(os.path.dirname(os.path.abspath(__file__))+'/../ansible/ansible.cfg') as f:
        for line in f.readlines():
            m = re.match(r'vault_password_file = (.+)', line)
            if m:
                vault_password_file = m.group(1)
                break

    if vault_password_file is None:
        return {}

    # Read vault password from S3
    secret = get_file_vault_secret(
        filename=os.path.dirname(os.path.abspath(__file__))+'/../ansible/'+vault_password_file,
        encoding="utf-8",
        loader=DataLoader()
    )
    secret.load()

    # DEFAULT_VAULT_ID_MATCH is False in default
    # http://docs.ansible.com/ansible/2.4/config.html#default-vault-id-match
    vault_lib = VaultLib([(False, secret)])

    with open(os.path.dirname(os.path.abspath(__file__))+'/vault_secrets.yml') as f:
        return yaml.safe_load(vault_lib.decrypt(f.read()))


vault_secrets = read_vault_secrets()

最後に、__init__.py の冒頭に以下の宣言を追加します。

from vault import vault_secrets

これにより、__init__.py 内から vault_secrets['db_user'] のようにして、vault_secrets.yml 内の機密情報を参照できるようになります。

このコードを書くにあたっては、Ansible Vault 機能のソースコードを読んでみたを参考にさせていただきました。これを読んで ansible モジュールに含まれる VaultLib クラスの存在を知り、最終的には ansible/__init__.py at stable-2.4を読みながら試行錯誤で実装しました。

ちなみに、Fabric3 という、Fabric からフォークされた Python 3 対応版が存在しており、ヌーラボ社内ではすでに使われているのですが、Backlog は過去の資産が多くてまだ Fabric3 に移行完了していません……。Fabric3 は py2.7/py3.4+ compatible fork とのことなので、上記は Fabric3 でも使える方法かと思います。

Serverspec が使う機密情報

Serverspec は、Ruby で実装された、サーバ環境に対するテストツールです。Ansible 公式のライブラリは無いため、tpickett66/ansible-vault-rb: A Ruby implementation Ansible’s vault file format で公開されている ansible-vault gem を使っています。

まず、Serverspec の実行に必要な機密情報を /serverspec/vault_secrets.yml に格納します(ファイル名は自由)。このファイルは、Fabric の場合と同様に、ansible-vault コマンドを実行して編集します。

次に、/serverspec/spec/spec_helper.rb に以下のコードを追加します。ansible-vault gem はパスワードファイルがスクリプトの場合に対応していないので、事前にスクリプトを実行する必要があるのがポイントです。

require 'ansible/vault'

ansible_dir = File.expand_path('../../ansible', File.dirname(__FILE__))

# Find vault password file
vault_pass = nil
File.open(File.expand_path('ansible.cfg', ansible_dir), 'r') do |f|
  vault_password_file = f.read.match(/^vault_password_file = (.+)$/)[1]
  if vault_password_file
    # Read vault password from S3
    vault_pass = `#{File.expand_path(vault_password_file, ansible_dir)}`.chomp
    break
  end
end

# Read vault_secrets.yml
vault_secrets =
    YAML.load(Ansible::Vault.read(
        path: File.expand_path('../vault_secrets.yml', File.dirname(__FILE__)), password: vault_pass))

あとは、vault_secrets の内容をプロパティに設定すれば完了です。

上記のプラクティスの課題

この方法で今のところ問題なく運用できているのですが、いくつか課題が残っています。

1点目の課題は、Fabric と Serverspec では vars ファイルと vault ファイルを分けるプラクティスが使えないため、vault_secrets.yml を復号化しないと変数名がわからないことです。vault_secrets.yml から読み込んだ変数を、明示的に別の変数に代入することはできますが、そこまですべきか悩んでおり、まだ実装していません。

2点目の課題は、一部の機密情報が、ansible、fabfile、serverspec の各ディレクトリ以下に重複してしまうことです。この問題を避けるために、Backlog では上記のコードを拡張して、spec_helper.rb から /ansible/group_vars/ROLE_NAME/vault の一部を読み込んでいます。しかし、そうすると vault ファイルに含まれる変数がどこから参照されているかわかりにくくなる点が悩みの種です。

最終的には、Ansible 以外のソフトウェアも含めて、Single Encrypted Variable に統一できるのが理想だろうと考えています。

上記のプラクティスを導入できないケース

デプロイに Ansible や Fabric を使っていないケースでは、上記のプラクティスは導入できません。Backlog でも、一部の実験的なアプリなどがそのようなケースに該当しています。

そのようなケースでも、権限管理は AWS IAM に集約したかったため、秘密情報を含む設定ファイルそのものを S3 からダウンロードする方法で対処しています。また、ダウンロードするファイルは必ず .gitignore に設定しています。

事故防止:git-secrets で機密情報のプッシュを防ぐ

Ansible Vault で AWS アクセスキーを暗号化するようにしていても、デプロイツールに詳しくない開発者が、ローカルファイルに AWS アクセスキーを書き、そのファイルをうっかりプッシュしてしまうことはありえます。Ansible Vault を使っていてもそのような事故は防げません。

git push を未然に防ぐとなると、Git hook を使おうと考えるのが自然でしょう。Amazon.com が公開している git-secrets というツールが、

  • プリセットされた AWS アクセスキーのスキャン条件
  • ローカルリポジトリに対するスキャン機能
  • コミット時にスキャンを自動実行する Git hook の登録機能

を備えており、現在このツールの導入を進めています。また、ヌーラボ社内では、AWS アクセスキーのスキャン条件に加えて、ヌーラボの各プロダクト(BacklogCacooTypetalk、社内システム)に関する機密情報のスキャン条件も追加しています。

導入前の検証では、OS X および Linux では問題なく動作しました。また、Windows でも公式のインストーラでのインストール時に MinTTY を選択し、root 権限で git-secrets を /usr/bin にコピーすれば問題なく動きました(make コマンドが無いため make install はできませんでした)。

ただ、OS X 上で SourceTree を使いたい場合は、System Integrity Protecton (SIP) を一度無効にして、git-secrets を /usr/bin にコピーする必要がありました。OS X と SourceTree の組み合わせで使っているユーザーは多いので、git-secrets 導入の妨げになりそうで悩ましいところです。この問題については、以下のページを参考にさせていただきました。

まとめ

今回の記事では、AWS アクセスキーを安全に Git リポジトリへ登録するために、Ansible Vault を活用する方法をご紹介しました。これは、実際にヌーラボの Backlog で採用しているプラクティスです。また、Ansible Vault では防げないタイプの事故を防ぐ方法として、git-secrets というツールもご紹介しました。

Python や Ruby 用のライブラリを使うことで、他の運用管理ツールが使う機密情報も Ansible Vault で暗号化・復号化することができます。Ansible を主な構成管理ツールとして使っている方は、是非こちらのプラクティスの採用を検討してみてください。

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる