Support BYOK (Bring Your Own Key) for Cloud Gateways
Implementation guide for enabling custom AI provider API key management in cloud-deployed OpenClaw gateways with secure storage and billing transparency.
π Overview
This guide covers the implementation requirements for adding Bring Your Own Key (BYOK) support to cloud-deployed OpenClaw gateways. Users must be able to provide their own AI provider API keys (OpenAI, Anthropic, Google AI, etc.) instead of relying on the application’s bundled credentials.
Feature Scope
The BYOK system for cloud gateways requires implementation across three layers:
- Client-Side UI: Settings interface for entering, viewing, and managing API keys per provider
- Secure Storage: Keychain integration on desktop clients, encrypted vault on mobile
- Gateway Configuration: Environment variable injection mechanism for cloud-deployed gateways
Existing Implementation Reference
Local gateways already implement BYOK via the setup wizard (see PR #221). The cloud gateway implementation should align with the established patterns while addressing cloud-specific considerations:
Local Gateway BYOK Architecture:
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Setup WizardββββββΆβ Secure StorageββββββΆβ .env.local β
β (UI) β β (Keychain) β β (File) β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
Cloud Gateway BYOK Architecture:
βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ βββββββββββββββ
β Settings UI ββββββΆβ Secure Vault ββββββΆβ Config API Patch ββββββΆβ Env Var β
β (Cloud) β β (Encrypted) β β (Gateway) β β Injection β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββββββ βββββββββββββββ
π§ Technical Requirements
Architecture Components
1. API Key Provider Registry
The system must maintain a registry of supported AI providers with their configuration requirements:
// src/providers/registry.ts
export interface ProviderConfig {
id: string;
name: string;
apiKeyEnvVar: string;
endpoint?: string;
requiresOrgId?: boolean;
documentationUrl: string;
}
export const AI_PROVIDERS: Record<string, ProviderConfig> = {
openai: {
id: 'openai',
name: 'OpenAI',
apiKeyEnvVar: 'OPENAI_API_KEY',
endpoint: 'https://api.openai.com/v1',
documentationUrl: 'https://platform.openai.com/docs/api-keys'
},
anthropic: {
id: 'anthropic',
name: 'Anthropic',
apiKeyEnvVar: 'ANTHROPIC_API_KEY',
documentationUrl: 'https://docs.anthropic.com/en/api/getting-started'
},
google: {
id: 'google',
name: 'Google AI',
apiKeyEnvVar: 'GOOGLE_API_KEY',
requiresOrgId: true,
documentationUrl: 'https://ai.google.dev/tutorials/setup'
}
};
2. Secure Storage Specification
Client-Side Storage (Keychain/Secure Enclave)
// src/storage/secure-keychain.ts
interface SecureKeyStorage {
// Store API key with provider identifier
setApiKey(provider: string, key: string): Promise<boolean>;
// Retrieve API key (returns null if not found)
getApiKey(provider: string): Promise<string | null>;
// List configured providers (without exposing keys)
listProviders(): Promise<string[]>;
// Remove API key
deleteApiKey(provider: string): Promise<boolean>;
// Validate key format before storage
validateKeyFormat(provider: string, key: string): ValidationResult;
}
interface ValidationResult {
valid: boolean;
error?: string;
maskedKey?: string; // e.g., "sk-...xyz"
}
Key Format Validation Patterns
// Validation patterns by provider
const KEY_PATTERNS = {
openai: /^sk-[A-Za-z0-9_-]{20,}$/,
anthropic: /^sk-ant-[A-Za-z0-9_-]{20,}$/,
google: /^[A-Za-z0-9_-]{39}$/,
azure: /^[A-Za-z0-9]{32}$/
};
3. Gateway Configuration API
The cloud gateway requires a secure configuration patch mechanism:
// Gateway Config API Endpoint
// POST /api/v1/gateway/config/patch
interface ConfigPatchRequest {
operation: 'set' | 'remove';
target: 'env' | 'secret';
key: string;
value?: string; // Required for 'set' operation
metadata?: {
provider?: string;
createdAt: string;
expiresAt?: string;
};
}
interface ConfigPatchResponse {
success: boolean;
appliedAt: string;
restartRequired: boolean;
error?: string;
}
π οΈ Implementation Steps
Phase 1: Settings UI Components
Step 1.1: Create API Key Management Panel
// src/components/Settings/ApiKeyManager.tsx
import { useState } from 'react';
import { KeychainStorage } from '@/storage/secure-keychain';
import { AI_PROVIDERS } from '@/providers/registry';
import { BillingDisclaimer } from './BillingDisclaimer';
export function ApiKeyManager() {
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [apiKey, setApiKey] = useState('');
const [isValidating, setIsValidating] = useState(false);
const [configuredProviders, setConfiguredProviders] = useState<string[]>([]);
// Load configured providers on mount
useEffect(() => {
KeychainStorage.listProviders().then(setConfiguredProviders);
}, []);
const handleSaveKey = async () => {
if (!selectedProvider || !apiKey) return;
setIsValidating(true);
const validation = KeychainStorage.validateKeyFormat(selectedProvider, apiKey);
if (!validation.valid) {
showError(validation.error);
setIsValidating(false);
return;
}
// Store securely
await KeychainStorage.setApiKey(selectedProvider, apiKey);
// Sync to cloud gateway
await syncKeyToGateway(selectedProvider, apiKey);
// Clear input and refresh list
setApiKey('');
setConfiguredProviders(await KeychainStorage.listProviders());
setIsValidating(false);
};
return (
<div className="api-key-manager">
<BillingDisclaimer />
<div className="provider-grid">
{Object.values(AI_PROVIDERS).map(provider => (
<ProviderCard
key={provider.id}
provider={provider}
isConfigured={configuredProviders.includes(provider.id)}
onSelect={() => setSelectedProvider(provider.id)}
/>
))}
</div>
{selectedProvider && (
<ApiKeyInputForm
provider={AI_PROVIDERS[selectedProvider]}
value={apiKey}
onChange={setApiKey}
onSubmit={handleSaveKey}
isLoading={isValidating}
/>
)}
</div>
);
}
Step 1.2: Create Billing Disclaimer Component
// src/components/Settings/BillingDisclaimer.tsx
export function BillingDisclaimer() {
return (
<div className="billing-disclaimer">
<div className="disclaimer-icon">π³</div>
<div className="disclaimer-content">
<h4>Billing Responsibility</h4>
<p>
When you provide your own API key, all usage costs are billed directly
to your account with the AI provider. OpenClaw does not process, markup,
or have visibility into your API usage or billing.
</p>
<ul>
<li>You're responsible for your provider's usage limits and quotas</li>
<li>API keys are transmitted securely and never stored in plaintext on servers</li>
<li>You can revoke access at any time from your provider's dashboard</li>
</ul>
<a href="../../docs/byok/billing-faq" target="_blank">
Learn more about BYOK billing β
</a>
</div>
</div>
);
}
Phase 2: Secure Storage Implementation
Step 2.1: Keychain Storage Service
// src/storage/secure-keychain.ts
export class KeychainStorage implements SecureKeyStorage {
private static readonly SERVICE_PREFIX = 'openclaw.byok';
static async setApiKey(provider: string, key: string): Promise<boolean> {
const service = `${this.SERVICE_PREFIX}.${provider}`;
// Platform-specific implementation
if (Platform.OS === 'ios' || Platform.OS === 'android') {
return this.setSecureItem(service, key);
}
// Desktop: Use electron-store with encryption or native Keychain
if (process.env.ELECTRON === 'true') {
return this.setElectronKeychain(service, key);
}
throw new Error(`Platform ${Platform.OS} does not support secure storage`);
}
static async getApiKey(provider: string): Promise<string | null> {
const service = `${this.SERVICE_PREFIX}.${provider}`;
if (Platform.OS === 'ios') {
return this.getIOSKeychain(service);
}
if (Platform.OS === 'android') {
return this.getAndroidKeystore(service);
}
if (process.env.ELECTRON === 'true') {
return this.getElectronKeychain(service);
}
return null;
}
static async listProviders(): Promise<string[]> {
// Return list of providers with stored keys (without exposing the keys)
const prefix = `${this.SERVICE_PREFIX}.`;
const services = await this.listSecureServices(prefix);
return services.map(s => s.replace(prefix, ''));
}
static validateKeyFormat(provider: string, key: string): ValidationResult {
const pattern = KEY_PATTERNS[provider];
if (!pattern) {
return { valid: false, error: `Unknown provider: ${provider}` };
}
const trimmedKey = key.trim();
if (!pattern.test(trimmedKey)) {
return {
valid: false,
error: `Invalid key format for ${provider}. Expected format: ${pattern.description}`
};
}
return {
valid: true,
maskedKey: this.maskKey(trimmedKey)
};
}
private static maskKey(key: string): string {
// Show first 3 and last 4 characters
if (key.length <= 10) return '***';
return `${key.slice(0, 3)}...${key.slice(-4)}`;
}
}
Phase 3: Gateway Configuration Sync
Step 3.1: Cloud Gateway API Client
// src/services/gateway-config-sync.ts
export class GatewayConfigSync {
private readonly gatewayApiBase: string;
constructor(gatewayId: string) {
this.gatewayApiBase = `https://${gatewayId}.gateways.openclaw.io`;
}
async syncApiKey(provider: string, apiKey: string): Promise<ConfigPatchResponse> {
const providerConfig = AI_PROVIDERS[provider];
if (!providerConfig) {
throw new Error(`Unknown provider: ${provider}`);
}
const response = await fetch(`${this.gatewayApiBase}/api/v1/config/patch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getGatewayToken()}`
},
body: JSON.stringify({
operation: 'set',
target: 'secret', // Use secret, not env, for API keys
key: providerConfig.apiKeyEnvVar,
value: apiKey,
metadata: {
provider,
createdAt: new Date().toISOString()
}
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to sync API key: ${error.message}`);
}
return response.json();
}
async removeApiKey(provider: string): Promise<ConfigPatchResponse> {
const providerConfig = AI_PROVIDERS[provider];
const response = await fetch(`${this.gatewayApiBase}/api/v1/config/patch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getGatewayToken()}`
},
body: JSON.stringify({
operation: 'remove',
target: 'secret',
key: providerConfig.apiKeyEnvVar
})
});
return response.json();
}
}
Step 3.2: Gateway-Side Secret Management
On the cloud gateway side, implement secret rotation and secure injection:
# gateway/src/middleware/secrets-injector.ts
import { SecretManager } from '@gateway/secrets';
export class SecretsInjector {
private secrets: Map<string, string> = new Map();
private secretManager: SecretManager;
constructor() {
this.secretManager = new SecretManager();
}
async loadSecrets(): Promise<void> {
// Load all BYOK secrets from secure storage
const userSecrets = await this.secretManager.listUserSecrets();
for (const secret of userSecrets) {
this.secrets.set(secret.key, secret.value);
}
}
getSecret(key: string): string | undefined {
return this.secrets.get(key);
}
// Called when AI provider is invoked
injectProviderCredentials(provider: string): Record<string, string> {
const envVars: Record<string, string> = {};
switch (provider) {
case 'openai':
envVars.OPENAI_API_KEY = this.getSecret('OPENAI_API_KEY') ?? '';
break;
case 'anthropic':
envVars.ANTHROPIC_API_KEY = this.getSecret('ANTHROPIC_API_KEY') ?? '';
break;
case 'google':
envVars.GOOGLE_API_KEY = this.getSecret('GOOGLE_API_KEY') ?? '';
break;
}
return envVars;
}
}
Phase 4: Configuration Migration
Before (Using Bundled Credentials)
# gateway/.env (managed by OpenClaw)
AI_PROVIDER=openai
OPENAI_API_KEY=sk-org-managed-key-12345
ANTHROPIC_API_KEY=sk-ant-org-managed-key-67890
After (User BYOK with Fallback)
# gateway/.env (partial, non-sensitive)
AI_PROVIDER=openai
USE_BUNDLED_CREDENTIALS=false
BYOK_ENABLED=true
# Secrets stored separately (never committed to repo)
# Loaded from secure secret manager at runtime
# OPENAI_API_KEY=sk-user-provided-key (injected from secrets)
π§ͺ Verification
Verification Checklist
Execute the following tests to validate the BYOK implementation:
Test 1: Key Storage Validation
// Unit test: Keychain storage operations
describe('KeychainStorage', () => {
it('should store and retrieve API key', async () => {
const testKey = 'sk-test-1234567890abcdefghijklmnop';
await KeychainStorage.setApiKey('openai', testKey);
const retrieved = await KeychainStorage.getApiKey('openai');
expect(retrieved).toBe(testKey);
});
it('should reject invalid key format', async () => {
const result = KeychainStorage.validateKeyFormat('openai', 'invalid-key');
expect(result.valid).toBe(false);
expect(result.error).toContain('Invalid key format');
});
it('should mask keys correctly', async () => {
const result = KeychainStorage.validateKeyFormat(
'openai',
'sk-1234567890abcdefghijklmnopqrstuvwxyz'
);
expect(result.maskedKey).toBe('sk-...qrst');
});
});
Test 2: Gateway Config Sync
// Integration test: Gateway configuration sync
describe('GatewayConfigSync', () => {
it('should sync API key to cloud gateway', async () => {
const sync = new GatewayConfigSync('test-gateway-123');
const response = await sync.syncApiKey('openai', 'sk-test-key');
expect(response.success).toBe(true);
expect(response.restartRequired).toBe(true);
expect(response.appliedAt).toBeDefined();
});
it('should handle unauthorized gateway access', async () => {
// Set up with invalid token
const sync = new GatewayConfigSync('unauthorized-gateway');
await expect(sync.syncApiKey('openai', 'sk-test'))
.rejects.toThrow('Failed to sync API key');
});
});
Test 3: End-to-End BYOK Flow
# E2E Test Script
#!/bin/bash
GATEWAY_ID="test-e2e-gateway"
PROVIDER="openai"
TEST_KEY="sk-test-$(date +%s)"
echo "=== BYOK End-to-End Test ==="
# 1. Store key locally
echo "1. Storing key in Keychain..."
# Client-side operation (pseudocode)
client.setApiKey --provider $PROVIDER --key $TEST_KEY
# 2. Sync to gateway
echo "2. Syncing to cloud gateway..."
curl -X POST "https://${GATEWAY_ID}.gateways.openclaw.io/api/v1/config/patch" \
-H "Authorization: Bearer $GATEWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": "set",
"target": "secret",
"key": "OPENAI_API_KEY",
"value": "'"$TEST_KEY"'"
}'
# 3. Verify gateway has key
echo "3. Verifying gateway configuration..."
RESPONSE=$(curl -s "https://${GATEWAY_ID}.gateways.openclaw.io/api/v1/config/status" \
-H "Authorization: Bearer $GATEWAY_TOKEN")
echo $RESPONSE | jq '.secrets.OPENAI_API_KEY.configured'
# Expected: true
# 4. Test AI request uses user key
echo "4. Testing AI request with BYOK credentials..."
curl -X POST "https://${GATEWAY_ID}.gateways.openclaw.io/api/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]}'
# Verify X-Billing-Mode header indicates BYOK
# Expected: X-Billing-Mode: byok
echo "=== Test Complete ==="
Expected Test Outputs
| Test | Expected Result |
|---|---|
| Key Storage | Key retrievable, masked display shows correct format |
| Format Validation | Invalid keys rejected with descriptive error |
| Gateway Sync | 200 OK, restartRequired: true |
| AI Request | Request succeeds with user-provided key |
β οΈ Common Pitfalls
Security Pitfalls
- Logging Secrets: Never log API keys, even partially. Ensure all logging statements redact sensitive values.
// β Incorrect logger.info(`Using API key: ${apiKey}`);// β Correct logger.debug(
Using API key for provider: ${provider}); - Error Message Leakage: Validation errors may expose key structure. Mask keys in all error responses.
// β Incorrect throw new Error(`Invalid key: ${providedKey}`);// β Correct throw new Error(
Invalid key format. Key must match pattern for ${provider}); - Memory Retention: API keys in memory should be cleared after use when possible. Consider using secure string patterns.
Platform-Specific Issues
| Platform | Issue | Solution |
|---|---|---|
| macOS | Keychain access denied | Request entitlement in provisioning profile |
| Windows | Credential Manager fallback | Ensure Electron’s safeStorage API is available |
| Linux | libsecret unavailable | Implement fallback with encrypted file storage |
| Mobile (iOS) | Secure Enclave limitations | Use ASAuthorizationManager for keychain access |
| Mobile (Android) | Keystore encryption | Require API 23+ for hardware-backed storage |
UX Pitfalls
- Unclear Billing Responsibility: Users must understand they're billed directly by the AI provider. The billing disclaimer is mandatory.
- No Key Rotation Warning: Warn users that changing API keys requires gateway restart.
- Missing Key Expiration Handling: Some providers (Azure) have key expiration. Implement proactive warnings.
Configuration Pitfalls
// β Pitfall: Overwriting bundled credentials
// If gateway has bundled credentials AND BYOK, which takes precedence?
// β
Solution: Explicit priority
const getEffectiveApiKey = (provider, byokKey, bundledKey) => {
if (byokKey) {
return { source: 'byok', key: byokKey };
}
if (bundledKey) {
return { source: 'bundled', key: bundledKey };
}
throw new Error('No API key configured');
};
π Related Features & Errors
Related Implementation
- #221 - Local Gateway BYOK: Existing implementation of BYOK via setup wizard. Cloud gateway BYOK should share core components while handling cloud-specific security requirements.
- Secret Rotation System: Planned enhancement for automated API key rotation (see roadmap).
- Multi-Provider Fallback: Allow configuring multiple providers with automatic failover.
Related Configuration Options
| Setting | Description | Default |
|---|---|---|
AI_PROVIDER | Active AI provider | openai |
USE_BUNDLED_CREDENTIALS | Fall back to bundled keys | true |
BYOK_ENABLED | Enable BYOK feature flag | true |
KEYCHAIN_SERVICE | Keychain service identifier | openclaw.byok |
Related Documentation
- BYOK Feature Overview
- Secret Storage Architecture
- OpenAI Provider Setup
- Anthropic Provider Setup
- BYOK Billing FAQ
Future Considerations
- Scoped Keys: Support for provider-scoped keys (e.g., Azure endpoint-specific keys) with improved validation.
- Team BYOK: Enterprise feature for team-shared API key management with audit logging.
- Cost Estimation: Integration with provider APIs to show usage estimates before requests.