WIFをMicrosoft Entra ID(Azure)と連携する — Managed IdentityとAKS
Azure上のワークロードをMicrosoft Entra IDのトークンでClaude APIに認証させる方法を、Managed Identityの構成とAKSのEntra Workload Identityの2経路で解説します。
どのワークロードにどちらの方式を使うか
Azure上のワークロードを、Workload Identity Federation(WIF)でClaude APIに認証させる手順です。Microsoft Entra IDが発行したJWTをAnthropicのPOST /v1/oauth/tokenで短命のアクセストークンに交換するので、APIキーを置かずに済みます。実行場所によって、Entra側のJWTの入手経路が2つに分かれます。
Entra側のJWTをどこから得るか
Managed Identity方式
VM・VM Scale Set・App Service・Functions・Container Appsが対象です。プラットフォームのローカルなトークンエンドポイントを1回呼ぶだけで、Entra発行のJWTが手に入ります。
Entra Workload Identity方式
Kubernetesが投影したサービスアカウントトークンを、Entraのトークンエンドポイントで交換します。Entra発行のJWTに辿り着くまでが2段階で、Podにはサービスアカウントの注釈とラベルが要ります。
WIFの用語(サービスアカウント・連合発行者・連合ルール)はWIFとは何かで扱っています。ここでは設定値とつまずきどころに集中します。
前提条件:
- WIFの基本概念(サービスアカウント・連合発行者・連合ルール)への理解
- Managed Identityを割り当てられる権限を持つAzureサブスクリプション(またはAKSでEntra Workload Identityを構成できる権限)
- Microsoft Entraテナントでアプリ登録とサービスプリンシパルを1つ作成できる権限
- AzureポータルのMicrosoft Entra ID → Overview → Tenant IDで確認できるテナントID
- Anthropic組織でサービスアカウント・連合発行者・連合ルールを作成できる権限
どちらの方式も、設定は次の4段階で進みます。
共通の4段階
- 1
オーディエンスをテナントに登録する
Claude API用のアプリ登録とサービスプリンシパルを、Entraテナントに1つ作ります。
- 2
プラットフォーム側のアイデンティティを用意する
Managed Identity、またはAKSのEntra Workload Identityです。
- 3
Anthropic側で発行者・サービスアカウント・ルールを組む
Consoleのウィザードか、Admin APIで登録します。
- 4
実行時にトークンを交換する
Entraのトークンを
/v1/oauth/tokenに渡し、返ってきたベアラートークンでClaudeを呼びます。
トークンのオーディエンスをテナントに登録する
Microsoft Entra IDは、要求されたオーディエンスがテナント内にアプリ登録として存在する場合にしかトークンを発行しません。Claude APIのオーディエンスを表すアプリ登録を1つ作れば、テナント内のすべてのワークロードがそのオーディエンス宛てにトークンを要求できます。
APP_ID=$(az ad app create --display-name claude-api-federation --query appId -o tsv)
az ad app update --id "$APP_ID" \
--identifier-uris "api://$APP_ID" \
--set api.requestedAccessTokenVersion=2
az ad sp create --id "$APP_ID"この登録を省くと、トークン要求がManaged IdentityエンドポイントからはAADSTS50001、EntraのトークンエンドポイントからはAADSTS500011という「リソースがテナント内に見つからない」エラーで失敗します。
Managed Identityを使う方式
Managed Identity用のJWTを取るエンドポイントは、実行場所ごとに違います。
Managed Identityの設定
Azureリソースにシステム割り当てまたはユーザー割り当てのManaged Identityを付与し、Object(principal)IDを控えます。このGUIDはトークンのsubクレームとoidクレームの両方に現れ、連合ルールがこの値をマッチ条件にします。システム割り当ての場合はリソースのIdentityページ、ユーザー割り当ての場合はManaged IdentityリソースのOverviewページで見つかります。Managed IdentityにはEntra ID上でサービスプリンシパルだけがあり、アプリ登録はありません。
プラットフォームごとにトークンエンドポイントが異なります。
| 実行場所 | エンドポイント | ヘッダー | api-version |
|---|---|---|---|
| VM・VM Scale Set | エンドポイントIMDS(http://169.254.169.254/metadata/identity/oauth2/token) | ヘッダーMetadata: true | api-version2018-02-01 |
| App Service・Functions・Container Apps | エンドポイントIDENTITY_ENDPOINT環境変数のURL | ヘッダーX-IDENTITY-HEADERにIDENTITY_HEADERの値 | api-version2019-08-01 |
App Service・Functions・Container AppsからはIMDSに到達できません。ユーザー割り当てのIdentityが複数あるリソースでは、client_idパラメーターで対象を明示します。Microsoft Learnでは「Required, if your VM has multiple user-assigned managed identities.」とされています。省略した場合、システム割り当てのIdentityも有効なら、要求は黙ってそちらに切り替わり、連合ルールのoidが一致せず失敗します。有効でなければ、2つ目のユーザー割り当てIdentityを付けた時点で要求そのものが失敗します。
取得したトークンをデコードすると、次のようなクレームが並びます。
{
"iss": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"sub": "9f8e7d6c-1a2b-3c4d-5e6f-...",
"aud": "<APP_ID>",
"oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
"tid": "<TENANT_ID>",
"azp": "<IDENTITY_CLIENT_ID>",
"ver": "2.0"
}ルールに載せるクレームは目的で選びます。
| クレーム | 値 | ルールで使う場面 |
|---|---|---|
oid | 値Managed IdentityのオブジェクトID(subと同じ) | ルールで使う場面特定の1つのIdentityだけを許可する。既定の選び方 |
azp | 値呼び出し元のクライアントID | ルールで使う場面1つのアプリ登録を共有する全ワークロードを許可する。Managed Identityではoidと同じ範囲になる |
aud | 値オーディエンスのアプリ登録のクライアントID | ルールで使う場面常に。ルールのaudienceと完全一致させる |
tid | 値テナントID | ルールで使う場面多重防御。発行者URLがすでにテナントを固定している |
Anthropic側の設定
Claude ConsoleのSettings → Workload identityで「Connect workload」を選び、「Microsoft Entra」タイルから進めます。「Token issuer」セレクターでv2.0(login.microsoftonline.com)を選びます(既定はv1になっているため注意が必要です)。
{
"name": "azure-prod-tenant",
"issuer_url": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"jwks": { "type": "discovery" },
"max_jwt_lifetime_seconds": 86400
}連合ルールはManaged IdentityのoidとテナントIDをマッチさせます。audienceには、デコードしたトークンのaudの値をそのまま入れます。
{
"name": "azure-inference-worker",
"issuer_id": "fdis_...",
"match": {
"audience": "<APP_ID>",
"claims": {
"oid": "9f8e7d6c-1a2b-3c4d-5e6f-...",
"tid": "<TENANT_ID>"
}
},
"target": { "type": "service_account", "service_account_id": "svac_..." },
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}トークンを取得して使う
ENTRA_TOKEN=$(curl -sS -H "Metadata: true" \
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://<APP_ID>" \
| jq -r .access_token)
RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" \
-d @- <<JSON
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$ENTRA_TOKEN",
"federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
"organization_id": "$ANTHROPIC_ORGANIZATION_ID",
"service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
"workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)
ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)App Service・Functions・Container Appsでは、1行目をローカルのトークンサービス宛てに差し替えます。
ENTRA_TOKEN=$(curl -sS -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
"$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=api://<APP_ID>" \
| jq -r .access_token)Claude SDKはidentity_token_providerにトークン取得関数を渡すと、交換と更新を引き受けます。引数名は言語ごとに違います。TypeScriptとPHPはidentityTokenProviderです。C#はIdentityTokenProvider、Goはoption.WithFederationTokenProvider、JavaはfederationTokenProviderを使います。Pythonでは次の形になります。
import os
import anthropic
from anthropic import WorkloadIdentityCredentials
client = anthropic.Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_entra_token, # 上のcurlに相当する関数
federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
),
)AKSでEntra Workload Identityを使う方式
AKSのPodでは、Entra Workload IdentityがKubernetesのサービスアカウントとユーザー割り当てManaged Identityを連合させます。KubernetesがAKSクラスターのOIDC発行者で署名したサービスアカウントトークンをAZURE_FEDERATED_TOKEN_FILEのパスにPodへ投影しますが、このトークン自体はEntra発行のトークンではありません。そのためワークロードは2段階の交換を行います。まずこのトークンをhttps://login.microsoftonline.com/<TENANT_ID>/oauth2/v2.0/token(client_credentialsグラント)でEntra発行のアクセストークンに交換し、そのEntraトークンをAnthropicのSDKへ渡します。
Entra Workload Identityの設定
クラスターでOIDC発行者とWorkload Identityを有効化し、発行者URLを控えます。
az aks update \
--resource-group <RESOURCE_GROUP> \
--name <CLUSTER_NAME> \
--enable-oidc-issuer \
--enable-workload-identity
AKS_OIDC_ISSUER=$(az aks show \
--resource-group <RESOURCE_GROUP> \
--name <CLUSTER_NAME> \
--query oidcIssuerProfile.issuerUrl -o tsv)このWebhook(azure-workload-identity)はAKSでは有効化と同時に導入されます。手動で入れるのはAKS以外のクラスターだけです。
続けてユーザー割り当てManaged Identityを作ります。控える値は2つあり、役割が違います。クライアントIDはサービスアカウントの注釈に入り、PodへAZURE_CLIENT_IDとして注入されます。オブジェクトIDはoidクレームになり、Anthropicのルールが照合します。
az identity create \
--resource-group <RESOURCE_GROUP> \
--name claude-inference-identity \
--location <LOCATION>
IDENTITY_CLIENT_ID=$(az identity show \
--resource-group <RESOURCE_GROUP> \
--name claude-inference-identity \
--query clientId -o tsv)
IDENTITY_OBJECT_ID=$(az identity show \
--resource-group <RESOURCE_GROUP> \
--name claude-inference-identity \
--query principalId -o tsv)Kubernetesのサービスアカウントには、クライアントIDをazure.workload.identity/client-idの注釈で紐づけます。
apiVersion: v1
kind: ServiceAccount
metadata:
name: claude-inference
namespace: inference
annotations:
azure.workload.identity/client-id: <IDENTITY_CLIENT_ID>最後にManaged Identity側へ、クラスターのOIDC発行者を信頼する連合クレデンシャルを作ります。--audience api://AzureADTokenExchangeはKubernetesサービスアカウントトークンを受け取るためのEntra固有のオーディエンスで、先に登録したClaude API用のオーディエンスとは別物です。
az identity federated-credential create \
--resource-group <RESOURCE_GROUP> \
--identity-name claude-inference-identity \
--name claude-inference-aks \
--issuer "$AKS_OIDC_ISSUER" \
--subject system:serviceaccount:inference:claude-inference \
--audience api://AzureADTokenExchangePodにはazure.workload.identity/use: "true"ラベルを付け、アノテーション済みのサービスアカウントで実行します。WebhookがAZURE_FEDERATED_TOKEN_FILE・AZURE_CLIENT_ID・AZURE_TENANT_IDをPodへ注入します。
apiVersion: v1
kind: Pod
metadata:
name: inference-worker
namespace: inference
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: claude-inference
containers:
- name: app
image: your-registry/inference-worker:latestAnthropicの連合ルールが実際に見るのは、投影されたファイルではなくclient_credentials交換で得たEntra発行トークンです。クレームの形はManaged Identity方式と同じですが、有効期間は24時間ではなくclient_credentialsトークンの既定である60〜90分のランダムな幅になります。
実際の2段階の交換は、Pod内で次のように書けます。<APP_ID>はオーディエンスのアプリ登録、$AZURE_CLIENT_IDはManaged IdentityのクライアントIDで、取り違えるとトークンが別の宛先になります。
ENTRA_JWT=$(curl -sS "https://login.microsoftonline.com/$AZURE_TENANT_ID/oauth2/v2.0/token" \
-d grant_type=client_credentials \
-d "client_id=$AZURE_CLIENT_ID" \
--data-urlencode "scope=api://<APP_ID>/.default" \
-d client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer \
--data-urlencode "client_assertion@$AZURE_FEDERATED_TOKEN_FILE" \
| jq -r .access_token)得たENTRA_JWTを、Managed Identity方式と同じ/v1/oauth/tokenの呼び出しにassertionとして渡します。
Anthropic側の設定
発行者のmax_jwt_lifetime_secondsは、client_credentialsトークンの既定寿命をカバーする7500秒(2時間強)で足ります。テナントのトークン有効期間ポリシーやCAE(継続的アクセス評価)がこれを超えて延長する場合は値を引き上げます。同じテナントでManaged Identity方式のワークロードも動かすなら、86400秒に統一しても両方をカバーできます。
{
"name": "azure-prod-tenant",
"issuer_url": "https://login.microsoftonline.com/<TENANT_ID>/v2.0",
"jwks": { "type": "discovery" },
"max_jwt_lifetime_seconds": 7500
}連合ルールの形はManaged Identity方式と同じで、oidとテナントIDをマッチさせます。
3つの有効期間を取り違えない
Azure連携で最も詰まりやすいのは、似た名前の有効期間が3つあることです。Entra側のJWTの寿命、発行者に設定する許容上限、Anthropicのトークンの寿命は別々の値で、次のように並びます。
有効期間に関わる数字
Managed Identityのトークン
最大24時間
Azureが最大24時間キャッシュする
client_credentialsのトークン
60〜90分
AKS方式の既定。ランダムな幅
発行者のmax_jwt_lifetime_seconds
86400 / 7500
方式で必要な値が変わる
ルールのtoken_lifetime_seconds
600
Anthropicのトークンの寿命
最後の値は、Entraのトークンとは独立に決まるわけではありません。Anthropicのトークンの寿命は、ルールのtoken_lifetime_seconds(既定3,600秒)と、提示したJWTの残り寿命の2倍のうち短いほうです。下限は60秒です。24時間キャッシュのManaged Identityトークンの期限が近い場合は、Anthropicのトークンも短めに切られます。
SDKはこのトークンを期限の120秒前に更新します。更新の仕様はWIFとは何かの「トークンはいつまで有効か」とWIFリファレンスにあります。
401のAuthentication failedが返ったときの切り分け
交換に失敗しても、レスポンスは常に401のauthentication_errorで、メッセージはAuthentication failedの固定文です。拒否の理由は、レスポンスではなくConsoleの認証履歴ページに記録されます。呼び出された発行者とルール、検査されたクレーム、失敗した検証ステップがそこに出るので、最初にここを見ます。
JWTそのものを調べるときは、次のjqでペイロードをデコードします。ダミーのJWT(署名部は仮の文字列)で実行すると、こう出力されます。
jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' <<< "$JWT"{
"iss": "https://login.microsoftonline.com/TENANT/v2.0",
"sub": "9f8e7d6c",
"aud": "APP",
"oid": "9f8e7d6c",
"tid": "TENANT",
"azp": "CID",
"ver": "2.0",
"iat": 1775520000,
"exp": 1775606400
}expからiatを引くと、トークンの幅が分かります。この例は86400秒で、Managed Identityの24時間キャッシュと同じ長さです。
Azure側の原因として代表的なのは、次の4つです。
Azure側で多い4つの原因
発行者の不一致
登録した
issuer_urlが、トークンのissと1文字でも違うと失敗します。verが1.0なら、issはsts.windows.net形式です。寿命の上限不足
Managed Identityの発行者が、ウィザード既定の7500秒か1時間のままです。
86400へ上げます。オーディエンスの不一致
ルールの
audienceが、デコードしたaudと一致していません。v2.0なら、アプリ登録のクライアントIDです。存在しないクレーム
トークンが持たないクレームにマッチさせても通りません。v1.0のトークンでは
azpでなくappidを使います。
認証履歴にjti_reusedやworkspace_id_requiredが出る場合や、ホストの時計が30秒を超えてずれている場合は、Azureに限らない原因です。切り分けはWIFとは何かの「交換が失敗したときの切り分け」にあります。
トークンがv1.0のとき
既存のアプリ登録を流用しrequestedAccessTokenVersionを未設定のままにすると、Entraはv1.0トークンを発行します。デコードしたverクレームが1.0なら、4点が変わります。発行者のissはhttps://sts.windows.net/<TENANT_ID>/になり、Consoleウィザードではv1(sts.windows.net)を選びます。audクレームはアプリ登録のクライアントIDではなく、resourceとして渡した識別子URI(api://<APP_ID>)になります。呼び出し元のクライアントIDはazpではなくappidクレームに入るため、azpにマッチするルールはv1.0トークンには通りません。oid・sub・tidは両バージョンで共通です。issのURLはsts.windows.net形式でも同じJWKSを共有するので、検出モード(discovery)で動きます。
ルールのスコープを絞る
EntraのGUIDは固定長の正規形式のため、subject_prefixに36文字の完全なオブジェクトIDを設定すればその主体だけにマッチします。Consoleのウィザードは、claims.oidとsubject_prefixの両方に同じ値を入れます。ワークロードごとにルールを分けておけば、1つのワークロードのアクセスだけを個別に無効化できます。
寿命の上限を86400秒に上げると、漏えいしたEntraトークンが交換可能なまま残る時間も延びます。漏えい時の手段はルールの無効化なので、oidを絞った細かいルールが効きます。
まとめ
AKSだけで使うなら、発行者の上限は7500秒のまま狭く保てます。Managed Identityのワークロードを後から足す時点が、発行者の上限を見直すタイミングです。AWS環境で同じことをするなら、WIFをAWSと連携するを参照してください。