Skip to content

ゲートウェイの共有

共有デプロイ向けのクライアント単位トークンと、プロキシやトンネル向けの SSE キープアライブ ping。

Updated View as Markdown

インバウンドのクライアントトークン

デフォルトでは shunt にインバウンド認証はありません — ループバックのみの個人ゲートウェイなら問題ありませんが、VPN/トンネル越しに共有すると、そこに到達できる誰もが、マッピングされたモデルでオペレーターのアカウントを消費できてしまいます(shunt はそれらに自身の api_key/chatgpt_oauth 認証情報を注入します)。パススルーモデルは懸念ではありません。各呼び出し元自身の Anthropic 認証情報を転送します。

[server.auth] は、注入された認証情報を使うルートとモデル discovery を、クライアント単位トークンでゲートします。

[server.auth]                        # both keys optional; defaults shown
header = "x-shunt-token"
tokens_env = "SHUNT_CLIENT_TOKENS"
# Gateway side: name:token pairs (names are labels for logging; tokens are secrets)
export SHUNT_CLIENT_TOKENS="minsu:$(openssl rand -hex 32),alice:$(openssl rand -hex 32)"

起動は、[server.auth] が存在するのに環境変数が未設定または不正な場合、フェイルクローズします。有効なトークンなしでマッピングされたモデルや GET /v1/models へのリクエストは 401 authentication_error を受け取ります。どちらのゲートも、標準の Anthropic 認証情報スロットのいずれでもクライアントトークンを受け付けます — 複数のスロットに有効なトークンがある場合の優先順位は、設定されたヘッダー(デフォルト x-shunt-token)、次に Authorization: Bearer、次に x-api-key です。GET /routes、GET|HEAD /、GET /health、およびパススルーモデルは開いたままです。GET /routes はルーティングのメタデータ(設定されたプロバイダー/上流モデルのマッピング)を公開する shunt ネイティブのエンドポイントであるため未認証です — 認証情報は決して公開しません。認証情報はプロバイダー設定にのみ存在し、そのハンドラーによって読まれることはありません。

ゲートされたルートでは、受け付けられる認証情報ヘッダーは転送前に常に除去され(shunt はそこに自身のプロバイダー認証情報を注入します)、マッチングは定数時間で、トークン値はログに記録されません(クライアント名はリクエストごとに記録されます)。 パススルールートでは shunt は何も注入しないため、呼び出し元自身の認証情報は送られたまま転送されます — ただしスロット単位の例外があります: Authorization と x-api-key は、そのスロット自身の値が shunt 自身のインバウンド認証情報(受理されたクライアントトークン、[server.gateway] JWT、または [server.admin] の認証情報 — 管理サーフェスは自身のヘッダーに加えて x-api-key でもこれを受理します)である場合にそれぞれクリアされます。したがってゲートで受理された認証情報が上流へ中継されることはなく、もう一方のスロットにある本物の上流認証情報はそのまま流れます。 各スロットは最初の値だけでなくすべての値を対象に判定され、どの位置であれ shunt の認証情報を含むスロットはヘッダーごと削除されます — 同じヘッダーを二度送り、一方の値に本物の認証情報を、もう一方に shunt の認証情報を載せた場合、両方とも失われます。 cookie ヘッダーは転送のたびにヘッダーごと除去されます — 管理サーフェスがセッション Cookie をそこで受理し、かつ shunt 自身は Cookie ジャーを持たないためです。副作用として、送信した無害な Cookie も一緒に落ちます。

クライアント側は、ゲートウェイが何を提供しているかで選びます。

  • プール/マッピング専用ゲートウェイ(例: Anthropic アカウントプールがデフォルトプロバイダー): クライアントトークンは、Claude Code がすでに送っている認証情報そのものにできます — 追加のヘッダー行は不要です。

    export ANTHROPIC_AUTH_TOKEN="<your client token>"   # Authorization: Bearer として送信される
  • パススルーモデルが混在する場合: 各呼び出し元の実際の Anthropic 認証情報に専用のスロットが必要なので、設定されたヘッダーで専用トークンを配布し、Authorization / x-api-key はそのために空けておいてください(ANTHROPIC_CUSTOM_HEADERS は 1 行ごとに 1 つの Name: Value を取ります)。 これは漏洩を防ぐ唯一の仕組みではなく衛生上の推奨です: Authorization や x-api-key に届いたクライアントトークン([server.gateway] JWT や [server.admin] の認証情報も同様)は、パススルー転送の前に値によってそのスロットから除去されます。

    export ANTHROPIC_CUSTOM_HEADERS="x-shunt-token: <your token>"

オプトインの管理 Web サーフェスも有効にする場合は、別個の管理トークンで保護し、HTTPS または信頼できるトンネル経由でのみ公開してください。

自組織の identity provider が発行する JWT の検証

組織がすでに identity provider を運用しているなら、[[server.auth.jwt]] で共有 secret の代わりにそのトークンを受け付けられます — shunt を認証サーバーにせずにユーザーごとの identity を得る方法です。shunt は検証するだけで、ログインフローもセッションストアも署名 secret もありません。gateway ログインと違い、クライアントの設定は base URL と bearer だけのままなので、Claude Code の gateway provider モードには入らず、そのモードで無効化される機能も失いません。

[server.auth]
# issuer を設定すれば静的トークンは不要です。
tokens_env = "SHUNT_CLIENT_TOKENS"

[[server.auth.jwt]]
issuer = "https://accounts.google.com"
audience = ["<shunt 用に作成した client id>"]
email_domains = ["example.com"]

各呼び出し元は、静的トークンと同じ場所にトークンを送ります:

export ANTHROPIC_AUTH_TOKEN="<IdP が発行した JWT>"   # Authorization: Bearer として送信

JWT は Authorization: Bearer でのみ受け付けます — 設定されたヘッダーと x-api-key は引き続き静的トークンだけを受け付けます。両方を同時に設定でき、静的トークンが先に確認されます。

設定時に押さえておく点が 2 つあります:

  • shunt 専用の audience を用意する。 shunt 専用の client id を作って audience に列挙すれば、同じテナントの別アプリケーション向けに発行されたトークンがここで認証されることはありません。
  • 短命なトークンを発行する。 shunt は失効状態を保持しないため、トークンは期限まで使えます。max_token_age_seconds(デフォルト 3600)はそれより長い寿命で発行されたトークンを拒否します — 失効をより厳しくしたい場合はこの値を下げ、IdP に分単位のトークンを発行させてください。

エントリの設定が誤っている場合は起動が fail closed します。audience はあるが email_domains / allowed_emails のルールがない場合も同様です。JWT 認証が api_error ボディを伴う 503 を返す場合(後述の上限超過時の overloaded_error ではない場合)、shunt は issuer の key set を取得できていません — 認証情報ではなく issuer の URL とネットワーク到達性を確認してください。キーの一覧は設定リファレンスにあります。

SSE キープアライブ ping

中間装置は静かなストリームを切断します — Cloudflare のプロキシは 1 バイトも来ないまま 100 秒で 524 を返し(Enterprise 未満では固定)、長い reasoning の合間はそれだけ静かになりえます。そのため shunt は、ストリーミングレスポンスがアイドルになるたびに、Anthropic プロトコル自身の ping イベント(api.anthropic.com 自身が発行し、すべてのクライアントが無視するもの)を注入します。

[server]
sse_keepalive_seconds = 30   # default; 0 disables

Ping は完全な SSE イベントの間にのみ(半分送信されたフレームの内部には決して)、text/event-stream レスポンスにのみ注入され、上流ストリームと共に停止します。アイドルタイムアウトのないトンネル(WireGuard/Tailscale)の背後では ping は無害です。バイト単位で同一の中継が欲しい場合は 0 で無効化してください。

インバウンド同時実行数の上限

共有ゲートウェイは多数のクライアントを同時に処理するため、shunt は処理中のインバウンドリクエスト数に上限を設けます:

[server]
max_concurrent_requests = 1024   # default; 0 disables the limit

上限を超えると、shunt はリクエストをキューに入れず即座に 503 と Retry-After: 1 で拒否します。キューに入れたリクエストもバッファ済みのボディをメモリに保持したままであり、この上限はまさにその増加を防ぐためのものだからです。ストリーミングレスポンスは、レスポンスボディが終わるかクライアントが切断するまで枠を保持します。つまり長時間続くストリームは、レスポンスヘッダーを生成する間だけでなく、その生存期間を通じて上限に数えられます。

GET / と GET /health は対象外なので、飽和したゲートウェイでも liveness プローブには応答し続け、ロードバランサーが実際に処理中のインスタンスを切り離すことはありません。

デフォルト値は、個人用や小規模チームのゲートウェイであれば到達しない程度に余裕があります。トラフィックの多い共有デプロイでは値を上げ、無制限に戻すには 0 を設定してください。この上限は起動時に固定されます。値の変更は config reload で受け入れられますが、実際に適用されるのは再起動後です。

クライアントが overloaded_error ボディを伴う 503 を報告する場合、ゲートウェイが上限に達しています。max_concurrent_requests を上げるか、枠を占有したままになっている長時間ストリームがないか確認してください。 拒否されたリクエストは shunt.requests_shed カウンターを増やし、拒否そのものは debug! でログ出力されます。

この上限が数えるのはバイト数ではなくリクエスト数であり、permit はリクエストが処理中である限り保持されます。インターネットに公開するデプロイでは次の 2 点に注意してください:

  • 受け入れた 1 リクエストは最大 64 MiB のボディをバッファリングしうるため、バイト予算は上限に比例して増えます。ホストがその積を吸収できない場合は max_concurrent_requests を下げてください。
  • リクエストボディを少しずつしか送らないクライアントや、停止した上流ストリームは、その間ずっと枠を占有します。shunt はインバウンドのリクエストタイムアウトを適用しません。ゲートウェイを公開している場合は、エッジプロキシで遅いクライアントを切断してください。

どちらもこの上限によって新たに生じた問題ではありません(上限によって、以前は無限だった天井が有限になりました)。ただし max_concurrent_requests だけではメモリの上限にはならない、ということです。

Navigation

Type to search…

↑↓ navigate↵ selectEsc close