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

# やりたいこと
- Credentialは1Passwordのビジネスアカウントの共有Vault上にセキュアに保管
- 平文のCredentialを[dotfiles](https://qiita.com/massy22/items/5bdb97f8d6e93517f916#dotfiles%E3%81%A8%E3%81%AF)に直書きしない（ダメ。ゼッタイ。）
- 1Passwordから取得したCredentialを[ユニバーサル変数](https://blog.nijohando.jp/post/starting-fishshell/#%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB%E5%A4%89%E6%95%B0)に代入する
- [dotfiles](https://qiita.com/massy22/items/5bdb97f8d6e93517f916#dotfiles%E3%81%A8%E3%81%AF)をGithubにpushした時に平文のCredentialが他人の目に晒されないようにする

#前提条件
- [1Password Business](https://1password.com/jp/business/)をサブスクリプション(下位プランでもできる気がしますが検証する予定なし)
- 使用するOSはmacOS Catalina(10.15)
- [Homebrew](https://brew.sh/index_ja)を導入済み
- JSONを取り扱いやすくするために[jq](https://stedolan.github.io/jq/)をinstall済み
- shellは[fish](https://fishshell.com/)を使用

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

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

# 1Passwordを採用した理由

### 厳しい業界標準に準拠しており、セキュアなPassword Managerであるため
- 強力なマルチキー暗号化により、自社のみが、会社のCredentialを管理できることを保証します。
- Multi-factor authentication(多要素認証)によって会社のCredentialをさらに強力に保護します。
- データの機密性、完全性、および可用性に関する最も厳しい業界標準([SOC2](https://1password.com/jp/soc/), [GDPR](https://1password.com/jp/legal/gdpr/))に準拠しています。

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

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

### CLI(Command Line Interface)があること
- [1Password CLI](https://app-updates.agilebits.com/product_history/CLI)が用意されています。これ、**でら重要**。

# 導入手順
## 1Password CLIのinstall
https://app-updates.agilebits.com/product_history/CLI からDarwin(aka macOS)をdownloadします。
どうでも良い情報ですが、akaとは赤ではなく、also known as(〜としても知られている、またの名を)の略です。
[前職](https://www.amazon.jobs/jp/teams/engineering-fulfillment)では、頻出ワードでした。
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に記載されている情報を使用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974/c3a447ab-f073-7b89-092c-83187009e373.png)

まずは**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で取得した値を[ユニバーサル変数](https://blog.nijohando.jp/post/starting-fishshell/#%E3%83%A6%E3%83%8B%E3%83%90%E3%83%BC%E3%82%B5%E3%83%AB%E5%A4%89%E6%95%B0)に代入するためのfish config
これで1Passwordに保存してあるCredentialを取得することができました。
その値をユニバーサル変数に代入していきます。ユニバーサル変数とは特殊な変数で

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

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

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

下記の例は`$AWS_DEFAULT_REGION`に値が代入されていない場合に、１Passwordから取得した情報をユニバーサル変数に代入するようにしています。こうすることで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 で取得できるか確認しながらやるのがおすすめです。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974/9f3a9b6e-8306-8555-b676-bee7aad59dca.png)

こうすればfishを起動するたびに1Passwordにサインインし、AWS CLIやGithubのCredentialを環境変数に格納することができます。[dotfiles](https://qiita.com/massy22/items/5bdb97f8d6e93517f916#dotfiles%E3%81%A8%E3%81%AF)をGithubにpushして中身を見られても問題ありません。