Grandream

Grandream

公開: 7 min read

sentinelが見えても注入は成功していない|OpenAI APIのはまりどころ

sentinelが見えても注入は成功していない|OpenAI APIのはまりどころ
AIエージェントの開発をご検討中ですか? AIエージェント開発サービスを見る →

連載「Docker Sandboxesでコーディングエージェントを隔離する」第4回/検証日: 2026年8月23日/sbx 0.39.0

第3回はGitHubのトークンを扱いました。今回は、開発中のアプリケーションが呼ぶAPIキー(Gemini・OpenAI)です。

この回の中心は実測の記録です。環境変数には実キーの代わりのダミー値(sentinel)が入っていて、proxyのlogでも通信がforwardされているのに、OpenAI APIがHTTP 401を返す。その原因と直し方、そして同じ状況で何をどの順に確認すべきかを書きます。

エージェント自身の認証と、アプリの認証は別物

CodexはOpenAIのアカウントで認証されて動きます。一方、開発中のアプリもOpenAI APIを呼びます。この2つは名前が同じだけで、まったく別のcredentialです。同じOPENAI_API_KEYへ押し込むと、エージェントの認証・アプリの認証・.env.localの値が三つ巴で競合し、「どのキーで動いたのか」が分からなくなります。

ここを混ぜると後の切り分けが成立しないので、アプリ用には専用のservice名と専用の環境変数名を切りました。

用途

service

サンドボックス内の変数

注入先

Gemini

sampleapp-gemini

APP_GEMINI_API_KEY

x-goog-api-key header

OpenAI Images

sampleapp-openai-images

APP_OPENAI_API_KEY

Authorization: Bearer ...

第1回で作った共通Kitへ、この2つをcredentialsとして足します。

credentials:
  - service: sampleapp-gemini
    description: Gemini API key used by the application server
    required: true
    apiKey:
      name: APP_GEMINI_API_KEY
      proxyManaged: true
      inject:
        - domain: generativelanguage.googleapis.com
          header: x-goog-api-key
          format: "%s"

  - service: sampleapp-openai-images
    description: OpenAI API key used by the application server
    required: true
    apiKey:
      name: APP_OPENAI_API_KEY
      proxyManaged: true
      inject:
        - domain: api.openai.com
          header: Authorization
          format: "Bearer %s"

登録は第3回のGitHubと同じく対話式です。

sbx secret set sampleapp-gemini
sbx secret set sampleapp-openai-images

アプリコード側も専用変数だけを読み、GEMINI_API_KEYOPENAI_API_KEYへのfallbackは残しません。fallbackを1つ残すだけで、「どのcredentialが使われたか」が一意に決まらなくなります。

const geminiApiKey = process.env.APP_GEMINI_API_KEY;
const openaiApiKey = process.env.APP_OPENAI_API_KEY;

GeminiはURLの?key=ではなくx-goog-api-key headerで渡す

Gemini REST APIは、APIキーをURLのquery stringでも受け付けます。ただしこの構成では使えません。credential proxyは「このdomainへの通信のこのheaderへ実キーを差す」という規則で動くので、キーをURLへ載せる経路はproxyの管理外になります。

const response = await fetch(endpoint, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-goog-api-key": process.env.APP_GEMINI_API_KEY ?? "",
  },
  body: JSON.stringify(payload),
});

header方式にすると、proxyの規則へ素直に乗るだけでなく、URLがログ・分析基盤・エラーメッセージへ落ちたときの漏えいも避けられます。OpenAIの公式SDKはキーをBearer headerで送るので、専用変数をapiKeyへ渡すだけで足ります。

なお、credential proxyが守るのは登録したcredentialの注入経路であって、ワークスペース内のファイルを見えなくする機能ではありません。ホストリポジトリの読み取り専用マウント経由で、鍵を書いたローカル設定ファイルが読める構成もあり得ます。サンドボックスへ渡すのは開発専用のキーにして、不要な秘密情報はワークスペースの外へ出しておきます。

sentinelが見えていても、headerが注入されているとは限らない

ここは公式仕様ではなくsbx 0.39.0での実測です。将来のバージョンでは挙動が変わっている可能性があります。

Kit referenceでは、Bearer認証の指定にscheme: bearerが使えます。ところが検証環境では、次の3つが同時に成立していました。

  • サンドボックス内のAPP_OPENAI_API_KEYにsentinelが存在する
  • proxy policy logでは、api.openai.comへの通信がforwardされている
  • それでもOpenAI APIがHTTP 401を返す

返ってきたエラーはこの趣旨のものでした。

Missing bearer authentication in header

読み違えやすいのはここです。このメッセージは「APIキーが無効」を意味しません。リクエストはOpenAIへ届いているのに、期待したAuthorization headerが付いていない、という状態です。キーを疑って作り直しても直りません。

scheme指定をやめ、次の明示形式へ変えると認証が通りました。

inject:
  - domain: api.openai.com
    header: Authorization
    format: "Bearer %s"

最終的に、副作用のないGETリクエストと最小サイズの実APIリクエストがどちらもHTTP 200になり、キー自体とproxy経路の健全性を確認できました。

得た教訓は2つです。sentinelが見えたことをcredential注入の成功と読み替えないこと。そして、認証エラーを「キーが間違っている」で片付けないことです。proxy方式では、キーの正しさとheaderの付き方が別々に失敗し得ます。

切り分けは外側から内側へ、一度に一要素だけ

credentialが動かないときは、秘密値を表示せずに、外側から内側へ順に降ります。

  1. secretの登録状態を見る(sbx secret lssbx secret ls --service sampleapp-openai-images
  2. サンドボックスとKitの状態を見る(sbx inspect sampleappsbx kit validate ./.sbx/sampleapp
  3. sentinelの存在だけ確認する(sbx exec sampleapp sh -c 'test -n "$APP_OPENAI_API_KEY"'/値は出さない)
  4. 副作用のないGETを投げ、HTTP statusとproviderのエラー種別を見る
  5. policy logでallow/forwardと宛先domainを確認する(sbx policy log sampleapp
  6. 同じKitを使う一時的なshell sandboxで再現するか試す。再現すればCodexやClaude固有の認証競合ではなく、Kit・secret・proxyの問題へ絞れる
  7. 一度に一要素だけ変えるscheme: bearerから明示headerformatへ、のように。secretやendpointやSDKを同時に触ると、何が効いたのか消える
  8. 許可がある場合だけ、最小サイズの実リクエストで課金APIの利用可否まで確認する

今回の401は、4と5が「通っている」と示していたせいで、キー側を疑い続けてしまった事例です。5でforwardされていることが分かった時点で、疑いは「宛先まで届いた後」つまり注入内容へ移すべきでした。

まとめ

第4回の要点です。

  • エージェント自身のOpenAI認証と、アプリが使うOpenAIキーは別物。service名と環境変数を分け、fallbackは残さない
  • GeminiはURLの?key=ではなくx-goog-api-key headerで渡す。proxyの規則に乗り、ログ経由の漏えいも避けられる(OpenAI SDKは元からBearer header)
  • credential proxyはワークスペースのファイルを隠さない。サンドボックスへ渡す鍵は開発専用にする
  • sentinelの存在=注入成功ではないscheme: bearerが通らず、明示headerformatで通った(sbx 0.39.0実測)
  • 切り分けは外側から内側へ8段。policy logがforwardを示したら、疑いは注入内容へ移す

次回は接続経路です。Codex AppとClaude DesktopからSSHで入り、実ブラウザやplugin連携と組み合わせる話、そして常設サンドボックスを使い回すときのworking treeの扱いを扱います。

連載の構成

  1. Docker Sandboxes入門とプロジェクト初期設定
  2. CodexとClaudeを同じSandboxに同居させる
  3. GH_TOKENでリポジトリ権限を絞る
  4. OpenAI APIアクセスのはまりどころ(header注入)(本記事)
  5. Codex AppとClaude DesktopからSSHで入る
  6. Sandboxを1つにした構成判断
  7. Orca ADEとSSH接続の並列開発(別記事)

参考資料


本記事のheader注入に関する記述はsbx 0.39.0で2026年8月23日に実測した挙動です。Kitのschemaと注入の実装は変更される可能性があるため、同じ症状に当たったら手元のバージョンでkit validatepolicy logを確認してください。

関連記事

Grandream

Grandream

株式会社グランドリーム

AI・システム開発のプロフェッショナルチームです。AIエージェント・業務自動化・Webシステム開発などを手がけています。

AIエージェント開発のご相談はお気軽に

PoC段階から本番運用まで一貫対応します。

AI開発について相談する

AIエージェント開発サービスの詳細を見る →

どのサービスが合うか30秒で診断する →