シークレット管理の記事の最後に、ひとつ問題が残りました。「ストアに入るための最初の鍵は、どこに置けばいいのか」という問題です。アプリがパスワードを持っている限り、その置き場所でずっと悩むことになります。
この記事では、その答えになる「ワークロード ID」を扱います。社員証のたとえを使いながら、Microsoft や GitHub のドキュメントで確かめ、手元に作った小さな模型で実際に動かした結果と一緒に、一歩ずつ図にしました。
- ワークロード ID とは何か、人の ID と何が違うか
- シークレット・証明書・基盤のトークンの違いと、使い分け
- 基盤がトークンを出すしくみ(マネージド ID の考え方)
- 外のトークンを交換するしくみ(ワークロード ID フェデレーション)と、照合のルール
- Azure のマネージド ID と、GitHub Actions からの連携の設定と注意点
先に覚えておく3つの言葉
最初に、いちばん大事な3つの言葉を、社員証にたとえて押さえておきます。
前の記事では、API キーやパスワードを「合鍵」にたとえました。合鍵は、持っている人なら誰でも使えます。この記事でするのは、その合鍵をやめて「社員証」に切り替える話です。社員証は会社が発行し、会社が本人を確かめ、期限が来れば会社が作り直してくれます。
ほかにも、この記事にはいくつか専門の言葉が出てきます。先に、ざっくりした意味を並べておきます。読んでいて迷ったら、ここに戻ってきてください。
| 言葉 | ざっくりした意味 | 社員証でたとえると |
|---|---|---|
| ワークロード | 仕事をするプログラム(アプリ・スクリプト・CI のジョブなど) | 社員 |
| 基盤 | プログラムが動いている土台(Azure や GitHub Actions など) | 会社の総務部 |
| トークン | 身分や権限を示す、電子的な札 | 社員証・入館証 |
| IdP | トークンを発行する役(Identity Provider) | 社員証を発行する部署 |
| 署名 | 発行元の秘密鍵で付ける、偽造できない印 | 会社の印鑑 |
| 公開鍵 | 署名が本物かを確かめるための鍵。配ってよい | 印鑑の見本 |
| iss・sub・aud・exp | トークンの中身。誰が出した・誰の・誰宛て・いつまで | 発行した会社・氏名・提出先・有効期限 |
この記事の地図
アプリが Azure の中で動くなら「マネージド ID」、外で動くなら「フェデレーション」。どちらもパスワードを使いません。
この記事には、2つの道が出てきます。先に全体の地図を見ておくと、迷いにくくなります。
上の道は、Azure の上で動くアプリの話です。Azure 自身が「このアプリは誰か」を知っているので、Azure がそのままトークンを出してくれます。下の道は、GitHub Actions のように Azure の外で動くプログラムの話です。外の基盤が出したトークンを、Entra ID で Azure 用のトークンに交換します。
まずはたとえで:合鍵から社員証へ
ワークロード ID は、プログラムに「合鍵(パスワード)」ではなく「社員証(基盤が出す身分)」を持たせる考え方です。
これまで、アプリがほかのサービスにつなぐときは、パスワードや API キーをアプリに渡すのがふつうでした。前の記事で見たとおり、このやり方には悩みがつきものです。
- どこかに置いておく場所が要る
- 漏れたら、拾った人が誰でも使える
- 期限が来ると止まる。取り替えの手間もかかる
ワークロード ID では、やり方が変わります。アプリが動いている基盤(Azure や GitHub Actions など)は、「このアプリは誰か」をもともと知っています。そこで基盤が、その場で期限の短いトークンを出します。アプリは長く使う秘密を持たないので、置き場所に悩むことも、取り替えを忘れることもありません。
ワークロード ID とは
人ではなく、アプリ・スクリプト・コンテナ・CI のジョブなどの「プログラム」が使う身分です。
Microsoft のドキュメントは、ワークロード ID を次のように説明しています。アプリケーション、サービス、スクリプト、コンテナなどのソフトウェアに割り当てて、ほかのサービスやリソースに認証してアクセスするための ID です。
身近な例で言うと、GitHub Actions が Azure にアプリをデプロイする場面です。このとき、GitHub Actions のジョブには「Azure に入るための身分」が要ります。それがワークロード ID です。
人の ID と比べると、違いがはっきりします。
| 比べること | 人の ID | ワークロード ID |
|---|---|---|
| 誰の身分か | 社員・取引先などの人 | アプリ・スクリプト・コンテナ・CI のジョブなど |
| 本人の確かめ方 | パスワード+MFA(多要素認証)など、その場で本人が操作する | 人は操作しない。シークレット・証明書・基盤のトークンで示す |
| 数 | 1人に1つが基本 | アプリやジョブの数だけ増える。相手ごとに別の資格情報を持つこともある |
| 管理の難しさ | 入社・退社で作る・消す | いつ作られ、いつ消すべきかが追いにくい |
Microsoft は、人でない ID(ワークロード ID とデバイスの ID)が急に増えていて、最近の攻撃では人より人でない ID が狙われることが増えている、と注意しています。
人の ID は、MFA で守るのが当たり前になりました。でも、プログラムは MFA の画面を操作できません。夜中に動くバッチが、スマートフォンの通知に「承認」を押すことはできないのです。だから、プログラムにはプログラム用の守り方が要ります。
認証のしかたの比べ方
使えるなら「基盤のトークン」。アプリが長く使う秘密を持たずに済みます。
プログラムが「自分は誰か」を示す方法は、大きく3つあります。
| 方法 | しくみ | たとえ |
|---|---|---|
| シークレット | パスワードや API キーを送る。値を持っていれば通る | 合鍵 |
| 証明書 | 証明書を見せ、秘密鍵で署名して本人と示す(mTLS の記事など) | 写真付きの身分証と、本人のサイン |
| 基盤のトークン | 基盤が「このアプリは誰か」を確かめて、期限の短いトークンを出す | 会社が出す、その日だけの社員証 |
証明書は、シークレットより強い方法です。ただし、秘密鍵をどこかに置き、期限が来る前に更新しなければなりません。結局、「秘密鍵の置き場所」という悩みは残ります。
基盤のトークンなら、秘密鍵にあたるものは基盤の中にあり、アプリは触れません。悩みのもとそのものがなくなります。ただし、このしくみに対応した基盤や発行元の上で動くときにしか使えません。
誰が何を持つか
署名の鍵を持つのは基盤だけ。ワークロードが持つのは、その場でもらう短いトークンだけです。
登場するのは次の4つです。
- 管理者:ID を設定する人
- 基盤(発行元):ID を出す土台
- ワークロード(app01):ID を使うプログラム
- 相手のサービス:ワークロードがつなぎに行く先
社員証でたとえてみます。基盤は「社員証を発行する総務部」です。総務部は、誰が社員かを知っていて、偽造できない会社の印鑑(署名の鍵)を持っています。相手のサービスは、取引先の受付です。受付は「どの会社の社員証なら信じるか」と「その人をどの部屋まで通すか」を決めておきます。ワークロードは、その日の社員証を受け取って、受付で見せるだけです。
全体の流れ
ここからは、2つの道を順に見ていきます。まず、基盤が直接トークンを出す道(マネージド ID の考え方)です。準備と、使うときに分けて見ます。そのあと、外のトークンを交換する道(フェデレーション)を見ます。
準備:ID を付けて、権限を決める
あらすじ:管理者がワークロードに ID を付け、相手のサービスでその ID に権限を付けます。パスワードを作る手順は、どこにもありません。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか |
|---|---|---|---|
| ① | 管理者・基盤で操作 | app01 に ID を付ける | パスワードは作らない |
| ② | 基盤 | ID を登録する | 署名の鍵は基盤の中だけにある |
| ③ | 管理者・相手のサービスで操作 | この ID に権限を付ける | 「読むだけ」など、必要な分だけ |
| ④ | 相手のサービス | 発行元を信じる設定をする | 発行元の公開鍵で署名を確かめられるようにする |
| ⑤ | 管理者 → ワークロード | コードには「どこにつなぐか」だけ書く | 設定にも Git にも秘密は入らない |
前の記事の準備と比べてみてください。前の記事では、「値を受け取る」「ストアに登録する」「手元のコピーを消す」という手順がありました。ここでは、そもそも渡す値がありません。だから、それらの手順がまるごとなくなります。
使うとき:その場でトークンをもらう
あらすじ:ワークロードが、基盤のすぐそばにある窓口に「相手用のトークンをください」と頼みます。もらったトークンを相手に見せて、仕事をします。
| 手順 | 誰が・どこで | 何をするか | 鍵(物)はどうなるか・使う鍵 |
|---|---|---|---|
| ⑥ | ワークロード → 基盤 | 相手用のトークンを求める | パスワードは送らない |
| ⑦ | 基盤 | 呼び出し元を確かめる | どの VM・どのジョブからか、基盤は自分で分かる |
| ⑧ | 基盤 | トークンを作って署名する | 基盤の鍵で署名。宛先(aud)と期限(exp)を入れる |
| ⑨ | 基盤 → ワークロード | トークンを返す | 期限は短い |
| ⑩ | ワークロード → 相手 | トークンを付けて呼ぶ | トークンが相手に渡る |
| ⑪ | 相手のサービス | 署名・発行元・宛先・期限を確かめる | 発行元の公開鍵を使う |
| ⑫ | 相手のサービス | ID の権限で処理する | ③で付けた権限だけ |
| ⑬ | 相手 → ワークロード | レスポンスを返す | ― |
いちばん大事なのは⑦です。ワークロードは、パスワードで自分を示すのではありません。「どこで動いているか」で、基盤に見分けてもらいます。社内にある社員証の受け取り窓口を思い浮かべてください。窓口は社内にあるので、そこに来られるのは社内の人だけです。だから、窓口はパスワードを聞かずに社員証を渡せます。
トークンの中身や、署名の確かめ方は、JWT の記事とJWT の検証の記事で見たものと同じです。
外で動くとき:トークンを交換する(フェデレーション)
あらすじ:外の基盤(GitHub など)が出したトークンを、交換窓口(Entra ID など)に持っていきます。交換窓口は「信頼の設定」と照らし合わせて、合っていれば自分の側のトークンに替えます。
GitHub Actions のように、Azure の外で動くプログラムは、Azure から直接トークンをもらえません。Azure は、そのプログラムが誰かを知らないからです。そこで、外の基盤が出したトークンを使います。
たとえるなら、取引先の会社に出向くときの受付です。自分の会社の社員証を受付で見せると、受付は来客名簿と照らし合わせ、合っていれば入館証に替えてくれます。この「来客名簿」にあたるのが「信頼の設定」で、Microsoft ではフェデレーション資格情報と呼びます。
| 項目 | 最初の図(図8) | この図(フェデレーション) |
|---|---|---|
| トークンを出すのは | 同じ基盤(⑧) | 外部の IdP(❷)と、交換窓口(❼)の2段 |
| 呼び出し元の確かめ方 | 基盤が自分で分かる(⑦) | 外部のトークンの署名と中身で確かめる(❺❻) |
| 交換窓口が持つもの | ― | 「どの発行元の、どのジョブなら交換するか」の信頼の設定 |
| ワークロードが持つもの | 基盤のトークン | 外部のトークン(数分)と、交換後のトークン |
Microsoft のドキュメントも、この流れを6つの手順で説明しています。かみ砕くと、次のとおりです。
- 外のプログラムが、外の IdP(GitHub など)にトークンを頼む
- 外の IdP がトークンを出す
- 外のプログラムが、そのトークンを Microsoft ID プラットフォーム(Entra ID)に送って、アクセストークンを頼む
- Entra ID が、信頼の設定と照らし合わせ、外の IdP の公開鍵(発行元 URL から取り寄せる)で署名を確かめる
- 合っていれば、Entra ID がアクセストークンを出す
- 外のプログラムが、そのアクセストークンで Azure などを使う
GitHub のドキュメントも、同じ利点を挙げています。クラウド側に信頼を設定しておけば、長く使うクラウドの資格情報を GitHub のシークレットに置かなくて済みます。クラウドからは、1つのジョブの間だけ使える短いトークンが出ます。
照合のルール:3つが完全に一致したときだけ
「誰が出した(iss)」「誰の(sub)」「誰宛ての(aud)」の3つが、信頼の設定と1文字も違わずに一致したときだけ交換します。
フェデレーションの安全は、この照合にかかっています。受付が来客名簿を見るときに、会社名・氏名・訪問先の3つを確かめるのと同じです。
| トークンの項目 | 信頼の設定 | 意味 | たとえ |
|---|---|---|---|
| iss(issuer) | issuer | トークンを出した発行元 | どの会社の社員証か |
| sub(subject) | subject | どのワークロードか(GitHub なら repo とブランチなど) | どの部署の誰か |
| aud(audience) | audiences | トークンの宛先 | どこの受付に出すための社員証か |
| exp | (共通の決まり) | 期限。過ぎたら使えない | 今日の日付が入っているか |
GitHub Actions のトークンで、具体的に見てみます。
- iss:
https://token.actions.githubusercontent.com(GitHub が出した、という意味) - sub の例1:
repo:my-org/my-repo:ref:refs/heads/main(my-repo の main ブランチから動いたジョブ) - sub の例2:
repo:octo-org/octo-repo:environment:prod(prod という環境のジョブ)
sub には、どのリポジトリの、どのブランチ(または環境)か、まで入ります。そのため、「main ブランチからのデプロイだけを許す」といった絞り込みができます。
Entra ID では、この3つの比較は大文字・小文字も区別した完全一致です。contoso と Contoso は別物として扱われます。
ちなみに、交換を頼むときの送り方は、クライアントクレデンシャルの記事で見た流れとほぼ同じです。外部のトークンを client_assertion として送り、client_assertion_type に urn:ietf:params:oauth:client-assertion-type:jwt-bearer を入れます。この値は RFC 7523 で決まっています。
クラウドに縛られない共通のしくみ:SPIFFE
SPIFFE は、どのクラウドでも同じ形でワークロード ID を扱うための、オープンな標準です。
ここまでの話は、クラウドごとに作りが少しずつ違います。それを共通の形でそろえようとしているのが SPIFFE(スピッフィー)です。考え方を知っておくと、各社のしくみも整理しやすくなります。
- SPIFFE ID:ワークロードの名前です。
spiffe://信頼ドメイン/ワークロードの名前という形をしています。 - SVID:その名前を示す書類です。X.509 証明書か JWT のどちらかです。
- Workload API:ワークロードが SVID を受け取る窓口です。
- トラストバンドル:信じる CA の一覧で、相手の SVID を確かめるのに使います。
SPIFFE のドキュメントは、Workload API を呼ぶワークロードは、自分の ID を知っている必要も、認証のトークンを持っている必要もない、と説明しています。つまり、認証の秘密をワークロードと一緒に配らなくていい、ということです。これは、図8 の⑥⑦と同じ考え方です。なお、SPIFFE の実装である SPIRE は、Entra ID のフェデレーションの相手にもできます。
試してみる(小さな模型で照合を確かめる)
実際のクラウドの代わりに、フェデレーションの照合だけを再現する小さな模型を作って試しました。使ったのは Python(PyJWT 2.15.0)と OpenSSL 3.0.13 です。
模型の登場人物は2つです。
- issue.py:トークンを出す「外部の IdP の役」(GitHub の役)
- exchange.py:トークンを確かめて、交換するかを決める「交換窓口の役」(Entra ID の役)
名前は試験用のドメイン(example.test)にしています。本物の Entra ID や GitHub ではありませんが、iss・sub・aud・exp と署名を確かめる考え方は同じです。
issue.py(外部の IdP の役:トークンを出す)
import sys, time, jwt
sub, aud, ttl = sys.argv[1], sys.argv[2], int(sys.argv[3])
key = open(sys.argv[4] if len(sys.argv) > 4 else "idp.key").read()
now = int(time.time())
claims = {"iss": "https://idp.example.test", "sub": sub, "aud": aud,
"iat": now, "exp": now + ttl}
print(jwt.encode(claims, key, algorithm="RS256", headers={"kid": "idp-key-1"}))
中身は、iss・sub・aud・発行時刻(iat)・期限(exp)を入れて、idp.key で署名するだけです。期限は「今の時刻+指定した秒数」です。
show.py(トークンの中身を表示する。署名は確かめない)
import sys, json, jwt
token = open(sys.argv[1]).read().strip()
print(json.dumps(jwt.get_unverified_header(token)))
print(json.dumps(jwt.decode(token, options={"verify_signature": False}), indent=2))
exchange.py(交換窓口の役:確かめて交換する)
import sys, json, jwt
trust = json.load(open("trust.json"))
pub = open("idp.pub").read()
token = open(sys.argv[1]).read().strip()
try:
c = jwt.decode(token, pub, algorithms=["RS256"],
audience=trust["audiences"], issuer=trust["issuer"])
except jwt.PyJWTError as e:
print("NG:", type(e).__name__, "-", e)
sys.exit(1)
if c["sub"] != trust["subject"]:
print("NG: subject mismatch -", c["sub"])
sys.exit(1)
print("OK: issue access token for", trust["name"])
exchange.py がしていることは2つだけです。
jwt.decodeで、署名・期限(exp)・宛先(aud)・発行元(iss)をまとめて確かめる- そのあと、sub が信頼の設定と完全に同じかを比べる
どれか1つでも合わなければ「NG」、全部合えば「OK」を表示します。
発行元の鍵を作る(図9の❹❺)
確かめること:署名の秘密鍵は発行元だけが持ち、交換窓口には公開鍵だけを渡すこと。
やっていること:会社の印鑑(秘密鍵)と、その印鑑の見本(公開鍵)を作ります。
外部の IdP の役で、署名用の鍵ペアを作る(外部の IdP で操作)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -quiet -out idp.key openssl pkey -in idp.key -pubout -out idp.pub head -n 2 idp.pub
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtl0SBLYw6RnLZMFPXCeN
結果の読み方:idp.key が署名用の秘密鍵、idp.pub が公開鍵です。表示しているのは公開鍵の先頭で、人に見せてもかまいません。本物の交換窓口は、発行元が公開している URL から公開鍵を取り寄せます(図9 の❹)。この模型では、手軽さのためにファイルで渡しています。
信頼の設定を書く(図9の❻)
確かめること:交換窓口が持つのは「どのトークンなら信じるか」の設定だけで、秘密は入っていないこと。
やっていること:受付に置く来客名簿を書きます。
信頼の設定(交換窓口で操作)
cat trust.json
{
"name": "app01-deploy",
"issuer": "https://idp.example.test",
"subject": "repo:contoso/app01:ref:refs/heads/main",
"audiences": ["api://token-exchange.example.test"]
}
結果の読み方:日本語にすると、「idp.example.test が出した、contoso/app01 リポジトリの main ブランチのジョブの、token-exchange 宛てのトークンなら信じる」という設定です。この中に、パスワードのような秘密は1つもありません。人に見られても、それだけでは何もできません。
ワークロードがトークンをもらう(図9の❶❷)
確かめること:トークンの中に iss・sub・aud・期限が入っていて、誰でも中身を読めること。
やっていること:main ブランチのジョブとして、期限5分(300秒)の社員証をもらいます。
main ブランチのジョブとして、期限5分(300秒)のトークンをもらう
python3 issue.py repo:contoso/app01:ref:refs/heads/main api://token-exchange.example.test 300 > main.jwt cut -c1-60 main.jwt
eyJhbGciOiJSUzI1NiIsImtpZCI6ImlkcC1rZXktMSIsInR5cCI6IkpXVCJ9
点(.)で区切って、3つの部分を見る
tr '.' '\n' < main.jwt | cut -c1-48
eyJhbGciOiJSUzI1NiIsImtpZCI6ImlkcC1rZXktMSIsInR5 eyJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLnRlc3QiLCJz PCnV0E7CnWc0k4ojhI6xDcD9mWEVV8i1Ll8wlX8LW4y39Lh4
トークンは、点で区切られた3つの部分からできています。1行目がヘッダ、2行目がペイロード(中身)、3行目が署名です(ここでは各行の先頭48文字だけを表示しています)。
中身を表示する
python3 show.py main.jwt
{"alg": "RS256", "kid": "idp-key-1", "typ": "JWT"}
{
"iss": "https://idp.example.test",
"sub": "repo:contoso/app01:ref:refs/heads/main",
"aud": "api://token-exchange.example.test",
"iat": 1793629832,
"exp": 1793630132
}
結果の読み方:
- 1行目はヘッダです。署名の方式(RS256)と、使った鍵の名前(idp-key-1)が入っています。
- その下がペイロードです。誰が出した(iss)・誰の(sub)・誰宛て(aud)・いつ出した(iat)・いつまで(exp)が並んでいます。
expからiatを引くと300秒です。つまり、5分で使えなくなります。
暗号化されていないので、誰でも中身を読めます。だから、トークンに秘密を入れてはいけません。GitHub のドキュメントにあるトークンの例でも、発行から期限までは300秒でした。
トークンを交換する(図9の❸〜❼)
確かめること:署名と iss・aud・exp・sub がすべて合えば、交換されること。
やっていること:受付に社員証を見せて、入館証に替えてもらいます。
交換窓口の役にトークンを渡す
python3 exchange.py main.jwt; echo "exit=$?"
OK: issue access token for app01-deploy
exit=0
結果の読み方:5つの確認がすべて通り、「app01-deploy のトークンを出す」という結果になりました。exit=0 は成功を表します。本物の Entra ID なら、ここでアクセストークンが返ってきます。
少しでも違えば断られる
確かめること:本物の発行元が署名したトークンでも、信頼の設定と1か所でも違えば断られること。
やっていること:名簿と少しだけ違う社員証を、3通り試します。
別のブランチ(feature)のジョブのトークン
python3 issue.py repo:contoso/app01:ref:refs/heads/feature api://token-exchange.example.test 300 > feature.jwt
python3 exchange.py feature.jwt; echo "exit=$?"
NG: subject mismatch - repo:contoso/app01:ref:refs/heads/feature
exit=1
組織名を大文字にしたトークン(Contoso)
python3 issue.py repo:Contoso/app01:ref:refs/heads/main api://token-exchange.example.test 300 > upper.jwt
python3 exchange.py upper.jwt; echo "exit=$?"
NG: subject mismatch - repo:Contoso/app01:ref:refs/heads/main
exit=1
宛先(aud)が違うトークン
python3 issue.py repo:contoso/app01:ref:refs/heads/main api://other.example.test 300 > aud.jwt
python3 exchange.py aud.jwt; echo "exit=$?"
NG: InvalidAudienceError - Audience doesn't match
exit=1
結果の読み方:3つとも、本物の idp.key で署名されたトークンです。それでも、すべて断られました(exit=1 は失敗)。
- feature ブランチ:名簿には main ブランチしか書いていないので、断られる。開発中のブランチから本番にデプロイされるのを防げる
- 大文字の Contoso:1文字の大小の違いでも、別人として断られる。Entra ID の照合と同じ考え方
- 宛先が違う:ほかの受付あての社員証は、ここでは使えない
期限切れや偽物も断られる
確かめること:期限が過ぎたトークンと、発行元の秘密鍵以外で署名したトークンは使えないこと。
やっていること:期限切れの社員証と、印鑑をまねた偽の社員証を試します。
60秒前に期限が切れたトークン
python3 issue.py repo:contoso/app01:ref:refs/heads/main api://token-exchange.example.test -60 > old.jwt
python3 exchange.py old.jwt; echo "exit=$?"
NG: ExpiredSignatureError - Signature has expired
exit=1
別の鍵(other.key)で署名したトークン
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -quiet -out other.key
python3 issue.py repo:contoso/app01:ref:refs/heads/main api://token-exchange.example.test 300 other.key > forged.jwt
python3 exchange.py forged.jwt; echo "exit=$?"
NG: InvalidSignatureError - Signature verification failed
exit=1
結果の読み方:
- 期限切れ:「署名の期限が切れている」というエラーで断られる。本物のトークンを盗まれても、数分で使えなくなる
- 別の鍵で署名:「署名を確かめられない」というエラーで断られる。中身をそっくりまねても、発行元の秘密鍵がなければ本物の署名は作れない
STEP 4〜6 をまとめると、通るのは「本物の発行元が」「最近」「この相手に宛てて」出したトークンだけ、ということになります。
ここからは Microsoft の場合です
Entra ID のワークロード ID の種類
Entra ID のワークロード ID は、アプリ(アプリの登録)・サービスプリンシパル・マネージド ID の3つです。
Microsoft Entra ID では、ワークロード ID は次の3つです。
- アプリケーション(アプリの登録):全テナント共通のひな形です。社員証の「デザインの原本」のようなものです。
- サービスプリンシパル:それぞれのテナント(会社ごとの Entra ID)での実体です。権限はここに付きます。原本をもとに、各社で発行された実際の社員証にあたります。
- マネージド ID:資格情報の管理をなくすための、特別なサービスプリンシパルです。
マネージド ID には2種類あります。システム割り当ては「その部屋専用の社員証」で、部屋(リソース)がなくなれば一緒に消えます。ユーザー割り当ては「持ち回りできる社員証」で、複数の部屋で使い回せます。
| 項目 | システム割り当て | ユーザー割り当て |
|---|---|---|
| 作り方 | Azure のリソース(VM や App Service など)の一部として作る | 独立した Azure のリソースとして作る |
| 寿命 | 元のリソースを消すと一緒に消える | 自分で消すまで残る |
| 共有 | できない(1つのリソース専用) | 複数のリソースに付けられる |
| 向いている場面 | 1台の VM で動くアプリなど | 複数の VM で同じ権限を使う、作り直しが多い、事前に権限を付けておきたい |
Microsoft は、マネージド ID の資格情報には利用者が触れることすらできず、追加の費用なしで使える、と説明しています。また、Microsoft のサービスでは、リソースと切り離して作れるユーザー割り当てのマネージド ID を推奨しています。
Azure VM のマネージド ID
あらすじ:VM の中のコードが、VM の中からだけ呼べる窓口(IMDS)に「トークンをください」と頼みます。すると、Azure が持つ証明書で Entra ID からトークンをもらって、返してくれます。
Microsoft のドキュメントによると、マネージド ID を作ると、Azure の内部でその ID 用の証明書が発行されます。この証明書の取り替えも Azure が行います。つまり、裏側では証明書が使われていますが、それを扱うのは Azure で、利用者やコードは一切触れません。
VM の中のコードは、Azure Instance Metadata Service(IMDS)という窓口にトークンを頼みます。IMDS は 169.254.169.254 というアドレスにあり、VM の中からしか呼べません。図8 の「社内の窓口」にあたります。
VM の中から IMDS にトークンを求める(Microsoft Learn より)
curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s
返ってくる値の例(Microsoft Learn より)
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJ0eXAi...",
"refresh_token": "",
"expires_in": "3599",
"expires_on": "1506484173",
"not_before": "1506480273",
"resource": "https://management.azure.com/",
"token_type": "Bearer"
}
| 項目 | 意味 |
|---|---|
resource | トークンの宛先。トークンの aud に入る。例は Azure Resource Manager 宛て |
Metadata: true | 必須のヘッダ。サーバ側リクエストフォージェリ(SSRF:ほかのサーバを踏み台に、内部の窓口を呼ばせる攻撃)への対策 |
expires_in | 発行から期限までの秒数 |
token_type | Bearer(持っている人が使えるトークン)。相手には Authorization ヘッダで渡す |
例の expires_in は 3599 なので、約1時間で期限が切れます。期限が来たら、また IMDS に頼めば新しいトークンがもらえます。
Microsoft は、マネージド ID のセキュリティの境界は、その ID を使うリソースそのものだと注意しています。言いかえると、VM の上で動くコードなら、どれでもその VM のマネージド ID のトークンを取れます。部屋の中にいる人なら誰でも、その部屋の社員証を使えてしまう、ということです。1台の VM に、権限の違う複数のアプリを同居させないことや、ID には必要な権限だけを付けることが大事です。
トークンは、マネージド ID のしくみの側でもキャッシュ(一時的に取っておくこと)されます。それでも Microsoft は、コードの側でもキャッシュし、期限切れに備えるよう勧めています。実際には、Azure Identity ライブラリ(ManagedIdentityCredential など)を使えば、トークンの取得もキャッシュもライブラリがやってくれます。
App Service・Azure Functions のマネージド ID
App Service と Azure Functions では、IMDS ではなく、アプリの中に用意される2つの環境変数を使います。
IDENTITY_ENDPOINT:トークンをもらう窓口の URLIDENTITY_HEADER:SSRF 対策のためにヘッダに入れる値。この値は基盤が取り替える
App Service の中からトークンを求める(Microsoft Learn より)
GET /MSI/token?resource=https://vault.azure.net&api-version=2019-08-01 HTTP/1.1
Host: <ip-address-:-port-in-IDENTITY_ENDPOINT>
X-IDENTITY-HEADER: <value-of-IDENTITY_HEADER>
窓口の場所が違うだけで、考え方は VM と同じです。ひとつ注意があります。ユーザー割り当てのマネージド ID を使うときは、client_id などでどの ID かを指定します。指定しないと、システム割り当ての ID のトークンを取りに行くので、思った ID と違う結果になります。
GitHub Actions から Azure へ(ワークロード ID フェデレーション)
あらすじ:ユーザー割り当てのマネージド ID(またはアプリの登録)に、「GitHub の、このリポジトリの、このブランチのジョブなら信じる」という来客名簿(フェデレーション資格情報)を付けます。
Microsoft のドキュメントは、ワークロード ID フェデレーションを「ユーザー割り当てのマネージド ID かアプリの登録に、外部の IdP のトークンを信頼させるしくみ」と説明しています。GitHub Actions のほかに、Kubernetes(AKS・EKS・GKE・オンプレミス)、Google Cloud、AWS、SPIFFE/SPIRE、Azure Pipelines などにも対応しています。
マネージド ID に、GitHub Actions 用のフェデレーション資格情報を付ける(Microsoft Learn より)
az identity federated-credential create \
--identity-name myGitHubDeployIdentity \
--resource-group <RESOURCE_GROUP> \
--name github-deploy-credential \
--issuer https://token.actions.githubusercontent.com \
--subject repo:<GITHUB_ORG>/<REPO_NAME>:ref:refs/heads/<BRANCH_NAME> \
--audiences api://AzureADTokenExchange
コマンドの中身は、STEP 2 の trust.json とほとんど同じです。--issuer に GitHub の発行元、--subject にリポジトリとブランチ、--audiences に宛先を入れています。
| 決まり | 内容 |
|---|---|
| 一致のしかた | issuer・subject・audience は、大文字・小文字も区別して完全に一致する必要がある |
| audience の値 | 推奨は api://AzureADTokenExchange |
| 数の上限 | 1つのアプリかユーザー割り当てのマネージド ID に、最大20個まで |
| 重複 | issuer と subject の組み合わせは、同じアプリの中で重複できない |
| 間違えたとき | subject を間違えても作成はエラーにならず、交換するときに初めて失敗する |
| 反映の時間 | 追加してすぐは、まだ広がっていなくて失敗することがある。少し待ち、再試行を入れる |
特に「間違えたとき」の行に気をつけてください。名簿の書き間違いは、書いたときには誰も教えてくれません。実際に交換しようとして、初めて失敗に気づきます。
GitHub の側には、テナント ID・クライアント ID・サブスクリプション ID だけを渡します。これらは「どこにつなぐか」を示す値で、パスワードではありません。
よくある間違い
| 間違い | どうするか |
|---|---|
| Azure の上で動くのに、アプリの登録とクライアントシークレットを使う | マネージド ID を使う。秘密を持たずに済む |
| GitHub Actions のために、長く使うシークレットを GitHub に保存する | ワークロード ID フェデレーションにする |
| subject を広く取る(どのブランチでも交換できる) | main ブランチや本番の環境など、必要なジョブだけに絞る |
| 1つのマネージド ID に強い権限をまとめて付ける | 必要な分だけ付ける。用途ごとに ID を分ける |
| 権限の違うアプリを同じ VM に同居させる | VM の上のコードはどれでも ID を使える。境界を分ける |
| 要らなくなった ID や権限を残す | アプリを消すときに ID と権限も見直す |
うまくいかないとき
ワークロード ID のトラブルは、次の3つを順に確かめると原因を絞れます。
- どのトークンを出したか(中身は STEP 3 のように表示できる)
- どの設定と比べたか(フェデレーション資格情報や権限)
- どこで断られたか(エラーの番号)
| 起きたこと | よくある理由 | 確認すること |
|---|---|---|
| AADSTS700213 | subject が合わない | トークンの sub と設定の subject を1文字ずつ比べる。大文字・小文字、ブランチ名や環境名 |
| AADSTS700211 | issuer が合わない | フェデレーション資格情報があるか。issuer の URL が正しいか |
| AADSTS70021 | 一致する設定がない・まだ反映されていない | 設定の直後なら少し待って再試行する。issuer と subject を見直す |
| AADSTS700024 | 外部のトークンが有効な時間の外 | トークンの期限が切れていないか。時刻がずれていないか |
| AADSTS70025 | フェデレーション資格情報がない | アプリかマネージド ID に設定したか |
| IMDS が 400 を返す | Metadata ヘッダがない | Metadata: true(すべて小文字)を付けているか |
| 違う ID のトークンになる | ユーザー割り当ての ID を指定していない | client_id などで ID を指定しているか |
STEP 5・6 で見たとおり、照合は「発行元・中身・宛先・期限・署名」のどれか1つでも合わなければ失敗します。エラーの番号から、どれで止まったかの見当をつけましょう。そのうえで、トークンの中身と設定を横に並べて、1文字ずつ見比べるのが近道です。
まとめ
| 項目 | 内容 | 社員証でたとえると |
|---|---|---|
| ワークロード ID | アプリ・スクリプト・CI のジョブなどが使う身分 | プログラムの社員証 |
| ねらい | アプリに長く使う秘密を持たせない | 合鍵をやめる |
| 基盤のトークン | 基盤が呼び出し元を確かめ、期限の短いトークンを出す | その日だけの社員証 |
| フェデレーション | 外部のトークンを、iss・sub・aud が一致したときだけ交換する | 他社の社員証を受付で入館証に替える |
| マネージド ID | Azure の上で動くワークロードの ID。システム割り当てとユーザー割り当て | 会社が預かる社員証 |
| 注意 | VM の上のコードはどれでも ID を使える。権限は必要な分だけ | 社員証で入れる部屋を絞る |
参考にした資料
この記事は、次の標準と公式ドキュメントで内容を確かめて書きました。仕様や対応状況は変わることがあるので、最新の情報はリンク先で確かめてください。
- RFC 7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants(IETF):client_assertion_type の値、JWT に必須の iss・sub・aud・exp
- SPIFFE Concepts(SPIFFE):SPIFFE ID の形、SVID、Workload API、トラストバンドル
- OpenID Connect(GitHub Docs):長く使う資格情報を置かなくて済むこと、短いトークン、発行元の URL、sub の例
- What are workload identities?・Workload identity federation concepts(Microsoft Learn):ワークロード ID の定義と種類、人でない ID が狙われていること、フェデレーションの流れと対応する場面、大文字・小文字を区別した一致
- Configure a user-assigned managed identity to trust an external identity provider・Important considerations and restrictions for federated identity credentials(Microsoft Learn):最大20個、issuer と subject の組の一意性、推奨の audience、作成時にエラーにならないこと、反映の時間と AADSTS70021
- What is managed identities for Azure resources?・How managed identities for Azure resources work with Azure virtual machines(Microsoft Learn):システム割り当てとユーザー割り当ての違い、推奨、内部の証明書と自動の取り替え、IMDS の流れ
- How to use managed identities for Azure resources on an Azure VM to acquire an access token(Microsoft Learn):要求と応答の例、Metadata ヘッダと SSRF、セキュリティの境界、キャッシュ、エラー
- Use managed identities for App Service and Azure Functions(Microsoft Learn):IDENTITY_ENDPOINT と IDENTITY_HEADER、ユーザー割り当ての指定
- Microsoft identity platform and the OAuth 2.0 client credentials flow・Deploy to Azure Functions by using GitHub Actions(Microsoft Learn):フェデレーションでの client_assertion、フェデレーション資格情報を作る Azure CLI
- Troubleshoot an Azure Resource Manager workload identity service connection・What’s new for authentication?(Microsoft Learn):AADSTS700211・700213・700024・70025 の意味、大文字・小文字を区別する一致への変更
あとがき
小さな模型を作って試してみて、フェデレーションの中身は「署名を確かめて、3つの文字列を比べる」という、思ったより単純なしくみだと分かりました。そのぶん、sub に入れる値を1文字でも間違えると、何も言わずに失敗します。トークンの中身を表示して設定と見比べる習慣が大事だと感じます。前の記事から続いていた「最初の鍵をどうするか」は、鍵そのものを持たない、という形で答えが出ました。次の記事からは、API の入口を守る API ゲートウェイを扱います。
コメント