Claude apps gatewayのデプロイと運用 — IdP登録からアップグレードまで
IdPへのOAuthクライアント登録からコンテナイメージのビルド、Kubernetes/Cloud Runへのデプロイ、シークレットローテーションとアップグレードまでの運用手順をまとめます。
本番に出す前に決めることと、手元でできる確認
基本の使い方ではDocker Composeで最小構成を動かしました。本番ではIdP登録、コンテナイメージ、Kubernetes/Cloud Run、ローテーション、アップグレードの順に進みます。最初の2段階は選択が必要で、動き出してからは障害時の挙動とローテーションの手順が効いてきます。
本番投入の流れ
- 1
IdPにOAuthクライアントを登録
リダイレクトURIは
/oauth/callbackの1つだけです。 - 2
固定バージョンのイメージをビルド
ネイティブの
claudeバイナリを署名検証して組み込みます。 - 3
Kubernetes/Cloud Runで稼働
TLSはIngressか内部ロードバランサーで終端します。
- 4
開発者マシンへゲートウェイURLを配信
managed settingsを入れるまで、
/loginにゲートウェイの選択肢は出ません。 - 5
運用に入る
ログ、ヘルスチェック、障害時の挙動、ローテーション、アップグレードを回します。
設定ミスは、デプロイする前に手元のclaudeだけで見つけられます。claude gatewayはv2.1.287の時点でオプションを--config1つしか持たないサブコマンドです。
$ claude gateway --help
Usage: claude gateway [options]
Run the enterprise auth/telemetry gateway
Options:
--config <path> Path to gateway YAML config
-h, --help Display help for command
$ claude gateway
error: required option '--config <path>' not specified
$ claude gateway --config ./nope.yaml
claude gateway: ENOENT: no such file or directory, open './nope.yaml'起動前の設定検証は、エラーを1件ずつではなくまとめて返します。次はlisten.portと存在しないキーbogus_keyだけを書いたYAMLを渡した出力です(v2.1.287で実行、配列の途中と、改行・一部フィールドは省略)。
$ claude gateway --config ./bad.yaml
claude gateway: [
{
"code": "custom",
"path": ["listen", "public_url"],
"message": "listen.public_url is required when listen.host is not a loopback address — set it to the externally-visible origin (e.g. https://claude-gateway.corp.example.com). Without it the IdP redirect_uri and token issuer would be derived from the client-controlled Host header."
},
{ "code": "invalid_type", "path": ["upstreams"], "message": "Required", ... },
{ "code": "invalid_type", "path": ["session"], "message": "Required", ... },
{ "code": "invalid_type", "path": ["store"], "message": "Required", ... },
{
"code": "unrecognized_keys",
"keys": ["bogus_key"],
"path": [],
"message": "Unrecognized key(s) in object: 'bogus_key'"
}
]この出力から3点が分かります。upstreams・session・storeは必須で、Postgresを指すstoreが無いと起動しません。未知のキーは無視されずエラーになります。そしてpublic_urlは、ループバック以外でリッスンするなら必須です。HostヘッダーをそのままIdPのリダイレクト先や発行者に使わないための制約です。
IdP登録でクレームが食い違う箇所
リダイレクトURIをhttps://<ゲートウェイのホスト>/oauth/callbackとする単一のOAuth/OIDC Webアプリケーションを登録し、ゲートウェイを使わせたいユーザーまたはグループに割り当てます。IdPは次の3条件を満たす必要があります。
/.well-known/openid-configurationをHTTPSで配信していること(本番。ループバックの発行者はさらにCLAUDE_GATEWAY_ALLOW_LOOPBACK=1が必要)- 認可コードフローに対応していること(PKCEは既定で有効。対応しないIdPでは
oidc.use_pkce: false) - id_tokenまたはuserinfoエンドポイントで
emailと、必要ならgroupsを返せること
3条件を満たしていても、Okta・Microsoft Entra ID・Google Workspaceではクレームの出し方が違います。
Oktaの組織認可サーバー(https://example.okta.com)はid_tokenが薄く、userinfo_fallback: trueが必須です。カスタム認可サーバーなら不要です。ただしOktaはgroupsスコープをoidc.scopesで要求し、アプリのグループクレームフィルターも通した場合にしかgroupsを出しません。userinfo_fallbackは、IdPに要求していないクレームを補えません。
EntraはグループをオブジェクトIDで返すため、managed.policies.match.groupsにはGUIDを書くか、人が読める名前にはApp Rolesを使います。テナントがgroupsでなくrolesで出す場合はoidc.groups_claim: rolesを指定します。
Googleのid_tokenにはグループクレームが無く、oidc.google_groupsでAdmin SDK Directory APIから取得します。使わないならallowed_email_domainsとメールドメインベースのポリシーで代替します。Googleはoffline_accessスコープも無視するので、リフレッシュトークンにはscopes: [openid, profile, email]とextra_auth_paramsを指定します。
# Okta(組織認可サーバー)
oidc:
issuer: https://example.okta.com
client_id: 0oa1example2
client_secret: ${OIDC_CLIENT_SECRET}
allowed_email_domains:
- example.com
userinfo_fallback: true # id_tokenがemail/groupsを落とすため/userinfoで補う# Google Workspace
oidc:
issuer: https://accounts.google.com
client_id: ${OIDC_CLIENT_ID}
client_secret: ${OIDC_CLIENT_SECRET}
scopes: [openid, profile, email] # offline_accessは無視されるため外す
extra_auth_params:
access_type: offline # リフレッシュトークンはこちらで要求する
prompt: consentリフレッシュトークンはサイレントなセッション更新とデプロビジョニングの両方を支えています。IdPでユーザーを無効化すると次回のリフレッシュが失敗し、session.ttl_hours以内にセッションが終了します。IdPがリフレッシュトークンを発行できない場合、セッションが切れるたびに開発者はブラウザでのログインをやり直します。ttl_hoursを8〜12に伸ばせば頻度は下がりますが、無効化したユーザーが残る時間も同じだけ延びます。
デプロイ方式の4つの論点
ゲートウェイはPostgresで状態を共有するステートレスなLinuxバイナリです。組織が他のステートレスサービスをデプロイする方法にそのまま乗せられます。決めるべき点は4つです。
| 論点 | 内容 |
|---|---|
| コスト | 内容別ライセンスやseat課金は無し。既存の推論契約分の費用と、実行に使うコンピュートのみ |
| バイパス | 内容ゲートウェイはモデルへの唯一の経路であることを強制しない。独自クレデンシャルを持つ開発者は直接プロバイダーを呼べるため、塞ぐにはネットワークポリシーでapi.anthropic.com等へのegressを制限する。これは各開発者マシンのWebFetchドメイン安全性チェックも壊すので、managed policyでskipWebFetchPreflight: trueを設定する |
| 複数ゲートウェイ | 内容それぞれ独立したデプロイで、CLIはゲートウェイのホスト名ごとに信頼とクレデンシャルを保持する。複数のOIDC発行者に対応するには別インスタンスを立てる |
| サーバーレス | 内容Cloud Runはmin-instances: 1を設定すればコールドスタートでのOIDCディスカバリー遅延を避けられる。Lambda・Cloud Functionsは長時間稼働のHTTPサーバーが必要なため使えない |
本番のトポロジーでは、Ingress・Cloud Runのフロントエンド・ALBのようなL7プロキシが、平文HTTPのレプリカの前段に立ちます。listen.trusted_proxiesにプロキシの送信元レンジを設定すると、X-Forwarded-ForからクライアントIPを読み取れます。設定しなければ全リクエストがプロキシのIPから来たように見え、IP単位のレート制限が1つのバケットに集約され、監査イベントにもプロキシのIPが残ります。
プロキシのアイドルタイムアウトは、ゲートウェイのキープアライブ間隔より長く設定します。Anthropic API以外のupstreamでは、ストリームが約15秒黙るとSSEのpingが書かれます。ALBの既定60秒でも、静かなストリームは切れません。
デバイス認可とトークンのエンドポイントには、HTTPからHTTPSやホスト名正規化のリダイレクトを掛けないでください。Claude Codeはこの2つのリクエストでリダイレクトを追わないため、Ingressの書き換えルールがサインインとトークン更新の両方を壊します。
コンテナイメージ
標準のClaude Codeリリースからネイティブのclaudeバイナリを組み込んだイメージを自分でビルドします。固定したリリースからイメージアーキテクチャに合うLinuxビルドをダウンロードし、GPG署名済みのmanifest.jsonで検証してからビルドコンテキストに組み込みます。ビルドが公開のリリースホストへ届かない環境では、内部レジストリにミラーしてフリート全体のバージョンを固定します。
イメージにはバイナリ以外に3点が必要です。
- glibcベースのイメージ(muslベースなら
linux-x64-musl等の追加ビルドとパッケージが必要) - 書き込み可能な状態ディレクトリ(最小イメージにはホームが無いため、
CLAUDE_CONFIG_DIRを/tmp/.claudeのような書き込めるパスに設定) - コンテナのコマンドを
claude gateway --config /etc/claude/gateway.yamlとし、設定ファイルは読み取り専用でマウント、シークレットは環境変数で渡す
ゲートウェイはlisten.port(既定8080)でリッスンします。
Kubernetes
他のステートレスサービスと同様にDeploymentとして稼働させます。設定はConfigMapから、シークレットはSecretからマウントし、YAML内では${file:/path/to/secret}か環境変数で参照します。TLSはIngressで終端し、listen.public_urlをIngressのホスト名に設定します。readinessプローブはGET /readyz、livenessプローブはGET /healthzに向けます。次はPod specの一例で、periodSecondsは例示用の値です。
containers:
- name: claude-apps-gateway
readinessProbe:
httpGet:
path: /readyz
port: 8080
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10クラウドのワークロードアイデンティティを静的キーより優先します。クロスクラウドの組み合わせ(GKE上でAmazon Bedrockをupstreamにする等)では、upstreamのauthブロックに明示的なクレデンシャルを設定します。
Cloud Run
listen.portは既定の8080のままか、port: ${PORT}に設定します。public_urlには外部から到達可能なオリジンを設定します。ただし/loginは公開アドレスを拒否し、*.run.appのURLは公開アドレスに解決されるため、そのURLはcurlやブラウザでのスモークテストにしか使えません。
例外は、Private Service ConnectとCloud DNSのプライベートゾーンで*.run.appがプライベートに解決される構成です。この構成なら、Cloud RunのURLをそのままpublic_urlにできます。それ以外の本番では、内部ロードバランサーのホスト名をpublic_urlに使います。設定はシークレットボリュームとしてマウントし、初回リクエストでのOIDCディスカバリー遅延を避けるためmin-instances: 1を設定します。
ゲートウェイURLを開発者マシンへ配信する
ゲートウェイが稼働したら、forceLoginMethod・forceLoginGatewayUrl・parentSettingsBehavior: "merge"をMDMまたはOS別のmanaged-settings.json経由で各マシンへ配信します。これが無いと、/loginは標準のアカウント選択画面を出し、ゲートウェイの選択肢は出ません。
配信を始めると、Claude Codeはそのマシンに残っていたAPIキーやclaude.aiのログインを使わなくなります。サインイン手順の案内は配信と同時に出す必要があります。キーの詳細はmanaged settingsガイドにあります。既存のBedrock構成からの切り替えを考えているなら、/setup-bedrockウィザードとの使い分けを先に決めておくと、移行の手戻りが減ります。
ログとヘルスチェック
ゲートウェイはstderrに2種類のログを書き出します。1つは監査イベントで、session.mint・device.authorize・inference・admin.limit.upsertのようなセキュリティ上のイベントごとに1行のJSONを出します。もう1つは[gateway]接頭辞の運用ログで、起動・警告・upstreamエラーが人間向けに読める形で出ます。
運用ログの詳細度はCLAUDE_GATEWAY_LOG_LEVEL(debug/info/warn/error、既定info)で変えられます。debugではid_tokenのクレーム名(値ではなく名前のみ)も出るので、PIIを出さずにgroups_claimやemail_claimの設定を診断できます。監査イベントはログレベルの影響を受けず、常に出力されます。
GET /healthzはliveness、GET /readyzはストアの到達性まで見るreadinessです。どちらもaccess_control.allow_cidrsの対象外なので、アクセスを絞ったリスナーでもプローブは動きます。起動が全部通ったかを見るには/.well-known/oauth-authorization-serverが使えます。設定の読み込み、OIDCディスカバリー、upstreamクライアントの構築、Postgresのマイグレーションがすべて成功した後にだけ200を返します。
Postgresが落ちたとき、IdPが落ちたとき
Postgresが落ちてもIdPが落ちても、ゲートウェイのプロセスは動き続けます。ただし効き方は、次の4点で違います。
Postgres停止中の挙動
既存セッション
ベアラートークンはJWTシークレットでローカルに検証されます。セッションの更新もストアに触れないため、推論は通ります。
新規サインイン
デバイスフローとレート制限のカウンターがPostgresにあるため、復旧まで失敗します。
支出上限の強制
既定でフェイルオープンです。推論は流れ続け、
enforcement.fail_closed_on_error: trueにすれば止める側に倒せます。IdP停止
既存セッションは
ttl_hoursまで動きます。新規ログインとリフレッシュは失敗し、リフレッシュはIdP復旧後に成功します。
分かれ道になるのがreadinessです。何を見てトラフィックを制御するかで、同じ障害の見え方が変わります。
readinessプローブの向け先
/readyz(既定)
全レプリカが同時にreadinessを落とします。readinessでトラフィックを制御する環境では、ゲートウェイが処理できたはずの推論もロードバランサー側で失敗します。
/healthz
障害中も全レプリカが通過します。ただし/healthzはnot-readyを返さないため、Postgres接続が戻らないレプリカも通過し続けます。
/readyz + 猶予
ストアが応答しなくなってからも、設定した秒数だけreadyを返します。データベースのフェイルオーバー程度なら、サインイン済みの開発者を止めずに済みます。
猶予の設定は、フェイルオーバーにかかる時間より長く、たとえば300にします。指定できるのは0から3600の整数で、既定は0(猶予なし)です。支出上限をフェイルオープンで使っている場合、レプリカがreadyの間の推論は、Postgresの復旧まで計量されません。値は、フェイルオーバーを覆える範囲でできるだけ小さくします。
この設定はゲートウェイ側がv2.1.282以降であることが条件で、それより古いゲートウェイはキーを見つけると起動を拒否します。追加の前に全レプリカを上げてください。
IdPのメンテナンス窓が多い環境では、ttl_hoursを長くすると既存セッションが保たれる時間が延びます。
ピーク時に応答が止まって見えるとき
各レプリカがupstreamへ同時に送るリクエストは、既定で最大256です。ストリーミング応答は、ストリームが終わるまで枠を使い続けます。枠が埋まると、新しいリクエストはゲートウェイの中で空きを待ち、開発者には「応答が始まらない」ように見えます。provider: anthropicのupstreamでは、timeouts.upstream_ttfb_msを超えて待つとそのupstreamを諦め、後続のupstreamが無ければ502になります。
枠が埋まる速さは、リクエスト数ではなく滞留時間で決まります。リクエストが平均10秒開いたままなら、256を埋めるのは毎秒約26リクエストです(256÷10)。ログには起動時のupstream requests:を含む行と、枠を超えたときのclient requests are openを含む警告(最大で1分に1回)が出ます。
対処はレプリカを増やすか、コンテナの環境変数BUN_CONFIG_MAX_HTTP_REQUESTSで枠を広げる(1〜65535の整数、設定後に再起動)かの2通りです。CPU使用率でオートスケールしている場合、枠が埋まったレプリカはCPUが上がらないまま待たせるので、警告が出たときのCPU水準より低い値をターゲットにします。待っているリクエストもボディをメモリに保持するため、枠を広げたときはメモリも見てください。メモリ不足で落ちたレプリカは、持っていた全ストリームを失います。
シークレットとTLS証明書のローテーション
署名鍵のローテーションは、既存セッションを無効にしない3ステップです。
JWTシークレットの入れ替え
- 1
新しいシークレットを先頭に追加
新しいシークレットを生成し、
session.jwt_secret配列の先頭に置きます。 - 2
デプロイをロール
新しいトークンは新シークレットで署名され、古いトークンも検証され続けます。
- 3
古いシークレットを外して再度ロール
ttl_hoursに余裕を足した時間が経ってから外します。
古いシークレットを配列に残さず一気に置き換えると、その時点で稼働中の全セッションが無効になります。逆に言えば、期限前にセッションを追い出す手段は、このローテーションしかありません。ベアラートークンはローカルで検証されるため、セッション単位の失効はありません。個別のオフボーディングは、IdP側の無効化に任せます。置き換え後に開発者の画面へ出る文言は、ログインエラー3種の記事で原因別に扱っています。
TLS証明書のローテーションは、開発者への影響が別種です。CLIはホスト名ごとにリーフ証明書のフィンガープリントを初回接続でピン留めし、サインイン、サイレントなセッション更新、managed settingsの取得のたびに照合します。推論リクエストは通常のTLS検証で、ピンは使いません。HTTPSプロキシ経由のリクエストはピンの照合を飛ばすため、ゲートウェイのホストをNO_PROXYに足して直接接続にします。証明書を更新すると、全開発者に信頼プロンプトが再表示されます。
プロンプトには、フィンガープリントの先頭16文字が小文字16進数でコロン無しに出ます。更新の前に、期待するフィンガープリントを開発者へ周知する運用が現実的です。
openssl x509 -noout -fingerprint -sha256 -in cert.pem | cut -d= -f2 | tr -d : | tr 'A-F' 'a-f'ゲートウェイのポリシーに承認が要る設定が含まれる場合は、新しい証明書の承認後に承認ダイアログも再表示されます。承認の記憶がピン留めした証明書に紐づくためです。ピン留めの不一致で/loginが止まるのは、レプリカ間で証明書が違うか、通信経路でTLSが傍受されている場合です。
Postgresとアップグレード
ゲートウェイは、次の5テーブルと_migrationsテーブルを保持します。
| テーブル | 中身 | 保持期間 |
|---|---|---|
kv | 中身デバイス許可(10分TTL)とレート制限カウンター | 保持期間行ごとのTTL |
spend | 中身主体ごとの期間累計の支出(セント単位) | 保持期間admin.spend_retention_months(既定13) |
spend_limits | 中身設定済みの支出上限 | 保持期間APIで削除するまで |
admin_audit | 中身管理APIの変更履歴 | 保持期間admin.audit_retention_days(既定365) |
principal_emails | 中身最後に見たメール・表示名・IdPグループ(PIIを含む) | 保持期間admin.identity_retention_days(既定90、最終活動から) |
30秒ごとのループが期限切れのkv行を削除し、1時間ごとのスイープが支出系テーブルの保持期間を強制するため、テーブルサイズは際限なくは増えません。支出上限を使わない構成では、書き込まれるのはkvだけです。ゲートウェイは起動時に自分でスキーマを作成・更新するので、DBロールにはテーブルの作成・変更権限が要ります。ゲートウェイ専用のデータベースかスキーマを割り当てると、その権限を狭くできます。
支出上限を使う構成では、データベースの喪失は再ログインでは済まず、支出の追跡と上限設定そのものの喪失です。定期バックアップが前提になります。退職した開発者を保持期間を待たずに消したいときは、DELETE FROM principal_emails WHERE principal = '<sub>'を直接実行します。メール・名前・グループを持つのはこのテーブルだけで、spendとadmin_auditは仮名化されたOIDCのsubしか参照しません。
レプリカはステートレスなので、ローリング再起動でゲートウェイの状態は失われません。起動時にスキーママイグレーションが走るため、新しいバイナリをデプロイするだけでデータベースも更新されます。複数のレプリカが同時に起動してもPostgresのアドバイザリロックで直列化され、各マイグレーションは1度だけ適用されます。
オーケストレーターがSIGTERMでレプリカを止めると、ゲートウェイは新規接続を受け付けなくなり、処理中のリクエストとストリームが終わるのを待ってから終了します。待つのは最大25秒(ドレイン時間)で、超えた分は閉じます。ドレインはv2.1.274以降の機能です。
数分続く生成のストリームを守りたいときは、次の2つを一緒に上げます。ドレイン時間はCLAUDE_GATEWAY_DRAIN_TIMEOUT_MSにミリ秒の正の整数で指定します。120sのような形式は無視され、25秒の既定のままです。もう1つは、KubernetesのterminationGracePeriodSecondsかECSのstopTimeoutです。
# ドレイン120秒の例(猶予はドレインより5秒以上長く。preStopがあればその時間も足す)
terminationGracePeriodSeconds: 130
containers:
- name: claude-apps-gateway
env:
- name: CLAUDE_GATEWAY_DRAIN_TIMEOUT_MS
value: "120000"両プラットフォームとも猶予の既定は30秒です。Kubernetesでは猶予のカウントがpreStopフックの実行前から始まるため、ゲートウェイがSIGTERMを受け取る前に時間が減ります。プラットフォーム側の上限もあります。FargateのstopTimeoutは最大120秒で、Cloud RunはSIGTERMの10秒後にインスタンスを止めるため、ドレイン時間をいくつにしても、開いているストリームが生きられるのは最大10秒です。ドレインが時間切れで終わると、drain window over afterを含む警告が出て、切ったリクエスト数と上げるべき2つの設定が示されます。
マイグレーションは追記のみです。より少ないマイグレーションしか知らない旧バイナリへロールバックしても、余分な行は無視されます。ただしロールバックの際には、旧バイナリのスキーマでYAMLが再検証されます。新リリースで追加されたキーを設定が使っていると、旧バイナリは起動に失敗するので、ロールバック前にそのキーを設定から外します。冒頭の検証例のbogus_keyと同じく、未知のキーはエラーになるためです。
イメージの中のバージョンは自分で固定しているので、新しいClaude Codeリリースの修正(セキュリティ修正を含む)は、固定の更新と再デプロイをしたときにだけ届きます。本番の資格情報を持つ他のサービスと同じパッチ周期に、ゲートウェイも載せることになります。
大規模展開でサインインが止まるとき
サインインはクライアントIP単位でレート制限され、既定値は小さなチーム向けです。各アドレスには、10分あたりサインイン開始30回とコード送信10回が割り当てられます。数千人規模の展開では、初日の朝に上限へ達することがあります。原因は2つに分かれます。
- ロードバランサーの先が見えない: デプロイ方式の節で触れた
listen.trusted_proxiesが最初の確認項目です。X-Forwarded-Forを初めて無視したとき、ゲートウェイは警告をログに出します - NATやVPNの出口アドレスを共有している:
trusted_proxiesが正しくても、出口アドレスごとの上限を共有します。この場合にrate_limitsを引き上げます
maxの見積もりは、出口アドレス1つあたりの開発者数から始めます。1万人が4つの出口アドレスを共有し、1時間で均等にサインインするなら、1アドレスあたり2,500人、10分あたり約420人です。再試行と、Claude CodeとClaude Desktopの両方にサインインする人を見込んで2倍にし、切り上げて1,000にします。
rate_limits:
device_authorization: { max: 1000, window_seconds: 600 }
device_verify: { max: 1000, window_seconds: 600 }device_verifyは他人のサインインコードの推測を防ぐ側の上限なので、見積もりが要る分だけ上げます。IdPがリフレッシュトークンを出す環境なら、展開後に上限を戻せます。出さない環境では、ttl_hoursごとに全員がサインインし直すため、その定常的な頻度を見込んだ値のままにします。
上限に達すると、v2.1.274以降のClaude CodeはThe gateway is limiting sign-in attempts right nowと表示します。同じバージョン以降のゲートウェイは、検証ページにToo many attempts came from your network addressを出し、ログに設定名を含むsign-in refusedの行を残します。
セキュリティ: データの流れと脅威モデル
設定したupstreamにAnthropic APIが含まれる場合だけ、推論(プロンプト・応答)はAnthropicへ送られます。Bedrock等の他のupstreamでは送られません。テレメトリ・ID情報・managed settings・監査ログはいずれもAnthropicへは送られず、組織が設定した宛先にだけ流れます。ただしこれはゲートウェイのデータプレーンの話です。開発者のマシン側には、別経路のトラフィックが残ります。
CLIはゲートウェイにサインインしている間は利用分析とエラーレポートを止めますが、初回サインイン前は起動イベントをAnthropicへ送ります。止めるには、ゲートウェイ強制のmanaged settingsと同じ配信物にDISABLE_TELEMETRYを入れます。WebFetchのホスト名チェックとバージョンチェックも各マシンからAnthropicへ送られ続け、CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1とmanaged policyのskipWebFetchPreflight: trueで初めて止まります。
脅威モデルは、ゲートウェイがネットワーク境界の内側にあり、開発者の個々のラップトップは信頼しないという前提です。開発者は生の上流キーではなく短命なJWTを持ち、CLIとゲートウェイ間はRFC 8628のデバイスグラントを使います。ゲートウェイとIdPの認可コード交換は既定でPKCEなので、傍受された認可コードは単体では使えません。
/deviceページで入力するuser_codeは、20文字のアルファベットから8文字を選ぶため、20の8乗で約256億通りあり、10分で失効します。デバイス認可のエンドポイントにはIP単位のレート制限も掛かり、総当たりへの耐性はこの2つで成り立ちます。
SSRF(サーバーサイドリクエストフォージェリ)ガードは、IdP・OTLP宛先・provider: anthropicのupstreamへの外向きリクエストに掛かります。DNS解決、リンクローカルとクラウドメタデータアドレスとループバックのブロック、解決済みIPへの接続ピン留めを行い、運用者が指定するURLをメタデータエンドポイントへ向けさせない設計です。RFC 1918のプライベートレンジは、IdPやOTLPコレクターが内部IPにあることが多いため、許可されています。他のプロバイダーでは、base_urlがこれらのアドレスやメタデータのホスト名を指す設定を読み込み時に拒否し、その先の接続はプロバイダーのSDKが担います。
ゲートウェイホスト自体の侵害と、悪意あるOIDCプロバイダーの2つは、組織自身のインフラとIdPのセキュリティの責任範囲で、脅威モデルの対象外です。ゲートウェイのホストは上流のクレデンシャルを持ち、managed settingsを全接続先へ配るため、その設定への支配はMDMへの支配に近い位置づけです。この脅威モデルが何から開発者を守り、何を運用側の責任として切り離しているかを、コンプライアンス質問票の観点でClaude apps gatewayの脅威モデルとコンプライアンス対応にまとめています。
エラーメッセージから原因を引く
Claude Codeやゲートウェイが実際に出す文言から、原因と直し方を引ける形にしました。
| 見えるもの | 原因 | 直し方 |
|---|---|---|
| ALBの背後でログインが失敗する | 原因public_urlがローカルか内部のhttp://のままで、IdPに誤ったredirect_uriが渡る | 直し方listen.public_urlを外部のhttps://にし、<public_url>/oauth/callbackをIdPに登録する |
Bedrockが全リクエスト502、ログにCould not load credentials from any providers | 原因EC2のIMDSv2既定のホップ制限1が、コンテナ内からのメタデータ要求を塞ぐ。AWS SDKが最初のリクエストでクレデンシャルを解決するため、起動と/readyzは通る | 直し方aws ec2 modify-instance-metadata-options --instance-id <id> --http-put-response-hop-limit 2で上げる。ただしインスタンス上の全コンテナに効くので、ECSタスクロールが使えるならそちらを優先する |
/login: Gateway hosts must be on your organization's private network; <host> resolves to the public (or unrecognized) address <ip> | 原因ゲートウェイ名のレコードの1つが公開アドレスに解決される。AWSの内部デュアルスタックLBが返す公開レンジのAAAAが典型 | 直し方開発者マシンでは、プライベートアドレスだけに解決されるようにする。公開レンジのレコードを外すか、内部専用のDNS名を別に出す |
/login: Gateway login would go through proxy <proxy>, which is not on a private network | 原因HTTPS_PROXYかHTTP_PROXYがゲートウェイに適用され、そのプロキシ名が公開アドレスに解決される | 直し方ゲートウェイのホストをNO_PROXYに足して直接接続にする。足すべき値はメッセージに出る |
起動時にAdministrator policy requires a Cloud gateway sign-in on this machine | 原因ANTHROPIC_API_KEY・ANTHROPIC_AUTH_TOKEN・apiKeyHelper、以前のConsoleログインの保存キーのいずれかが残っている | 直し方該当するものを外す(変数のunset、apiKeyHelperの削除、claude auth logout)。その後/loginでゲートウェイにサインインする |
起動時か/loginでClaude Code may not be enabled for your organization(403) | 原因ゲートウェイの設定取得ルートは403を返さない。access_controlのIPチェックか、前段のプロキシやWAFが返している | 直し方監査ログでaccess.deniedを探し、access_controlの一覧か前段を直す |
refresh failed ... invalid_token (… at userinfo_no_id_token …)が出て、ttl_hoursごとにCloud gateway session expired | 原因リフレッシュ応答にid_tokenが無く、userinfoでも更新後のアクセストークンが拒否される | 直し方v2.1.260以降のゲートウェイでoidc.scope_on_refresh: trueを設定する |
| 信頼プロンプトが繰り返し出る | 原因レプリカごと、またはリクエストごとにTLS証明書が変わっている | 直し方Ingressで安定した証明書を使うか、TLSを1か所で終端し、レプリカ間は平文HTTPにする |
ChromeでApproveボタンがform-actionで拒否される(SafariやFirefoxでは動く) | 原因IdPが別ホストへ転送し、Chromeはリダイレクト連鎖の全体にform-actionを適用する | 直し方連鎖に含まれる各オリジンをoidc.form_action_originsに足す。ブロックされたオリジンはDevToolsのConsoleに出る |
まとめ
本番化できるかどうかは、claude gateway --configの検証が通るかと、Postgres障害時にreadinessでレプリカを外すかどうかの2点でほぼ決まります。詳細な設定項目は設定リファレンスにあります。