GitHubで "API keys" plaintext is:issue を検索してみてください。実際に運用されている複数ユーザー向けアプリのメンテナーが「キーは現在データベースに平文で保存されており、これはプラットフォームにとってリスクです」といった文を書いているのが見つかるでしょう。私はこの1ヶ月だけで数十件の同様のissueを読んできました。それらのissueを書いた人たちにメールを送っていたからです。
需要側の理由は説明しやすいものです。ユーザーはBYOK(Bring Your Own Key)を求める理由として、すでに別の場所でAIの利用料を支払っていること、自分のプロバイダーアカウントでプロンプトを実行したいこと、または無料プランのレート制限に不満があることを挙げます。開発者がBYOKを望む理由は、推論コストが利用量に比例して増加する一方で収益がそれに追いつかないからです。
したがってBYOKは繰り返しリクエストされ続けていますが、実装が不十分なままになっています。以下は実際に目にした4つの段階を、最悪から本番グレードまでランク付けしたものです。
レベル0:データベースに平文のカラム
これは誰も認めたくないほど一般的です。users.openai_api_key というカラムに保存時に書き込み、リクエストごとに読み込むという方法です。
これが起こる理由は理解できます。機能としては午後1つで完成するからです。しかしプロバイダーのキーはパスワードハッシュとは異なります。実際に使えて課金につながる認証情報です。バックアップ経由、管理画面の設定ミス、あるいは1回のインジェクション脆弱性でデータベースが漏洩した場合、攻撃者が得るのは解読すべきハッシュではなく、課金情報付きの有効なキーです。
現在レベル0にある場合は、以下で説明する暗号化カラムへの移行は1日の作業です。今週中に実施してください。
レベル1:環境変数
デプロイ環境に OPENAI_API_KEY を置く方法です。セルフホストや、運用者がキーを所有する内部ツールには適しています。しかしホスト型で複数ユーザーの環境では、1つの環境変数ですべてのユーザーが1つのアカウント・レート制限・請求を共有することになります。これはまさにBYOKが解決すべき状況です。
レベル2:キーはブラウザに留める
localStorageやIndexedDBに保存し、リクエストごとにクライアント側でキーを添付し、サーバー側には一切保存しない方法です。ローカルファーストのツールでは適切な選択肢です。サーバーは見たことがないものを漏洩させることはありません。
ただし製品にサーバーサイドの機能が追加されると制限が現れます。バックグラウンドジョブ、スケジュール実行、webhook、チームワークスペース、モバイルクライアントなどは、キーが1つのブラウザタブにしか存在しない場合には利用できません。レベル2はローカルツールには有効ですが、ホスト型SaaSには行き止まりとなります。
レベル3:サーバーサイドの暗号化保管庫
これはホスト型・マルチテナント製品向けの答えであり、実際の作業の大部分を占めます。「キーを暗号化する」というのは一見シンプルに聞こえますが、暗号化部分は本当に20行程度のコードです。その周囲のすべてが製品そのものです。
以下は、本番環境向けBYOK実装に最低限求められるチェックリストです。
暗号化
- AES-256-GCMを使用し、暗号化ごとに固有のnonceを生成し、同じ鍵でnonceを再利用しない。
- 暗号化鍵はデータベースの外部に置く。KMS、シークレットマネージャー、または最低でもデータとは異なる信頼境界上の環境変数を使用する。
- 暗号文を行に紐づける(ユーザーIDまたは接続IDをAADとして使用)ことで、DB書き込み権限を持つ攻撃者が行間で暗号文を入れ替えることを防ぐ。
UXのルール
- 書き込み専用。保存後はクライアントにキーを返さない。
sk-...abc4のようなマスク表示のみとする。 - キーの変更は編集ではなく再入力とする。
運用上の衛生管理
- 復号はリクエスト時にのみ、プロバイダーを呼び出すコードパス内で行う。平文のキーはログ行、エラーレポート、アナリティクスイベントに一切触れないようにする。上流プロバイダーのエラーがリクエストヘッダーをエコーバックする場合もあるため、それらもマスクする。
ライフサイクル(見落とされがちな部分)
- 失効は即時でなければならない。ユーザーが切断をクリックした時点で、進行中のトラフィックのみを残して即座に停止する。
- 利用量の帰属。「昨日40ドルの利用をしたのはどのユーザーか」という質問に答えられるように、リクエストごと・ユーザーごとのトークン数をログに記録する。
- ローテーション。ユーザーはプロバイダーのキーをローテーションする。サポートチケットを介さずにそれを実行できる経路を用意する。
ほとんどの不十分な実装を検出する簡単なセルフテスト:サポートスタッフはキーを閲覧できるか?ログにキーが表示されるか?今夜データベースが漏洩した場合、攻撃者は何を手にすることになるか?ユーザーは1クリックでアクセスを停止できるか?どのユーザーがどれだけ利用したかを把握できるか?これらの質問のいずれかで不快感を覚える場合は、まだ完了していません。
購入オプション
ここで私の利害関係を明記しておきます。私はこれを構築した一人です。そのためこのセクションの残りは宣伝になります。
Monet はレベル3の保管庫をホスト型サービスとして提供し、ユーザーがすでに慣れ親しんでいるOAuthのような同意フローを採用しています。ユーザーはホストされた接続ページにアクセスし、ChatGPT PlusやClaude Proアカウントを接続するか、独自のプロバイダーキーを貼り付けます。認証情報はAES-256-GCMで暗号化され、アプリが触れることのない保管庫に保存されます。アプリは標準的な認可コード交換を通じて不透明なベアラートークンを受け取り、OpenAI互換の単一エンドポイントを呼び出します。
from openai import OpenAI
client = OpenAI(
base_url="https://monet.gg/api/v1",
api_key=monet_access_token, # opaque token from the OAuth exchange, not a provider key
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)
Enter fullscreen mode Exit fullscreen mode
プロキシはトークンを適切なユーザーの認証情報に解決し、レスポンスをストリーミングで返し、プロバイダーが報告した実際のトークン数を含む利用イベントを記録するため、メータリングとコスト転嫁が自動的に行われます。接続を失効させると保管庫の行がクリアされ、トークンは解決されなくなります。サーバー側にはトークンのSHA-256ハッシュのみが保存されるため、Monetのトークンテーブルが完全にダンプされた場合でも使用することはできません。
ベータ期間中は無料です。エンドツーエンドのフローを確認したい場合は demo.monet.gg で動作するライブデモアプリがあり、開発者ダッシュボードは monet.gg でご覧いただけます。
自分で構築したい場合は、上記のチェックリストが正直な最低限の要件です。どちらの道を選ぶにしても、比較検討をお手伝いします。この1ヶ月で十分な数の平文キーに関するissueを読んできたので、こうした問題が減ることを願っています。
関連記事: 「AIサブスクリプション向けOAuth」を15歳で構築した——完全なアーキテクチャ はホスト型オプションの完全な設計を、ユーザーのAI利用料金を負担し続けるのをやめる はBYOKの経済的根拠を解説しています。
私はShlok、15歳でMonetを一人で構築しています。すべての返信を読みます:[email protected]、github.com/shlok-madhekar、@shlokbuilds。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.