LLM APIキーをフロントに置かない:サーバー保管の構成と環境変数の扱い方
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

結論から言うと、サービス運営者の静的なLLM APIキーはブラウザやスマホアプリのコードに置いてはいけません。保管先はアクセスを制御できるサーバー側とし、環境変数から読む方法や、本番ではシークレットマネージャーなどの管理サービスから読み込む方法があります。静的APIキーを使う構成では、フロントはあなたのサーバーのエンドポイントだけを呼び、LLM提供元のAPIはサーバーが代わりに呼びます。キーを配布せず短命トークンで認証する方式は、後の表で別に示します。 この記事では、なぜ置けないのかを短く確認し、保管場所ごとの可否表、環境変数から読む擬似コード、漏洩したときの手順を順に示します。
なぜフロントに置けないのか
ブラウザに届くJavaScript、ネットワークタブで見えるリクエストヘッダー、アプリに同梱した設定ファイルや定数は、利用者の手元にあります。難読化や分割をしても、「利用者の端末で実行される以上、読める」という前提は変わりません。キーが取り出されると、第三者があなたのアカウントでAPIを呼べます。被害は不正利用による請求と、利用上限の消費です。
各社の公式ドキュメントも同じ方針です。
- OpenAIのキー安全管理ガイドは、キーを他人と共有したり、ブラウザやモバイルアプリに配置したりしないよう求め、自社バックエンド経由での呼び出し、環境変数の利用、本番でのキー管理サービスの検討を勧めています(Best practices for API key safety、2026-10-29に確認)。
- Gemini APIのドキュメントは、WebやモバイルアプリにAPIキーを直接ハードコードしないこと、本番ではGoogle Cloud Secret Managerのような安全な保管先を使うことを案内しています(Using Gemini API keys、同日確認)。
- Claude APIの認証ページは、静的なAPIキーの用途を「ローカル開発、試作、スクリプト、シークレットの保管を自分で管理できるサーバー」としています。保管にはシークレットマネージャーを使い、定期的にローテーションし、漏洩の疑いがあるキーは無効化か削除するよう書かれています(Claude Authentication、同日確認)。
保管場所別の可否表
次の表は、公式の記述を踏まえて編集部が整理した判断です。保管場所の判断は「サービス運営者の静的APIキー」を前提とし、末尾の2行は静的APIキーを使わない認証方式です。
- フロントのJSソースやバンドル、可否:不可、理由・注意点:配信された時点で誰でも読めます
- ビルド時にフロントへ埋め込まれる環境変数、可否:不可、理由・注意点:環境変数でも、値がフロントのバンドルに含まれれば利用者に公開されます。埋め込みの条件は使用中のフレームワークの文書で確認してください
- モバイルアプリ内の定数、設定ファイル、アセット、可否:不可、理由・注意点:配布物から取り出せます。Geminiの文書もクライアントコードへの埋め込みを禁じています
- Gitリポジトリ(.envのコミットを含む)、可否:不可、理由・注意点:Geminiの文書はソース管理へのチェックインを避けるよう求めています。履歴にも残ります
- localStorageやCookie、可否:原則不可、理由・注意点:運営者のキーを置く用途には不向きです。利用者が自分のキーを自分で入力する設計は別の話で、その場合も保管と通信の責任を設計に含める必要があります
- サーバーの環境変数、可否:条件付き可、理由・注意点:3社とも環境変数から読む方式を案内しています。読み取り権限を制限し、ログやエラー表示、ダンプへの流出を防いでください。コンテナでは後述のOWASPの注意も確認してください
- シークレットマネージャー等から起動時に注入、可否:推奨、理由・注意点:OpenAI、Gemini、Claudeの文書が管理サービスの利用に触れています。権限の絞り込みと監査ログも使えます
- CIのシークレット、可否:条件付き可、理由・注意点:デプロイ用の注入にだけ使います。フロントのビルド工程へ流し込まないでください
- クラウドのワークロードID連携(Claude)、可否:可(キー自体をなくす選択肢)、理由・注意点:Claudeの文書によると、IDプロバイダーが発行した短命トークンで認証でき、長期のAPIキーを環境に置かずに済みます。対応する環境が前提です
- iOS/macOSのApp Attest(Claude)、可否:可(Claude限定)、理由・注意点:正規のアプリのインストールにだけ1時間有効の短命トークンを渡し、キーを同梱せずに直接呼べる仕組みです。Messages API向けのClaudeの認証方式です
環境変数についての補足です。OWASPのシークレット管理チートシートは、コンテナの定義にシークレットを直接書く方法は定義ごと漏れやすいとして避けるよう述べ、代替としてオーケストレーターがマウントするボリュームや、サイドカーが管理サービスから取得する方式を挙げています(OWASP Secrets Management Cheat Sheet、同日確認)。さらに同資料は、環境変数が他のプロセスから参照されたり、ログやシステムダンプに含まれたりする可能性を挙げ、コンテナでは他の方法が使えない場合を除き推奨していません。本番では注入元だけでなく、環境変数、マウントしたファイル、メモリ内での取得のどれで渡すかまで設計してください。
サーバー経由の構成と擬似コード
構成は三段です。
- フロントは、自社サーバーの/api/chatのようなエンドポイントに、ユーザーの入力だけを送る
- サーバーはログイン状態と利用回数を確認し、環境変数のキーでLLM APIを呼ぶ
- サーバーは生成結果のテキストだけを返す
ここで重要なのは、サーバーが「誰でも使える中継所」にならないことです。キーが見えなくても、認証なしで呼べるエンドポイントを公開すれば、同じ不正利用が起きます。モデルや出力上限はサーバー側で固定し、入力の長さにも上限を付けます。
次は説明用の擬似コードです。実行も動作確認もしていません。llm-sdkは実在しない仮のライブラリ名で、各社のSDKに読み替えてください。
// 説明用の擬似コード(未実行)
import express from "express";
import { LlmClient } from "llm-sdk"; // 仮のSDK名
// 1. 起動時に一度だけ環境変数から読む。未設定なら起動を止める
const apiKey = process.env.LLM_API_KEY;
const model = process.env.LLM_MODEL; // モデルIDは設定で差し替える
if (!apiKey || !model) {
throw new Error("LLM_API_KEY または LLM_MODEL が未設定です"); // 値は出力しない
}
const llm = new LlmClient({ apiKey });
const MAX_INPUT_CHARS = 2000;
const app = express();
app.use(express.json({ limit: "20kb" }));
app.post("/api/chat", async (req, res) => {
// 2. 自社の認証。未ログインは弾く(実装は自社の方式)
const user = await authenticate(req);
if (!user) return res.status(401).json({ error: "unauthorized" });
// 3. 利用回数の上限。超えたらLLMを呼ばない
if (!(await withinQuota(user.id))) {
return res.status(429).json({ error: "quota_exceeded" });
}
// 4. 入力検証。フロントから受け取るのはテキストだけ
const text = String(req.body?.text ?? "");
if (text.length === 0 || text.length > MAX_INPUT_CHARS) {
return res.status(400).json({ error: "invalid_input" });
}
try {
// 5. モデル・出力上限・システム指示はサーバー側で固定
const result = await llm.generate({
model,
maxOutputTokens: 500,
system: "社内FAQの回答アシスタントです。",
input: text,
});
res.json({ answer: result.text }); // キーやヘッダーは返さない
} catch (err) {
console.error("llm_call_failed", { userId: user.id }); // errの全体は出さない
res.status(502).json({ error: "upstream_error" });
}
});環境変数の設定はデプロイ先の機能に任せます。ローカルでは.envを使う場合でも、.gitignoreに加えてから作ってください。本番では、可能ならシークレットマネージャーの値をサービスに注入する形にします。
組み込み前の確認手順
人が確認する項目を、順に並べます。
- ビルド成果物の検索:フロントのビルド出力フォルダ内を、自分のキーの先頭数文字で検索します(例:grep -r "キーの先頭8文字" dist/)。ヒットしたら構成を見直してください。未実行の例です。
- 履歴の確認:過去のコミットに.envや設定ファイルが入っていないか確認します。見つかったキーは、履歴から消しても漏洩済みとして扱います。
- エンドポイントの確認:ログアウト状態で/api/chatを呼び、401が返るかを試します。
- ログの確認:エラー時にリクエストヘッダーやSDKの設定オブジェクトが丸ごと出力されていないかを見ます。OWASPもシークレット本体をログに残さないよう求めています。
- キーの分離:環境ごと(開発・本番)にキーを分けます。Claudeでは、共有や自動化の用途にサービスアカウントのキーを使うことが勧められています。
コード生成AIにレビューを任せる場合の依頼文は、次のとおりです。実際のキーは貼らず、ダミー値に置き換えてください。
次のExpressサーバーとフロントのコードを、APIキー漏洩の観点でレビューしてください。
確認してほしい点:
1. キーがフロントのバンドルやログ、エラー応答に出る経路
2. 認証・利用回数制限・入力長制限が抜けていないか
3. 環境変数の読み込み箇所と、未設定時の挙動
キーの値は "DUMMY_KEY" に置き換えてあります。修正案は差分で出し、自信のない点は「要確認」と書いてください。AIの指摘は参考にとどめ、最終判断は自分でコードとデプロイ設定を見て行います。
漏洩したときの手順
疑いの段階で動くのが基本です。Geminiの文書は、新しいキーの生成、アプリケーションコードの更新、動作確認後の旧キーの無効化または削除、請求ログと利用状況の監査という順序を挙げています。Claudeの文書では、コンソールの「無効化(Disable)」は元に戻せ、「削除(Delete)」は恒久的です。作成時に有効期限を設定すれば、漏れたキーが使える期間を限定できますが、保管の代わりにはならないとも書かれています。
ただし、OWASPは露出したキーの即時失効を求めています。不正利用が進行中、または漏洩が確認された場合は、サービス停止が必要でも旧キーの無効化を先に行います。次の流れは、切り替えまで旧キーを有効にしておくリスクを許容できる場合の手順です。
- 新しいキーを発行して、サーバーの環境変数かシークレットマネージャーを更新する
- 本番で正常に動くことを確認する
- 旧キーを無効化し、問題がなければ削除する
- 漏れていた期間の利用量と請求を確認し、身に覚えのない呼び出しがないか調べる
- 原因の経路(バンドル、履歴、ログなど)をふさぐ
提供元ごとの上限設定や予算アラートの有無は、プランやコンソールの仕様で異なります。契約中のコンソールで確認してください。

