【図解】シークレット管理とは?APIキーやパスワードを安全に置く・渡す・更新するしくみを初心者向けに解説

mTLS の記事までで、API キー、クライアントシークレット、証明書の秘密鍵と、「持っていれば本人として扱われる値」がいくつも出てきました。では、その値をどこに置き、どうアプリに渡し、いつ取り替えればよいのか。この記事では、シークレット管理の考え方を、会社の合鍵の管理にたとえながら、OWASP と Microsoft のドキュメントで確かめ、テスト用の環境で Git・detect-secrets・age を使って実際に試した結果と一緒に、一歩ずつ図にしました。

この記事でわかること
  1. シークレット(秘密の値)とは何か、どこから漏れやすいか
  2. 置き場所の比べ方と、「シークレットストア」に置く理由
  3. 作る・しまう・渡す・取り替える・止めるの流れ(ライフサイクル)
  4. Git に入れてしまうと何が起きるか、コミットの前に止める方法
  5. Azure Key Vault と App Service の Key Vault 参照の使い方

先に覚えておく3つの言葉

この記事では、カタカナの言葉がいくつも出てきます。先に、いちばん大事な3つを合鍵にたとえて押さえておきます。

シークレットたとえ:合鍵持っていれば「本人」として通れてしまう値シークレットストアたとえ:キーボックス合鍵をまとめて預け、貸し出しを記録する場所ローテーションたとえ:鍵の定期交換新しい値に取り替えて、古い値を使えなくするこの3つが分かれば、この記事の話はほぼ追える
図1 先に覚えておく3つの言葉

ほかの言葉は、出てきたところで説明します。迷ったら、「合鍵の話だとどうなるか」に置き換えて読んでみてください。

まずはたとえで:会社の合鍵

会社の倉庫の合鍵を、机の引き出しに入れたり、付箋に貼ったりしていたらどうでしょう。誰でも持ち出せて、いつ誰が使ったかも分かりません。何年も同じ鍵のままなら、昔の社員がまだ持っているかもしれません。

よくない合鍵の管理机の引き出しやメモに置く誰でも持ち出せるいつ誰が使ったか分からない何年も同じ鍵のままよい合鍵の管理(キーボックス)決まった場所にまとめて置く借りられる人を決めておく貸し出しの記録が残る定期的に交換、なくしたらすぐ交換シークレット管理は、API キーやパスワードという「デジタルの合鍵」の管理
図2 たとえで見るシークレット管理

きちんとした会社では、合鍵をキーボックスにまとめ、借りられる人を決め、貸し出しを記録し、定期的に鍵を交換します。なくしたら、すぐに鍵ごと取り替えます。

API キーやパスワードは、この合鍵と同じです。シークレット管理(secrets management)は、こうした「デジタルの合鍵」を、決まった場所に置き、使える相手を決め、記録を残し、定期的に取り替えるための考え方と仕組みです。この記事の話は、すべてこの図の右側を、コンピュータの世界でどう実現するか、という話です。

シークレットとは

ひとことで言うと

シークレットは、「それを持っていれば本人として通れてしまう値」です。

ふつうの設定値、たとえば接続先のサーバ名やポート番号は、人に知られても困りません。シークレットは違います。値そのものが鍵になっていて、知った人は誰でもその鍵で扉を開けられます。OWASP(Web のセキュリティの情報をまとめている団体)の Secrets Management Cheat Sheet は、API キー、データベースの資格情報、SSH の鍵、証明書などを例に挙げています。

API キー外部の API を呼ぶパスワード・接続文字列データベースやサービスにつなぐクライアントシークレットOAuth でトークンを取る秘密鍵(証明書)TLS・mTLS・署名に使うアクセスキー・トークンクラウドのストレージなどSSH の秘密鍵サーバに入る共通点:その値を持っている人やプログラムが「本人」として扱われる値が漏れると、持った人が誰でも本人になりすませる
図3 シークレットの種類

このシリーズで扱ってきたものの多くが、ここに入ります。たとえば API キーの記事で見たように、API キーは HTTP のヘッダに入れて送るだけで通ります。値をコピーされたら、コピーした人が「正しい利用者」として API を呼べてしまいます。

漏れやすい場所

ひとことで言うと

シークレットは、「置いた場所」から、コピーやログを通じて気づかないうちに広がります。

合鍵なら、どこにあるかは目で見て分かります。デジタルの値はコピーしても元の値は減らないので、どこまで広がったかが見えにくいのがやっかいなところです。OWASP は、ソースコードに平文(暗号化していない、そのままの文字)で書かれたシークレット、パイプラインの出力に出るシークレット、ログやダンプに入る環境変数などを注意点に挙げています。

シークレットソースコード値をそのまま書くGit の履歴消しても前のコミットに残る設定ファイル共有やバックアップで広がるログ・エラー出力値ごと記録してしまうCI/CD の出力パイプラインのログに出る環境変数ほかのプロセスやダンプから見えるシークレットは「置いた場所」から、気づかないうちに広がる
図4 シークレットが漏れやすい場所
場所どうして漏れるか合鍵でたとえると
ソースコードコードを見られる人は全員、値も見られる。共有や公開で一気に広がる鍵を設計図に貼って配る
Git の履歴ファイルから消しても、前の版(コミット)に残る鍵を外しても、昔の写真に写っている
設定ファイル共有フォルダやバックアップに一緒にコピーされる引き出しごと持ち出される
ログ・エラー出力エラーの調査のために、設定値をまるごと書き出してしまう貸出簿に鍵の型まで書く
CI/CD の出力自動ビルド・自動デプロイ(CI/CD)のログに値が表示される掲示板に鍵の写真を貼る
環境変数同じマシンのほかのプロセスや、障害時のダンプから見えることがある机の上に置きっぱなし

なかでも見落としやすいのが Git の履歴です。Git は「過去のすべての版」を残す道具なので、今のファイルから値を消しても、前の版を開けば値が出てきます。後半の「試してみる」で、実際に確かめます。

置き場所の比べ方

ひとことで言うと

コードには書かない。本番はシークレットストアに置く。そもそも値を持たずに済むなら、それがいちばん。

シークレットをどこに置くかを、6段階で比べます。右に行くほど漏れにくく、管理もしやすくなります。図の下には、合鍵でたとえるとどうなるかを書きました。

1コードに直接書く玄関のドアに貼っておく2設定ファイル・.env机の引き出しに入れておく3環境変数机の上に置いておく4暗号化したファイル鍵付きの箱に入れる5シークレットストア管理人のいるキーボックス6シークレットを持たない顔パスで通れるようにする漏れにくい・管理しやすい →合鍵でたとえるといちばん良いのは、置かなくて済むようにすること。置くならシークレットストア
図5 シークレットの置き場所の比較
置き場所気をつけること使ってよい場面
1 コードに直接書くGit の履歴や配布物に入り、消しても残る使わない
2 設定ファイル・.envGit に入れない設定(.gitignore)が要る。共有やバックアップで広がる手元の開発や試験
3 環境変数ほかのプロセスやログ、ダンプから見えることがあるほかの方法が使えないとき
4 暗号化したファイルファイルを開くための鍵を、別の場所で守る必要がある設定を Git で管理したいとき
5 シークレットストアアプリがストアに入るための ID が要る本番の基本
6 シークレットを持たない使えるサービスが限られる使えるなら最優先

5 のシークレットストアは、シークレットをまとめて暗号化して預かり、読める相手を決め、誰がいつ読んだかを記録するサービスです。管理人のいるキーボックスにあたります。Azure Key Vault や HashiCorp Vault などがあります。

6 の「持たない」は、合鍵を持たずに顔パスで通れるようにする考え方です。クラウドでは、基盤(Azure などのクラウドそのもの)が「このアプリは誰か」を知っていて、アプリにパスワードを持たせずに認証できます。これをマネージド ID といい、次の記事で扱います。

環境変数について

環境変数は、プログラムの外から設定値を渡すしくみで、「コードに書かない」ためによく使われます。ただし OWASP は、環境変数はたいていすべてのプロセスから読め、ログやシステムのダンプに入ることがあるとして、ほかの方法が使えない場合を除いて、シークレットの置き場所に使うことを勧めていません。コードから外す第一歩にはなりますが、本番ではストアから読む形をめざします。

ライフサイクル:作ってから捨てるまで

ひとことで言うと

シークレットは「作って置いたら終わり」ではなく、取り替えて、最後に止めるまでが仕事です。

ライフサイクルは、作ってから使い終わるまでの一生のことです。OWASP は、作る・取り替える・止める・期限を切るといった段階ごとに、やるべきことを挙げています。どの段階でも、誰が何をしたかを記録しておきます。

1 作る推測できない値で2 保管するストアで暗号化3 渡す・使う必要な相手だけ4 更新する定期的に交換5 無効にする要らない・漏れた記録する(監査)誰が・いつ・どれをシークレットは「作って終わり」ではなく、更新と無効化までが仕事
図6 シークレットのライフサイクル
段階やること合鍵でたとえると
1 作る推測できない、十分に長い値を作る。人が考えた文字列にしない複製しにくい鍵を作る
2 保管するストアに1か所だけ置き、暗号化してしまうキーボックスにしまう
3 渡す・使う必要なアプリにだけ「読む権限」を渡す(最小権限)借りられる人を決める
4 更新する定期的に新しい値に替え、盗まれた値が使える時間を短くする定期的に鍵を交換する
5 無効にする要らなくなったら、また漏れたかもしれないときは、すぐ使えなくする。できれば最初から期限を付けるなくしたら鍵穴ごと交換する
記録する誰が・いつ・どの値を読んだか、失敗した操作も含めて残し、消されないように守る貸し出しの記録簿

「最小権限」は、仕事に必要な分だけの権限を渡す、という考え方です。倉庫の鍵だけが要る人に、ビル全体のマスターキーは渡しません。

誰が何を持つか

ひとことで言うと

アプリは合鍵そのものを持たず、「キーボックスを開けられる社員証」だけを持ちます。

シークレットストアを使う形で、登場するものと持ち物を整理します。登場するのは、値を登録する管理者、値を預かるストア、値を使うアプリ(app01)、値を照合する相手の API の4つです。

管理者シークレットストアアプリ(app01)相手の API登録・更新の権限値を書き込める手元には残さないメモ・ファイルに置かない暗号化された値バージョンごとに期限・有効/無効いつまで使えるかアクセス権誰が読めるか監査ログ誰が・いつ読んだか自分の IDストアに入るため読む権限だけ書き換えはできないメモリ上の値ファイルやログに書かない照合用の値発行したキー発行・無効化2本目の発行などアプリが持つのは「値」ではなく「ストアに入る ID」。値は使うときに取りに行く
図7 誰が何を持つか

ポイントは2つです。1つめは、アプリが値そのものではなく、「ストアに入るための ID」と「読むだけの権限」を持つことです。値は、使うときにストアへ取りに行きます。2つめは、管理者も値を手元に残さないことです。値を登録したら、メモやファイルは消します。値を知っているのは、ストアと相手の API だけにします。OWASP も、担当者が全部のシークレットを見られる状態にせず、最小権限にするよう勧めています。

最初の1つの鍵

ここで疑問が出ます。アプリがストアに入るにも、何かの「鍵」が要ります。それをパスワードにして設定ファイルに書けば、そのパスワードの置き場所で、また同じ問題が起きます。キーボックスの暗証番号を、キーボックスに貼っておくようなものです。

ストアに入る鍵も、シークレットだとアプリストアストアに入るためのパスワードを設定ファイルに書くと、その値の置き場所でまた困る基盤が ID を用意するアプリストアクラウドの基盤がアプリに ID を与え、アプリはパスワードなしでストアに入る(マネージド ID。次の記事で扱う)「最初の1つの鍵」をどう渡すかが、シークレット管理の最後の課題
図8 ストアに入るための「最初の鍵」

この問題を解くのが、さきほどの「顔パス」です。クラウドでは、基盤がアプリに ID を与え、アプリはパスワードを持たずにストアへ入れます。Azure ではマネージド ID がこれにあたり、Microsoft も Key Vault へのアクセスにマネージド ID を使うよう勧めています。しくみは次の記事で詳しく扱います。

全体の流れ

ここからは、取引先の注文 API の API キーを、アプリ(app01)で使う例で、流れを順に見ます。準備、使うとき、取り替えるとき、漏れたとき、の4つです。

準備:シークレットをストアに登録する

あらすじ:管理者が相手の API でキーを発行し、ストアに預け、アプリには「読む権限」と「置き場所」だけを渡します。

管理者相手の API(発行元)シークレットストアアプリ(app01)① API キーを発行相手の管理画面で操作② API キーの値③ 値を登録期限・タグも付ける④ 暗号化して保存バージョン 1⑤ 手元のコピーを消す⑥ アプリの ID に権限読むだけ(書き換え不可)⑦ 設定に「場所と名前」だけ書く値そのものは書かない⑧ ストアで操作監査ログを残す⑨ ストアで操作期限前に通知する準備が終わると、値を知っているのはストアと相手の API だけアプリの設定や Git には「どこにある、何という名前の値か」しか残らない値はストアに1か所だけ。アプリには「読む権限」と「場所」だけを渡す
図9 準備の流れ(①〜⑨)
手順誰が・どこで何をするか値(物)はどうなるか
①管理者・相手の管理画面で操作API キーを発行する相手の API に照合用の値ができる
②相手の API → 管理者キーの値を受け取る値が管理者の画面に出る(いちばん危ない瞬間)
③管理者 → ストア値を登録する。期限やタグ(メモ書き)も付ける値がストアに入る
④ストア暗号化して保存するバージョン 1(1つめの版)として残る
⑤管理者手元のコピーを消す値を知るのはストアと相手の API だけになる
⑥管理者 → ストアアプリの ID に「読むだけ」の権限を付けるアプリは書き換えも削除もできない
⑦管理者 → アプリ設定に「ストアの場所と値の名前」を書く設定や Git には値が入らない
⑧ストアで操作監査ログ(誰が何をしたかの記録)を残す設定にする誰がいつ読んだかが分かる
⑨ストアで操作期限が近づいたら知らせる設定にする取り替えを忘れない

⑦のところが大事です。アプリの設定に書くのは「どのキーボックスの、何番の鍵か」だけで、鍵そのものは書きません。だから設定ファイルを Git に入れても、値は漏れません。

使うとき:アプリが値を取りに行く

あらすじ:アプリは起動したら自分の ID でストアに入り、値を受け取ってメモリに置き、それを使って相手の API を呼びます。

アプリ(app01)シークレットストア相手の API⑩ 起動する設定から場所を読む⑪ 自分の ID で認証して値を要求名前を指定する⑫ 権限を確かめるapp01 は読める⑬ 監査ログに記録誰が・いつ・どの値⑭ 値を返すTLS で暗号化された通信⑮ メモリに置くファイル・ログに書かない⑯ リクエスト+API キー⑰ 値を照合⑱ レスポンス毎回ストアに取りに行くと遅く、回数の上限にもかかるので、メモリにしばらく置く(キャッシュ)その間に値が更新されることがあるので、取り直すしくみも用意するアプリは使うときに値を取りに行き、ストアには「誰が読んだか」が残る
図10 使うときの流れ(⑩〜⑱)
手順誰が・どこで何をするか値(物)はどうなるか・使う ID
⑩アプリ起動して、設定からストアの場所を読むまだ値は持っていない
⑪アプリ → ストア自分の ID で認証して、名前を指定して値を求めるアプリの ID(社員証)を使う
⑫ストア権限を確かめるapp01 は「読むだけ」が許されている
⑬ストア監査ログに記録する誰が・いつ・どの値を読んだかが残る
⑭ストア → アプリ値を返すTLS で暗号化された通信で届く
⑮アプリメモリに置いて使うファイルやログには書かない
⑯アプリ → 相手の APIAPI キーを付けて呼ぶ値が相手に渡る
⑰相手の API値を照合する①で発行したキーと比べる
⑱相手の API → アプリレスポンスを返す―

⑮の「メモリに置く」は、パソコンの作業机にしばらく置いておくイメージです。毎回キーボックスまで取りに行くと時間がかかり、ストアの呼び出し回数の上限(スロットリング)にもかかります。Microsoft も、アプリで値をメモリにキャッシュ(一時的に取っておくこと)し、値が更新されたら取り直すよう勧めています。

取り替える:2本のキーを重ねて切り替える

あらすじ:新しいキーを先に用意し、アプリが新しいほうに切り替わったのを確かめてから、古いキーを止めます。

シークレットを定期的に取り替えることを、ローテーションと呼びます。もし値が盗まれていても、取り替えればその値は使えなくなるので、悪用できる時間を短くできます。ただし、急に古いキーを止めると、まだ古いキーを使っているアプリが止まってしまいます。そこで、相手が2本のキーを同時に持てるなら、2本を重ねて使う方法をとります。

管理者(または自動化)相手の APIシークレットストアアプリ(app01)❶ 期限が近いと通知❷ 2本目のキーを発行1本目もまだ使える❸ 新しいバージョンで登録バージョン 2❹ 次に読むとき新しい値キャッシュを取り直す❺ 新しいキーで呼ぶ❻ 古いキーが使われていないことを確かめる❼ 古いキーを無効化❽ 古いバージョンを無効❷〜❺の間は、古いキーと新しいキーの両方が使えるアプリが新しい値に切り替わるのを待ってから、古いキーを止める(❼)1本しか持てないサービスでは、交換の瞬間に失敗することがあるので、交換の時間を決めたり、アプリに再試行させたりする2本のキーを重ねて使い、切り替わってから古いほうを止める
図11 シークレットの更新(❶〜❽)
項目最初の図(図10)この図(更新)
相手の API のキー1本❷〜❻の間は2本とも使える
ストアの値バージョン 1バージョン 2 を足し(❸)、古いほうは無効にする(❽)
アプリ読んだ値を取っておいて使う次に読むときに新しい値に替わる(❹)
古いキーを止める時期―使われていないことを確かめてから(❻❼)

合鍵でたとえると、新しい鍵を全員に配り終えてから、古い鍵が使えないように鍵穴を替える、という順番です。Microsoft も、止めずに取り替えるために、2組の資格情報を持てるリソースでは2組を使うよう勧めています。期限が近いという通知(❶)をきっかけに、❷〜❽を自動で行う仕組みも作れます。Azure での例は後半で見ます。

漏れたとき:止める・替える・消す・調べる

あらすじ:まず漏れたキーを使えなくし、新しいキーに替え、それから漏れたコピーを片付けて、使われていないかを調べます。

合鍵を落としたときを考えてみてください。まず鍵穴を替えて落とした鍵を使えなくし、新しい鍵を配り、落とした場所を探し、防犯カメラで誰か入っていないかを確かめ、次から落とさない工夫をします。シークレットも同じ順番です。OWASP も、漏れたキーをすぐに無効にし、新しい値に替え、漏れたコピーを消し、記録を調べる、という順を挙げています。

担当者相手の APIシークレットストアGit・ログ(漏れた場所)❶ 漏れたキーを無効化まず止める❷ 新しいキー交換する❸ 新しいバージョンで登録アプリはこれを読む❹ 漏れたコピーを消すコード・ログ・履歴❺ 使われたかを調べるアクセスログ❺ 誰が読んだかを調べる監査ログ❻ 再発を防ぐ検出ツール・コミット前の確認順番が大事履歴を消すより先に、キーを止めるか交換する。push した後は、ほかの人の手元のコピーやフォークに残っているかもしれないので、消しただけでは安全にならない漏れたら「止める → 交換 → 消す → 調べる → 防ぐ」の順に進める
図12 シークレットが漏れたときの流れ(❶〜❻)
手順何をするか理由合鍵でたとえると
❶漏れたキーを無効にするほかの誰かが使えないようにする鍵穴を替える
❷❸新しいキーに替え、ストアに登録するアプリが新しい値で動けるようにする新しい鍵を配る
❹漏れたコピーを消すコード・ログ・履歴に残った値を減らす落とした鍵を探して回収する
❺使われたか、誰が読んだかを調べる被害があったかを確かめる防犯カメラを確かめる
❻再発を防ぐ検出ツールやコミット前の確認を入れる落とさない工夫をする

よくある間違いは、❹の「消す」から始めてしまうことです。GitHub のドキュメントも、シークレットを含むコミットを push(共有のサーバへ送ること)してしまったら、まずそのシークレットを無効にするか取り替えることが最初の手順だ、としています。履歴を書き換えても、ほかの人がすでにコピー(clone)した分や、派生したリポジトリ(フォーク)に残ったものは消せないからです。

試してみる(Git・detect-secrets・age)

ここからは、テスト用の環境で Git 2.43.0、detect-secrets 1.5.0、age 1.1.1 を使って試した結果を見ていきます。API キーの値(9f2c…)は、この試験のためにでたらめに作ったもので、どのサービスでも使えません。

先に、このあと出てくる Git の言葉をまとめておきます。

言葉意味
コミットファイルの今の状態を、1つの版として Git に記録すること。版ごとに b77df10 のような番号(ハッシュ)が付く
履歴これまでのコミットの積み重ね。昔の版をいつでも取り出せる
push・clonepush は共有のサーバへ送ること、clone はサーバから手元へ丸ごとコピーすること
フックコミットなどの直前・直後に、自動で動かすスクリプト

API キーをコードに書いてコミットする

確かめること:コードに値を書いてコミットすると、値が履歴に入ること。まずは、よくない例です。注文 API を呼ぶ app.py に、API キーをそのまま書いてコミットします。

作業フォルダapp.pyAPI_KEY = “9f2c…”3行目に値をそのまま書いたcommitGit の履歴b77df10add order clientこのコミットの app.py に値がそのまま入るコミットした瞬間に、値は「履歴の一部」になる
図13 STEP 1 の様子

app.py(最初の版)

import requests

API_KEY = "9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b"

def get_orders():
    return requests.get("https://api.example.test/orders",
                        headers={"X-API-Key": API_KEY})

コミットする

git add app.py && git commit -m "add order client"
[main (root-commit) b77df10] add order client
 1 file changed, 7 insertions(+)
 create mode 100644 app.py

1つめの版(b77df10)ができました。この版の app.py には、3行目の値がそのまま入っています。

値を消してコミットし直しても、履歴に残る

確かめること:今のファイルから値を消しても、昔の版から取り出せること。app.py を環境変数から読む形に直して、もう一度コミットします。

Git の履歴(古い → 新しい)b77df10add order client7fff2eeread API key from env…app.py に値があるgit show b77df10:app.py で見えるapp.py は環境変数から読む今のファイルを grep しても見つからないgit log -S は、その文字列が増えたり減ったりしたコミットを探すpush 済みなら、ほかの人の clone やフォークにも同じ履歴があるファイルから消しても、前のコミットには値が残る
図14 STEP 2 の様子

app.py(直した版)

import os
import requests

API_KEY = os.environ["ORDERS_API_KEY"]

def get_orders():
    return requests.get("https://api.example.test/orders",
                        headers={"X-API-Key": API_KEY})

コミットする

git commit -am "read API key from environment"
[main 7fff2ee] read API key from environment
 1 file changed, 2 insertions(+), 1 deletion(-)

今のファイルに値があるかを探す

grep -n "9f2c1e7b" app.py; echo "exit=$?"
exit=1

grep は文字列を探すコマンドで、見つからないと終了コード 1 を返します。今のファイルには、もう値はありません。でも、履歴を探すと出てきます。

履歴を見る

git log --oneline
7fff2ee read API key from environment
b77df10 add order client

値が増えたり減ったりしたコミットを探す

git log -p -S 9f2c1e7b --oneline | grep -E "^[0-9a-f]{7} |9f2c1e7b"
7fff2ee read API key from environment
-API_KEY = "9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b"
b77df10 add order client
+API_KEY = "9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b"

古いコミットの app.py を見る

git show b77df10:app.py | grep -n API_KEY
3:API_KEY = "9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b"
7:                        headers={"X-API-Key": API_KEY})

git log -S は、指定した文字列の数が変わった(足された・消された)コミットを探すオプションです。値を足した版(b77df10、行頭が +)と、消した版(7fff2ee、行頭が -)の両方が見つかりました。さらに git show で、古い版の app.py をそのまま取り出せました。この履歴を push していれば、clone した人の手元にも同じものがあります。「消したから大丈夫」ではない、ということです。

detect-secrets で見つける

確かめること:ツールで、コードの中のシークレットらしい文字列を見つけられること。detect-secrets は、コードの中から「シークレットらしいもの」を探してくれるツールです。OWASP の資料でも、シークレットを見つけるツールの例として挙がっています。新しいリポジトリ(Git で管理するフォルダ)で、STEP 1 と同じ app.py を調べます。

app.pydetect-secrets scanHex High Entropy Stringでたらめに見える16進の文字列Secret KeywordKEY・password などの名前への代入結果app.py の3行目見つけた種類:2つhashed_secret:値の SHA-1値そのものは書かない「値らしい文字列」と「値らしい名前」の両方で探し、場所を教えてくれる
図15 STEP 3 の様子

app.py を調べる

detect-secrets scan app.py | jq '.results'
{
  "app.py": [
    {
      "type": "Hex High Entropy String",
      "filename": "app.py",
      "hashed_secret": "f1284e4cf38bba5ee051b52d2f2ff9cb126bb757",
      "is_verified": false,
      "line_number": 3
    },
    {
      "type": "Secret Keyword",
      "filename": "app.py",
      "hashed_secret": "f1284e4cf38bba5ee051b52d2f2ff9cb126bb757",
      "is_verified": false,
      "line_number": 3
    }
  ]
}

3行目が、2つの見方で見つかりました。

  • Hex High Entropy String:「でたらめさ(エントロピー)が高い16進の文字列」です。人が書く文章や名前は規則的ですが、鍵の値はでたらめな文字の並びなので、そこで見分けます。
  • Secret Keyword:KEY や password のような、いかにもシークレットらしい名前に値を入れている行です。

結果にある hashed_secret は、値そのものではなく、値のハッシュです。ハッシュは、元の値から計算した「指紋」のようなもので、指紋から元の値には戻せません。本当にハッシュかを確かめてみます。

値の SHA-1(ハッシュの計算方法の1つ)を計算して比べる

echo -n 9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b | sha1sum
f1284e4cf38bba5ee051b52d2f2ff9cb126bb757  -

結果の hashed_secret と同じ値になりました。結果のファイルを人に見せても、値そのものは広がらない作りになっています。

コミットする前に止める

確かめること:コミットの直前にチェックを入れると、値が履歴に入る前に止められること。STEP 2 で見たとおり、履歴に入ってからでは消しきれません。そこで、コミットの直前に自動で動く Git のフック(pre-commit フック)で detect-secrets を動かし、見つかったらコミットを止めます。改札で切符を確かめるように、入口で止めるイメージです。

開発者pre-commitフックGit の履歴git commitコミットしようとするdetect-secrets-hook追加・変更したファイルを調べるERROR:値が見つかったexit=1 でコミットを止める履歴には入らない値を外してもう一度環境変数から読む形に見つからない → コミット0bd08a1履歴に入る前に止めれば、「消しても残る」問題は起きない
図16 STEP 4 の流れ

.git/hooks/pre-commit の中身

#!/bin/sh
git diff --cached --name-only --diff-filter=ACM | xargs -r detect-secrets-hook

中身は1行で、「今回コミットしようとしているファイル」を detect-secrets-hook に渡して調べさせています。

フックを有効にして、コミットしてみる

chmod +x .git/hooks/pre-commit
git add app.py && git commit -m "add order client"; echo "exit=$?"
ERROR: Potential secrets about to be committed to git repo!

Secret Type: Hex High Entropy String
Location:    app.py:3

Secret Type: Secret Keyword
Location:    app.py:3

Possible mitigations:
  - For information about putting your secrets in a safer place, please ask in
    #security
  - Mark false positives with an inline `pragma: allowlist secret` comment

If a secret has already been committed, visit
https://help.github.com/articles/removing-sensitive-data-from-a-repository
exit=1

「シークレットがコミットされそうだ」というエラーで、コミットが止まりました。exit=1 は失敗を表し、履歴には何も入っていません。メッセージにある pragma: allowlist secret は、誤検知(シークレットではないのに見つかったもの)を除くための書き方です。本物の値に付けて通すためのものではありません。

GitHub のドキュメントも、コミット前のフックで、よく知られた検出ツールを動かすことを勧めています。GitHub.com には、push の段階でシークレットを含むコミットを止める機能(push protection)もあります。

コードから値を外す(.env と .gitignore)

確かめること:値をコードの外のファイルに置き、Git に入らないようにできること。手元の試験では、値を .env というファイルに置き、.gitignore(Git に無視させるファイルの一覧)に書いて、Git の対象から外します。

作業フォルダapp.pyos.environ から読む.gitignore中身は「.env」の1行.env値はここだけ-rw——-(自分だけ)git status ではA .gitignoreA app.py.env は出ないGit の履歴に入るGit には入らないコードと値を分ける。ただし .env は「最後の手段」で、本番はストアから読む
図17 STEP 5 の様子

.env を作り、Git の対象から外し、自分だけが読めるようにする

printf 'ORDERS_API_KEY=9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b\n' > .env
printf '.env\n' > .gitignore
chmod 600 .env
ls -l .env
-rw------- 1 root root 48 Nov  2 13:14 .env

chmod 600 は、ファイルの持ち主だけが読み書きできるようにする設定です。-rw------- の表示は「持ち主は読める・書ける、ほかの人は何もできない」という意味です。

Git に入るファイルを見る

git add -A && git status --short
A  .gitignore
A  app.py

.env が外れている理由を見る

git check-ignore -v .env
.gitignore:1:.env	.env

コミットする(フックは通る)

git commit -m "read API key from environment"
[main (root-commit) 0bd08a1] read API key from environment
 2 files changed, 9 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 app.py

Git に入るのは .gitignore と app.py の2つだけで、.env は出てきません。git check-ignore -v の結果から、.gitignore の1行目の指定で外れていることが分かります。app.py は環境変数から読む形なので、STEP 4 のフックも通りました。

ただし、.env は手元の開発用です。机の引き出しと同じで、共有フォルダやバックアップに入ると、引き出しごと持ち出されます。本番では使いません。

暗号化してから置く(age)

確かめること:暗号化したファイルは、ペアの秘密鍵がないと開けないこと。設定ファイルを Git で管理したい場合は、暗号化してから置く方法があります。ここでは、ファイルを暗号化するツールの age を使います。age は「公開鍵で暗号化し、ペアの秘密鍵でだけ開ける」方式です。公開鍵は「誰でも閉められる南京錠」、秘密鍵は「その南京錠を開けられる唯一の鍵」と考えると分かりやすいです(くわしくは暗号の基礎の記事で扱いました)。

鍵(Git の外)app01.txt秘密鍵・~/keys に置く-rw——-app01.pub公開鍵・配ってよい.env公開鍵で暗号化.env.ageGit に入れてよいapp01.txt で復号値が戻るother.txt では開けない置き場所を分ける暗号化したファイルと復号する秘密鍵を同じ場所に置かない暗号化しても、守る対象が「値」から「復号する鍵」に移るだけ
図18 STEP 6 の様子

鍵ペアを作る(秘密鍵は Git の外に置く)

age-keygen -o ~/keys/app01.txt
ls -l ~/keys/app01.txt
Public key: age1zap0ke6c0tdugkv572p5g4rz4kapkf95mv7hwn50lvemdjej7e3q26dw7v
-rw------- 1 root root 184 Nov  2 13:14 /root/keys/app01.txt

Public key: の行が公開鍵です。秘密鍵は ~/keys/app01.txt に保存され、持ち主だけが読めるようになっています。

公開鍵を取り出す

age-keygen -y ~/keys/app01.txt > app01.pub
cat app01.pub
age1zap0ke6c0tdugkv572p5g4rz4kapkf95mv7hwn50lvemdjej7e3q26dw7v

公開鍵で .env を暗号化する

age -R app01.pub -o .env.age .env
head -n 2 .env.age
age-encryption.org/v1
-> X25519 OaH6ik4Pn8jJuuHz8Q9iI13MeV4gjXX2ZTicKLqA2xk

暗号化したファイル(.env.age)の先頭は age の形式を示す行で、その後は読めない文字の並びです。

秘密鍵で開く(復号する)

age -d -i ~/keys/app01.txt .env.age
ORDERS_API_KEY=9f2c1e7b4a6d8e0f3b5a7c9d1e2f4a6b

別の鍵(other.txt)で開いてみる

age -d -i ~/keys/other.txt .env.age
age: error: no identity matched any of the recipients
age: report unexpected or unhelpful errors at https://filippo.io/age/report

ペアの秘密鍵では元の値が戻り、別の鍵では「合う鍵がない」というエラーで開けませんでした。

暗号化したファイルと公開鍵をコミットする

git add .env.age app01.pub
git commit -m "add encrypted env"
[main 8616cdc] add encrypted env
 2 files changed, 6 insertions(+)
 create mode 100644 .env.age
 create mode 100644 app01.pub

暗号化したファイルは Git に入れても中身は読めず、STEP 4 のフックも通りました。ただし、これで守る対象が「値」から「開けるための秘密鍵」に移っただけです。鍵付きの箱に入れても、箱の鍵を箱の横に置いたら意味がありません。OWASP も、暗号化に使う鍵を、それで暗号化したシークレットのそばに置かないよう注意しています。秘密鍵をどこに置き、誰に渡すかは、やはりシークレット管理の問題として残ります。

ここからは Microsoft の場合です

Azure Key Vault

ひとことで言うと

Azure Key Vault は、「管理人のいるキーボックス」をクラウドで用意したサービスです。

Azure Key Vault には、パスワード、接続文字列、API キーなどのシークレットのほか、鍵や証明書も預けられます。シークレットは保存するときに自動で暗号化され、読むときに復号されるので、利用者が暗号化を意識する必要はありません。これまで見てきたライフサイクルの機能が、ひととおりそろっています。

Azure Key Vault でシークレットを管理する機能何ができるか合鍵でたとえるとバージョン値を変えるたびに新しい版。古い版も残る鍵の交換の履歴期限・有効期限・使い始め・有効/無効を付けられる鍵の使用期限権限(RBAC)読むだけ/管理できる、を人ごとに決める借りられる人の名簿通知期限の30日前にお知らせを出す交換時期のお知らせ削除の保護消しても既定で90日は戻せるごみ箱ログ誰が・いつ・どの値を読んだか貸し出しの記録簿Key Vault は「管理人のいるキーボックス」をクラウドで用意したもの
図19 Azure Key Vault でシークレットを管理する
  • バージョンと期限:値を変えるたびに新しいバージョンになり、古い版も残ります。期限(exp)・使い始め(nbf)・有効か無効か(enabled)を付けられます。取り替えの予定や担当者などのメモは、値の中ではなくタグに入れるよう勧められています。
  • 権限:Azure RBAC(役割ごとに権限を決めるしくみ)を使います。アプリには値を読むだけの「Key Vault Secrets User」、管理者には管理用の「Key Vault Secrets Officer」を割り当てます。「Key Vault Contributor」は Key Vault という入れ物を管理するロールで、中のシークレットの値は読めません。キーボックスの設置係と、鍵を借りられる人が別、というイメージです。
  • 分け方:Microsoft は、アプリ・リージョン・環境(開発・本番など)ごとに Key Vault を分けるよう勧めています。1つのキーボックスに全部の鍵を入れると、そこが破られたときの被害が大きくなるからです。IP アドレスや機能の切り替えのような、ふつうの設定は Key Vault ではなく Azure App Configuration に置きます。
  • 削除の保護:論理的な削除(soft delete)は、ごみ箱のようなものです。新しい Key Vault では最初から有効で、消したものを既定で90日(7〜90日で設定)は元に戻せます。消去の保護(purge protection)を有効にすると、その期間中はごみ箱を空にできず、誰も完全には消せません。
  • 通知と記録:期限の30日前に Microsoft.KeyVault.SecretNearExpiry というお知らせ(イベント)が、Azure のイベント配信サービスの Event Grid に出ます。Key Vault のログを有効にすると、操作の記録が残ります。

管理者に Key Vault Secrets Officer を割り当てる(Microsoft Learn のクイックスタートより)

az role assignment create --role "Key Vault Secrets Officer" \
    --assignee-object-id $(az ad signed-in-user show --query id -o tsv) \
    --scope $(az keyvault show --name $keyVaultName --query id -o tsv)

サインインしている自分に、この Key Vault のシークレットを管理する役割を付けるコマンドです。

期限とタグを付けてシークレットを登録する(Microsoft Learn のチュートリアルより)

tomorrowDate=$(date -u -d "+1 day" +"%Y-%m-%dT%H:%M:%SZ")
az keyvault secret set --name storageKey --vault-name vaultrotation-kv --value <key1-value> --tags "CredentialId=key1" "ProviderAddress=<storage-account-resource-id>" "ValidityPeriodDays=60" --expires $tomorrowDate

このコマンドは、ストレージのキーを2本交互に取り替えるチュートリアルの一部で、試すために期限を翌日にしています。チュートリアルでは、期限の30日前のお知らせで Azure Functions(必要なときだけ動くプログラム)が動き、使っていないほうのキーを作り直して、新しいバージョンとして登録します。図11 の❶〜❸を自動にした形です。

App Service の Key Vault 参照

ひとことで言うと

アプリの設定に「Key Vault のどの値か」を書いておくと、App Service が代わりに取ってきてくれます。

Azure App Service は、Web アプリを動かすための Azure のサービスです。アプリ設定の値に「Key Vault 参照」という書き方をすると、App Service がアプリのマネージド ID で Key Vault から値を取ってきて、ふつうのアプリ設定としてアプリに渡します。アプリのコードを変える必要はありません。Azure Functions なども同じしくみを使えます。

アプリのコードApp Service(基盤)Key Vault相手の API❶ アプリ設定にKey Vault 参照を見つける❷ マネージド ID で取得パスワードは使わない❸ 権限を確かめるKey Vault Secrets Userログに記録❹ 値を返す❺ アプリ設定として渡す(24時間キャッシュ)❻ ふつうの設定値コードは変えない❼ API キーを付けて呼ぶ参照の形:@Microsoft.KeyVault(SecretUri=…)バージョンを書かなければ、新しいバージョンに24時間以内に切り替わる値は Key Vault に1か所。App Service がマネージド ID で取りに行き、アプリに渡す
図20 App Service の Key Vault 参照の流れ(❶〜❼)

Key Vault 参照の書き方(Microsoft Learn より)

@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret)
@Microsoft.KeyVault(VaultName=myvault;SecretName=mysecret)

1つめは値の場所を URL で、2つめは Key Vault の名前と値の名前で指定する書き方です。どちらも、アプリ設定の値の欄に書きます。

項目最初の図(図10)App Service の Key Vault 参照(この図)
ストアに取りに行くのはアプリのコード(⑪)App Service の基盤(❷)
使う IDアプリの IDアプリのマネージド ID(既定はシステム割り当て)
アプリへの渡し方コードがメモリに置く(⑮)ふつうのアプリ設定として渡す(❺❻)
取り替えへの追従アプリが取り直すバージョンを書かなければ、24時間以内に新しい値になる

App Service は Key Vault 参照の値を取っておき、24時間ごとに取り直します。アプリの設定を変えると再起動して、すぐに取り直します。開発用・本番用で Key Vault を分けるので、Key Vault 参照のアプリ設定は、多くの場合スロット(本番用・検証用などの入れ替えられる環境)ごとに固定する設定にします。

Microsoft Entra ID のクライアントシークレット

Entra ID のアプリ登録で作るクライアントシークレット(アプリ用のパスワード)も、シークレット管理の対象です。期限は最長2年(24か月)で、Microsoft は12か月より短くするよう勧めています。値は作ったときに一度しか表示されないので、その場でストアに登録します。さらに Microsoft は、本番のアプリではクライアントシークレットではなく、証明書かフェデレーションの資格情報を使うよう勧めています。

シークレットを持たない方向へ

Microsoft のドキュメントは、Azure のサービスへの認証にはマネージド ID がいちばん良く、ストレージのアカウントキーなども、できるならキーを使わない Entra ID の認証に切り替えて、取り替えそのものを不要にするよう勧めています。シークレットストアは、どうしても値が要る相手のための置き場所、と考えると整理しやすくなります。

よくある間違い

間違いどうするか
コードから値を消したので、安全になったと思う履歴に残る。push 済みなら、まず値を無効にして取り替える
履歴の書き換えを先にやる先に止める・替える。ほかの人のコピーやフォークは消せない
全部のアプリで1つのストアを共有し、どのアプリも全部読めるアプリ・環境ごとに分け、アプリには自分の値を読む権限だけを渡す
ストアに入るパスワードを設定ファイルに書くマネージド ID など、基盤が用意する ID を使う
期限なしのシークレットを作る期限を付け、期限前の通知と取り替えの手順を決めておく
例外やログに設定値をまるごと出すシークレットはログやエラーメッセージに書かない。パイプラインの出力も確かめる

うまくいかないとき

シークレット管理のトラブルは、「値がどこにあり、誰が読めて、いつ取り替えられたか」を順に確かめると、原因を絞れます。

起きたことよくある理由コミットが止まった値らしい文字列がある(誤検知もある)もう push してしまった履歴とほかの人のコピーに残る参照の文字列がそのまま値に権限・ネットワーク・書き方の誤り管理者なのに値が読めないデータを読むロールがない取り替えたのに古い値のままアプリ側で取っておいた値暗号化したファイルが開けない開けるための鍵が違うまず「値がどこにあり、誰が読めて、いつ取り替えられたか」を確かめる
図21 シークレット管理がうまくいかないとき
起きたことよくある理由確認すること
コミットが止まった値らしい文字列がある本物の値なら外してストアへ。誤検知なら pragma: allowlist secret で除く
もう push してしまった履歴とほかの人のコピーに残るまず値を無効にして取り替える。そのうえで履歴の扱いを決める
参照の文字列がそのまま値になる権限・ネットワーク・書き方の誤りマネージド ID に Key Vault Secrets User があるか。値が消えていないか。アプリ設定の画面で参照の状態を見る
管理者なのに値が読めないデータを読むロールがないKey Vault Contributor ではなく、Secrets Officer などのロールが要る
取り替えたのに古い値のままアプリ側で取っておいた値Key Vault 参照は24時間以内に追従。急ぐときは設定を変えて再起動する
暗号化したファイルが開けない開けるための鍵が違うSTEP 6 の no identity matched。暗号化に使った公開鍵とペアの秘密鍵か

App Service で、Key Vault への通信を絞っている(ネットワークを制限している)場合は、アプリの仮想ネットワークから Key Vault に届くことも必要です。Microsoft のドキュメントは、参照がうまくいかないときの原因として、権限の設定の誤り、シークレットがもう無いこと、参照の書き方の誤りを挙げています。

まとめ

項目内容合鍵でたとえると
シークレットとは持っていれば本人として通れてしまう値(API キー、パスワード、秘密鍵など)合鍵
置き場所コードには書かない。本番はシークレットストア。持たずに済むならそれがいちばんキーボックス/顔パス
ライフサイクル作る・しまう・渡す・取り替える・止める と、その記録鍵の一生と貸出簿
アプリが持つもの値ではなく、ストアに入る ID と読む権限キーボックスを開ける社員証
取り替え2本を重ねて使い、切り替わってから古いほうを止める新しい鍵を配ってから鍵穴を替える
漏れたとき止める → 替える → 消す → 調べる → 防ぐまず鍵穴を替える
Git消しても履歴に残る。コミット前のフックで止める昔の写真に写った鍵
MicrosoftKey Vault(権限・期限・通知・削除の保護)と、App Service の Key Vault 参照クラウドのキーボックス

参考にした資料

この記事は、次の資料と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。

あとがき

試してみて改めて感じたのは、Git に一度入った値は、ファイルから消しても git log -S ひとつで出てくるということでした。消すことより、入る前に止めることと、入ってしまったらまず値そのものを止めることのほうが大事だと、手を動かして納得できました。また、暗号化しても「開けるための鍵をどこに置くか」が残り、最後はストアに入るための ID の話に行き着きます。次の記事では、その ID を基盤が用意する、ワークロード ID(マネージド ID)を扱います。

コメント