1Passwordを使って、ローカルにファイル(~/.configや.env)として置かれてる生のパスワードなどを削除した
最近、コミットはされないがローカルのディレクトリに置かれている.env
のようなファイルから生のパスワードやAPI Tokenを削除しました。
これは、ローカルでマルウェアを実行した場合に、ローカルに置かれている生のパスワードやAPI Tokenを盗まれる可能性があるためです。
最近は、npm install
時のpostinstall
でのデータを盗むようなマルウェアを仕込んだりするソフトウェアサプライチェーン攻撃が多様化しています。
- Compromised PyTorch-nightly dependency chain between December 25th and December 30th, 2022. | PyTorch
- What’s Really Going On Inside Your node_modules Folder? - Socket
- Microsoft spots malicious npm package stealing data from UNIX systems | ZDNET
- GitGot: GitHub leveraged by cybercriminals to store stolen data
これは、次の記事とも関係しています。
- パスワード管理/MFA管理の戦略 | Web Scratch
- Secretlint 6リリース: .bash_historyや.zsh_historyに残ったトークンをマスキングする | Web Scratch
自分は、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を使うことで、gh
やaws
コマンドを実行するときに、
トークンを1Passwordがロードできるようにコマンドをラップしてくれます。
サービスごとにプラグインが実装されていて、Go言語でプラグインを書けるようになっています。
導入方法は簡単で1PasswordのCLIであるop
コマンドをインストールして、プラグインを追加するだけです。
# opコマンドをインストール
$ brew install --cask 1password/tap/1password-cli
# GitHubのプラグインを追加
$ op plugin init gh
- Get started with 1Password CLI | 1Password Developer
- Use 1Password to securely authenticate GitHub | 1Password Developer
これで、~/.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
- run | 1Password CLI | 1Password Developer
- Go Ahead, Delete Your .env.example File | 1Password
- 1Password の CLI で環境変数を管理する
これでファイルとして永続的に生のパスワードなどが.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 get
やop 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に移行できていないものです。
.env
にop://
で書く場合、それを読み取るプロセスごとに1Passwordでの認証が必要になります。
そのため、頻繁にプロセスが変わって、頻繁に.env
を読み取るものは、1Passwordに移行するとノイズになります。
具体的には、次のpostcommit
hookで使ってるようなトークンです。(コミットのたびに別のプロセスになってしまう)
一応、.env
はop://
のテンプレートから作るようにしていますが、ローカルのディレクトリにトークンが残ってしまいます。
op inject --in-file .env.local.template --out-file .env.local
この辺の頻度が高くプロセスが変わるものをいい感じに扱える方法がほしいです。
また、このローカルディレクトリからデータを盗むのはどちらかという無差別なサプライチェーン攻撃に多いような気もします。 標的型攻撃になるとCricle CIのインシデントのように、ブラウザのセッションを盗むことでMFA自体を回避する攻撃が増えています。 これらにも何かしらの対策が必要になっている気がします。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。