小さなチームでの秘密情報の管理

Posted by Katsuya Oda on June 08, 2017 · 7 mins read

こんにちは、東京支社の小田です。先週の週末は、やっと解禁されたHouse of Cardsのシーズン5をひたすらぶっ通しで見ていました。経験上、シーズンものは4や5までくるとマンネリ化しておもしろくなくなってくるんですが、House of Cardsは違いました。凄かったです。みなさんもぜひ。

簡単に説明すると、House of Cardsは組閣からもれ屈辱を味わった下院議員のフランクが、再度権力の階段を上がっていく政治ドラマです。人間は性欲なり金銭欲なり暖かい家庭を夢見たり、いろんな欲をバランスをとりながらもっていると思いますが、フランクは違います。フランクは権力(Power)だけ欲しそれのみを追求していきます。その徹底の程度が凄く(フランクはRuthless Pragmatismといっていますが)、敵だけではなく何人もの味方が特に良心をもっているほど、傷つき擦り切れ壊れていきます。どうですか?おもしろそうでしょ。

ちなみにNetflixで見る方は、Audio Descriptionが便利です。本当は視覚障害者の方用の音声モードですが、騙されたと思って試してみてください。

さて本題ですが、今日は小さなチームで秘密情報を管理する方法をご紹介します。管理に利用するツールは、PassというGnuPGとGitを利用したパスワードマネージャです。実行環境は、LinuxかmacOSを想定しています。

よくあること

中小企業の小さなチームで開発をしていると、チームで使っているWEBサービスのアカウントだったりお客様の環境のログインユーザのアカウントだったり、いろいろなアカウント情報を管理する必要に迫られると思います。それはLDAPやActive Directoryを活用しても完全になくすことはできないのかなというのが実感です。お客様によって環境が異なりますからね。全て、AWSだったら楽なんですが。

そのようなアカウントは一人の担当者が責任をもって管理するかチームで管理することになると思います。一人の担当者が管理する場合は、その人がいないときや辞めたらどうするかという問題が常に残ります。チームで管理する場合は以下のようなパターンになると思います。

  • どうせ社内だしパスワードを簡単なやつにして覚えることから解放されよう。
  • いや、みんながみれるところに適当な強度を持っているパスワードを平文で書いておこう。
  • いや、複数のパスワードを一つのパスワードで暗号化して、そのパスワードだけ覚えよう。

大きいところは予算や人員を確保できるので、開発者がこのような問題とは無縁に過ごせるように工夫・設計しているところも多いと思います。羨ましいですね。今回は最後のパターンから派生した管理方法をご紹介します。

管理方法の概要

今回ご紹介する方法は以下の方針でパスワードを管理します。

  • 複数のパスワードを一つ一つ暗号化して、それぞれ一つのファイルに保存する。
  • パスワードの暗号化はGnuPGを使用して、チームのメンバーのそれぞれの公開鍵で暗号化する。それぞれの公開鍵で暗号化されたデータはGnuPGを使用して一つのファイルにまとめる。
  • 暗号化したファイルを用途(サイトやサービス)ごとにディレクトリにわけGitで管理する。

言葉にすると分かりにくいんですが、擬似コードにすると以下のようになります。Rubyってとってもわかりやすいですね。

require 'openssl'

passwords = {
  # 'パスワードを管理したいサイト': 'そのサイトのパスワード'
  'foo.example.com': 'foo',
  'bar.example.com': 'bar',
  'baz.example.com': 'baz'
}
member_public_keys = ['path/to/alice.pub', 'path/to/bob.pub', 'path/to/trent.pub']

passwords.each do |service, password|
  encrypted_password =
    member_public_keys.map do |path|
      public_key = OpenSSL::PKey::RSA.new(File.read(path))
      public_key.public_encrypt(password) # 実際はGnuPGでpasswordを暗号化
    end.join('') # 実際はGnuPGを使用して暗号化したバイナリをまとめる

  File.write(encrypted_password, "path/to/#{service}")
end

この方法の利点は以下になります。

  • メンバーが自身の秘密鍵のみでパスワードを復号できる。結果として、マスターパスワード(パスワードを暗号化するためのパスワード)を共有することから解放される。
  • 他のメンバーが追加したパスワードを自身の秘密鍵のみで復号できる。結果として、誰でもパスワードを追加できて、追加処理が分散できる。
  • いちメンバーのディスクがぶっ飛んでも悲劇にならない。Gitで分散管理しているため。
  • Gitで管理しているため過去に戻れる。誰かが間違って暗号化したため、すべてが見れなくなるということがない。

この方法での注意点は以下になります。

  • 適当なメンバーが適当なパスフレーズで秘密鍵を管理した場合、悲しい結果になります。そのため、パスワードを管理できるメンバーの選択は慎重に検討する必要があります。
  • 信頼できるGitリポジトリを使用する。パブリックなリポジトリは使用できないと思います。すべてのコミットに署名し、pullするときに必ず署名を確認する運用をしても難しいと思います。

実際の管理方法

Passを使用して管理を行います。PassはBashでかかれたパスワードマネージャーです。UIがとても優れていて、わかりやすいサブコマンド名、目的のパスワードにすぐにアクセスできる補完機能などがとても素敵です。また、各ディストリビューションの公式リポジトリからyumやaptitudeでインストールできることもうれしい点です。ただ、最後の決め手はシンプルであることです。パスワードを管理するツールは、ツール自体の内容を検査する必要があります。Passは1ファイルのシェルスクリプトでかかれているため検査が非常に楽です。

基本的な使い方

Linuxを前提として、実際に端末にセットアップしながらPassの基本的な使い方を説明します。以下では、チーム名を「nacl」としています。

まず、それぞれのディストリビューション用のパッケージマネージャーを使用して、Passをインストールします。

$ sudo yum install pass # for Fedora / RHEL
$ sudo apt-get install pass # for Ubuntu / Debian

次に、「pass init」コマンドを使用して、naclチーム用のGitリポジトリを「~/pass-nacl」に作成します。デフォルトのリポジトリのパスは「~/.password-store」となっています。ここでは複数のチーム用のリポジトリを作成することを前提に、別のパスを指定しています。パスワードの暗号化と復号化に使用する鍵として、「alice@netlab.jp」を指定しています。鍵については補足を参照してください。

$ PASSWORD_STORE_DIR=~/pass-nacl pass init alice@netlab.jp
mkdir: created directory '/home/katsuya/pass-nacl/'
Password store initialized for alice@netlab.jp

パスワードの追加は、「pass insert」コマンドを使用します。保存するパスワードは標準入力から入力します。パスワードは引数に指定した「~/pass-nacl」からの相対パスに暗号化されて保存されます。パスワードの入力にエディタを使いたい場合は「-m」オプションをつけてください。

$ PASSWORD_STORE_DIR=~/pass-nacl pass insert test.com/user/katsuya
mkdir: created directory '/home/katsuya/pass-nacl/test.com'
mkdir: created directory '/home/katsuya/pass-nacl/test.com/user'
Enter password for test.com/user/katsuya:
Retype password for test.com/user/katsuya:

暗号化したパスワードは、「pass show」コマンドで復号化できます。復号化時には秘密鍵のパスワードの入力が促されます。復号化されたパスワードは標準出力に出力されます。先ほど入力したパスワードは「test_password」になります。

$ PASSWORD_STORE_DIR=~/pass-nacl pass test.com/user/katsuya
test_password

登録されているパスワードの一覧は、「pass」コマンドで表示することができます。

PASSWORD_STORE_DIR=~/pass-nacl pass
Password Store
└── test.com
    └── user
            └── katsuya

メンバーの追加

メンバーの追加は以下の手順で行います。

  • メンバーの公開鍵を追加する。
  • 追加するメンバーと既存のメンバーの公開鍵で全パスワードを再暗号化する。暗号化で使用する公開鍵の一覧は、デフォルトでは「~/neo-secrets/.gpg-id」に含まれています。

まず以下のコマンドで、メンバーの公開鍵をKeyサーバ上から検索します(メンバーの公開鍵がKeyサーバ上にあることを前提にしています)。引数に指定してされている「bob@netlab.jp」は公開鍵のUser IDになります。User IDとして指定可能な識別子については、How to Specify a User Idに一覧があります。

$ gpg --search-keys bob@netlab.jp
gpg: data source: https://gpg.NebrWesleyan.edu:443
(1)     Bob <bob@netlab.jp>
          4096 bit RSA key 8BAA0B1E1111F58B, created: 2016-09-05

次に、メンバーの公開鍵をKeyサーバから取得します。取得する公開鍵の指定は、emailや短い形式のid(short id)ではなく、長い形式のIDかfingerprintを使用してください。emailやshort idは被る可能性があり、意図していない公開鍵が追加される場合があります。Fake Linus Torvalds’ Key Found in the Wild, No More Short-IDs.

$ gpg --recv-keys 8BAA0B1E1111F58B
gpg: key 8BAA0B1E1111F58B: public key "Bob <bob@netlab.jp>" imported
gpg: Total number processed: 1
gpg:               imported: 1

次に、自身の秘密鍵で取得した公開鍵で署名します。この処理を行うことで、取得した公開鍵を信頼し、その公開鍵でファイルの暗号化を行えるようになります。これは面倒くさいですがメリットがあって、例えば悪意のあるユーザが「~/neo-secrets/.gpg-id」に任意の公開鍵のidを追加しても、そのidに対応する公開鍵が署名されていなければエラーになります。

$ gpg --lsign-key 8BAA0B1E1111F58B

最後に、「pass init」コマンドを使用し、メンバーの公開鍵を「~/pass-nacl」に追加し全パスワードファイルの再暗号化を行います。

$ PASSWORD_STORE_DIR=~/pass-nacl pass init alice@netlab.jp bob@netlab.jp
Password store initialized for alice@netlab.jp, bob@netlab.jp
test.com/user/katsuya: reencrypting to 2536005AD0C48603 DDE060FC59D93066

リモートリポジトリの設定

「example.com」の「pass-nacl」リポジトリへのpushする場合は、以下のように設定します。

$ PASSWORD_STORE_DIR=~/pass-nacl pass git init
Initialized empty Git repository in /home/katsuya/pass-nacl/.git/
$ PASSWORD_STORE_DIR=~/pass-nacl pass git remote add origin example.com:pass-nacl

リポジトリへのpushは以下のコマンドで行えます。

$ PASSWORD_STORE_DIR=~/pass-nacl pass git push origin

チーム用のシェル関数の作成

現状の構成では、「pass」コマンドを実行するたに環境変数PASSWORD_STORE_DIRを設定する必要があります。また、PASSWORD_STORE_DIRがデフォルトと異なるため、「pass」コマンドの便利な補完機能が動作しないという問題もあります。そのため、以下のようなシェル関数をチーム毎に用意するのが便利だと思います。

bashの場合は以下になります。

pass-nacl() { PASSWORD_STORE_DIR=$HOME/pass-nacl/ pass "$@"; }
_pass-nacl() {
  declare -f _pass >/dev/null || {
    declare -f _completion_loader >/dev/null && _completion_loader "pass"; }
  PASSWORD_STORE_DIR=$HOME/pass-nacl/ _pass
}
complete -o filenames -o nospace -F _pass-nacl pass-nacl

zshの場合は以下になります。

pass-nacl() { PASSWORD_STORE_DIR=$HOME/pass-nacl pass "$@"; }
_pass-nacl() { PASSWORD_STORE_DIR=$HOME/pass-nacl _pass; }
compdef _pass-nacl pass-nacl

上記の定義を「.bashrc」や「.zshrc」に読み込ませると以下のようにpassを使うことができます。

$ pass-nacl test.com/user/katsuya
test_password

エラーになる場合は、現在のシェルに追加したコードが適切に読み込まれていないか、Passがインストールされていないかのどちらかだと思います。ログインしなおすか、Passのinstallを行ってください。

終わりに

必要最低限のPassの使用方法を説明しました。今まで説明した知識の範囲で、チーム毎にリポジトリを分けたりすることもできると思います。さらに詳しい説明は、PassについてもGnuPGについてもGitについてもmanを参照するのがいいと思います。これらのツールに関しては、manが常にベストです。

最後に完全に主観ですが、Passを使った結果以下のようなメリットを感じています。

  • 自分がミスしても大丈夫。自身の秘密鍵が吹っ飛んでも誰かが復号化できる信頼感は、管理の精神的な負担解消につながっています。
  • パスワードの変更の負担が減った。git diffで過去のパスワードを閲覧できるので、失敗を恐れずにパスワードを変えれます。ちなみに、git diffはなかでgpgを呼び出して、その度に復号化しています。
  • Import・Exportがらくちん。汎用的な統一的な方法で管理しているので、簡単なシェルスクリプトでパスワードを移行できます。個人用のPassで管理していたパスワードの一部をチーム用のPassに移すことも簡単にできました。

補足

GnuPGの鍵の生成

おそらくほとんどの方は鍵を持っていないか、個人用の鍵は持っていても会社用の鍵は持っていなかったりすると思います。古い鍵を持っている方も、最近libgcryptのRNGにバグ(Security fixes for Libgcrypt and GnuPG 1.4)が見つかったので確認したほうがいいとおもいます。RSAの鍵の場合、この発表の時点で調べた限りは問題がないそうです。

また、使用するGnuPGのバージョンはGnuPG 2を使用するのがいいと思います。モダンなアルゴリズムをサポートしていますし、もうすぐDebian系のディストリビューションでもGnuPG 2がデフォルトになります。他にもキーサーバとの接続にHKPSが使えたりします。詳しくは、Debian to shift to a modern GnuPGに記載されています。

鍵生成は「gpg2 –quick-generate-key」コマンドで行うことができます。鍵長や鍵の構成については他のサイトを参照してください。人によっては、RSAの鍵の鍵長は4096にするべきだとか、暗号化用のサブキーだけではなく署名用のサブキーも作るのがいいとか、いろいろな意見があります。なぜそうするべきなのかを調べてからそうしてください。

ここでは、ただの例として以下の設定で生成しています。鍵の識別子として「test@example.com」を、鍵のアルゴリズムとしてRSAを指定しています。鍵長は4096です。生成する鍵は署名用と暗号化用の二つです。鍵は失効しないように指定しています。

$ gpg2 --quick-generate-key test@example.com rsa4096 default never
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key 0x9E474EB95B420F79 marked as ultimately trusted
gpg: directory '/home/katsuya/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/katsuya/.gnupg/openpgp-revocs.d/948A540148629A0D328036C19E474EB95B420F79.rev'
public and secret key created and signed.

Note that this key cannot be used for encryption.  You may want to use
the command "--edit-key" to generate a subkey for this purpose.
pub   rsa4096/0x9E474EB95B420F79 2017-06-06 [SC]
      Key fingerprint = 948A 5401 4862 9A0D 3280  36C1 9E47 4EB9 5B42 0F79
uid                              test@example.com

生成した鍵は一回中身をじっくり見ておいた方がいいと思います。鍵のダンプは以下のコマンドで行えます。「gpg –list-packets」のダンプはRFCを読んでないと分からんと思います。「pgpdump」はなんとなく分かるのでおすすめです。pgdumpはおそらくモダンなディストリビューションでインストールできるはずです。

$ gpg -a --export "test@example.com" | gpg --list-packets --verbose
$ gpg -a --export "test@example.com" | pgpdump # more detail

最後に、生成した鍵は他のユーザが取得できるように以下のコマンドでキーサーバにアップロードすることができます。様々な方法で人に配ることができますが、配布のコストを下げるためにキーサーバを使うのがいいと思います。

$ gpg --send-key test@example.com

gpg-agentのパスワードのキャッシュの設定変更

Passを使い出すと何度も秘密鍵のパスワードの求められるのに気づきます。マスターパスワードだからと言ってパスワードの文字数を長くしていると、結構煩わしかったりします。対応方法としては以下の三つがあります。パスワードの長さとキャッシュの時間は結局トレードオフの関係なんだと思います。

  • パスワードを何度打ち込んでもストレスがない長さにする。
  • パスワードの長さをそのままで、パスワードをキャッシュする時間を長くする。
  • ただただ耐える。

gpgは復号化するとき秘密鍵の情報を必要とします。GnuPG-2から秘密鍵の情報はgpg-agentが一括して管理しているため、gpgは秘密鍵に関する操作のすべてをgpg-agentに移譲しています。つまり、秘密鍵のパスワードのキャッシュの設定変更はgpg-agentの設定を変える必要があります。該当の設定は以下の二つになります。

  • default-cache-ttl - 最後のアクセス時間から指定した秒数が経過したらキャッシュは失効されます。デフォルトは600秒(10分)です。
  • max-cache-ttl - 最後に秘密鍵のパスワードを入力してから指定した秒数が経過したらキャッシュは失効されます。デフォルトは7200秒(2時間)です。

設定は、「~/.gnupg/gpg-agent.conf」に指定します。以下はどちらも2時間に指定した例になります。2時間はmax-cache-ttlのデフォルトなので指定する必要はないですが、例として指定しました。

$ cat <<CONF | tee -a .gnupg/gpg-agent.conf
default-cache-ttl 7200
max-cache-ttl 7200
CONF

設定が適切にできている場合は、「gpgconf」の出力で該当行のけつに「7200」と出ているはずです。問題なければ、以下のようにgpg-agentに設定をリロードしろと伝えてください。

$ gpgconf --list-options gpg-agent | grep cache-ttl
default-cache-ttl:24:0:expire cached PINs after N seconds:3:3:N:600::7200
default-cache-ttl-ssh:24:1:expire SSH keys after N seconds:3:3:N:1800::
max-cache-ttl:24:2:set maximum PIN cache lifetime to N seconds:3:3:N:7200::7200
max-cache-ttl-ssh:24:2:set maximum SSH key lifetime to N seconds:3:3:N:7200::
$ gpgconf --reload gpg-agent

Passのトレース

使い初めの時期は、「pass」コマンドが「gpg」コマンドをどのように呼び出しているか確認したい場合があります。Passはbashで書かれているため、「-x」オプションを使用すると実行したコマンドをトレースしてくます。以下のように「pass」コマンドを実行すると「pass show」コマンドが「gpg2 -d –quiet –yes –compress-algo=none –no-encrypt-to –batch –use-agent /home/katsuya/pass-nacl/test.com/user/katsuya.gpg」実行していることがわかります。

$ PASSWORD_STORE_DIR=~/pass-nacl bash -x pass show test.com/user/katsuya
+ umask 077
+ set -o pipefail
+ GPG_OPTS=("--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to")
+ GPG=gpg
++ tty
+ export GPG_TTY=/dev/pts/3
+ GPG_TTY=/dev/pts/3

[snip]

+ [[ -f /home/katsuya/pass-nacl/test.com/user/katsuya.gpg ]]
+ [[ 0 -eq 0 ]]
+ gpg2 -d --quiet --yes --compress-algo=none --no-encrypt-to --batch --use-agent /home/katsuya/pass-nacl/test.com/user/katsuya.gpg
test
+ exit 0