doodle-on-web

自分で調べたことや、仕事の中で質問されたことなどをまとめています。

AADSTS700016でGitHub Actionsのazure/loginが失敗した話 — ローカルでは動くのにCIで死ぬ理由

スポンサーリンク

まず症状

検索でここに来た人向けに、症状を先に置きます。同じならビンゴです。

  • ローカルでは az login --service-principal問題なくログインできる
  • 同じ App Registration を使っているはずの GitHub Actions ではこれが出る
AADSTS700016: Application with identifier '<app-id>' was not found in the directory '<tenant-id>'.
This can happen if the application has not been installed by the administrator of the tenant
or consented to by any user in the tenant.
  • App Registration は Azure Portal で確認できる、確かに存在している
  • Tenant ID も合っている(コピペで確認済み)
  • Client ID も合っている(コピペで確認済み)
  • なのに Application ... was not found と言われる

AADSTS700016 GitHub Actions azure/login Application not found Federated Credential subject 不一致 あたりで検索して来た人は、たぶんこれです。

原因はだいたい以下のどれかで、App Registration の存在問題ではないことがほとんどです:

  1. Federated Credential の subject が、実際に GitHub Actions が送ってくる subject と一致していない
  2. テナントを間違えている(個人テナントと組織テナントが混ざる)
  3. App ID と Object ID を混同している
  4. App Registration は残っているが Service Principal(Enterprise Application 側)が削除されている
  5. multi-tenant App で 対象テナントに consent していない

詳細は下に書きます。


構成

ローカル端末
   │  az login --service-principal -u <app-id> -p <secret> --tenant <tenant-id>
   ▼
Entra ID
   │  ✅ ログイン成功
   ▼
az group list  ✅ 動く

──────────────────────────────────────

GitHub Actions
   │  azure/login@v2
   │  client-id / tenant-id / subscription-id (OIDC)
   ▼
Entra ID
   │  ❌ AADSTS700016
   ▼
何もできない

「同じ Client ID 使ってるはずなのに」が出発点。 ここで「同じ Client ID」という思い込みが、実は事故の核でした。


最初の勘違い

正直に書きます。最初に AADSTS700016 を見たとき、こう思いました。

「App ID 間違えたか、Tenant ID 間違えたかのどっちかでしょ」

これが全部間違ってた。両方合ってました。

何を勘違いしていたかを並べておきます。

勘違い1: 「同じ Client ID なら、ローカルでも CI でも同じ認証ができる」

違います。 ローカルの az login --service-principalClient Secret を使う認証。 GitHub Actions の azure/login@v2 は OIDC 連携で Federated Credential を使う認証認証方式が違うので、Azure 側でも「別の経路で来てる」と扱われます。

そして OIDC 側には subject の照合という追加チェックがある。Client ID と Tenant ID が合ってても、subject が一致しなければ、Azure 側から見ると「その subject から認証可能なアプリが見つからない」状態になり、AADSTS700016 が返ってきます。

エラーメッセージが「Application not found」なので、つい App Registration 自体を疑ってしまうけど、真因は Federated Credential の subject 不一致ということが結構あります。

勘違い2: 「App ID = Object ID」

違います。これ Azure 経験者全員が一度はやらかすやつ。

  • Application (Client) ID: App Registration を識別する ID。az login などで使う
  • Object ID: Entra ID ディレクトリ内のオブジェクトを識別する ID。RBAC の --assignee-object-id などで使う
  • しかも App Registration の Object IDService Principal の Object ID はさらに別物

GitHub Actions のシークレットに「AZURE_CLIENT_ID」と書いてあったから Client ID を入れたつもりが、Portal でコピーしたのが Object ID だった、というやつ。コピペ元のボタン位置が紛らわしい。

勘違い3: 「App Registration があれば Service Principal もある」

違います。 App Registration(Entra ID のアプリの定義)と Service Principal(特定テナントでのインスタンス)は別物。

App Registration を削除してないけど、Enterprise Applications 側で Service Principal だけ消されてた、というケースが地味にあります。「テナント整理」をしたタイミングで、知らずに消されてることも。 この状態だと App Registration はあるのに「そのテナントにはそのアプリのインスタンスが無い」状態になり、AADSTS700016 が出ます。

勘違い4: 「Tenant ID は1つしかないでしょ」

違います。 個人で Microsoft アカウントを使ってる人は、個人テナントxxxxxxxx.onmicrosoft.com)と、組織テナント(会社のテナント)の両方に所属していることがあります。 az account show で出てくる tenantId が、実は意図と違うテナントだった、ということが起きます。

特にハマるパターン:

  • 会社の App Registration を使うつもりなのに、az login がデフォルトで個人テナントを選んでる
  • multi-tenant App を作って「どのテナントからでも使える」と思っていたが、対象テナントで一度も consent されてない

試したこと(時系列)

試行1: Client ID と Tenant ID を Portal から再コピペ

GitHub Actions のシークレットを全部消して、Portal から目視で一文字ずつ確認して入れ直す。

→ 変わらず AADSTS700016。

試行2: ローカルで再現を試みる

同じ Client ID と Tenant ID で、ローカルの az login --service-principal --tenant <id> -u <id> -p <secret> を実行。

動く

ここで「ローカルで動くなら、認証情報自体は合ってる」と思い込んでしまった。 実はこの思い込みが時間を溶かす元凶でした。ローカルは Secret 認証、CI は OIDC 認証で、別の経路だから別の確認が要る

試行3: Tenant を疑って az account show

az account show --query "{tenantId:tenantId, user:user.name}"

→ Tenant ID は合ってた。

ここで完全に詰まりました。「全部合ってるのに何で?」が30分続くと、「もしかして Azure 側のバグでは?」という現実逃避が始まる。これも前回(ACI 編)と同じ症状で、だいたい自分のミスです。

試行4: Federated Credential の subject を確認

冷静になって、Portal で App Registration → Certificates & secrets → Federated credentials を開いて、設定されてる subject を確認しました。

設定値(イメージ):

repo:myorg/myrepo:ref:refs/heads/main

GitHub Actions が実際に送ってくる subject は、ワークフローのトリガーによって変わる:

トリガー subject
main ブランチへの push repo:myorg/myrepo:ref:refs/heads/main
develop ブランチへの push repo:myorg/myrepo:ref:refs/heads/develop
Pull Request repo:myorg/myrepo:pull_request
Tag push repo:myorg/myrepo:ref:refs/tags/v1.0.0
Environment 指定あり repo:myorg/myrepo:environment:production

自分のワークフローは Pull Request トリガーだったのに、Federated Credential には refs/heads/main だけが登録されていた。

つまり Azure 側から見ると、「main ブランチからの push なら知ってる SPN だけど、PR から来た subject は知らない」という状態。だからアプリ自体は存在するのに 「そのアプリへの認証経路は無い」 → AADSTS700016 を返してきていた。

エラーメッセージが Application ... was not found なので App Registration を疑い続けてしまうけど、Azure としては嘘は言ってなくて、「この subject に対応する Federated Credential を持つアプリは無い」というのが真意でした。


本当の原因

GitHub Actions の OIDC 認証で AADSTS700016 が出る場合、ほとんどはこの3つのどれか:

  1. Federated Credential の subject が、実際のワークフロー実行時の subject と一致していない
  2. 必要なトリガーごとに Federated Credential を登録していない(main 用しか作ってないのに PR から実行してる、など)
  3. Environment を使っているのに、Federated Credential を branch ベースで作っている

修正方法:

Portal の App Registration → Certificates & secrets → Federated credentials で、ワークフローのトリガーごとに登録する。 PR でも動かしたいなら pull_request 用のエントリを追加。Environment 経由なら environment:production 用を追加。

Federated Credential 1: repo:myorg/myrepo:ref:refs/heads/main
Federated Credential 2: repo:myorg/myrepo:pull_request
Federated Credential 3: repo:myorg/myrepo:environment:production

GitHub Actions 側のワークフローで、id-token: write パーミッションも忘れずに:

permissions:
  id-token: write
  contents: read

これを抜くと subject が送られなくて別のエラーになる。


図で整理:「同じApp Registrationなのに、ローカルとCIで動作主体が違う」

これが今回の本質です。

┌──────────────────────────────────────────────────────────────┐
│ App Registration: my-app                                      │
│   Client ID: xxxx-xxxx-xxxx                                   │
│                                                                │
│   ┌─ 認証手段A: Client Secret ──────────────────────┐         │
│   │   ローカル端末から az login で使う                │         │
│   │   subject 照合なし、Secret さえ合えば通る         │         │
│   └──────────────────────────────────────────────────┘         │
│                                                                │
│   ┌─ 認証手段B: Federated Credential (OIDC) ────────┐         │
│   │   GitHub Actions / Terraform Cloud などから       │         │
│   │   subject の完全一致が必要                         │         │
│   │   ・repo:org/repo:ref:refs/heads/main             │         │
│   │   ・repo:org/repo:pull_request                    │         │
│   │   ・repo:org/repo:environment:production          │         │
│   │   ※ どれか1つでも未登録なら、そのトリガーは死ぬ  │         │
│   └──────────────────────────────────────────────────┘         │
└──────────────────────────────────────────────────────────────┘

軸:
  認証主体  = ローカルユーザーか / CI ジョブか / 別 SaaS か
  認証手段  = Secret か / OIDC か / 証明書か
  Subject   = OIDC では誰として来たか(branch / PR / environment)
  対象      = どのテナントの、どのアプリに対して

「同じ App Registration を使ってる」と思っていても、認証手段ごとに別の経路があり、それぞれに別の登録が要る。 ローカルで Secret が通っても、それは「Secret 経路は生きてる」というだけで、「OIDC 経路も生きてる」ことの証明にはならない。


同じ構造の他サービス

「認証主体のズレ」は GitHub Actions 固有じゃないです。Azure に外から認証する場面はだいたい同じ構造で、サービスごとに subject の形式が違うのが余計にハマる原因。

環境 認証手段 subject の形式(例) よくある事故
ローカル端末 Client Secret / 証明書 (subject 照合なし) Tenant 間違い、Secret 期限切れ
GitHub Actions OIDC + Federated Credential repo:org/repo:ref:refs/heads/main branch / PR / environment ごとに登録漏れ
Azure DevOps Service Connection (Workload Identity) sc://org/project/connection-name Service Connection 再作成で subject 変わる
Terraform Cloud OIDC + Federated Credential organization:org:project:proj:workspace:ws:run_phase:plan plan/apply で別 subject、両方登録が要る
GitLab CI OIDC + Federated Credential project_path:group/proj:ref_type:branch:ref:main プロジェクトパス変更で subject 変わる

特に注意したいのが Terraform Cloud。 plan と apply で送られてくる subject が違うので、片方しか登録してないと「terraform plan は通るのに apply で死ぬ」という事故が起きます。これは初見で必ずやらかすやつ。

つまり Azure に外部から認証する全ての経路で、「subject に何が入ってくるか」を毎回明示的に確認するのが安全です。デフォルトで察してくれません。


Azureの「認証主体ズレ」を切り分ける順番(テンプレ)

このフローは AADSTS700016 以外にも、AADSTS7000215(Secret 期限切れ)や AADSTS50020(テナント不一致)など、AAD 系エラー全般で使えます。

① 「ローカルで動くのに CI で動かない」と「CI で動くのにローカルで動かない」を区別する

これが最初の分岐。

  • ローカルで動く → Secret 経路は生きてる。OIDC 経路を疑う
  • CI で動く → OIDC 経路は生きてる。ローカル側の Secret / Tenant を疑う
  • 両方動かない → App Registration / SPN そのものを疑う

② エラーコードを原文で確認する

コード 意味
AADSTS700016 App が見つからない(or その subject 経路が無い)
AADSTS7000215 Secret が間違っている / 期限切れ
AADSTS50020 テナント不一致(multi-tenant 関連が多い)
AADSTS70011 scope が不正
AADSTS90002 テナントが存在しない / Tenant ID 間違い

「Application not found」というメッセージで App Registration を疑い続けるのは罠。エラーコードで判断する。

③ ID の種類を毎回ラベル付きで確認する

Application (Client) ID  : xxxx-xxxx-...
Object ID (App Reg)      : yyyy-yyyy-...   ← 別物
Object ID (Service Princ): zzzz-zzzz-...   ← さらに別物
Tenant ID                : aaaa-aaaa-...
Subscription ID          : bbbb-bbbb-...

Portal でコピーボタンを押すたびに、何の ID をコピーしたかをラベル付きで確認する。 「とりあえずコピペ」で半日溶けます。

④ 「今、誰として実行しているか」を毎回 az account show で確認する

az account show --query "{tenantId:tenantId, user:user.name, type:user.type}"

user.typeservicePrincipaluser かで全然意味が違う。 個人 Microsoft アカウントで個人テナントに繋がってる、というのが地味によくある事故。

⑤ OIDC の場合、Federated Credential の subject を確認する

GitHub Actions では、azure/login のログや OIDC 関連のデバッグ出力から、実際に使われる subject を確認できる場合があります:

Run details:
  ...
  Subject: repo:myorg/myrepo:pull_request

これと Portal の Federated Credential 登録値を 目で diff する。 1文字でも違えば AADSTS700016。

⑥ App Registration と Service Principal の両方が存在することを確認する

# App Registration の確認
az ad app show --id <client-id> --query "{appId:appId, displayName:displayName}"

# Service Principal の確認(同じ Client ID で)
az ad sp show --id <client-id> --query "{appId:appId, objectId:id}"

App Registration はあるけど SP が消されてる、というケースはこれで一発で分かる。

az ad sp list --filter "appId eq '<client-id>'" --query "[].{tenant:appOwnerOrganizationId, displayName:displayName}"

対象テナントに SP が無ければ、admin consent が必要。


次回どう防ぐか(チェックリスト)

GitHub Actions × Azure OIDC で、最初に確認するリスト:

  • [ ] Federated Credential を「ワークフローのトリガーごと」に登録(main / PR / tag / environment)
  • [ ] ワークフローに permissions: id-token: write を書いた
  • [ ] azure/login@v2client-id tenant-id subscription-idPortal からラベル確認しながらコピー
  • [ ] App ID と Object ID を混同していない
  • [ ] az ad sp show --id <client-id> で SP の存在を確認した
  • [ ] エラー出たら、コード(AADSTS700016 等)で判断する。メッセージ本文に騙されない
  • [ ] ワークフローログで「実際に送られた subject」を確認する
  • [ ] ローカルで動いても OIDC 経路の保証にはならないことを覚えておく

AIに聞いた結果

ハマってる最中、Claude にも他の AI にも聞きました。

返ってきた答え:

  • 「App Registration の Client ID と Tenant ID を確認してください」
  • 「Service Principal が存在するか確認してください」
  • 「Secret の有効期限を確認してください」

全部合ってる。全部合ってるんだけど、今回の真因(subject 不一致)にはなかなか辿り着かない。

これも前回の ACI 編と同じで、Web 上の解説記事の多くが「Client ID / Tenant ID / Secret を確認」までしか書いていないので、AI もその平均値を返してきます。 Federated Credential の subject 一致は公式ドキュメントには書いてあるけど、解説記事の表面には出てきにくい一行。

最終的に解決したのは、ワークフローログで実際に送られた subject を見て、Portal の登録値と目で diff した瞬間でした。

AI は「ありがちな原因リスト」は得意。 でも「自分の状況に固有の subject 文字列が違う」を当てるのは苦手。

これはたぶん構造的にそうで、ログを見ないと当てられない種類のエラーは、人間がログを読みに行くしかない。 AI に依頼するときは「実際に送られた subject はこれです、Portal の登録値はこれです」までこちらが揃えて渡せば、当ててくれる確率は跳ね上がります。


おわりに:シリーズの軸として

前回(ACI × ACR 編)の結論はこうでした:

Azure の 401 / 403 はほぼ全部、Identity / Scope / Timing / Plane の4軸に分解できる

今回の AADSTS700016 は、その中の Identity(誰) をさらに分解した話です。

「同じ App Registration を使ってる」と思っていても、認証手段ごとに別の主体として扱われる。 ローカル Secret の自分と、GitHub Actions OIDC の自分は、Azure から見ると別人。

「でも昨日までは動いてた」が始まったら危険です。 Azure 認証事故は、だいたい『今、自分が誰として実行しているか』の認識がズレているときに起きます。 CLI で成功した時点で「認証情報そのものは合ってる」と思い込み始めるけど、Azure では「どこから・どの手段で実行したか」で別主体として扱われる。

このシリーズで一貫して書きたいのは、

Azure は『誰が・何をしてるか』を明示しないと死ぬ

という一点です。同じ沼に落ちた人の役に立ったら嬉しいです。