Cloud GatewayでのBYOK(Bring Your Own Key)サポート - Support BYOK (Bring Your Own Key) for Cloud Gateways
クラウドにデプロイされたゲートウェイで、ユーザーが独自のAIプロバイダーAPIキー(Anthropic、OpenAI)を提供できるようにするための実装ガイド。カスタム請求とクォータ管理を可能にします。
🔍 症状
BYOKサポートが必要であることを示すユースケースシナリオ
クラウドにデプロイされたゲートウェイを操作する際に、ユーザーが以下の状況に遭遇します:
シナリオ1:請求の統合管理
ユーザーは、AI APIの使用料がアプリケーションの請求システムではなく、独自の企業アカウントに表示されることを望んでいます。
# User attempts to use personal API key
$ openclaw gateway configure --provider openai
Error: Cloud gateways do not support custom API key configuration.
Bundled credentials will be used instead.
シナリオ2:クォータ管理
企業ユーザーは、アプリケーションのプールされた制限に縛られるのではなく、既存のAPIレート制限とクォータ割り当てを活用する必要があります。
# Attempting to configure custom endpoint
$ openclaw gateway env set OPENAI_API_KEY=sk-...
Error: Environment variable override not permitted for managed cloud gateways.
シナリオ3:マルチプロバイダー環境
複数のAIプロバイダー(Claude用のAnthropic、GPTモデル用のOpenAI)に同時にアクセスする必要がある組織は、自分のアカウント間で使用量の請求を分割することができません。
# Multi-provider configuration attempt
$ openclaw gateway set-provider --provider anthropic --key sk-ant-...
ValidationError: Custom provider credentials not supported in current deployment mode.
シナリオ4:コンプライアンスと監査要件
厳格なコンプライアンス義務を持つ企業では、監査証跡の目的で、すべてのAPI呼び出しを自分の認証情報で行う必要があります。
# Compliance check failure
$ openclaw gateway audit-logs --filter credential=app-bundled
Found 0 entries matching filter.
All requests used bundled application credentials.
🧠 原因
アーキテクチャのギャップ分析
クラウドゲートウェイのBYOKサポートの欠如は、3つの根本的なアーキテクチャ上の制限に起因しています:
1. 認証情報注入モデル
現在のクラウドゲートウェイのデプロイでは、APIキーがコンテナ/ビルド時に埋め込まれるバンドル認証情報モデルを使用しています。デプロイパイプラインはCI/CDプロセス中に共有アプリケーション認証情報を注入します:
# Current deployment architecture
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Build Time │───▶│ Baked-in Keys │───▶│ Runtime │
│ Config │ │ (immutable) │ │ Gateway │
└─────────────┘ └──────────────────┘ └─────────────┘
# No runtime override capability
gateway-config.yamlには、ランタイムキー注入を有効にするcredential_modeフィールドが欠落しています。
2. セキュリティレイヤー制約
現在のセキュリティアーキテクチャには、クラウドデプロイ用のキーチェーンやシークレット管理統合レイヤーが含まれていません:
# lib/gateway/security/index.ts
interface GatewaySecurityConfig {
// Current state - no BYOK support
bundledCredentials: true;
runtimeOverride: false; // ← This is the gap
}
シークレットの注入はデプロイ時に行われ、ランタイム時には行われないため、ユーザーはデプロイ後に認証情報を更新できません。
3. プロバイダー設定スキーマ
ゲートウェイ設定スキーマは静的なプロバイダー定義のみをサポートしています:
# config/schema/gateway-config.json
{
"providers": {
"type": "object",
"properties": {
"name": { "type": "string" },
"endpoint": { "type": "string" }, // ← No credential field
"model": { "type": "string" }
}
}
// Missing: credential_mode, user_provided_key field
}
4. UI/UXコンポーネントのギャップ
設定インターフェースには、ユーザーが提供するAPIキーをキャプチャして検証するために必要なコンポーネントがありません:
# ui/components/settings/api-key-manager.tsx
// Current implementation - missing BYOK UI components
export function ApiKeyManager() {
return null; // Not implemented
}
🛠️ 解決手順
BYOKサポートの実装ロードマップ
フェーズ1:設定スキーマの更新
ステップ1.1:ランタイム認証情報注入をサポートする_gatewayway設定スキーマを更新します。
# config/schema/gateway-config.json
{
"$schema": "openclaw://schema/gateway/v2",
"credential_mode": "user_provided | bundled | hybrid",
"providers": {
"anthropic": {
"credential_mode": "user_provided",
"user_key_ref": "vault://anthropic-api-key", // Reference to secure storage
"fallback_key": "env:ANTHROPIC_API_KEY"
},
"openai": {
"credential_mode": "user_provided",
"user_key_ref": "keychain://openai-primary",
"fallback_key": "env:OPENAI_API_KEY"
}
}
}
ステップ1.2:認証情報プロバイダーインターフェースを作成します:
# lib/credentials/provider.ts
export interface CredentialProvider {
getCredential(provider: AIProvider): Promise;
setCredential(provider: AIProvider, key: string): Promise;
deleteCredential(provider: AIProvider): Promise;
validateCredential(provider: AIProvider, key: string): Promise;
}
export type AIProvider = 'anthropic' | 'openai' | 'custom';
export enum CredentialMode {
USER_PROVIDED = 'user_provided',
BUNDLED = 'bundled',
HYBRID = 'hybrid'
}
フェーズ2:セキュアストレージの実装
ステップ2.1:クライアントサイド(Keychain/Keystore)のセキュアストレージを実装します:
# lib/credentials/storage/keychain.ts
import { KeychainStorage } from '@openclaw/keychain';
export class SecureKeyStorage implements CredentialProvider {
private storage: KeychainStorage;
async setCredential(provider: AIProvider, key: string): Promise {
const sanitizedKey = this.sanitizeKey(key);
await this.storage.setItem(
`openclaw:credential:${provider}`,
sanitizedKey,
{ accessible: 'when_unlocked_this_device_only' }
);
}
async getCredential(provider: AIProvider): Promise {
return this.storage.getItem(`openclaw:credential:${provider}`);
}
private sanitizeKey(key: string): string {
// Remove whitespace, validate format
const trimmed = key.trim();
if (!this.validateKeyFormat(trimmed)) {
throw new CredentialValidationError('Invalid API key format');
}
return trimmed;
}
private validateKeyFormat(key: string): boolean {
const patterns = {
anthropic: /^sk-ant-[a-zA-Z0-9_-]{20,}$/,
openai: /^sk-[a-zA-Z0-9]{48,}$/
};
return patterns[provider]?.test(key) ?? false;
}
}
ステップ2.2:ゲートウェイサイド(環境変数注入)のセキュアストレージを実装します:
# lib/credentials/storage/gateway-secrets.ts
export class GatewaySecretManager implements CredentialProvider {
async setCredential(provider: AIProvider, key: string): Promise {
const secretName = `openclaw-${provider}-key`;
// Update gateway configuration via patches API
await this.configPatchService.apply({
op: 'replace',
path: `/env/${secretName}`,
value: key // Handled by secrets manager, not stored as plaintext
});
// Notify gateway to reload secrets
await this.gatewayService.signalReload(ReloadTrigger.SECRET_UPDATE);
}
}
フェーズ3:設定UIの実装
ステップ3.1:APIキー管理コンポーネントを作成します:
# ui/components/settings/api-key-manager.tsx
import { CredentialProvider, AIProvider } from '@openclaw/credentials';
interface ApiKeyManagerProps {
onCredentialSaved?: (provider: AIProvider) => void;
}
export function ApiKeyManager({ onCredentialSaved }: ApiKeyManagerProps) {
const [activeProvider, setActiveProvider] = useState('openai');
const [apiKey, setApiKey] = useState('');
const [showKey, setShowKey] = useState(false);
const [validationStatus, setValidationStatus] = useState('idle');
const handleSave = async () => {
setValidationStatus('validating');
try {
const isValid = await credentialProvider.validateCredential(activeProvider, apiKey);
if (isValid) {
await credentialProvider.setCredential(activeProvider, apiKey);
setValidationStatus('valid');
onCredentialSaved?.(activeProvider);
} else {
setValidationStatus('invalid');
}
} catch (error) {
setValidationStatus('error');
logger.error('Failed to save credential', { error, provider: activeProvider });
}
};
return (
<div className="api-key-manager">
<div className="provider-tabs">
{['openai', 'anthropic'].map(p => (
<button
key={p}
className={activeProvider === p ? 'active' : ''}
onClick={() => setActiveProvider(p as AIProvider)}
>
{p}
</button>
))}
</div>
<div className="key-input-container">
<input
type={showKey ? 'text' : 'password'}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={`Enter ${activeProvider} API key`}
/>
<button onClick={() => setShowKey(!showKey)}>
{showKey ? 'Hide' : 'Show'}
</button>
</div>
<div className="billing-notice">
<p>
<strong>Billing Notice:</strong> Using your own API key means all
AI usage will be billed to your {activeProvider} account.
Ensure your account has sufficient quota.
</p>
</div>
<button onClick={handleSave} disabled={!apiKey}>
Save {activeProvider} Key
</button>
</div>
);
}
ステップ3.2:BYOKメッセージングコンポーネントを追加します:
# ui/components/settings/byok-disclosure.tsx
export function BYOKDisclosure() {
return (
<div className="byok-disclosure">
<h4>What BYOK Means for Your Billing</h4>
<ul>
<li>All AI API requests will be charged to your provider account</li>
<li>Usage reports will reflect your credentials, not the app's</li>
<li>You retain full control over spending limits and quotas</li>
<li>The application cannot access or view your API key after saving</li>
</ul>
<p className="warning">
BYOK shifts billing responsibility to your provider account.
Monitor usage at your provider's dashboard.
</p>
</div>
);
}
フェーズ4:ゲートウェイランタイムサポート
ステップ4.1:ランタイム認証情報注入をサポートする_gatewaywayを更新します:
# lib/gateway/runtime/credential-loader.ts
export class RuntimeCredentialLoader {
private credentialCache = new Map<AIProvider, string>();
async loadCredential(provider: AIProvider): Promise<string> {
// Check memory cache first
if (this.credentialCache.has(provider)) {
return this.credentialCache.get(provider)!;
}
// Check environment variables (set by secrets manager)
const envKey = this.getEnvVarName(provider);
const envValue = process.env[envKey];
if (envValue) {
this.credentialCache.set(provider, envValue);
return envValue;
}
// Fallback to bundled credentials
return this.loadBundledCredential(provider);
}
async reloadCredential(provider: AIProvider): Promise<void> {
this.credentialCache.delete(provider);
return this.loadCredential(provider);
}
private getEnvVarName(provider: AIProvider): string {
const mapping = {
anthropic: 'OPENCLAW_ANTHROPIC_KEY',
openai: 'OPENCLAW_OPENAI_KEY'
};
return mapping[provider];
}
}
ステップ4.2:設定パッチエンドポイントを実装します:
# lib/gateway/api/patch-endpoint.ts
router.patch('/gateway/config', async (req, res) => {
const { operation, path, value } = req.body;
// Validate operation is allowed for BYOK
if (path.startsWith('/env/OPENCLAW_')) {
const secretName = path.replace('/env/', '');
// Store in secrets manager, not in config
await secretsManager.set(secretName, value);
// Signal gateway to reload
await gatewayRuntime.reloadSecret(secretName);
return res.json({ success: true, message: 'Secret updated' });
}
return res.status(403).json({
error: 'Patch operation not permitted for this path'
});
});
🧪 検証
BYOK実装のテスト手順
テストケース1:APIキーの保存と取得
# Test: Store and retrieve API key
$ cd /path/to/openclaw
# Create test script
$ cat > test-byok-storage.ts << 'EOF'
import { SecureKeyStorage } from './lib/credentials/storage/keychain';
const storage = new SecureKeyStorage();
async function testStorage() {
// Test OpenAI key storage
await storage.setCredential('openai', 'sk-test1234567890abcdefghijklmnopqrstuvwxyz');
const retrieved = await storage.getCredential('openai');
console.log('OpenAI key stored:', retrieved === 'sk-test1234567890...' ? 'PASS' : 'FAIL');
// Test Anthropic key storage
await storage.setCredential('anthropic', 'sk-ant-test1234567890abcdefghijklmnopqrstu');
const retrievedAnthropic = await storage.getCredential('anthropic');
console.log('Anthropic key stored:', retrievedAnthropic ? 'PASS' : 'FAIL');
// Verify key format validation
try {
await storage.setCredential('openai', 'invalid-key');
console.log('Invalid key validation: FAIL - should have rejected');
} catch (e) {
console.log('Invalid key validation: PASS - correctly rejected');
}
}
testStorage().catch(console.error);
EOF
$ npx ts-node test-byok-storage.ts
OpenAI key stored: PASS
Anthropic key stored: PASS
Invalid key validation: PASS
テストケース2:ゲートウェイ設定パッチ
# Test: Gateway configuration patch for secrets
$ curl -X PATCH http://localhost:3000/gateway/config \
-H "Content-Type: application/json" \
-d '{"op":"replace","path":"/env/OPENCLAW_OPENAI_KEY","value":"sk-test-key"}'
# Expected response
{
"success": true,
"message": "Secret updated",
"secretName": "openclaw-openai-key"
}
# Verify gateway reloaded the secret
$ curl http://localhost:3000/gateway/status | jq '.secrets'
{
"openai_key": "loaded",
"anthropic_key": "bundled"
}
テストケース3:UIコンポーネントの検証
# Test: Settings UI BYOK components
$ cd ui && npm run test -- --grep "ApiKeyManager"
# Expected output
PASS ApiKeyManager renders provider tabs
PASS ApiKeyManager handles key input
PASS ApiKeyManager shows billing disclosure
PASS ApiKeyManager validates on save
$ npm run test -- --grep "BYOKDisclosure"
# Expected output
PASS BYOKDisclosure displays billing notice
PASS BYOKDisclosure renders warning message
テストケース4:エンドツーエンドBYOKフロー
# Complete integration test
$ cat > test-byok-e2e.ts << 'EOF'
import { createTestingEnvironment } from '@openclaw/test-utils';
async function testBYOKFlow() {
const env = await createTestingEnvironment();
// Step 1: Configure BYOK via UI
await env.ui.navigateToSettings();
await env.ui.click('[data-testid="api-key-manager"]');
await env.ui.selectProvider('openai');
await env.ui.enterApiKey('sk-test-e2e-key-1234567890abcdefghijklmnop');
await env.ui.click('Save');
// Step 2: Verify key stored securely
const stored = await env.credentialProvider.getCredential('openai');
console.assert(stored === 'sk-test-e2e-key-...', 'Key stored correctly');
// Step 3: Deploy to cloud gateway
await env.cli.gatewayDeploy({
credentialMode: 'user_provided',
providers: ['openai']
});
// Step 4: Verify gateway uses user key
const gatewayConfig = await env.gateway.getConfig();
console.assert(
gatewayConfig.providers.openai.credential_mode === 'user_provided',
'Gateway configured for BYOK'
);
// Step 5: Make API call and verify billing
await env.gateway.sendRequest({ model: 'gpt-4' });
const usage = await env.billing.getUsage({ provider: 'openai' });
console.assert(usage.account === 'user-provided', 'Usage billed to user');
console.log('BYOK E2E Test: PASS');
}
testBYOKFlow().catch(e => {
console.error('BYOK E2E Test: FAIL', e);
process.exit(1);
});
EOF
$ npx ts-node test-byok-e2e.ts
BYOK E2E Test: PASS
⚠️ よくある落とし穴
エッジケースと実装の罠
- ゲートウェイの再起動なしのキーローテーション:ユーザーがAPIキーをローテートするとき、完全な再デプロイなしにゲートウェイが変更を認識することを確認します。適切なロックを伴うシグナルベースのリロードメカニズムを実装します。
- キー形式検証の違い:APIキー形式はプロバイダー間で異なり、変化する可能性があります。アプリケーションの新バージョンをリリースせずに更新できる検証サービスを作成します。
- コンテナでの環境変数注入:一部のコンテナオーケストレーションプラットフォーム(例:AWS ECS、GKE)は、シークレット注入の処理が異なります。サポートされているすべてのプラットフォームに対してゲートウェイをテストします。
- 保存時の認証情報ストレージ暗号化:Keychain/Keystoreに保存されたキーは暗号化する必要があります。コンプライアンスドキュメントで指定されたセキュリティ要件が満たされていることを確認します。
- ログでのメモリ露出:APIキーがアプリケーションログに表示されないことを確認します。あらゆるログ出力でキーをマスクするリダクションレイヤーを実装します。
- マルチアカウントシナリオ:同じプロバイダーの複数のアカウント(例:開発用と本番用のOpenAIアカウント)を持つユーザーは、両方のキーを混乱なく管理するための明確なUIが必要です。
- フォールバック動作の曖昧さ:ユーザーが提供するキーが失敗した場合(期限切れ、取り消し)、ゲートウェイはバンドル認証情報にサイレントにフォールバックするのではなく、失敗を明確に通知する必要があります。
- 既存デプロイメントへの移行パス:既存のクラウドゲートウェイを持つユーザーは、現在の設定を失うことなくBYOKを有効にする移行パスが必要です。
セキュリティ上の考慮事項
# Pitfall: Logging sensitive data
// ❌ WRONG
logger.info('User configured API key', { key: apiKey });
// ✅ CORRECT
logger.info('User configured API key', {
keyPrefix: apiKey.substring(0, 8) + '...',
keyLength: apiKey.length
});
# Pitfall: Storing plaintext in config files
// ❌ WRONG - config persisted with actual key
{
"openai_key": "sk-actualkeyvaluehere"
}
// ✅ CORRECT - reference to secrets manager
{
"openai_key_ref": "keychain://openai-primary"
}
🔗 関連するエラー
文脈的に 관련된問題と歴史的背景
| Reference | Description | Relation |
|---|---|---|
| #221 | Local gateway BYOK support via setup wizard | Prior implementation that this feature extends. Local gateway already has the UI and storage patterns needed; this issue adapts them for cloud deployments. |
| #189 | API key secure storage specification | Defines the secure storage architecture (Keychain/Keystore) that BYOK must leverage for client-side credential management. |
| #215 | Gateway configuration schema v2 | Updates the configuration schema to support runtime credential injection modes required for BYOK. |
| #98 | Multi-provider gateway support | Foundational work for handling multiple AI providers; BYOK builds on this to allow per-provider key management. |
| #178 | Secrets injection for managed services | Provides the secrets management infrastructure that BYOK uses for gateway-side secure storage. |
ドキュメント内の類似パターン
docs/cloud-gateways/security.md— Security architecture for cloud deployments (includes BYOK requirements)docs/providers/anthropic-setup.md— Provider-specific key configurationdocs/providers/openai-setup.md— Provider-specific key configurationdocs/settings/byok-settings-reference.md— Settings UI specification for BYOK components