【図解】ワークロードIDとは?マネージドIDとワークロードIDフェデレーションでシークレットなしにつなぐしくみを初心者向けに解説

シークレット管理の記事の最後に、ひとつ問題が残りました。「ストアに入るための最初の鍵は、どこに置けばいいのか」という問題です。アプリがパスワードを持っている限り、その置き場所でずっと悩むことになります。

この記事では、その答えになる「ワークロード ID」を扱います。社員証のたとえを使いながら、Microsoft や GitHub のドキュメントで確かめ、手元に作った小さな模型で実際に動かした結果と一緒に、一歩ずつ図にしました。

この記事でわかること
  1. ワークロード ID とは何か、人の ID と何が違うか
  2. シークレット・証明書・基盤のトークンの違いと、使い分け
  3. 基盤がトークンを出すしくみ(マネージド ID の考え方)
  4. 外のトークンを交換するしくみ(ワークロード ID フェデレーション)と、照合のルール
  5. Azure のマネージド ID と、GitHub Actions からの連携の設定と注意点

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

最初に、いちばん大事な3つの言葉を、社員証にたとえて押さえておきます。

ワークロード IDたとえ:プログラムの社員証人ではなく、アプリや CI のジョブなどに付ける身分マネージド IDたとえ:会社が預かる社員証鍵はクラウドが持ち、アプリは秘密を知らないフェデレーションたとえ:他社の証明書を交換外の発行元のトークンを、自分の側のトークンに替えるこの3つが分かれば、この記事の話はほぼ追える
図1 先に覚えておく3つの言葉

前の記事では、API キーやパスワードを「合鍵」にたとえました。合鍵は、持っている人なら誰でも使えます。この記事でするのは、その合鍵をやめて「社員証」に切り替える話です。社員証は会社が発行し、会社が本人を確かめ、期限が来れば会社が作り直してくれます。

ほかにも、この記事にはいくつか専門の言葉が出てきます。先に、ざっくりした意味を並べておきます。読んでいて迷ったら、ここに戻ってきてください。

言葉ざっくりした意味社員証でたとえると
ワークロード仕事をするプログラム(アプリ・スクリプト・CI のジョブなど)社員
基盤プログラムが動いている土台(Azure や GitHub Actions など)会社の総務部
トークン身分や権限を示す、電子的な札社員証・入館証
IdPトークンを発行する役(Identity Provider)社員証を発行する部署
署名発行元の秘密鍵で付ける、偽造できない印会社の印鑑
公開鍵署名が本物かを確かめるための鍵。配ってよい印鑑の見本
iss・sub・aud・expトークンの中身。誰が出した・誰の・誰宛て・いつまで発行した会社・氏名・提出先・有効期限

この記事の地図

ひとことで言うと

アプリが Azure の中で動くなら「マネージド ID」、外で動くなら「フェデレーション」。どちらもパスワードを使いません。

この記事には、2つの道が出てきます。先に全体の地図を見ておくと、迷いにくくなります。

Azure の中で動くとき → マネージド IDワークロード秘密なしAzure(基盤)トークンを出す相手のサービストークンを確かめるAzure の外で動くとき → フェデレーションワークロード秘密なし外の基盤GitHub など交換窓口Entra ID相手のサービスどちらの道でも、アプリはパスワードを持たない
図2 この記事の地図

上の道は、Azure の上で動くアプリの話です。Azure 自身が「このアプリは誰か」を知っているので、Azure がそのままトークンを出してくれます。下の道は、GitHub Actions のように Azure の外で動くプログラムの話です。外の基盤が出したトークンを、Entra ID で Azure 用のトークンに交換します。

まずはたとえで:合鍵から社員証へ

ひとことで言うと

ワークロード ID は、プログラムに「合鍵(パスワード)」ではなく「社員証(基盤が出す身分)」を持たせる考え方です。

これまで、アプリがほかのサービスにつなぐときは、パスワードや API キーをアプリに渡すのがふつうでした。前の記事で見たとおり、このやり方には悩みがつきものです。

  • どこかに置いておく場所が要る
  • 漏れたら、拾った人が誰でも使える
  • 期限が来ると止まる。取り替えの手間もかかる
これまで:プログラムに合鍵を渡すアプリパスワード設定やストアに置く場所が要る漏れたら誰でも使える期限切れで止まる・交換の手間これから:基盤が社員証を出す基盤(クラウド・CI)期限の短いトークンアプリ(長く使う秘密は持たない)置き場所の心配がない取り替えは基盤が自動で行うワークロード ID は、プログラムに「合鍵」ではなく「社員証」を持たせる考え方
図3 合鍵から社員証へ

ワークロード ID では、やり方が変わります。アプリが動いている基盤(Azure や GitHub Actions など)は、「このアプリは誰か」をもともと知っています。そこで基盤が、その場で期限の短いトークンを出します。アプリは長く使う秘密を持たないので、置き場所に悩むことも、取り替えを忘れることもありません。

ワークロード ID とは

ひとことで言うと

人ではなく、アプリ・スクリプト・コンテナ・CI のジョブなどの「プログラム」が使う身分です。

Microsoft のドキュメントは、ワークロード ID を次のように説明しています。アプリケーション、サービス、スクリプト、コンテナなどのソフトウェアに割り当てて、ほかのサービスやリソースに認証してアクセスするための ID です。

身近な例で言うと、GitHub Actions が Azure にアプリをデプロイする場面です。このとき、GitHub Actions のジョブには「Azure に入るための身分」が要ります。それがワークロード ID です。

Web アプリ・APIデータベースや Key Vault につなぐバッチ・スクリプト夜間に集計してストレージに書くコンテナ・KubernetesPod ごとに別の身分で動くCI/CD パイプラインGitHub Actions からデプロイするほかのクラウドの VMAWS や Google Cloud から Azure へAI エージェント自分で判断して API を呼ぶ共通点:人がその場でサインインできない。だから人の守り方(パスワード+MFA)が使えない人の代わりに動くプログラムには、プログラム用の身分が要る
図4 ワークロード ID を使う場面

人の ID と比べると、違いがはっきりします。

比べること人の IDワークロード ID
誰の身分か社員・取引先などの人アプリ・スクリプト・コンテナ・CI のジョブなど
本人の確かめ方パスワード+MFA(多要素認証)など、その場で本人が操作する人は操作しない。シークレット・証明書・基盤のトークンで示す
数1人に1つが基本アプリやジョブの数だけ増える。相手ごとに別の資格情報を持つこともある
管理の難しさ入社・退社で作る・消すいつ作られ、いつ消すべきかが追いにくい

Microsoft は、人でない ID(ワークロード ID とデバイスの ID)が急に増えていて、最近の攻撃では人より人でない ID が狙われることが増えている、と注意しています。

人の ID は、MFA で守るのが当たり前になりました。でも、プログラムは MFA の画面を操作できません。夜中に動くバッチが、スマートフォンの通知に「承認」を押すことはできないのです。だから、プログラムにはプログラム用の守り方が要ります。

認証のしかたの比べ方

ひとことで言うと

使えるなら「基盤のトークン」。アプリが長く使う秘密を持たずに済みます。

プログラムが「自分は誰か」を示す方法は、大きく3つあります。

シークレットパスワード・API キー証明書秘密鍵で署名する基盤のトークンマネージド ID・連携長く使う秘密アプリが持つアプリが持つ(秘密鍵)持たない漏れたら持った人が使える秘密鍵ごと漏れたら使えるトークンは短時間で切れる取り替え人が手で行う期限前に更新が要る基盤が自動で行う使える場所どこでもどこでも対応した基盤・発行元使えるなら「基盤のトークン」。アプリが長く使う秘密を持たずに済む
図5 ワークロードの認証のしかたの比較
方法しくみたとえ
シークレットパスワードや API キーを送る。値を持っていれば通る合鍵
証明書証明書を見せ、秘密鍵で署名して本人と示す(mTLS の記事など)写真付きの身分証と、本人のサイン
基盤のトークン基盤が「このアプリは誰か」を確かめて、期限の短いトークンを出す会社が出す、その日だけの社員証

証明書は、シークレットより強い方法です。ただし、秘密鍵をどこかに置き、期限が来る前に更新しなければなりません。結局、「秘密鍵の置き場所」という悩みは残ります。

基盤のトークンなら、秘密鍵にあたるものは基盤の中にあり、アプリは触れません。悩みのもとそのものがなくなります。ただし、このしくみに対応した基盤や発行元の上で動くときにしか使えません。

誰が何を持つか

ひとことで言うと

署名の鍵を持つのは基盤だけ。ワークロードが持つのは、その場でもらう短いトークンだけです。

登場するのは次の4つです。

  • 管理者:ID を設定する人
  • 基盤(発行元):ID を出す土台
  • ワークロード(app01):ID を使うプログラム
  • 相手のサービス:ワークロードがつなぎに行く先
管理者基盤(発行元)ワークロード相手のサービスID を作って付けるどのアプリの身分か相手に権限を付けるID に対して誰が呼んだかを知るどの VM・どのジョブか署名用の鍵基盤の中だけトークンを発行期限つき長く使う秘密はない置き場所の心配なしその場のトークン数分〜数時間で切れる信じる発行元公開鍵の取り寄せ先ID ごとの権限何を許すか秘密(署名の鍵)を持つのは基盤だけ。ワークロードは短いトークンだけを持つ
図6 誰が何を持つか

社員証でたとえてみます。基盤は「社員証を発行する総務部」です。総務部は、誰が社員かを知っていて、偽造できない会社の印鑑(署名の鍵)を持っています。相手のサービスは、取引先の受付です。受付は「どの会社の社員証なら信じるか」と「その人をどの部屋まで通すか」を決めておきます。ワークロードは、その日の社員証を受け取って、受付で見せるだけです。

全体の流れ

ここからは、2つの道を順に見ていきます。まず、基盤が直接トークンを出す道(マネージド ID の考え方)です。準備と、使うときに分けて見ます。そのあと、外のトークンを交換する道(フェデレーション)を見ます。

準備:ID を付けて、権限を決める

あらすじ:管理者がワークロードに ID を付け、相手のサービスでその ID に権限を付けます。パスワードを作る手順は、どこにもありません。

管理者基盤(発行元)ワークロード(app01)相手のサービス① app01 に ID を付ける基盤で操作② ID を登録する署名の鍵は基盤の中③ この ID に権限を付ける読むだけ、など④ 発行元を信じる設定⑤ コードには「どこにつなぐか」だけパスワードは渡さない準備のどこにも「パスワードを作って渡す」手順がない管理者が決めるのは「誰に・何を許すか」だけID を付けて権限を決めるだけ。秘密を作らないので、配る手順もない
図7 準備の流れ(①〜⑤)
手順誰が・どこで何をするか鍵(物)はどうなるか
①管理者・基盤で操作app01 に ID を付けるパスワードは作らない
②基盤ID を登録する署名の鍵は基盤の中だけにある
③管理者・相手のサービスで操作この ID に権限を付ける「読むだけ」など、必要な分だけ
④相手のサービス発行元を信じる設定をする発行元の公開鍵で署名を確かめられるようにする
⑤管理者 → ワークロードコードには「どこにつなぐか」だけ書く設定にも Git にも秘密は入らない

前の記事の準備と比べてみてください。前の記事では、「値を受け取る」「ストアに登録する」「手元のコピーを消す」という手順がありました。ここでは、そもそも渡す値がありません。だから、それらの手順がまるごとなくなります。

使うとき:その場でトークンをもらう

あらすじ:ワークロードが、基盤のすぐそばにある窓口に「相手用のトークンをください」と頼みます。もらったトークンを相手に見せて、仕事をします。

ワークロード(app01)基盤(ローカルの窓口)相手のサービス⑥ 相手用のトークンをくださいパスワードは送らない⑦ 呼び出し元を確かめるどの VM・どのジョブか⑧ トークンを作って署名宛先(aud)・期限(exp)⑨ トークン期限は短い⑩ リクエスト+トークン⑪ 確かめる署名・発行元宛先・期限⑫ ID の権限で処理⑬ レスポンス期限が来たら、また⑥から取り直す(多くのライブラリが自動で行う)盗まれても使えるのは短い間だけ。長く使う秘密はどこにもないワークロードは「その場で」基盤からトークンをもらい、相手に見せる
図8 使うときの流れ(⑥〜⑬)
手順誰が・どこで何をするか鍵(物)はどうなるか・使う鍵
⑥ワークロード → 基盤相手用のトークンを求めるパスワードは送らない
⑦基盤呼び出し元を確かめるどの VM・どのジョブからか、基盤は自分で分かる
⑧基盤トークンを作って署名する基盤の鍵で署名。宛先(aud)と期限(exp)を入れる
⑨基盤 → ワークロードトークンを返す期限は短い
⑩ワークロード → 相手トークンを付けて呼ぶトークンが相手に渡る
⑪相手のサービス署名・発行元・宛先・期限を確かめる発行元の公開鍵を使う
⑫相手のサービスID の権限で処理する③で付けた権限だけ
⑬相手 → ワークロードレスポンスを返す―

いちばん大事なのは⑦です。ワークロードは、パスワードで自分を示すのではありません。「どこで動いているか」で、基盤に見分けてもらいます。社内にある社員証の受け取り窓口を思い浮かべてください。窓口は社内にあるので、そこに来られるのは社内の人だけです。だから、窓口はパスワードを聞かずに社員証を渡せます。

トークンの中身や、署名の確かめ方は、JWT の記事とJWT の検証の記事で見たものと同じです。

外で動くとき:トークンを交換する(フェデレーション)

あらすじ:外の基盤(GitHub など)が出したトークンを、交換窓口(Entra ID など)に持っていきます。交換窓口は「信頼の設定」と照らし合わせて、合っていれば自分の側のトークンに替えます。

GitHub Actions のように、Azure の外で動くプログラムは、Azure から直接トークンをもらえません。Azure は、そのプログラムが誰かを知らないからです。そこで、外の基盤が出したトークンを使います。

たとえるなら、取引先の会社に出向くときの受付です。自分の会社の社員証を受付で見せると、受付は来客名簿と照らし合わせ、合っていれば入館証に替えてくれます。この「来客名簿」にあたるのが「信頼の設定」で、Microsoft ではフェデレーション資格情報と呼びます。

ワークロード(CI のジョブ)外部の IdP(GitHub など)交換窓口(Entra ID など)相手のサービス❶ トークンをください❷ 外部のトークンiss・sub・aud・数分の期限❸ 外部のトークンで交換を求める❹ 公開鍵を取り寄せる発行元の URL から❺ 署名を確かめる外部 IdP の公開鍵で❻ 信頼の設定と照合iss・sub・aud が一致❼ 自分の側のアクセストークン❽ トークンを付けて呼ぶ❾ レスポンスワークロードが持つのは❷の外部のトークンだけ。交換窓口は「どの発行元の、どのジョブなら交換するか」を信頼の設定として持つ外で生まれたトークンを、信頼の設定に合うときだけ自分の側のトークンに替える
図9 ワークロード ID フェデレーションの流れ(❶〜❾)
項目最初の図(図8)この図(フェデレーション)
トークンを出すのは同じ基盤(⑧)外部の IdP(❷)と、交換窓口(❼)の2段
呼び出し元の確かめ方基盤が自分で分かる(⑦)外部のトークンの署名と中身で確かめる(❺❻)
交換窓口が持つもの―「どの発行元の、どのジョブなら交換するか」の信頼の設定
ワークロードが持つもの基盤のトークン外部のトークン(数分)と、交換後のトークン

Microsoft のドキュメントも、この流れを6つの手順で説明しています。かみ砕くと、次のとおりです。

  1. 外のプログラムが、外の IdP(GitHub など)にトークンを頼む
  2. 外の IdP がトークンを出す
  3. 外のプログラムが、そのトークンを Microsoft ID プラットフォーム(Entra ID)に送って、アクセストークンを頼む
  4. Entra ID が、信頼の設定と照らし合わせ、外の IdP の公開鍵(発行元 URL から取り寄せる)で署名を確かめる
  5. 合っていれば、Entra ID がアクセストークンを出す
  6. 外のプログラムが、そのアクセストークンで Azure などを使う

GitHub のドキュメントも、同じ利点を挙げています。クラウド側に信頼を設定しておけば、長く使うクラウドの資格情報を GitHub のシークレットに置かなくて済みます。クラウドからは、1つのジョブの間だけ使える短いトークンが出ます。

照合のルール:3つが完全に一致したときだけ

ひとことで言うと

「誰が出した(iss)」「誰の(sub)」「誰宛ての(aud)」の3つが、信頼の設定と1文字も違わずに一致したときだけ交換します。

フェデレーションの安全は、この照合にかかっています。受付が来客名簿を見るときに、会社名・氏名・訪問先の3つを確かめるのと同じです。

外部のトークンの中身信頼の設定(交換窓口)isshttps://idp.example.testissuerhttps://idp.example.testsubrepo:contoso/app01:…mainsubjectrepo:contoso/app01:…mainaudapi://token-exchange…audiencesapi://token-exchange…exp今から5分後期限内か(共通の決まり)1文字でも違えば交換しない(Entra ID では大文字・小文字も区別する)
図10 外部のトークンと信頼の設定の照合
トークンの項目信頼の設定意味たとえ
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://example.test/app01SVID身分を示す書類X.509 証明書か JWTWorkload APISVID を受け取る窓口秘密を持たずに呼べるトラストバンドル信じる CA の一覧相手の SVID を確かめるSPIFFE は、クラウドに縛られないワークロード ID の共通のしくみ
図11 SPIFFE の4つの言葉
  • 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つだけです。

  1. jwt.decode で、署名・期限(exp)・宛先(aud)・発行元(iss)をまとめて確かめる
  2. そのあと、sub が信頼の設定と完全に同じかを比べる

どれか1つでも合わなければ「NG」、全部合えば「OK」を表示します。

発行元の鍵を作る(図9の❹❺)

確かめること:署名の秘密鍵は発行元だけが持ち、交換窓口には公開鍵だけを渡すこと。

やっていること:会社の印鑑(秘密鍵)と、その印鑑の見本(公開鍵)を作ります。

外部の IdP の役idp.key署名用の秘密鍵idp.pub公開鍵渡す交換窓口の役idp.pub署名を確かめるのに使う(本物は発行元の URL から取得)秘密鍵は発行元だけ。交換窓口には公開鍵だけを渡す
図12 STEP 1 の様子

外部の 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の❻)

確かめること:交換窓口が持つのは「どのトークンなら信じるか」の設定だけで、秘密は入っていないこと。

やっていること:受付に置く来客名簿を書きます。

trust.json(交換窓口が持つ信頼の設定)nameこの設定の名前issuerどの発行元のトークンを信じるかsubjectどのワークロード(ジョブ)なら交換するかaudiencesトークンの宛先が何であるべきか「誰が出した、誰の、誰宛てのトークンなら信じるか」を書いておく
図13 STEP 2 の trust.json の項目

信頼の設定(交換窓口で操作)

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秒)の社員証をもらいます。

ヘッダalg・kidどの鍵・方式で署名したか.ペイロードiss・sub・aud・iat・exp誰が・誰の・誰宛て・いつまで.署名idp.key で署名改ざんされていない証拠ヘッダとペイロードは暗号化ではなく base64url で書いただけ。誰でも読めるので、秘密は入れないJWT は「ヘッダ.ペイロード.署名」の3つを点でつないだもの
図14 STEP 3 の JWT の作り

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 がすべて合えば、交換されること。

やっていること:受付に社員証を見せて、入館証に替えてもらいます。

main.jwtexchange.py(交換窓口の役)署名:idp.pub で確かめられるiss:trust.json の issuer と同じaud:audiences に入っているexp:期限内sub:subject と完全に同じOK交換する5つがすべてそろったときだけ、app01-deploy のトークンを出す
図15 STEP 4 の様子

交換窓口の役にトークンを渡す

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)sub:…refs/heads/featuresub が subject と違う → 断る組織名が大文字(Contoso)sub:repo:Contoso/…1文字の大小でも違う → 断る宛先が別(other)aud:api://other.example.testaud が audiences にない → 断る本物の IdP が署名したトークンでも、設定と違えば交換しない
図16 STEP 5 の様子

別のブランチ(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秒前に終了)exp:過去の時刻exp を過ぎている → 断る別の鍵で署名other.key で署名idp.pub で署名を確かめられない → 断る中身をまねても、発行元の秘密鍵がなければ署名は作れない本物のトークンを盗んでも、数分で期限が切れる「本物の発行元が」「最近」出したトークンだけが通る
図17 STEP 6 の様子

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(特別なサービスプリンシパル)システム割り当てリソースと一緒に生まれて消える1つのリソース専用ユーザー割り当て独立したリソース複数のリソースで共有できる(Microsoft の推奨)資格情報は Azure が持ち、利用者は触れないEntra ID のワークロード ID は、アプリ・サービスプリンシパル・マネージド ID
図18 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 からトークンをもらって、返してくれます。

VM 上のコードIMDS(VM の中の窓口)Entra IDAzureStorage など❶ トークンを求めるMetadata: true❷ トークンを求めるAzure が持つ証明書で❸ アクセストークン❹ access_tokenexpires_in など❺ Bearer トークンで呼ぶ❻ 確かめる署名・宛先RBAC の権限❼ レスポンスIMDS(169.254.169.254)は VM の中からだけ呼べる窓口。証明書は Azure が作って自動で取り替えるそのかわり、VM の上で動くコードなら、どれでもこの ID のトークンを取れる(守る境界は VM そのもの)コードは「ください」と言うだけ。証明書の出し入れは Azure が行う
図19 Azure VM のマネージド 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_typeBearer(持っている人が使えるトークン)。相手には Authorization ヘッダで渡す

例の expires_in は 3599 なので、約1時間で期限が切れます。期限が来たら、また IMDS に頼めば新しいトークンがもらえます。

守る境界は VM そのもの

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:トークンをもらう窓口の URL
  • IDENTITY_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 などにも対応しています。

フェデレーション資格情報(GitHub Actions の例)issuerhttps://token.actions.githubusercontent.comGitHub が出したトークンsubjectrepo:contoso/app01:ref:refs/heads/mainこの repo の main ブランチaudiencesapi://AzureADTokenExchange推奨の値大文字・小文字も区別して完全に一致1つの ID に最大20個までissuer と subject の組は重複できない追加してすぐは失敗することがあるEntra ID は、この3つと GitHub のトークンを照合して、交換するかを決める
図20 フェデレーション資格情報(GitHub Actions の例)

マネージド 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つを順に確かめると原因を絞れます。

  1. どのトークンを出したか(中身は STEP 3 のように表示できる)
  2. どの設定と比べたか(フェデレーション資格情報や権限)
  3. どこで断られたか(エラーの番号)
起きたことよくある理由AADSTS700213subject が合わない(大小の違いも)AADSTS700211issuer が合わない・設定がないAADSTS70021設定の直後でまだ広がっていないAADSTS700024外部のトークンの期限・時刻のずれIMDS が 400Metadata: true のヘッダがない違う ID のトークンユーザー割り当てで ID を指定していないまず「どのトークンを、どの設定と比べて、どこで断られたか」を確かめる
図21 ワークロード ID がうまくいかないとき
起きたことよくある理由確認すること
AADSTS700213subject が合わないトークンの sub と設定の subject を1文字ずつ比べる。大文字・小文字、ブランチ名や環境名
AADSTS700211issuer が合わないフェデレーション資格情報があるか。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 が一致したときだけ交換する他社の社員証を受付で入館証に替える
マネージド IDAzure の上で動くワークロードの ID。システム割り当てとユーザー割り当て会社が預かる社員証
注意VM の上のコードはどれでも ID を使える。権限は必要な分だけ社員証で入れる部屋を絞る

参考にした資料

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

あとがき

小さな模型を作って試してみて、フェデレーションの中身は「署名を確かめて、3つの文字列を比べる」という、思ったより単純なしくみだと分かりました。そのぶん、sub に入れる値を1文字でも間違えると、何も言わずに失敗します。トークンの中身を表示して設定と見比べる習慣が大事だと感じます。前の記事から続いていた「最初の鍵をどうするか」は、鍵そのものを持たない、という形で答えが出ました。次の記事からは、API の入口を守る API ゲートウェイを扱います。

コメント