やりたいこと
- 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 ADDRESSとEMAIL ADDRESSを使用します。
下記はサンプル値ですので、実際のアドレスの値に置き換えて入力してください。
op signin example.1password.com wendy_appleseed@example.com
その後、SECRET KEYとMASTER PASSWORDの入力を求められます。
Enter the Secret Key for wendy_appleseed@example.com at example.1password.com:
Enter the password for wendy_appleseed@example.com 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 wendy_appleseed@example.com 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して中身を見られても問題ありません。