JieGouのLLMレイヤーを設計した際、ほとんどのプラットフォームにはない制約がありました:顧客が独自のAPIキーを使用でき、保存時に平文のキーを見ることがないようにする必要がありました。この記事では、Bring Your Own Key(BYOK)システムの仕組み、暗号化スキーム、プロバイダールーティングアーキテクチャ、そしてすべてを信頼性高く動作させるガードレールについて説明します。
BYOKが重要な理由
ほとんどのAIプラットフォームは、独自のAPIキーを通じて呼び出しをプロキシします。これにより、データは各社のアカウントを経由し、使用量は各社のレート制限に従い、どのモデルやエンドポイントが使用されるかを制御できません。
BYOKでは、各顧客が独自のAnthropic、OpenAI、またはGoogle APIキーを接続します。呼び出しは顧客の認証情報を使用してプロバイダーに直接送信されます。JieGouはworkflowをオーケストレーションしますが、BYOKキーが使用されている場合、リクエストやレスポンスのペイロードを見ることはありません。
キー暗号化:AES-256-GCM
APIキーは、12バイトの初期化ベクトルと16バイトの認証タグを使用したAES-256-GCMで保存時に暗号化されます。暗号化キーは環境変数として保存された64文字の16進数文字列から導出された256ビット値です——データベースに触れることはありません。
保存形式はIV + authTag + ciphertextのBase64エンコードされた連結です。ユーザーが復号化せずにどのキーが保存されているかを識別できるよう、表示用に8文字の安全なプレフィックス(「sk-proj…」)も一緒に保存します。
キーはFirestoreのaccount_api_keysコレクションに、アカウントID、プロバイダー名、暗号化キーブロブ、有効性フラグのフィールドとともに保存されます。
キー解決フロー
workflowステップがLLMを呼び出す必要がある場合、キーリゾルバーは以下のシーケンスを実行します:
- プランゲーティング — アカウントのサブスクリプションがBYOKを許可しているか確認(10分のTTLでRedisにキャッシュ)。
- Redisキャッシュ検索 — 復号化されたキーは5分間キャッシュされます。センチネル値(
__none__)は、以前検索されたキーが存在しないことを示し、Firestoreの繰り返し読み取りを回避します。 - Firestore検索 — キャッシュミスの場合、
account_api_keysから取得。 - 有効性チェック — 自動無効化システムによって無効とマークされたキーをスキップ。
- 復号化 — AES-256-GCM復号化がオンザフライでメモリ内で行われます。
- Fail-open — いずれかのステップで問題が発生した場合、プラットフォーム自体のAPIキーにフォールバック。ユーザーエクスペリエンスを決して劣化させません。
fail-open設計は意図的です。Redisがダウンしている場合、復号化が失敗した場合、Firestoreの読み取りがエラーになった場合——workflowは引き続き実行され、プラットフォームキーを使用します。ユーザーは暗号的なエラーを受け取るのではなく、作業が完了するのを見ます。
プロバイダールーティング
LLMレイヤーは、Anthropic、OpenAI、Googleをサポートするプロバイダー抽象化を備えたVercel AI SDK上に構築されています。各プロバイダーには2つのインスタンス化パスがあります:
プラットフォームキー(シングルトン) — 起動時に作成される共有インスタンスで、無料ティアのアカウントまたはBYOKフォールバックとして使用されます。
BYOK(エフェメラル) — 顧客の復号化されたキーを使用して呼び出しごとに作成される新しいプロバイダーインスタンス。このインスタンスはキャッシュされません——1つのリクエストに使用されてから破棄されるため、復号化されたキーがメモリに残留しません。
workflowはステップごとに異なるモデルを指定できます。コンテンツパイプラインではステップ1でニュアンスのあるライティングにClaude、ステップ2で構造化データの抽出にGPTを使用するかもしれません。プロバイダールーティングがこれを透過的に処理します。
自動無効化
LLM呼び出しが認証エラー(HTTP 401、402、または403、またはレスポンスボディが「invalid api key」や「unauthorized」などのパターンに一致)を返した場合、システムはそのキーをFirestoreで自動的に無効としてマークし、Redisキャッシュから削除します。
ユーザーには明確なメッセージが表示されます:「{Provider}のAPIキーが無効であるか、取り消されています。アカウント設定でAPIキーを更新してください。」後続の呼び出しは、ユーザーが新しいキーを提供するまでプラットフォームキーにフォールバックします。
プロバイダー間で11の既知のエラーパターンに対してチェックしています。ローテーションされたキー、取り消されたキー、支出制限を超えたキーをキャッチします。
サーキットブレーカー
各LLMプロバイダーにはプロバイダーごとのサーキットブレーカーがあります(アカウントごとではなく——1つのプロバイダーがダウンすると全員に影響します)。
ブレーカーは60秒のウィンドウ内で5回のエラー後にトリップします。オープンになると、単一のプローブリクエストを許可する前に30秒間オープンのままです(ハーフオープン状態)。プローブが成功するとサーキットがクローズします。
サーバーサイドの障害のみがカウントされます:5xxレスポンス、タイムアウト、接続エラー。無効なAPIキーなどのクライアントエラー(4xx)はブレーカーをトリップしません——これらは代わりに自動無効化で処理されます。
サーキットブレーカー自体もfail-openです。Redisが状態追跡に利用できない場合、すべてのリクエストが通過します。これは全体的な哲学と一致しています:インフラストラクチャが劣化した場合、ユーザーの作業を続行させます。
同時実行制御
各アカウントはRedisベースのセマフォを介して10の同時LLM呼び出しに制限されています。これにより、単一のアカウントの大規模バッチ実行がプロバイダーへの利用可能なすべての接続を消費することを防ぎます。
セマフォは5分のTTLセーフティネット付きのINCR/DECRを使用します(プロセスがデクリメントせずにクラッシュした場合、カウンターは自動的に期限切れになります)。容量超過時にはリークを避けるためにカウンターがすぐにデクリメントされます。他のすべてと同様に、Redisエラー時にはfail-openです。
学んだ教訓
**fail-openはオプション機能の正しいデフォルトです。**BYOKは拡張機能であり、要件ではありません。暗号化パイプライン、キャッシュレイヤー、またはキー解決が失敗した場合、ユーザーはworkflowを引き続き実行できるべきです。調査のために障害をログに記録しますが、実行をブロックすることはありません。
キャッシュセンチネル値がサンダリングハードを防ぎます。__none__センチネルがなければ、BYOKキーのないアカウントはすべてのLLM呼び出しでFirestoreにアクセスすることになります。否定的な結果を5分間キャッシュすることで、Firestoreの読み取りを予測可能に保ちます。
**エフェメラルプロバイダーインスタンスがキー漏洩を防ぎます。**呼び出しごとに新しいプロバイダーインスタンスを作成しガベージコレクションに任せることで、復号化されたキーがメモリに存在するウィンドウを最小化します。ハードウェアセキュリティモジュールほど強力ではありませんが、SaaSアプリケーションにとって攻撃対象面の有意な縮小です。
**プロバイダーごとのサーキットブレーカーとアカウントごとの同時実行が適切な粒度です。**プロバイダーの障害はグローバルイベントです。同時実行はテナントごとの関心事です。混合すると、攻撃的すぎる(1つのアカウントがブレーカーを壊すと全員に影響)か、寛容すぎる(プロバイダー全体の障害に対する保護なし)かのいずれかになります。