パイプラインがシークレットを保存せずにアクセスする方法
アプリケーションのビルド、テスト、デプロイを行うパイプラインがあるとします。その過程のどこかで、データベースのパスワードやAPIキー、証明書が必要になります。多くの場合、そのシークレットをパイプライン変数や設定ファイルに入れたり、スクリプトにハードコードしたりしたくなるでしょう。しかし、これには問題があります。一度シークレットがパイプラインに入り込むと、意図しない場所に漏れ出す可能性があるのです。
シークレットはビルドログに漏れ出します。Dockerイメージに焼き付けられます。アーティファクトファイルに現れます。ワークスペースのキャッシュに残り続けます。シークレットがパイプラインコードに触れた瞬間、その行き先を制御できなくなります。
解決策は、パイプラインからシークレットを完全に排除することではありません。パイプラインが仕事をするためにはシークレットが必要です。解決策は、シークレットをメモリ上にのみ、タスクの実行中にのみ存在させ、永続的な場所には決して保存しない方法で注入することです。これには3つの一般的なアプローチがあり、それぞれにトレードオフがあります。
環境変数:シンプルだが漏洩しやすい
最も一般的なアプローチは、Vaultやシークレットストアからシークレットを取得し、実行中のプロセスの環境変数として設定することです。パイプラインが npm test や dotnet run を実行するとき、DB_PASSWORD 変数はすでにプロセスメモリ上で利用可能になっています。
このアプローチはシンプルで高速です。ほとんどのツールやフレームワークで動作します。アプリケーションが設定を読み取る方法を変更する必要はありません。ほとんどの言語とランタイムは環境変数をネイティブにサポートしています。
例えば、Vault CLIを使用してシークレットを取得し、ビルドを実行する前にエクスポートできます:
# Vaultからデータベースパスワードを取得し、環境変数としてエクスポート
DB_PASSWORD=$(vault kv get -field=password secret/db-prod)
export DB_PASSWORD
# シークレットを必要とするビルドコマンドを実行
npm run build
問題は、環境変数が漏洩しやすいことです。多くのアプリケーションはデバッグ中にすべての環境変数を出力します。ロギングフレームワークはデフォルトで環境変数をログファイルに書き込むことがよくあります。一度シークレットがログに現れると、ログシステムにアクセスできる誰もがそれを読めるようになります。これには開発者、サポートスタッフ、そしてログシステムが侵害された場合の攻撃者も含まれます。
もう一つのリスクはビルドアーティファクトです。パイプラインがJARファイル、Dockerイメージ、コンパイル済みバイナリを作成するとき、ビルドプロセスが誤ってすべての変数を読み取ると、環境変数が取り込まれる可能性があります。ARG や ENV 命令を使用するDockerビルドは、シークレットをイメージレイヤーに埋め込む可能性があります。一度シークレットがイメージに入ると、すべてを再ビルドして再デプロイしない限り、そこに残り続けます。
環境変数は、ログとビルドプロセスを厳密に制御できる短命なパイプラインには適しています。アーティファクトを生成するパイプラインや、冗長なログ出力があるパイプラインでは危険です。
ファイルのマウント:より制御しやすいが、後片付けが必要
2つ目のアプローチは、Vaultからシークレットを取得し、コンテナまたはワークスペース内の一時ファイルに書き込むことです。アプリケーションは起動時にそのファイルを読み取ります。タスクが完了したら、ファイルは削除されます。
このアプローチでは、より細かい制御が可能です。アプリケーションプロセスのみがファイルを読み取れるようにファイル権限を設定できます。ファイルを読み取り専用でマウントできます。使用後すぐに削除できます。多くの最新フレームワークはファイルからの設定読み取りをサポートしています。Spring Bootは application.properties やYAMLファイルから読み取ります。.NETはJSON設定ファイルから読み取ります。これらのフレームワークを、その実行に必要なシークレットのみを含む一時ファイルに向けることができます。
リスクは、ファイルが残ってしまうことです。パイプラインが終了後にワークスペースをクリーンアップしない場合、シークレットファイルはディスクに残ります。コンテナ環境では、権限が正しく設定されていない場合、マウントされたファイルを同じコンテナ内の他のプロセスが読み取れる可能性があります。パイプラインがDockerレイヤーキャッシュなどのキャッシュを使用する場合、シークレットファイルがキャッシュされ、後続のビルドに現れる可能性があります。
ファイルのマウントは、ファイルの寿命と権限を制御できるため、環境変数よりも安全です。ただし、規律が必要です。実行ごとに後片付けを確実に行い、キャッシュメカニズムがファイルを保持しないようにする必要があります。
直接API呼び出し:パイプラインにシークレットを持たせない
3つ目のアプローチは、パイプラインにシークレットを一切渡さないことです。代わりに、アプリケーションまたはスクリプトがシークレットが必要になるたびにVault APIを直接呼び出します。パイプラインはシークレットを扱いません。環境変数を設定しません。ファイルを書き込みません。アプリケーション自身がVaultにアクセスし、必要なものを取得します。
例えば、データベースパスワードを環境変数として渡す代わりに、アプリケーションはデータベースに接続する必要があるときに GET /v1/secret/db-password を呼び出します。Vaultがリクエストを認証し、シークレットを返し、アプリケーションはそれをすぐに使用します。
以下のシーケンス図はこの流れを示しています:
これは最も安全なアプローチです。シークレットはパイプラインメモリに存在しません。ファイルに書き込まれることはありません。ログに現れることはありません。誰かがパイプラインのワークスペースにアクセスできたとしても、シークレットはそこに一度も存在しなかったため、見つけることができません。
トレードオフは可用性です。アプリケーションはVaultに到達可能であることに依存するようになります。Vaultがダウンしたり到達不能になったりすると、アプリケーションは起動できません。API呼び出しのたびにレイテンシが追加され、Vaultの監査ログに痕跡が残ります。このアプローチは、使用頻度が低いシークレットや、数分で期限切れになる一時的なデータベース認証情報のような短命な動的シークレットに最適です。
直接API呼び出しは、セキュリティ要件が高く、インフラストラクチャチームがVaultの可用性を保証できる本番環境に理想的です。シークレット漏洩のリスクが低い開発環境やテストパイプラインには過剰です。
適切なアプローチの選択
単一の最善の方法はありません。選択は、シークレットの使用頻度、アクセス制御の厳格さ、Vaultインフラストラクチャの信頼性によって異なります。
シークレットがほとんど変更されない迅速な開発パイプラインでは、ログとアーティファクト作成を制御できる限り、環境変数で問題ありません。シークレットが漏れてはならない本番デプロイメントでは、直接API呼び出し、または厳格な後片付けを伴うファイルのマウントが適しています。コンテナ化環境では、読み取り専用権限とコンテナ終了後の自動クリーンアップを備えたファイルのマウントが効果的です。
重要なのは、各アプローチの弱点を理解し、それらのギャップを埋めるようにパイプラインを設計することです。最も簡単な方法を選ぶのではなく、リスク許容度と運用能力に合った方法を選んでください。
実践的なチェックリスト
パイプラインがシークレットにアクセスする方法を決定する前に、以下の点を確認してください:
- パイプラインは、環境変数を取り込む可能性のあるアーティファクト(Dockerイメージ、JARファイル、コンパイル済みバイナリ)を生成しますか?
- ロギングフレームワークはデフォルトで環境変数を出力しますか?
- コンテナ環境でマウントされたシークレットにファイル権限を設定できますか?
- パイプラインは実行後にワークスペースファイルをクリーンアップしますか?
- Vaultインフラストラクチャは、アプリケーションからの直接API呼び出しに十分信頼できますか?
- すべてのシークレットアクセスに監査証跡が必要ですか?
本当の課題
シークレットを保存せずにパイプラインに取り込むことは、問題の半分に過ぎません。より難しいのは、シークレットがログ、アーティファクト、バージョン管理、キャッシュされたビルドレイヤーなどの予期しない場所に漏れ出さないようにすることです。各アプローチはシークレットを注入する方法を提供しますが、どれも自動的に漏洩を防ぐわけではありません。それには継続的な規律、自動化されたチェック、そしてシークレットがどこに行き着く可能性があるかについての明確な理解が必要です。
目標は完璧な方法を見つけることではありません。目標は、自分の環境に合った方法を選び、その弱点の周りに保護策を構築することです。