Claude Media
Claude apps gatewayの監査ログを読む — public_clientなど主要イベント

Claude apps gatewayの監査ログを読む — public_clientなど主要イベント

Claude apps gatewayが出す監査イベントの種類と、client_ipを持つ項目、access.public_clientやxff_unparseableの読み方、admin.deniedの3つの理由をまとめます。

Claude apps gatewayは、セキュリティに関わる出来事を1行1イベントのJSONでstderrに出します。ログ基盤に流しておけば、誰がいつサインインし、どのアドレスから何が拒否されたかを後から追えます。ただしイベントの種類が多く、項目の持ち方もイベントごとに違います。ここでは、運用で目にする頻度が高いものから順に意味を読み解きます。

監査イベントとオペレーションログは別の流れ

ゲートウェイはstderrに2種類の出力を書きます。

種類形用途
監査イベント形1行JSON用途セキュリティ上の出来事の記録。ログ基盤へ送る
オペレーションログ形[gateway]で始まる人間向けの行用途起動、警告、上流エラー

監査イベントとして出るイベント名は次の17種です。

config.load / session.mint / session.refresh / device.authorize / device.verify / device.callback / auth.denied / access.denied / access.public_client / inference / managed.serve / desktop_bootstrap.serve / desktop_bootstrap.denied / spend.blocked / admin.denied / admin.limit.upsert / admin.limit.delete

絞り込みは、イベント名の文字列で足ります。

# 例: Kubernetesで動かしている前提で、公開アドレスからの到達だけを抜く
kubectl logs deploy/claude-gateway | grep '"access.public_client"'

オペレーションログの詳しさは環境変数CLAUDE_GATEWAY_LOG_LEVELで変えます。値はdebug・info・warn・errorで、既定はinfoです。この変数は監査イベントに影響しません。監査イベントは常に出力されます。ログレベルをerrorに下げても、監査の記録は欠けないということです。

debugにすると、サインインと更新のたびに、id_tokenに含まれるクレーム名が記録されます。値ではなく名前だけです。userinfoフォールバックが何かを返したときは、そのクレーム名も出ます。email_claimやgroups_claimの設定がずれているときに、個人情報をログに残さず原因を探せます。

誰がサインインしたかを追う: session.mintとsession.refresh

認証が成功して発行(mint)と更新(refresh)が起きると、イベントにsub、email、client_ip、結果が載ります。ユーザーを特定できるのは、この系統のイベントです。

一方、auth.deniedとaccess.deniedには、まだユーザーの身元がありません。載るのは拒否の理由とクライアントIPで、auth.deniedにはさらにリクエストパスが付きます。「誰が拒否されたか」はこのログから分からず、「どのアドレスから、どのパスで」を手がかりにします。拒否の画面側の読み方はgatewayログインエラー3種の記事にあります。

access.deniedの2つの理由で持つ項目が変わる

access.deniedはaccess_controlに弾かれたときのイベントです。理由のうち2つは、載る項目が通常と違います。

理由追加で載るもの載らないもの
xff_unparseable追加で載るもの読めなかったX-Forwarded-Forの値載らないものなし
client_ip_unknown追加で載るものなし載らないものクライアントIP

xff_unparseableは、信頼したプロキシが送ったX-Forwarded-Forの項目がIPアドレスとして読めなかったときの理由です。本当のクライアントが分からないので、許可リストか拒否リストが適用される場面では403で拒否されます。どちらも適用されない場面では、リクエストは通り、プロキシ自身のアドレスがクライアントIPとして扱われます。イベントに読めなかった値そのものが載るので、前段のロードバランサーが何を書いているかを確かめる材料になります。

client_ip_unknownは、接続にピアアドレスが無いのにaccess_controlのリストが設定されている場合の理由です。クライアントIPが分からないため、イベントにもIPがありません。IP欄が空のaccess.deniedを見たら、この理由を疑います。

access.public_clientは「公開側から届いた」という合図

access.public_clientは拒否ではなく、警告に近いイベントです。access_control.allow_cidrsが空の間に、公開アドレスから最初のリクエストが届いたとき、プロセスごとに1回だけ出ます。ゲートウェイはそのリクエストを通常どおり処理します。イベントが伝えるのは、ゲートウェイがインターネットから到達できる状態かもしれない、という点です。

公開とみなされない範囲は、プライベートレンジとリンクローカルです。起動時のオペレーションログには、許可リストを次のレンジに絞る推奨が出ます。

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 100.64.0.0/10
  • 127.0.0.0/8
  • ::1/128
  • fc00::/7

加えて、開発者が接続してくる社内レンジがあれば足します。リンクローカルの169.254.0.0/16とfe80::/10は公開に数えません。/healthzと/readyzは、この判定の前に応答されます。公開レンジのヘルスチェックが、このイベントを起こすことはありません。

見落としやすい穴が1つあります。ロードバランサー、ポートフォワード、トンネルが中継していて、それがlisten.trusted_proxiesに入っていないと、ゲートウェイには中継機器のアドレスしか見えません。たいていはプライベートアドレスなので、access.public_clientも、プライベートレンジだけの許可リストも、中継経由の通信を捉えられません。イベントが出ていないことは、公開されていない証拠にならないわけです。まずtrusted_proxiesを設定し、そのうえで、ゲートウェイとその前段をインターネットから到達できない状態に保ちます。許可リストを足すだけでは足りず、順序が逆だと、中継経由の通信は許可リストの検査をすり抜けたままになります。

admin.deniedの3つの理由

管理APIの認証に失敗するとadmin.deniedが出ます。載るのはクライアントIP、メソッド、パス、理由です。提示された鍵の中身は記録されません。

理由どういうときか
invalid_keyどういうときかx-api-keyを渡したが、設定済みの鍵のどれにも一致しなかった
bearer_rejectedどういうときかAuthorizationヘッダーだけを渡し、admin.admin_groupsに入るゲートウェイセッションとして検証できなかった
no_credentialsどういうときかどちらのヘッダーも渡さなかった

invalid_keyが続くなら、古い鍵を使い続けているスクリプトがないかを疑います。bearer_rejectedは、ベアラートークンだけで管理APIを呼び、それがadmin.admin_groupsのセッションとして通らなかった場合です。

起動と設定、利用の側のイベント

項目が定義されているのは、次の2つです。

  • inference: どの上流がリクエストに応えたかと、応答ステータス
  • desktop_bootstrap.denied: Claude Desktopの設定取得が拒否された記録。理由はnot_configured、policy_not_opted_in、no_policy_matchedのいずれかで、ユーザーの身元が付く

Desktopで設定を取得できないと報告されたら、このイベントの理由欄を確認します。ポリシーにdesktopキーを足す手順はdesktopブロックの記事にあります。

config.load、managed.serve、spend.blocked、admin.limit.upsert、admin.limit.deleteは、イベント名だけが一覧にあり、出力項目は定義されていません。集計やアラートの条件にするなら、実際に出た行のキーを自分の環境で確かめてからにします。支出上限の仕組みは支出上限の記事で扱っています。

症状から引くイベント早見表

画面側の症状から、どのイベントを検索するかを逆引きします。

症状まず探すイベント見る項目と読み方
起動や/loginのあとに「Claude Code may not be enabled for your organization」と出るまず探すイベントaccess.denied見る項目と読み方失敗した時刻の理由欄。管理設定の取得が403になっていて、access_controlのIP検査か、前段のプロキシやWAFのどちらかが原因
「sign-in attempts right now」やToo many attemptsが出るまず探すイベント結果がrate_limitedのイベント見る項目と読み方同じclient_ipが1つか少数に偏っていれば、trusted_proxiesの漏れかNAT・VPNの共有出口
Claude Desktopが設定を取得できないまず探すイベントdesktop_bootstrap.denied見る項目と読み方理由欄。desktopキーを持たないポリシーに当たったか、どのポリシーにも当たっていない
管理APIが使えないまず探すイベントadmin.denied見る項目と読み方理由欄の3値。鍵の不一致か、グループ外のセッションか、資格情報なしか
誰も拒否されていないのに公開が心配まず探すイベントaccess.public_client見る項目と読み方出ていれば公開側から届いた証拠。出ていないことは安全の証拠にならない

この表の行は、原因が2つ以上ある症状ほど、イベントの項目が絞り込みの決め手になります。rate_limitedならclient_ipの偏りで、access.deniedなら理由欄の値で見分けます。

見落としやすい落とし穴

431はログにも監査イベントにも残らない

サインイン後にリクエストが431になる症状は、所属するIdPグループが多い開発者に起きます。ヘッダーの合計が256KiBを超える(limits.max_request_header_bytesを設定していればその値を超える)と返るステータスで、このときゲートウェイはログ行も監査イベントも書きません。監査ログを検索して何も出ないことは、リクエストが届かなかった証拠ではなく、この種の失敗がそもそもログに出ない仕様のためです。症状だけがあってイベントが見つからないときは、431を疑って、前段のアクセスログとヘッダーの大きさを確かめます。

前段のプロキシが全員を同じIPにする

listen.trusted_proxiesを設定していないと、すべてのリクエストがプロキシのアドレスから来たように見えます。影響は、IP単位のレート制限が1つの共有バケットにまとまることと、監査イベントのclient_ipがプロキシのIPで埋まることの2つです。監査ログのclient_ipが全行で同じなら、trusted_proxiesを最初に疑います。ポート付きでX-Forwarded-Forを書くロードバランサーは、ipv4:portと[ipv6]:portの形ならポートを落として読まれます。括弧なしでIPv6にポートを付ける形は別のアドレスとして読まれることがあり、プロキシ側でポート出力を切る必要があります。

許可と拒否の評価順

deny_cidrsが先に評価され、一致したクライアントはallow_cidrsにも一致していても拒否されます。allow_cidrsが空でなければ、載っていないアドレスはすべて既定で拒否されます。/healthzと/readyzはallow_cidrsの対象外なので、締めたリスナーでも死活監視は通ります。許可リストを絞った直後にaccess.deniedが増えたなら、まず拒否されたアドレスが許可側のレンジに入っているかを見ます。

保管期間は3か所で別々に決まる

ログ基盤の保管期間、admin_auditの保持日数(admin.audit_retention_days、既定365日)、そして支出カウンターの保持月数(spend_retention_months、既定13か月)は別の設定です。たとえば管理APIの操作履歴はデータベースに1年残るのに、ログ基盤側が30日で消えていれば、サインインや拒否の記録は30日より前を追えません。調査したい期間を先に決めて、3つをそろえます。

アラートに向くイベントと向かないイベント

イベントごとに出方が違うので、通知の条件も分けて考えます。

イベント出方通知の向き不向き
access.public_client出方プロセスごとに最初の1回通知の向き不向き件数ではなく「出たこと」自体を通知する。レプリカが複数なら、台数分まで出うる。再起動や再デプロイのたびに再び出る
admin.denied出方失敗のたびに1行通知の向き不向きinvalid_keyの連続は鍵の使い回しか総当たりのどちらか。クライアントIPとパスで見分ける
access.denied出方拒否のたびに1行通知の向き不向き許可リストの変更直後は増えて当然。変更前後で比べる
session.mint出方サインインの成功ごと通知の向き不向き通知向きではない。誰がいつ入ったかを後から引くための記録

プロセスごとに1回という性質は、access.public_clientを「しばらく出ていないから安全」と読む根拠にもなりません。長く動いているプロセスでは、起動直後の1回が古いログに埋もれているだけの場合があります。保管期間の外に押し出されていれば、検索しても見つかりません。

ログの置き場と運用の目安

監査イベントはstderrにしか出ないため、コンテナ環境ではログ収集の設定が保存先になります。ローテーションはゲートウェイの外の設計です。管理APIの操作履歴は別にadmin_auditとしてデータベースに残ります。

もう1点、session.mintとsession.refreshにはメールアドレスが載ります。ゲートウェイのデータベースでは、メールアドレスや表示名、グループを持つprincipal_emailsの行が、既定で最後の確認から90日(identity_retention_days)を過ぎると消えます。一方、ログ基盤に流した監査イベントの側には、その期間の定めがありません。退職者の情報をいつまで残すかは、ログ基盤の保管期間で決まることになるため、社内の個人情報の扱いと突き合わせておきます。

debugログで確かめたいのは、email_claimとgroups_claimのずれです。サインインしても期待したグループに入らないときは、CLAUDE_GATEWAY_LOG_LEVEL=debugにして1回だけサインインし直し、id_tokenに含まれるクレーム名の中に、設定したクレーム名があるかを見ます。値は記録されないので、名前の綴りの違いはわかりますが、値の中身の違いまではわかりません。調べ終わったらinfoに戻します。

ログとヘルスチェックの概要はデプロイと運用の記事にあり、access_controlなどの設定キーは設定リファレンスの記事で引けます。

この記事を共有:XはてブLinkedIn