セキュリティ●●●○○

シークレット管理

Secrets Management ・ しーくれっとかんり

APIキーやパスワードなどの秘密情報を、集中保管・認可つき配布・自動ローテーションで扱う仕組み。

✦ 30秒で試す — 航海鍵を守るための鍵を、さらに別の鍵で包んでおくetcdのバックアップを手にした人は、パスワードを?予想してみる

概要

シークレット管理は、APIキー・データベースのパスワード・暗号化の鍵・TLS 証明書の秘密鍵といった「秘密情報(シークレット)」を扱うための仕組みと運用の総称です。安全に保管し、必要とするアプリケーションにだけ配り、定期的に取り替えます(ローテーション)。アプリケーションが外部のサービスやほかのシステムと連携するかぎり、シークレットは必ずどこかに存在します。問題は「持つかどうか」ではなく「どう持つか」です。

置き場所の選択肢はいくつもあります。コードに直接書く、設定ファイルに置く、環境変数で渡す、専用のシークレットマネージャから実行時に取り出す。それぞれに漏えいのリスクと運用の手間のトレードオフがあります。チームが小さいうちは雑に扱っても事故が表に出にくいのですが、リポジトリの公開、メンバーの入れ替わり、システムの増殖とともに、シークレットは「いちばん漏れやすく、漏れたときにいちばん痛い情報」になっていきます。

なぜ生まれたか

典型的な事故の形は昔から変わりません。開発中に「とりあえず動かすため」に APIキーをソースコードへ書き込み、そのまま Git にコミットしてしまう。リポジトリが公開された瞬間、あるいは非公開のリポジトリでも、退職者の手元のクローンや乗っ取られたアカウントを通じて、キーは第三者の手に渡ります。厄介なのは、Git が履歴をすべて保持するため、あとからコードを消しても過去のコミットにシークレットが残り続けることです。公開リポジトリを常時巡回してキーを集めるボットもあり、クラウドのアクセスキーを push して数分で悪用され、高額の請求が届いた、という報告は珍しくありません。

この反省から、12 Factor App(2011年)は「設定はコードから分離し、環境変数に置く」と説きました。ただ、環境変数にも限界があります。誰がいつ値を読んだかの記録が残らず、値を変えるには再デプロイが要り、値は平文のまま各所に散らばります。「散らばったシークレットを一箇所に集め、暗号化して保管し、取り出しを認可と監査の下に置き、機械で更新できるようにする」。この要求に応えるために、HashiCorp Vault(2015年)やクラウド各社のシークレットマネージャに代表される専用の基盤が生まれました。

詳細

置き場所の進化 — ハードコードから集中管理へ

シークレットの置き場所は、おおまかに三段階で進化してきました。それぞれが、前の段階の弱点を潰しています。

ハードコードコードに直接書くリポジトリごと流出履歴に残り続ける変えるにはコード修正環境変数コードと設定を分離ログやエラー画面に露出読んだ記録が残らない更新に再デプロイが必要シークレットマネージャ暗号化して集中保管認可つきの取り出し監査ログ・自動ローテーション期限つきの動的シークレット分離集中「値をどこに書くか」から「誰が・いつ・何を取り出せるかを制御する」へシークレットは静的な文字列から、発行と失効を管理されるライフサイクルへ
シークレットの置き場所の進化。各段階が前の段階の弱点を解消し、関心は「値をどこに書くか」から「誰が・いつ取り出せるか」へ移った

第一段階のハードコードは論外に見えますが、いまでも新しい漏えいの原因の上位を占めます。第二段階の環境変数は 12 Factor App 以降の標準で、コードとシークレットを分けたという大きな前進でした。ただし環境変数はプロセスの属性として平文で保持されます。エラー通知のツールが環境変数ごと送信してログに残る、同じマシンのほかのプロセスから覗かれる、子プロセスに意図せず引き継がれる、といった露出の経路が残ります。

第三段階のシークレットマネージャ(HashiCorp Vault、AWS Secrets Manager、Google Cloud の Secret Manager など)は、シークレットを暗号化して一箇所に保管します。アプリケーションは起動時や実行時に、認証つきの API で必要なものだけを取り出します。「誰が・いつ・どのシークレットを取り出したか」が監査ログに残り、値の更新は保管庫側の一回の操作で済みます。漏えいが疑われたときは、取り出しの履歴から影響範囲を絞り込めます。どのアプリがどのシークレットを取り出せるかはポリシーで決め、最小権限の原則どおり「そのアプリに必要な分だけ」に絞ります。

保管庫の中身をどう守るか — エンベロープ暗号

シークレットを暗号化して保管すると、今度は「その暗号化の鍵をどこに置くか」という問題が出てきます。鍵を同じデータベースに置いては意味がありません。クラウドの KMS(Key Management Service)は、この問題を「エンベロープ(封筒)暗号」で解きます。

KMS認可と監査ログの下で動くマスター鍵KMS の外に出ないアプリデータ鍵平文・使ったら捨てる保存先DB・ストレージ暗号化されたデータ包まれたデータ鍵1231 KMS がデータ鍵を作り、平文の鍵と、マスター鍵で包んだ鍵の2つを返す2 アプリは平文の鍵でデータを暗号化し、包んだ鍵と一緒に保存する。平文の鍵は捨てる3 読むときは、包んだ鍵を KMS に渡して開けてもらう。この呼び出しは認可され、記録に残る
エンベロープ暗号。マスター鍵は KMS の外に出ない。データはその場で作るデータ鍵で暗号化し、データ鍵のほうをマスター鍵で包んで一緒に保存する

こうすると、大量のデータの暗号化はアプリの手元で速く行えます。一方、マスター鍵は KMS(多くはハードウェアのセキュリティモジュール)の外に一度も出ません。保存先のデータベースが丸ごと盗まれても、包まれたデータ鍵を開けるには KMS への認可された呼び出しが必要です。マスター鍵を取り替えるときも、包み直すのは小さなデータ鍵だけで済みます。Vault の Transit 機能も同じ発想で、「暗号化と復号を肩代わりするサービス」としてアプリに鍵を持たせません。アプリは平文を渡して暗号文を受け取るだけなので、鍵の管理そのものを保管庫に預けられます。

動的シークレットとローテーション

シークレットマネージャの本領は、静的な値の保管庫にとどまらない点にあります。Vault が広めた「動的シークレット」は、アプリケーションが要求したその時点で、データベースの一時ユーザーなどを作り、有効期限(TTL)つきで貸し出す方式です。

データベース保管庫アプリデータベース保管庫アプリ期限が来たら保管庫がユーザーを削除する自分の身元を証明して、DB の資格情報を要求一時ユーザー v-app-7f3 を作成・権限は読み取りのみユーザー名とパスワード・有効期限1時間一時ユーザーで接続v-app-7f3 を削除

使い終われば自動で消えるので、「漏れても短い時間しか使えない」状態を作れます。アプリごと、起動ごとに別のユーザーになるので、どの接続がどのアプリのものかもデータベース側のログから分かります。これは OAuth が短命なアクセストークンで被害を時間で区切る発想と同じです。現代のシークレット管理は、「絶対に漏らさない」から「漏れても被害が小さい」設計へと軸足を移しています。

静的なシークレットについても、ローテーションを機械に任せることが大切です。手作業のローテーションは「変えると何かが壊れるかもしれない」という恐れから先送りされ、結果として同じキーが何年も使われ続けます。新旧の値を一時的に併存させて切り替える手順を自動化しておけば、ローテーションは怖い作業ではなく日常の営みになります。

CI/CD と実行環境への注入

シークレットがいちばん動く場所が CI/CD パイプラインです。デプロイにはクラウドの資格情報が、テストには外部 API のキーが要ります。GitHub Actions などの CI サービスはシークレットの置き場を内蔵していて、ワークフローのログでは値を伏せ字にします。ただし、echo で加工した値や base64 に変換した値まで伏せてくれるわけではありません。一歩進んだ形として、CI のジョブに OIDC の ID トークンを発行させ、クラウド側がそれを検証して短命の資格情報を渡す方式が広まっています。長命のキーを CI に一切置かずに済みます(詳しくはサービスアカウントの記事で扱います)。

実行環境の側では、Kubernetes の Secret リソースがよく使われます。ここに大きな落とし穴があります。Secret の値は既定では Base64 で符号化されているだけで、暗号化されていません。クラスタの設定を保存する etcd に平文同然で書き込まれ、Secret を読む権限を持つ人や、etcd のバックアップを手にした人は中身を読めます。etcd の保存時暗号化(EncryptionConfiguration、できれば KMS プラグイン)を有効にし、Secret を読める権限を RBAC で絞るのが前提です。さらに、外部のシークレットマネージャを正本にして、コンテナの起動時に取り出してファイルとして渡す方式もあります。取り出しを受け持つ小さなコンテナを横に置くサイドカー方式や、CSI ドライバでボリュームとして見せる方式です。IaC のコードにも値そのものは書かず、保管庫の参照だけを書くのが原則です。

漏えいに備える — 検知と失効の手順

どれだけ整えても、漏えいの可能性はゼロになりません。防御する側が用意しておくべきものは三つです。一つ目は検知です。git-secrets や Gitleaks のようなスキャナをコミット前のフックと CI に仕込み、シークレットらしき文字列の混入を機械で止めます。二つ目は失効の手順です。「このキーが漏れたら、どこで無効にし、何を再発行し、どのシステムに配り直すか」を平時に書いておきます。漏えい対応で最悪なのは、「このキーを止めたら何が壊れるか分からないので止められない」状態です(漏れたキーを消しても止まらず、失効させて初めて止まる様子は、APIキーの記事の部品で確かめられます)。三つ目は監査です。シークレットマネージャの取り出しログを監視し、ふだんと違う取り出し元・取り出し量を異常として扱います。シークレットの一覧と持ち主が把握できていること自体が、いちばんの防御力になります。

試問

APIキーやデータベースのパスワードのような秘密情報(シークレット)を、あるチームがいろいろな方法で扱っています。守る側の目で、8つのやり方を「秘密が守られている」か「まだ誰かに読める・使える」かで分けます。

✦ 命題挑戦中

シークレットの8つの扱い方を、守られているものと、まだ誰かに読める・使えるものに仕分けよ

守られている読める人・使える時間が絞られている
まだ読める・使える守ったつもりの穴が残る

項目を選んでから、入れる箱を選びます