doodle-on-web

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

AADSTS700016の正体はFederated Credentialのsubject不一致 — GitHub ActionsのOIDCが落ちる本当の原因

スポンサーリンク

GitHub Actions の azure/loginAADSTS700016(Application not found)が出る場合、その原因はほぼ Federated Credential の subject 不一致です。App Registration も Client ID も Tenant ID も正しいのに落ちるのは、OIDC 経路の登録が足りていないから。エラー文言に騙されて App Registration を疑い続けると半日溶けます。

ローカルの az login --service-principal は通るのに、同じ Client ID を渡した CI だけが死ぬ。この非対称こそが切り分けの入口でした。

症状の確認

以下に当てはまるならこの記事の対象です。

  • ローカルでは az login --service-principal で問題なくログインできる
  • GitHub Actions の azure/login@v2 でだけ AADSTS700016: Application with identifier '<app-id>' was not found in the directory '<tenant-id>' が出る
  • App Registration は Portal に確かに存在している
  • Client ID も Tenant ID もコピペで確認済み

半日の内訳はこうでした。最初の2時間は Client ID と Tenant ID の再コピペ、次の2時間は Secret の期限と SP の存在確認、その後に Portal の Federated credentials 画面を開いて、登録が refs/heads/main の1件しかないことに気づいた。原因に到達するまで、エラーメッセージの言葉どおりに App Registration ばかり疑っていたわけです。

700016 と 70021 の出し分け

ここは正確に押さえてください。subject 不一致で本来返るのは AADSTS70021(No matching federated identity record found)です。AADSTS700016 はアプリ/SP そのものが見つからない系のコードです。

にもかかわらず 700016 が出るケースがあります。よくある原因は次の3つ。

  • Tenant ID が別テナントを指している:そのテナントに当該アプリが存在しないので、正しく 700016
  • Enterprise Applications 側の Service Principal が消えている:App Registration だけ残っていても 700016
  • azure/login に渡した Client ID が Object ID などの別 ID:ディレクトリ内で解決できず 700016

つまり実務上は「700016 が出たら ID / テナント / SP を、70021 が出たら subject と audience を疑う」の二段構えが正しい判断です。ログのコードを取り違えたまま片側だけ疑い続けるのが、最も時間を溶かすパターンでした。

なぜローカルは動いてCIで死ぬのか

同じ App Registration でも、認証手段ごとに別経路として扱われるからです。

  • ローカル:Client Secret 認証。subject の照合は無く、Secret が合えば通る
  • GitHub Actions:OIDC + Federated Credential。Client ID / Tenant ID に加えて subject と audience の完全一致が要る

ローカルで Secret が通ることは、OIDC 経路が生きている証明にはならない。ここを混同すると、正しい認証情報を延々と再コピペするループに入ります。

実際に送られた subject を取り出す

GitHub Actions の通常ログに生の subject は出ません。 claim を見るには自分で取り出す必要があります。方法は2つ。

ひとつはリポジトリの Settings → Secrets and variables → Actions で ACTIONS_STEP_DEBUGtrue にし、デバッグログを出す方法。もうひとつは、ID トークンを取得してデコードするステップを一時的に挟む方法です。

permissions:
  id-token: write
  contents: read

jobs:
  debug:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/github-script@v7
        with:
          script: |
            const t = await core.getIDToken('api://AzureADTokenExchange')
            const c = JSON.parse(
              Buffer.from(t.split('.')[1], 'base64').toString()
            )
            core.info(`sub: ${c.sub}`)
            core.info(`aud: ${c.aud}`)
            core.info(`iss: ${c.iss}`)

出力された sub / aud / iss を、Portal の登録値と1文字ずつ突き合わせます。この3行が出た瞬間に原因が確定します。

Federated Credential に不足している subject を追加する

subject はワークフローのトリガーによって変わります。

トリガー subject
main への push repo:org/repo:ref:refs/heads/main
Pull Request repo:org/repo:pull_request
Tag push repo:org/repo:ref:refs/tags/v1.0.0
Environment 指定 repo:org/repo:environment:production

PR トリガーなのに refs/heads/main しか登録されていなかったのが真因でした。注意点として、PR でも environment: を指定したジョブは subject が environment:production 側になりますpull_request を登録しただけでは通りません。

Portal なら App Registration → Certificates & secrets → Federated credentials → Add credential。CLI ならこうです。

az ad app federated-credential create \
  --id <app-id> \
  --parameters '{
    "name": "gh-pull-request",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:org/repo:pull_request",
    "audiences": ["api://AzureADTokenExchange"]
  }'

audiencesapi://AzureADTokenExchange が正です。ここを https:// 付きにしたり空にしたりする事故も頻出で、subject が合っていても 70021 になります。既存登録の確認は az ad app federated-credential list --id <app-id>

ワークフロー側の permissions: id-token: write も必須です。抜けると subject 自体が送られません。

それでも直らないときの勘違い3種

  • App ID = Object ID:Application (Client) ID、App Registration の Object ID、Service Principal の Object ID は全部別物。Portal のコピーボタンは隣接していて取り違えやすい
  • App Registration があれば SP もある:Enterprise Applications 側の Service Principal だけ消えていると 700016 になる。az ad sp show --id <client-id> で確認
  • Tenant は1つだけ:個人テナントと組織テナントに両方所属し、az login が意図と違う方を選んでいることがある。az account show --query "{tenantId:tenantId, user:user.name}" で実行主体を確認

他サービスでも同じ構造

環境 subject の形式(例) 事故りやすい点
GitHub Actions repo:org/repo:ref:refs/heads/main branch / PR / environment ごとの登録漏れ
Azure DevOps sc://org/project/connection-name Service Connection 再作成で subject が変わる
Terraform Cloud organization:my-org:project:my-project:workspace:my-ws:run_phase:plan plan と apply で subject が別。片方だけだと apply で死ぬ
GitLab CI project_path:group/proj:ref_type:branch:ref:main プロジェクトパス変更で subject が変わる

AIに聞くときのプロンプトテンプレ

ハマっている最中、Claude にも他の AI にも聞きました。返ってきたのは「Client ID / Tenant ID を確認」「SP の存在を確認」「Secret の期限を確認」。全部正しいけれど、subject 不一致には辿り着かない。Web 上の解説記事の多くが ID と Secret の確認までしか書いておらず、AI もその平均値を返すからです。

事実を揃えて渡せば的中率は跳ね上がります。次の型を埋めて投げてください。

Azure OIDC 認証が GitHub Actions で失敗します。
- エラーコード: AADSTS70021(本文全文: ...)
- 送信された claim: sub=repo:org/repo:pull_request / aud=api://AzureADTokenExchange
- Portal の登録値: subject=repo:org/repo:ref:refs/heads/main / audiences=api://AzureADTokenExchange
- トリガー: pull_request / environment 指定なし
- ローカルの Secret 認証: 成功
この差分から原因と修正コマンドを示してください。

まとめ

  • 700016 は ID / テナント / SP 系、70021 は subject・audience 系。コードで判断を分ける
  • ローカルの Secret 認証が通っても、OIDC 経路の保証にはならない
  • Federated Credential は トリガーごとに登録。environment 指定時は subject が切り替わる
  • audiencesapi://AzureADTokenExchange、ワークフローには id-token: write
  • subject は推測せず、ID トークンをデコードして実物を見る

よくある質問

AADSTS700016 と AADSTS70021 の違いは?

700016 は「そのディレクトリにアプリが見つからない」、70021 は「一致する Federated Credential が無い」です。前者は Tenant ID の取り違え・SP の削除・Client ID に別 ID を渡したケースで、後者は subject や audience の不一致で出ます。まずログのコードを確定させてから疑う対象を選んでください。

実際に送信された subject はどこで見られる?

通常のワークフローログには出ません。ACTIONS_STEP_DEBUG を有効にするか、actions/github-scriptcore.getIDToken() を呼び、JWT のペイロードをデコードして sub / aud を出力します。確認後はそのステップを削除してください。

Pull Requestトリガーでも認証を通すには?

Federated Credential に repo:org/repo:pull_request を追加します。ただしそのジョブが environment を指定している場合、subject は repo:org/repo:environment:<name> になるため、そちらの登録が必要です。