dotfilesにcredentialを平文で書かずに済むようにする方法


やりたいこと

  • Credentialは1Passwordのビジネスアカウントの共有Vault上にセキュアに保管
  • 平文のCredentialをdotfilesに直書きしない(ダメ。ゼッタイ。)
  • 1Passwordから取得したCredentialをユニバーサル変数に代入する
  • dotfilesをGithubにpushした時に平文のCredentialが他人の目に晒されないようにする

前提条件

  • 1Password Businessをサブスクリプション(下位プランでもできる気がしますが検証する予定なし)
  • 使用するOSはmacOS Catalina(10.15)
  • Homebrewを導入済み
  • JSONを取り扱いやすくするためにjqをinstall済み
  • shellはfishを使用

Credentialとは

ここで言うCredentialとは各種システムにログイン/アクセスするために必要な認証情報(ID, Password, Access key, Secret Key, Tokenなど)を指します

Credentialをセキュアに管理するために使うもの

世の中にはPassword Managerという便利なものが存在します。
前職では、Passwordsafe(Windows用), PasswordGorilla(Mac用)を使用していました。
Passwordデータベースファイルを共有ファイルサーバに配置しておき、そのPasswordデータベースのパスワード(マスターパスワード)は関係者のみが知るようにします。マスターパスワードだけ覚えておけば良いので、でら便利です。しかも無償です。
でも、弊社のような小規模駆け出し企業で共有ファイルサーバをそのためだけに用意するのは割に合わないですし、
そもそもそのやり方ですとデータベースファイルが格納されているフォルダをマウントしなければならないですし、
ある程度、人数が増えた場合、データベースファイルにアクセスする時にもっさりします
そして、Passwordデータベースファイルが消失・破損するリスクが付き纏います。実際、何度かありました。
そのためクラウド上でPasswordデータベースを管理できるPassword Managerとして1Passwordを採用することにします。

1Passwordを採用した理由

厳しい業界標準に準拠しており、セキュアなPassword Managerであるため

  • 強力なマルチキー暗号化により、自社のみが、会社のCredentialを管理できることを保証します。
  • Multi-factor authentication(多要素認証)によって会社のCredentialをさらに強力に保護します。
  • データの機密性、完全性、および可用性に関する最も厳しい業界標準(SOC2, GDPR)に準拠しています。

プライベートのアカウントとビジネスアカウントを統合利用できるため

  • プライベートアカウントとビジネスアカウントをシームレスに切り替えることができます。
  • ビジネスアカウントの中でもプライベートのVault(金庫)と共有のVaultを使い分けることができます。

ビジネスに耐える管理機能が備わっていること

  • 管理コンソールによって誰が何を表示できて共有できるかを管理でき、簡単に管理権限を委任できます。
  • レポート機能によって従業員の使用状況を細かく把握できます。
  • 会社の規模が大きくなったとしても管理していくために必要な機能が一式備わっています。

CLI(Command Line Interface)があること

  • 1Password CLIが用意されています。これ、でら重要

導入手順

1Password CLIのinstall

https://app-updates.agilebits.com/product_history/CLI からDarwin(aka macOS)をdownloadします。
どうでも良い情報ですが、akaとは赤ではなく、also known as(〜としても知られている、またの名を)の略です。
前職では、頻出ワードでした。
downloadしたら下記の手順でCLIをinstallします。
https://support.1password.com/command-line-getting-started/

downloadしたzipファイルをunzipします。

unzip op_darwin_amd64_v0.6.2.zip

gpgで署名に使われた秘密鍵と対になっている公開鍵を使って、

  • 署名されたファイルの差出人が確かに本人であること
  • ファイルの内容が不正に改変されていないこと

を検証します。

gpg --receive-keys 3FEF9748469ADBE15DA7CA80AC2D62742012EA22
gpg --verify op.sig op

opを/usr/local/binに配置します。

mv op /usr/local/bin

1Password CLIの動作確認

正しくinstallされたことを確認するためにversionを確認してみます。versionが表示されれば正しくinstallされています。

$ op --version
0.6.2

一度installしてしまえばversion updateはコマンドで実施できます。

$ op update
You are running the latest version (0.6.2). Thank you for staying up-to-date!

CLIを使って1Passwordにログインしてみます。
ログインするためには1Password Emergency Kitに記載されている情報を使用します。

まずはSIGN-IN ADDRESSEMAIL ADDRESSを使用します。
下記はサンプル値ですので、実際のアドレスの値に置き換えて入力してください。

op signin example.1password.com [email protected]

その後、SECRET KEYMASTER PASSWORDの入力を求められます。

Enter the Secret Key for [email protected] at example.1password.com: 
Enter the password for [email protected] at example.1password.com:

認証が失敗すると、下記のように出力されます。アドレスの間違いやシークレットキー、マスターパスワードが間違っていないか確認し、入力し直してください。

[LOG] 2019/10/19 12:58:46 (ERROR)  401: Authentication required.

認証が成功すると、下記のように出力されます。
OP_SESSION_exampleという環境変数にtokenのような値が格納されます。

export OP_SESSION_example="Ay-gbqc_9znYzqiG3hyMulGZi8QBguj_RX2nRt3XYgh"
# This command is meant to be used with your shell's eval function.
# Run 'eval $(op signin example)' to sign into your 1Password account.
# If you wish to use the session token itself, pass the --output=raw flag value.

OP_SESSION_exampleに値が格納されたら、次回からはevalコマンドを実行することで、MASTER PASSWORDの情報のみでサインインできるようになります。前提条件に記したとおり、私はfishを使っていますので、()の前にある$を付けると実行に失敗しますので、外して実行します。

$ eval (op signin example)
Enter the password for [email protected] at example.1password.com:
$

尚、session tokenの有効期限は30分間です。有効期限が切れた場合は下記のエラーがでます。エラーが出たら、再度サインインしてください。

[LOG] 2019/10/19 15:56:25 (ERROR)  You are not currently signed in. Please run `op signin --help` for instructions

1Password CLIとjqでお目当てのCredentialを参照する

Vaultに格納されているTestというCredentialを参照してみます。
JSONで出力されますので、整形するためにjqを使っています。
jqコマンドがインストールされていない場合は、brew install jq でinstallしてください。

$ op get item Test | jq
{
  "uuid": "r8feyd5p9vfefaufbfamrnztsy",
  "templateUuid": "001",
  "trashed": "N",
  "createdAt": "2019-10-19T04:28:16Z",
  "updatedAt": "2019-10-19T04:34:05Z",
  "changerUuid": "ZDX6UW8ZMJZYLFNZ7KB9LYCV4NY",
  "itemVersion": 2,
  "vaultUuid": "9trzw8wpqsfr4cbj6gwy2xyzyb",
  "details": {
    "fields": [
      {
        "designation": "username",
        "name": "username",
        "type": "T",
        "value": "test"
      },
      {
        "designation": "password",
        "name": "password",
        "type": "P",
        "value": "P&Wv>uT{2FoN43E9G]hZ"
      }
    ],
    "sections": [
      {
        "name": "linked items",
        "title": "Related Items"
      }
    ]
  },
  "overview": {
    "URLs": [
      {
        "u": "https://testtest.test"
      }
    ],
    "ainfo": "test",
    "pbe": 95.81823938799934,
    "pgrng": true,
    "ps": 100,
    "title": "Test",
    "url": "https://testtest.test"
  }
}

usernameだけを取得したい場合。

$ op get item Test | jq -r '.details.fields[] | select(.designation=="username").value'
test

passwordだけを取得したい場合。

$ op get item Test | jq -r '.details.fields[] | select(.designation=="password").value'
P&Wv>uT{2FoN43E9G]hZ

1Password CLIで取得した値をユニバーサル変数に代入するためのfish config

これで1Passwordに保存してあるCredentialを取得することができました。
その値をユニバーサル変数に代入していきます。ユニバーサル変数とは特殊な変数で

  • 異なるfishプロセス間で透過的に共有されます。宣言と同時に即時に他のfishプロセスから参照できます。
  • 宣言すると明示的に削除しない限り永続します。fishプロセスを全て終了したり、OSを再起動したりしても失われません。

それでは設定を書いていきます。fishは起動時に表示されるメッセージをfish_greetingというfunctionを変更することでカスタマイズすることができます。
つまりfish起動時にfish_greetingが呼び出されていることを意味します。
1Passwordから取得した情報をユニバーサル変数に代入する処理を実行するタイミングとしては好都合なので、fish_greetingに処理を追記することにします。

$ vi ~/.config/fish/functions/fish_greeting.fish

下記の例は$AWS_DEFAULT_REGIONに値が代入されていない場合に、1Passwordから取得した情報をユニバーサル変数に代入するようにしています。こうすることでfishを起動するたびに1Passwordのマスターパスワードを入力しなくても済むようになります。起動するたびに何度も何度もマスターパスワードを入力するのはつらいですからね。
ただし、このやり方ですと1Passwordに格納されているCredentialが他のメンバーに変更された場合(セキュリティーの観点からCredentialは90日ごとに更新することが推奨されています)に変更前の古い値がユニバーサル変数に代入された状態になってしまうことが予想されますので、24時間毎にcron jobでユニバーサル変数を削除するようにするなど、運用を考える必要があります。ユニバーサル変数の削除は

set -e AWS_DEFAULT_REGION

で削除できます。
ユニバーサル変数の削除のタイミング・頻度については会社の運用に合わせて処理を考えてみてください。ちなみにマスターパスワード入力をTouch ID認証にすることができないかと調べてみましたが、今のところはまだ1Password CLI側が対応していないようです。
参考情報: https://discussions.agilebits.com/discussion/99408/support-for-touchid
Touch ID認証を利用できれば、起動時毎回でも良いかなと思ったりもします。今後に期待です。

function fish_greeting

  # If the required universal variable is empty, get it from 1Password
  if test -z $AWS_DEFAULT_REGION
    # Sign-in to 1Password
    eval (op signin vitalica)

    # AWS CLI
    set -U AWS_DEFAULT_REGION (op get item nclpqka5smm2uyi2ib5rzg7pxe | jq -r '.details.sections[1].fields[0].v')
    set -U AWS_ACCESS_KEY (op get item nclpqka5smm2uyi2ib5rzg7pxe | jq -r '.details.fields[] | select(.designation=="username").value')
    set -U AWS_SECRET_ACCESS_KEY (op get item nclpqka5smm2uyi2ib5rzg7pxe | jq -r '.details.fields[] | select(.designation=="password").value')

    # Github
    set -U GITHUB_TOKEN (op get item hf3pi7d2xfgflnamfpf4mrm2ya | jq -r '.details.password')

  else
    # AWS CLI
    set -Ux TF_VAR_aws_default_region $AWS_DEFAULT_REGION
    set -Ux TF_VAR_aws_access_key $AWS_ACCESS_KEY
    set -Ux TF_VAR_aws_secret_access_key $AWS_SECRET_ACCESS_KEY

    # Github
    set -Ux TF_VAR_github_token $GITHUB_TOKEN
  end 
end

処理を追記したら一度fishのプロセスを再起動します

exec fish -l

そうすると1Passwordのマスターパスワードを聞かれますので、入力しEnterを押します。

苦戦したこと

username、password欄の値を取得するためのサンプルはインターネット上にゴロゴロころがっていますので容易に取得できるかと思いますが
それ以外のSection欄に自分で追加した情報(自分の場合はregionというsectionを作り、そこにap-northeast-1という値を格納しています)はjq -r '.details.sections[1].fields[0].v' 辺りにいます。
jqのfilterの書き方に迷ったら https://stedolan.github.io/jq/manual/#Basicfilters をよく読み、
https://jqplay.org で取得できるか確認しながらやるのがおすすめです。

こうすればfishを起動するたびに1Passwordにサインインし、AWS CLIやGithubのCredentialを環境変数に格納することができます。dotfilesをGithubにpushして中身を見られても問題ありません。