mTLS の記事までで、API キー、クライアントシークレット、証明書の秘密鍵と、「持っていれば本人として扱われる値」がいくつも出てきました。では、その値をどこに置き、どうアプリに渡し、いつ取り替えればよいのか。この記事では、シークレット管理の考え方を、会社の合鍵の管理にたとえながら、OWASP と Microsoft のドキュメントで確かめ、テスト用の環境で Git・detect-secrets・age を使って実際に試した結果と一緒に、一歩ずつ図にしました。
- シークレット(秘密の値)とは何か、どこから漏れやすいか
- 置き場所の比べ方と、「シークレットストア」に置く理由
- 作る・しまう・渡す・取り替える・止めるの流れ(ライフサイクル)
- Git に入れてしまうと何が起きるか、コミットの前に止める方法
- Azure Key Vault と App Service の Key Vault 参照の使い方
先に覚えておく3つの言葉
この記事では、カタカナの言葉がいくつも出てきます。先に、いちばん大事な3つを合鍵にたとえて押さえておきます。
ほかの言葉は、出てきたところで説明します。迷ったら、「合鍵の話だとどうなるか」に置き換えて読んでみてください。
まずはたとえで:会社の合鍵
会社の倉庫の合鍵を、机の引き出しに入れたり、付箋に貼ったりしていたらどうでしょう。誰でも持ち出せて、いつ誰が使ったかも分かりません。何年も同じ鍵のままなら、昔の社員がまだ持っているかもしれません。
きちんとした会社では、合鍵をキーボックスにまとめ、借りられる人を決め、貸し出しを記録し、定期的に鍵を交換します。なくしたら、すぐに鍵ごと取り替えます。
API キーやパスワードは、この合鍵と同じです。シークレット管理(secrets management)は、こうした「デジタルの合鍵」を、決まった場所に置き、使える相手を決め、記録を残し、定期的に取り替えるための考え方と仕組みです。この記事の話は、すべてこの図の右側を、コンピュータの世界でどう実現するか、という話です。
シークレットとは
シークレットは、「それを持っていれば本人として通れてしまう値」です。
ふつうの設定値、たとえば接続先のサーバ名やポート番号は、人に知られても困りません。シークレットは違います。値そのものが鍵になっていて、知った人は誰でもその鍵で扉を開けられます。OWASP(Web のセキュリティの情報をまとめている団体)の Secrets Management Cheat Sheet は、API キー、データベースの資格情報、SSH の鍵、証明書などを例に挙げています。
このシリーズで扱ってきたものの多くが、ここに入ります。たとえば API キーの記事で見たように、API キーは HTTP のヘッダに入れて送るだけで通ります。値をコピーされたら、コピーした人が「正しい利用者」として API を呼べてしまいます。
漏れやすい場所
シークレットは、「置いた場所」から、コピーやログを通じて気づかないうちに広がります。
合鍵なら、どこにあるかは目で見て分かります。デジタルの値はコピーしても元の値は減らないので、どこまで広がったかが見えにくいのがやっかいなところです。OWASP は、ソースコードに平文(暗号化していない、そのままの文字)で書かれたシークレット、パイプラインの出力に出るシークレット、ログやダンプに入る環境変数などを注意点に挙げています。
| 場所 | どうして漏れるか | 合鍵でたとえると |
|---|---|---|
| ソースコード | コードを見られる人は全員、値も見られる。共有や公開で一気に広がる | 鍵を設計図に貼って配る |
| Git の履歴 | ファイルから消しても、前の版(コミット)に残る | 鍵を外しても、昔の写真に写っている |
| 設定ファイル | 共有フォルダやバックアップに一緒にコピーされる | 引き出しごと持ち出される |
| ログ・エラー出力 | エラーの調査のために、設定値をまるごと書き出してしまう | 貸出簿に鍵の型まで書く |
| CI/CD の出力 | 自動ビルド・自動デプロイ(CI/CD)のログに値が表示される | 掲示板に鍵の写真を貼る |
| 環境変数 | 同じマシンのほかのプロセスや、障害時のダンプから見えることがある | 机の上に置きっぱなし |
なかでも見落としやすいのが Git の履歴です。Git は「過去のすべての版」を残す道具なので、今のファイルから値を消しても、前の版を開けば値が出てきます。後半の「試してみる」で、実際に確かめます。
置き場所の比べ方
コードには書かない。本番はシークレットストアに置く。そもそも値を持たずに済むなら、それがいちばん。
シークレットをどこに置くかを、6段階で比べます。右に行くほど漏れにくく、管理もしやすくなります。図の下には、合鍵でたとえるとどうなるかを書きました。
| 置き場所 | 気をつけること | 使ってよい場面 |
|---|---|---|
| 1 コードに直接書く | Git の履歴や配布物に入り、消しても残る | 使わない |
| 2 設定ファイル・.env | Git に入れない設定(.gitignore)が要る。共有やバックアップで広がる | 手元の開発や試験 |
| 3 環境変数 | ほかのプロセスやログ、ダンプから見えることがある | ほかの方法が使えないとき |
| 4 暗号化したファイル | ファイルを開くための鍵を、別の場所で守る必要がある | 設定を Git で管理したいとき |
| 5 シークレットストア | アプリがストアに入るための ID が要る | 本番の基本 |
| 6 シークレットを持たない | 使えるサービスが限られる | 使えるなら最優先 |
5 のシークレットストアは、シークレットをまとめて暗号化して預かり、読める相手を決め、誰がいつ読んだかを記録するサービスです。管理人のいるキーボックスにあたります。Azure Key Vault や HashiCorp Vault などがあります。
6 の「持たない」は、合鍵を持たずに顔パスで通れるようにする考え方です。クラウドでは、基盤(Azure などのクラウドそのもの)が「このアプリは誰か」を知っていて、アプリにパスワードを持たせずに認証できます。これをマネージド ID といい、次の記事で扱います。
環境変数は、プログラムの外から設定値を渡すしくみで、「コードに書かない」ためによく使われます。ただし OWASP は、環境変数はたいていすべてのプロセスから読め、ログやシステムのダンプに入ることがあるとして、ほかの方法が使えない場合を除いて、シークレットの置き場所に使うことを勧めていません。コードから外す第一歩にはなりますが、本番ではストアから読む形をめざします。
ライフサイクル:作ってから捨てるまで
シークレットは「作って置いたら終わり」ではなく、取り替えて、最後に止めるまでが仕事です。
ライフサイクルは、作ってから使い終わるまでの一生のことです。OWASP は、作る・取り替える・止める・期限を切るといった段階ごとに、やるべきことを挙げています。どの段階でも、誰が何をしたかを記録しておきます。
| 段階 | やること | 合鍵でたとえると |
|---|---|---|
| 1 作る | 推測できない、十分に長い値を作る。人が考えた文字列にしない | 複製しにくい鍵を作る |
| 2 保管する | ストアに1か所だけ置き、暗号化してしまう | キーボックスにしまう |
| 3 渡す・使う | 必要なアプリにだけ「読む権限」を渡す(最小権限) | 借りられる人を決める |
| 4 更新する | 定期的に新しい値に替え、盗まれた値が使える時間を短くする | 定期的に鍵を交換する |
| 5 無効にする | 要らなくなったら、また漏れたかもしれないときは、すぐ使えなくする。できれば最初から期限を付ける | なくしたら鍵穴ごと交換する |
| 記録する | 誰が・いつ・どの値を読んだか、失敗した操作も含めて残し、消されないように守る | 貸し出しの記録簿 |
「最小権限」は、仕事に必要な分だけの権限を渡す、という考え方です。倉庫の鍵だけが要る人に、ビル全体のマスターキーは渡しません。
誰が何を持つか
アプリは合鍵そのものを持たず、「キーボックスを開けられる社員証」だけを持ちます。
シークレットストアを使う形で、登場するものと持ち物を整理します。登場するのは、値を登録する管理者、値を預かるストア、値を使うアプリ(app01)、値を照合する相手の API の4つです。
ポイントは2つです。1つめは、アプリが値そのものではなく、「ストアに入るための ID」と「読むだけの権限」を持つことです。値は、使うときにストアへ取りに行きます。2つめは、管理者も値を手元に残さないことです。値を登録したら、メモやファイルは消します。値を知っているのは、ストアと相手の API だけにします。OWASP も、担当者が全部のシークレットを見られる状態にせず、最小権限にするよう勧めています。
最初の1つの鍵
ここで疑問が出ます。アプリがストアに入るにも、何かの「鍵」が要ります。それをパスワードにして設定ファイルに書けば、そのパスワードの置き場所で、また同じ問題が起きます。キーボックスの暗証番号を、キーボックスに貼っておくようなものです。
この問題を解くのが、さきほどの「顔パス」です。クラウドでは、基盤がアプリに ID を与え、アプリはパスワードを持たずにストアへ入れます。Azure ではマネージド ID がこれにあたり、Microsoft も Key Vault へのアクセスにマネージド ID を使うよう勧めています。しくみは次の記事で詳しく扱います。
全体の流れ
ここからは、取引先の注文 API の API キーを、アプリ(app01)で使う例で、流れを順に見ます。準備、使うとき、取り替えるとき、漏れたとき、の4つです。
準備:シークレットをストアに登録する
あらすじ:管理者が相手の API でキーを発行し、ストアに預け、アプリには「読む権限」と「置き場所」だけを渡します。
| 手順 | 誰が・どこで | 何をするか | 値(物)はどうなるか |
|---|---|---|---|
| ① | 管理者・相手の管理画面で操作 | API キーを発行する | 相手の API に照合用の値ができる |
| ② | 相手の API → 管理者 | キーの値を受け取る | 値が管理者の画面に出る(いちばん危ない瞬間) |
| ③ | 管理者 → ストア | 値を登録する。期限やタグ(メモ書き)も付ける | 値がストアに入る |
| ④ | ストア | 暗号化して保存する | バージョン 1(1つめの版)として残る |
| ⑤ | 管理者 | 手元のコピーを消す | 値を知るのはストアと相手の API だけになる |
| ⑥ | 管理者 → ストア | アプリの ID に「読むだけ」の権限を付ける | アプリは書き換えも削除もできない |
| ⑦ | 管理者 → アプリ | 設定に「ストアの場所と値の名前」を書く | 設定や Git には値が入らない |
| ⑧ | ストアで操作 | 監査ログ(誰が何をしたかの記録)を残す設定にする | 誰がいつ読んだかが分かる |
| ⑨ | ストアで操作 | 期限が近づいたら知らせる設定にする | 取り替えを忘れない |
⑦のところが大事です。アプリの設定に書くのは「どのキーボックスの、何番の鍵か」だけで、鍵そのものは書きません。だから設定ファイルを Git に入れても、値は漏れません。
使うとき:アプリが値を取りに行く
あらすじ:アプリは起動したら自分の ID でストアに入り、値を受け取ってメモリに置き、それを使って相手の API を呼びます。
| 手順 | 誰が・どこで | 何をするか | 値(物)はどうなるか・使う ID |
|---|---|---|---|
| ⑩ | アプリ | 起動して、設定からストアの場所を読む | まだ値は持っていない |
| ⑪ | アプリ → ストア | 自分の ID で認証して、名前を指定して値を求める | アプリの ID(社員証)を使う |
| ⑫ | ストア | 権限を確かめる | app01 は「読むだけ」が許されている |
| ⑬ | ストア | 監査ログに記録する | 誰が・いつ・どの値を読んだかが残る |
| ⑭ | ストア → アプリ | 値を返す | TLS で暗号化された通信で届く |
| ⑮ | アプリ | メモリに置いて使う | ファイルやログには書かない |
| ⑯ | アプリ → 相手の API | API キーを付けて呼ぶ | 値が相手に渡る |
| ⑰ | 相手の API | 値を照合する | ①で発行したキーと比べる |
| ⑱ | 相手の API → アプリ | レスポンスを返す | ― |
⑮の「メモリに置く」は、パソコンの作業机にしばらく置いておくイメージです。毎回キーボックスまで取りに行くと時間がかかり、ストアの呼び出し回数の上限(スロットリング)にもかかります。Microsoft も、アプリで値をメモリにキャッシュ(一時的に取っておくこと)し、値が更新されたら取り直すよう勧めています。
取り替える:2本のキーを重ねて切り替える
あらすじ:新しいキーを先に用意し、アプリが新しいほうに切り替わったのを確かめてから、古いキーを止めます。
シークレットを定期的に取り替えることを、ローテーションと呼びます。もし値が盗まれていても、取り替えればその値は使えなくなるので、悪用できる時間を短くできます。ただし、急に古いキーを止めると、まだ古いキーを使っているアプリが止まってしまいます。そこで、相手が2本のキーを同時に持てるなら、2本を重ねて使う方法をとります。
| 項目 | 最初の図(図10) | この図(更新) |
|---|---|---|
| 相手の API のキー | 1本 | ❷〜❻の間は2本とも使える |
| ストアの値 | バージョン 1 | バージョン 2 を足し(❸)、古いほうは無効にする(❽) |
| アプリ | 読んだ値を取っておいて使う | 次に読むときに新しい値に替わる(❹) |
| 古いキーを止める時期 | ― | 使われていないことを確かめてから(❻❼) |
合鍵でたとえると、新しい鍵を全員に配り終えてから、古い鍵が使えないように鍵穴を替える、という順番です。Microsoft も、止めずに取り替えるために、2組の資格情報を持てるリソースでは2組を使うよう勧めています。期限が近いという通知(❶)をきっかけに、❷〜❽を自動で行う仕組みも作れます。Azure での例は後半で見ます。
漏れたとき:止める・替える・消す・調べる
あらすじ:まず漏れたキーを使えなくし、新しいキーに替え、それから漏れたコピーを片付けて、使われていないかを調べます。
合鍵を落としたときを考えてみてください。まず鍵穴を替えて落とした鍵を使えなくし、新しい鍵を配り、落とした場所を探し、防犯カメラで誰か入っていないかを確かめ、次から落とさない工夫をします。シークレットも同じ順番です。OWASP も、漏れたキーをすぐに無効にし、新しい値に替え、漏れたコピーを消し、記録を調べる、という順を挙げています。
| 手順 | 何をするか | 理由 | 合鍵でたとえると |
|---|---|---|---|
| ❶ | 漏れたキーを無効にする | ほかの誰かが使えないようにする | 鍵穴を替える |
| ❷❸ | 新しいキーに替え、ストアに登録する | アプリが新しい値で動けるようにする | 新しい鍵を配る |
| ❹ | 漏れたコピーを消す | コード・ログ・履歴に残った値を減らす | 落とした鍵を探して回収する |
| ❺ | 使われたか、誰が読んだかを調べる | 被害があったかを確かめる | 防犯カメラを確かめる |
| ❻ | 再発を防ぐ | 検出ツールやコミット前の確認を入れる | 落とさない工夫をする |
よくある間違いは、❹の「消す」から始めてしまうことです。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・clone | push は共有のサーバへ送ること、clone はサーバから手元へ丸ごとコピーすること |
| フック | コミットなどの直前・直後に、自動で動かすスクリプト |
API キーをコードに書いてコミットする
確かめること:コードに値を書いてコミットすると、値が履歴に入ること。まずは、よくない例です。注文 API を呼ぶ app.py に、API キーをそのまま書いてコミットします。
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 を環境変数から読む形に直して、もう一度コミットします。
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.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 を動かし、見つかったらコミットを止めます。改札で切符を確かめるように、入口で止めるイメージです。
.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 の対象から外します。
.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 の外に置く)
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 キーなどのシークレットのほか、鍵や証明書も預けられます。シークレットは保存するときに自動で暗号化され、読むときに復号されるので、利用者が暗号化を意識する必要はありません。これまで見てきたライフサイクルの機能が、ひととおりそろっています。
- バージョンと期限:値を変えるたびに新しいバージョンになり、古い版も残ります。期限(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 なども同じしくみを使えます。
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 を使う |
| 期限なしのシークレットを作る | 期限を付け、期限前の通知と取り替えの手順を決めておく |
| 例外やログに設定値をまるごと出す | シークレットはログやエラーメッセージに書かない。パイプラインの出力も確かめる |
うまくいかないとき
シークレット管理のトラブルは、「値がどこにあり、誰が読めて、いつ取り替えられたか」を順に確かめると、原因を絞れます。
| 起きたこと | よくある理由 | 確認すること |
|---|---|---|
| コミットが止まった | 値らしい文字列がある | 本物の値なら外してストアへ。誤検知なら 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 | 消しても履歴に残る。コミット前のフックで止める | 昔の写真に写った鍵 |
| Microsoft | Key Vault(権限・期限・通知・削除の保護)と、App Service の Key Vault 参照 | クラウドのキーボックス |
参考にした資料
この記事は、次の資料と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- Secrets Management Cheat Sheet(OWASP):シークレットの例、最小権限、ライフサイクル(作成・更新・無効化・期限)、監査、漏れたときの順番、環境変数の注意、鍵をシークレットのそばに置かないこと、検出ツール
- Removing sensitive data from a repository・Push protection(GitHub Docs):最初に無効化・取り替えをすること、履歴を書き換えても clone やフォークに残ること、コミット前のフック、push の段階で止める機能
- git-diff Documentation(Git):-S オプションの意味
- Yelp/detect-secrets・FiloSottile/age(GitHub):detect-secrets の使い方と allowlist の書き方、age の鍵の作り方と暗号化・復号
- Secure your Azure Key Vault secrets・Secure your Azure Key Vault(Microsoft Learn):保管するもの・しないもの、タグ、取り替えと2組の資格情報、キャッシュ、アプリ・環境ごとの分け方、マネージド ID、論理的な削除と消去の保護
- About Azure Key Vault secrets・Azure Key Vault soft-delete overview・Provide access to Key Vault keys, certificates, and secrets with Azure role-based access control(Microsoft Learn):暗号化、exp・nbf・enabled、既定90日の保持、組み込みロール
- Azure Key Vault as Event Grid source・Automate the rotation of a secret for resources that have two sets of authentication credentials(Microsoft Learn):期限の30日前のイベント、2本のキーを交互に取り替える例と Azure CLI
- Use Key Vault references as app settings in Azure App Service, Azure Functions, and Azure Logic Apps (Standard)(Microsoft Learn):参照の書き方、マネージド ID と Key Vault Secrets User、24時間の取り直し、うまくいかないときの原因
- Add and manage application credentials in Microsoft Entra ID(Microsoft Learn):クライアントシークレットの最長期限と推奨、本番では証明書かフェデレーションの資格情報を使うこと
あとがき
試してみて改めて感じたのは、Git に一度入った値は、ファイルから消しても git log -S ひとつで出てくるということでした。消すことより、入る前に止めることと、入ってしまったらまず値そのものを止めることのほうが大事だと、手を動かして納得できました。また、暗号化しても「開けるための鍵をどこに置くか」が残り、最後はストアに入るための ID の話に行き着きます。次の記事では、その ID を基盤が用意する、ワークロード ID(マネージド ID)を扱います。
コメント