最近、コミットはされないがローカルのディレクトリに置かれている.envのようなファイルから生のパスワードやAPI Tokenを削除しました。 これは、ローカルでマルウェアを実行した場合に、ローカルに置かれている生のパスワードやAPI Tokenを盗まれる可能性があるためです。 最近は、npm install時のpostinstallでのデータを盗むようなマルウェアを仕込んだりするソフトウェアサプライチェーン攻撃が多様化しています。

これは、次の記事とも関係しています。

自分は、1Passwordを使っているので、.envに書くようなパスワードやAPI Tokenも1Passwordに集約することで、 ファイルとして置かれている生のパスワードやAPI Tokenの大部分を削除しました。

ローカルにファイルとして置かれる生のパスワードやAPI Tokenにはいくつか種類があるので、それぞれの対応を書いていきます。 実際には全てのものは対応できていませんが、できるだけファイルに生のパスワードやAPI Tokenを置かないようにしていきます。

SSHの秘密鍵を1Passwordに移行する

1Passwordには、SSH Agentがあります。 これを使うとSSHの秘密鍵を1Passwordに保存できるため、~/.sshから秘密鍵を削除できます。

ついでに、GitHubのSigning Keyも追加すると、Gitでのコミット時に自動的に署名してくれます。

ブラウザに1Passwordの拡張が入ってるなら、https://github.com/settings/keysにアクセスして、ポチポチするだけで終わるので、~/.sshに秘密鍵を置くのはやめましょう。

Bitwardenでも似たようなことができます(仕組みは違います)。

macOSだとsecretiveも同様のことができます。ただし、仕組み的にキーをバックアップはできないため、マシンを変更する場合はSSHキーの再発行が必要です。

CLIが利用するCredentialを1Passwordに移行する

GitHub CLIは~/.config/gh/に、AWS CLIは ~/.aws/credentials にCredentialをデフォルトで配置します。 色々なCLIがそれぞれのサービスのAPIトークンをホームディレクトリ以下に配置しています。

1Password Shell Pluginsを使うことで、ghawsコマンドを実行するときに、 トークンを1Passwordがロードできるようにコマンドをラップしてくれます。

サービスごとにプラグインが実装されていて、Go言語でプラグインを書けるようになっています。

op plugin

導入方法は簡単で1PasswordのCLIであるopコマンドをインストールして、プラグインを追加するだけです。

# opコマンドをインストール
$ brew install --cask 1password/tap/1password-cli
# GitHubのプラグインを追加
$ op plugin init gh

これで、~/.aws/credentialsなどに置かれているトークンは不要となるため、単純に削除できます。

.envに入っているCredentialを1Passwordに移行する

.envには生のパスワードやトークンが入ってることが多いです。

.envは普通はコミットしませんが(Secretlintにも検知ルールがあります)、.envからパスワードなどが消えれば、コミットしても大丈夫な状態まで持っていけます。(実際にはコミットはしてないですが)

ghqを使ってリポジトリはまとめられているので、次のコマンドで .env を検索して1コずつ移行していきました。

❯ find . -name ".env" -not -path "*/node_modules/*"

.envの中の特定のキーだけ保存するなら、エディタのプラグインを使うと便利です。

.envの中身を1つのアイテムにまとめるにはop item createコマンドなどを使ってまとめる必要がありました。

例えば、次のような.envファイルがあったとします。

APP_CF_NAMESPACE_USER="SECRET_XXXXXXXX"
APP_CF_AUTHKEY="SECRET_XXXXXXXX"
APP_CF_ACCOUNT_ID="SECRET_XXXXXXXX"
APP_CF_AUTH_EMAIL="SECRET_XXXXXXXX"
APP_LOGFLARE_API_KEY="SECRET_XXXXXXXX"
APP_SESSION_COOKIE_SECRET="SECRET_XXXXXXXX"
APP_STATS_AWS_ACCESS_KEY_ID="SECRET_XXXXXXXX"
APP_STATS_AWS_SECRET_ACCESS_KEY="SECRET_XXXXXXXX"
APP_GOOGLE_OAUTH_CLIENT_ID="SECRET_XXXXXXXX"
APP_GOOGLE_OAUTH_CLIENT_SECRET="SECRET_XXXXXXXX"
APP_OAUTH_REDIRECT_URL="SECRET_XXXXXXXX"
APP_PROD_CF_NAMESPACE_USER="SECRET_XXXXXXXX"
APP_PROD_GOOGLE_OAUTH_CLIENT_ID="SECRET_XXXXXXXX"
APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET="SECRET_XXXXXXXX"

これを次のようなシェルスクリプトに変換して、それぞれの値をアイテムにまとめて1Passwordに保存します。

op item create --category=login --title='My Example'
op item edit 'My Example' APP_CF_NAMESPACE_USER="SECRET_XXXXXXXX"
op item edit 'My Example' APP_CF_AUTHKEY="SECRET_XXXXXXXX"
op item edit 'My Example' APP_CF_ACCOUNT_ID="SECRET_XXXXXXXX"
op item edit 'My Example' APP_CF_AUTH_EMAIL="SECRET_XXXXXXXX"
op item edit 'My Example' APP_LOGFLARE_API_KEY="SECRET_XXXXXXXX"
op item edit 'My Example' APP_SESSION_COOKIE_SECRET="SECRET_XXXXXXXX"
op item edit 'My Example' APP_STATS_AWS_ACCESS_KEY_ID="SECRET_XXXXXXXX"
op item edit 'My Example' APP_STATS_AWS_SECRET_ACCESS_KEY="SECRET_XXXXXXXX"
op item edit 'My Example' APP_GOOGLE_OAUTH_CLIENT_ID="SECRET_XXXXXXXX"
op item edit 'My Example' APP_GOOGLE_OAUTH_CLIENT_SECRET="SECRET_XXXXXXXX"
op item edit 'My Example' APP_OAUTH_REDIRECT_URL="SECRET_XXXXXXXX"
op item edit 'My Example' APP_PROD_CF_NAMESPACE_USER="SECRET_XXXXXXXX"
op item edit 'My Example' APP_PROD_GOOGLE_OAUTH_CLIENT_ID="SECRET_XXXXXXXX"
op item edit 'My Example' APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET="SECRET_XXXXXXXX"

そして、op:// URLの形式で.envに保存していた値を置き換えます。 次のようなコマンドを使うと、op:// URLを取得しながら.env形式で取得できます。

$ op item get --format json 'My Example' | jq -r '.fields[] | select(.value) | (.label) + "=\"" + (.reference) + "\""'
APP_CF_NAMESPACE_USER="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_NAMESPACE_USER"
APP_CF_AUTHKEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_AUTHKEY"
APP_CF_ACCOUNT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_ACCOUNT_ID"
APP_CF_AUTH_EMAIL="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_AUTH_EMAIL"
APP_LOGFLARE_API_KEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_LOGFLARE_API_KEY"
APP_SESSION_COOKIE_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_SESSION_COOKIE_SECRET"
APP_STATS_AWS_ACCESS_KEY_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_STATS_AWS_ACCESS_KEY_ID"
APP_STATS_AWS_SECRET_ACCESS_KEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_STATS_AWS_SECRET_ACCESS_KEY"
APP_GOOGLE_OAUTH_CLIENT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_GOOGLE_OAUTH_CLIENT_ID"
APP_GOOGLE_OAUTH_CLIENT_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_GOOGLE_OAUTH_CLIENT_SECRET"
APP_OAUTH_REDIRECT_URL="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_OAUTH_REDIRECT_URL"
APP_PROD_CF_NAMESPACE_USER="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_CF_NAMESPACE_USER"
APP_PROD_GOOGLE_OAUTH_CLIENT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_GOOGLE_OAUTH_CLIENT_ID"
APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET"

これで、.envの中身は次のようになりました(SectionでProductionとDevelopを同じアイテム内で分けて保存とかもできます)。

APP_CF_NAMESPACE_USER="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_NAMESPACE_USER"
APP_CF_AUTHKEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_AUTHKEY"
APP_CF_ACCOUNT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_ACCOUNT_ID"
APP_CF_AUTH_EMAIL="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_CF_AUTH_EMAIL"
APP_LOGFLARE_API_KEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_LOGFLARE_API_KEY"
APP_SESSION_COOKIE_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_SESSION_COOKIE_SECRET"
APP_STATS_AWS_ACCESS_KEY_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_STATS_AWS_ACCESS_KEY_ID"
APP_STATS_AWS_SECRET_ACCESS_KEY="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_STATS_AWS_SECRET_ACCESS_KEY"
APP_GOOGLE_OAUTH_CLIENT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_GOOGLE_OAUTH_CLIENT_ID"
APP_GOOGLE_OAUTH_CLIENT_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_GOOGLE_OAUTH_CLIENT_SECRET"
APP_OAUTH_REDIRECT_URL="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_OAUTH_REDIRECT_URL"
APP_PROD_CF_NAMESPACE_USER="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_CF_NAMESPACE_USER"
APP_PROD_GOOGLE_OAUTH_CLIENT_ID="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_GOOGLE_OAUTH_CLIENT_ID"
APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET="op://Private/My Example/Section_azkwev6t5djujcfqbb6hpmyk3m/APP_PROD_GOOGLE_OAUTH_CLIENT_SECRET"

このままではアプリケーションの環境変数にはop://の値がそのまま入ります。 そのため、実際にアプリケーションを実行する際には、op runというコマンドを経由します。

次のように、op runコマンドを実行すると、op://の値が実際の値に置き換わった状態の環境変数がアプリケーションのプロセスに渡されます。

op run --env-file="./.env" -- npm start

これでファイルとして永続的に生のパスワードなどが.envに残らないようにできます。 この移行が一番大変だったので、.env to 1Passwordの移行スクリプトを誰か作ってほしいです。

毎回コマンドを覚えるのは大変なので、自分はopr npm startとして実行できるようにしています。

opr () {
	who=$(op whoami)
	if [[ $? != 0 ]]
	then
		eval $(op signin)
	fi
	if [[ -f "$PWD/.env" ]]
	then
		op run --env-file=$PWD/.env -- $@
	else
		op run --env-file=${グローバルのenv置き場}/.env.1password -- $@
	fi
}

類似ツール:

dotfileにかかれたパスワードを1Passwordに移行する

ZshやBashなどのシェルに使ってるような、dotfileにパスワードが書かれているものがあったので、それも1Passwordに移行しました。

よく使う環境変数(GITHUB_TOKENのような決まり文句があるもの)は、グローバル用の.envを用意しoprで実行しています。 そのほかのトークンは、1Passwordに保存して、op item getop item readで取得して利用しています。

たとえば、npm publishに必要なTOTPは次のように取得できます(npmのセキュリテキーのフローがまだ使いにくいため。パスワード管理/MFA管理の戦略 | Web Scratch)。

alias npm-get-otp='op item get --otp xxxxxxxxid'
npm publish --otp=$(npm-get-otp)

まとめ

これで、大部分のパスワードやトークンが1Passwordに移行できました。 これによって、ローカルのディレクトリを読み取られても、パスワードが漏洩するケースがかなり少なくなりました。

まだ、一部のトークンが残ったりはしています。 主に認証の実行頻度の問題で、1Passwordに移行できていないものです。

.envop://で書く場合、それを読み取るプロセスごとに1Passwordでの認証が必要になります。 そのため、頻繁にプロセスが変わって、頻繁に.envを読み取るものは、1Passwordに移行するとノイズになります。 具体的には、次のpostcommit hookで使ってるようなトークンです。(コミットのたびに別のプロセスになってしまう)

一応、.envop://のテンプレートから作るようにしていますが、ローカルのディレクトリにトークンが残ってしまいます。

op inject --in-file .env.local.template --out-file .env.local

この辺の頻度が高くプロセスが変わるものをいい感じに扱える方法がほしいです。

また、このローカルディレクトリからデータを盗むのはどちらかという無差別なサプライチェーン攻撃に多いような気もします。 標的型攻撃になるとCricle CIのインシデントのように、ブラウザのセッションを盗むことでMFA自体を回避する攻撃が増えています。 これらにも何かしらの対策が必要になっている気がします。