Grandream
sentinelが見えても注入は成功していない|OpenAI APIのはまりどころ
連載「Docker Sandboxesでコーディングエージェントを隔離する」第4回/検証日: 2026年8月23日/
sbx0.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 |
|
|
|
OpenAI Images |
|
|
|
第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_KEYやOPENAI_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が動かないときは、秘密値を表示せずに、外側から内側へ順に降ります。
- secretの登録状態を見る(
sbx secret ls、sbx secret ls --service sampleapp-openai-images) - サンドボックスとKitの状態を見る(
sbx inspect sampleapp、sbx kit validate ./.sbx/sampleapp) - sentinelの存在だけ確認する(
sbx exec sampleapp sh -c 'test -n "$APP_OPENAI_API_KEY"'/値は出さない) - 副作用のないGETを投げ、HTTP statusとproviderのエラー種別を見る
- policy logでallow/forwardと宛先domainを確認する(
sbx policy log sampleapp) - 同じKitを使う一時的なshell sandboxで再現するか試す。再現すればCodexやClaude固有の認証競合ではなく、Kit・secret・proxyの問題へ絞れる
- 一度に一要素だけ変える。
scheme: bearerから明示header+formatへ、のように。secretやendpointやSDKを同時に触ると、何が効いたのか消える - 許可がある場合だけ、最小サイズの実リクエストで課金APIの利用可否まで確認する
今回の401は、4と5が「通っている」と示していたせいで、キー側を疑い続けてしまった事例です。5でforwardされていることが分かった時点で、疑いは「宛先まで届いた後」つまり注入内容へ移すべきでした。
まとめ
第4回の要点です。
- エージェント自身のOpenAI認証と、アプリが使うOpenAIキーは別物。service名と環境変数を分け、fallbackは残さない
- GeminiはURLの
?key=ではなくx-goog-api-keyheaderで渡す。proxyの規則に乗り、ログ経由の漏えいも避けられる(OpenAI SDKは元からBearer header) - credential proxyはワークスペースのファイルを隠さない。サンドボックスへ渡す鍵は開発専用にする
- sentinelの存在=注入成功ではない。
scheme: bearerが通らず、明示header+formatで通った(sbx0.39.0実測) - 切り分けは外側から内側へ8段。policy logがforwardを示したら、疑いは注入内容へ移す
次回は接続経路です。Codex AppとClaude DesktopからSSHで入り、実ブラウザやplugin連携と組み合わせる話、そして常設サンドボックスを使い回すときのworking treeの扱いを扱います。
連載の構成
- Docker Sandboxes入門とプロジェクト初期設定
- CodexとClaudeを同じSandboxに同居させる
GH_TOKENでリポジトリ権限を絞る- OpenAI APIアクセスのはまりどころ(header注入)(本記事)
- Codex AppとClaude DesktopからSSHで入る
- Sandboxを1つにした構成判断
- Orca ADEとSSH接続の並列開発(別記事)
参考資料
本記事のheader注入に関する記述はsbx 0.39.0で2026年8月23日に実測した挙動です。Kitのschemaと注入の実装は変更される可能性があるため、同じ症状に当たったら手元のバージョンでkit validateとpolicy logを確認してください。
関連記事
Grandream
株式会社グランドリーム
AI・システム開発のプロフェッショナルチームです。AIエージェント・業務自動化・Webシステム開発などを手がけています。



