TL;DR(結論)

  • Cloud Run のネイティブ IAP(gcloud run deploy --iap / iapEnabled: true)は、サービス間のプログラマティック認証に対応している
  • ただし、通常の Cloud Run service-to-service 認証の常識(Google 発行の OIDC ID トークン)はそのままでは通らない。ネイティブ IAP のデフォルトは「Google マネージド OAuth クライアント」であり、これは仕様としてプログラマティックアクセスに使えない
  • 正解は サービスアカウントの自己署名 JWT(iamcredentials.signJwt を使い、**aud に「リクエスト先と完全一致する URL」または「パスワイルドカード付き URL(`https://xxx.run.app/*`)」を指定**すること
  • aud をサービスのルート URL だけにすると Audience specified does not match requested endpoint で弾かれる。ここが最大のハマりポイント
✗ aud = https://my-service-xxxx.asia-northeast1.run.app      → 401
✓ aud = https://my-service-xxxx.asia-northeast1.run.app/*    → 通る
✓ aud = https://my-service-xxxx.asia-northeast1.run.app/api/records → 通る(完全一致)

背景・目的

Gemini Enterprise Agent Platform の Agent Runtime(旧称: Vertex AI Agent Engine)上で動く ADK エージェントから、社内向け Web アプリ(Cloud Run)の REST API を呼び出す構成を作っていました。

[Agent Runtime]  ← 呼び出し元(SA: my-agent@PROJECT.iam.gserviceaccount.com)
      │ POST /api/records
      ▼
[Cloud Run: webapp]  ← IAP 有効(ネイティブ IAP)
      │
      ▼
  FastAPI アプリ

Web アプリは社内ユーザーがブラウザで閲覧するため、Google アカウント認証が必要です。2025 年に GA したネイティブ IAP(ロードバランサー不要で --iap フラグだけで有効化できる)を使いました。お手軽です。

ところが、エージェント(サービスアカウント)からの API 呼び出しが、何をどうやっても 401 で通らない。本記事は、試して全滅した方法の記録と、最終的に判明した根本原因、そして実機検証済みの正しい実装方法をまとめたものです。

試して全滅したこと

Claude Opus 4.8とともにCloud Run のサービス間認証や IAP のドキュメントを参考にしながら、以下を順に試しました。トークンはすべて Cloud Logging でクレームを確認しており、`iss=https://accounts.google.com` の正当な Google 署名トークンであることは確認済みです。

# 方式 aud 結果
1 自己署名 JWT(signJwt Cloud Run の URL(ルート) 401 Audience specified does not match requested endpoint
2 metadata server の OIDC トークン IAP の OAuth クライアント ID 401 Invalid bearer token. Invalid JWT audience.
3 metadata server の OIDC トークン Cloud Run の URL 401 同上
4 fetch_id_token() IAP の OAuth クライアント ID 401 同上
5 generateIdToken API IAP の OAuth クライアント ID 401 同上
6 OIDC トークンを Proxy-Authorization ヘッダーで送信 同上 401 empty token

IAP の OAuth クライアント ID は、未認証アクセス時の OAuth リダイレクト URL(client_id=...)から取得しました。通常の IAP(ロードバランサー型)のプログラマティック認証では「OIDC トークンの aud に IAP クライアント ID を指定する」が定石らしいですが通らなかったようです。

この時点での(誤った)結論は「Cloud Run ネイティブ IAP は service-to-service 認証に非対応」。一度はアーキテクチャを変更して IAP を迂回しました(エージェントは Firestore に直接書き込み、Web アプリは読むだけ)。

根本原因

2026/06/10、Anthropicは新しいモデルであるClaude Fable 5をリリースしました。
性能調査も兼ねて当時残しておいたドキュメントをコンテキストとして与えて再調査を実行したところ、あっと言う間に解決しました。
以下はFable 5から提示された2つの事実です。

原因①: Google マネージド OAuth クライアントはプログラマティックアクセス不可(仕様)

ネイティブ IAP を --iap で有効化すると、OAuth クライアントは Google マネージドクライアントが自動で使われます。そして公式ドキュメントに、はっきりこう書いてあります。

“If you configured your application to use the Google-managed OAuth 2.0 client (the default when IAP is enabled without a custom OAuth 2.0 client), you cannot use programmatic access.”
Programmatic authentication | IAP

つまり、リダイレクト URL から見つけたクライアント ID を aud にした OIDC トークン(表の 2〜6)は、audience の値が合っているかどうか以前に、方式そのものが対応外だったのです。Invalid JWT audience というエラーメッセージは「audience の値が違う」ではなく「その audience は許可リストにない(そして Google マネージドクライアントは永遠に許可されない)」という意味でした。

原因②: 自己署名 JWT の aud はリクエストパスまで含めて一致が必要

実は表の 1(自己署名 JWT)だけはエラーメッセージが違っており、これが唯一の正解ルートでした。失敗した原因は aud の形式です。

“For the aud field, specify the exact URL or the URL with a path wildcard (/*) of the IAP-secured resource — for example, https://example.com/` orhttps://example.com/*`.”
Programmatic authentication | IAP

aud = https://xxx.run.app`(ルート)でPOST /api/recordsを呼んでいたため、「audience がリクエスト先エンドポイントと一致しない」と弾かれていました。エラーメッセージAudience specified does not match requested endpoint` は、読み返せばそのまんまのことを言っていたわけです。

なお、パスワイルドカード(/*)対応は 2025-10-28 に GA した比較的新しい機能です(IAP release notes)。

解決策(実機検証済み)

必要な IAM 設定

# (1) 呼び出し元 SA に、IAP リソースへのアクセス権を付与
#     (これがないと、有効なトークンでも 403 になる)
gcloud iap web add-iam-policy-binding \
  --member=serviceAccount:my-agent@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/iap.httpsResourceAccessor \
  --resource-type=cloud-run \
  --region=asia-northeast1 \
  --service=my-webapp \
  --project=PROJECT_ID

# (2) 呼び出し元 SA が「自分自身の鍵で signJwt を呼べる」ように付与
#     (metadata server は自己署名 JWT を発行できないため、IAM Credentials API を使う)
gcloud iam service-accounts add-iam-policy-binding \
  my-agent@PROJECT_ID.iam.gserviceaccount.com \
  --member=serviceAccount:my-agent@PROJECT_ID.iam.gserviceaccount.com \
  --role=roles/iam.serviceAccountTokenCreator \
  --project=PROJECT_ID

ポイント 2 つ:

  • gcloud iap web add-iam-policy-binding--resource-type=cloud-run を正式サポートしています(--region 必須)
  • (2) は「SA が自分自身に serviceAccountTokenCreator」という一見不思議なバインディングですが、signJwtiam.serviceAccounts.signJwt 権限を要求するため必要です。SA キー(JSON ファイル)でローカル署名する場合は不要ですが、キーレス運用なら必須です

トークン取得の実装(Python)

import json
import time

from google.cloud import iam_credentials_v1

SA_EMAIL = "my-agent@PROJECT_ID.iam.gserviceaccount.com"
WEBAPP_URL = "https://my-webapp-xxxx.asia-northeast1.run.app"


def get_iap_token() -> str:
    """IAP 保護された Cloud Run 用の自己署名 JWT を取得する。"""
    client = iam_credentials_v1.IAMCredentialsClient()  # ADC で動く
    now = int(time.time())
    claims = {
        "iss": SA_EMAIL,
        "sub": SA_EMAIL,
        "aud": f"{WEBAPP_URL}/*",  # ← パスワイルドカードが最重要ポイント
        "iat": now,
        "exp": now + 3600,
    }
    resp = client.sign_jwt(
        name=client.service_account_path("-", SA_EMAIL),
        payload=json.dumps(claims),
    )
    return resp.signed_jwt

呼び出し側はこのトークンを Authorization: Bearer に載せるだけです。

import requests

token = get_iap_token()
resp = requests.post(
    f"{WEBAPP_URL}/api/records",
    headers={"Authorization": f"Bearer {token}"},
    json={...},
)

通常の Cloud Run service-to-service 認証(google.oauth2.id_token.fetch_id_token() や metadata server の identity エンドポイント)と違い、Google 発行の OIDC トークンではなく「SA の鍵で署名した素の JWT」を送るのがミソです。IAP はこの JWT の署名を IAM 経由で検証します。

補足(ローカル検証時): IAMCredentialsClient はデフォルトで gRPC を使うため、HTTP/2 の ALPN を中継できない社内プロキシ配下では 503 Cannot check peer: missing selected ALPN property で失敗します。その場合は IAMCredentialsClient(transport="rest") を指定してください(Agent Runtime 等の GCP 上では gRPC のままで問題ありません)。

検証結果

aud の形式だけを変えて同一エンドポイント(GET /api/records)を叩いた結果がこちらです。

aud HTTP 応答元
https://xxx.run.app`(ルートのみ) | 401Audience specified does not match requested endpoint` IAP
https://xxx.run.app/*` | 401{“detail”:”Not authenticated”}` アプリ(IAP 通過!)
https://xxx.run.app/api/records`(完全一致) | 401{“detail”:”Not authenticated”}` アプリ(IAP 通過!)
`https://xxx.run.app/*` + アプリの API キー 200 アプリ

2〜3 行目の 401 は IAP ではなく FastAPI アプリ側の認証層(API キー必須)が返した JSON です。

その後、Agent Runtime 上で動く ADK エージェント本体にもこの方式を組み込み、エージェントの処理フロー(Drive アップロード → POST /api/records → Slack 通知)がエンドツーエンドで 201 を返して完走することを確認しています。コード変更は前述のトークン取得部分の差し替えだけで、IAM はキーレス運用のための SA 自身への serviceAccountTokenCreator(付与済み)以外に追加はありませんでした。

ちなみにこの検証で 1 回だけ 403 が返りましたが、ボディは {"detail":"Invalid API Key"}——つまり IAP は通過しており、アプリに渡す API キーの設定ミスでした。エラーの応答元が IAP かアプリかをボディで見分けるのは、この構成のデバッグで一貫して有効です。

代替案: カスタム OAuth クライアント + 許可リスト

「どうしても OIDC トークン(fetch_id_token など既存コード)を使い続けたい」場合は、こちらの方式もあるようです。

  1. カスタム OAuth クライアントを作成し、IAP に設定する
  2. そのクライアント ID をプログラマティックアクセスの許可リストに登録する
cat > settings.yaml <<EOF
access_settings:
  oauth_settings:
    programmatic_clients: ["CUSTOM_CLIENT_ID"]
EOF

gcloud iap settings set settings.yaml \
  --project=PROJECT_ID \
  --resource-type=cloud-run \
  --region=asia-northeast1 \
  --service=my-webapp
  1. aud=CUSTOM_CLIENT_ID の OIDC トークンを取得して送る

ただし OAuth クライアントの作成・管理が増えるので、特別な理由がなければ自己署名 JWT 方式(OAuth クライアント不要)が推奨になるでしょう。

ハマりどころまとめ

  1. aud はパスまで見られる。ルート URL だけ指定すると does not match requested endpoint/* を付ける(または完全一致 URL にする)
  2. Google マネージドクライアントの ID を aud にしても永遠に通らない。OAuth リダイレクト URL から client_id を発見して使いたくなるが、罠
  3. roles/run.invoker + aud=サービスURL の通常 Cloud Run 認証は IAP 有効時には通らない。IAP は IAM チェックより先にリクエストをインターセプトする(Known limitations)。「特定の SA だけ IAP をバイパス」する仕組みもない
  4. 401 と 403 の切り分け: 401 Invalid IAP credentials はトークン自体の拒否(認証段階)。iap.httpsResourceAccessor 不足は有効トークン到達後の 403。ずっと 401 が返るなら、IAM ではなくトークンの方式・aud を疑う
  5. signJwt をキーレスで呼ぶには SA 自身への serviceAccountTokenCreator が必要。metadata server では自己署名 JWT は作れない

まとめ

  • Cloud Run ネイティブ IAP への service-to-service 認証は「非対応」ではなく、「従来の OIDC トークン方式が(デフォルト構成では)対応外」なだけだった
  • 正解は自己署名 JWT + aud にパスワイルドカード。コード変更はトークン取得部分の差し替えのみ
  • エラーメッセージ Audience specified does not match requested endpointInvalid JWT audience は別物。前者が出たら方式は合っていて aud の形式だけが違う、というところまで来ている
  • Fable 5すごかった

同じ構成(Agent Runtime / Cloud Run / Cloud Functions などから IAP 保護 Cloud Run を呼ぶ)でハマっている方の助けになれば幸いです。

参考リンク