Compatibilidad con BYOK para Pasarelas en la Nube - Support BYOK (Bring Your Own Key) for Cloud Gateways
Guía de implementación para habilitar la gestión de claves API de proveedores de IA personalizados en puertas de enlace OpenClaw desplegadas en la nube con almacenamiento seguro y transparencia en la facturación.
🔍 Descripción general
Esta guía cubre los requisitos de implementación para agregar soporte de Bring Your Own Key (BYOK) a las puertas de enlace OpenClaw implementadas en la nube. Los usuarios deben poder proporcionar sus propias claves de API de proveedores de IA (OpenAI, Anthropic, Google AI, etc.) en lugar de depender de las credenciales incluidas en la aplicación.
Alcance de la función
El sistema BYOK para puertas de enlace en la nube requiere implementación en tres capas:
- Interfaz de usuario del cliente: Interfaz de configuración para ingresar, visualizar y administrar claves API por proveedor
- Almacenamiento seguro: Integración de keychain en clientes de escritorio, bóveda cifrada en dispositivos móviles
- Configuración de puerta de enlace: Mecanismo de inyección de variables de entorno para puertas de enlace implementadas en la nube
Referencia de implementación existente
Las puertas de enlace locales ya implementan BYOK a través del asistente de configuración (ver PR #221). La implementación de BYOK para puertas de enlace en la nube debe alinearse con los patrones establecidos mientras aborda consideraciones específicas de la nube:
Arquitectura BYOK de puerta de enlace local:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Setup Wizard│────▶│ Secure Storage│────▶│ .env.local │
│ (UI) │ │ (Keychain) │ │ (File) │
└─────────────┘ └──────────────┘ └─────────────┘
Arquitectura BYOK de puerta de enlace en la nube:
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Settings UI │────▶│ Secure Vault │────▶│ Config API Patch │────▶│ Env Var │
│ (Cloud) │ │ (Encrypted) │ │ (Gateway) │ │ Injection │
└─────────────┘ └──────────────┘ └──────────────────┘ └─────────────┘
🧠 Requisitos técnicos
Componentes de arquitectura
1. Registro de proveedores de claves API
El sistema debe mantener un registro de proveedores de IA admitidos con sus requisitos de configuración:
// 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. Especificación de almacenamiento seguro
Almacenamiento del lado del cliente (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"
}
Patrones de validación de formato de clave
// 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. API de configuración de puerta de enlace
La puerta de enlace en la nube requiere un mecanismo seguro de parche de configuración:
// 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;
}
🛠️ Pasos de implementación
Fase 1: Componentes de la interfaz de configuración
Paso 1.1: Crear panel de administración de claves API
// 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>
);
}
Paso 1.2: Crear componente de aviso de facturación
// src/components/Settings/BillingDisclaimer.tsx
export function BillingDisclaimer() {
return (
<div className="billing-disclaimer">
<div className="disclaimer-icon">💳</div>
<div className="disclaimer-content">
<h4>Responsabilidad de facturación</h4>
<p>
Cuando proporcionas tu propia clave API, todos los costos de uso se facturan directamente
a tu cuenta con el proveedor de IA. OpenClaw no procesa, aumenta ni tiene visibilidad sobre
tu uso de API o facturación.
</p>
<ul>
<li>Eres responsable de los límites de uso y cuotas de tu proveedor</li>
<li>Las claves API se transmiten de forma segura y nunca se almacenan en texto plano en servidores</li>
<li>Puedes revocar el acceso en cualquier momento desde el panel de tu proveedor</li>
</ul>
<a href="../../../docs/byok/billing-faq" target="_blank">
Más información sobre facturación BYOK →
</a>
</div>
</div>
);
}
Fase 2: Implementación de almacenamiento seguro
Paso 2.1: Servicio de almacenamiento Keychain
// 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)}`;
}
}
Fase 3: Sincronización de configuración de puerta de enlace
Paso 3.1: Cliente API de puerta de enlace en la nube
// 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();
}
}
Paso 3.2: Gestión de secretos del lado de la puerta de enlace
En el lado de la puerta de enlace en la nube, implementar rotación de secretos e inyección segura:
# 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;
}
}
Fase 4: Migración de configuración
Antes (usando credenciales incluidas)
# 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
Después (BYOK del usuario con respaldo)
# 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)
🧪 Verificación
Lista de verificación de verificación
Ejecutar las siguientes pruebas para validar la implementación de BYOK:
Prueba 1: Validación de almacenamiento de claves
// 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');
});
});
Prueba 2: Sincronización de configuración de puerta de enlace
// 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');
});
});
Prueba 3: Flujo BYOK de extremo a extremo
# 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 ==="
Resultados esperados de las pruebas
| Prueba | Resultado esperado |
|---|---|
| Almacenamiento de claves | Clave recuperable, la visualización enmascarada muestra el formato correcto |
| Validación de formato | Claves inválidas rechazadas con error descriptivo |
| Sincronización de puerta de enlace | 200 OK, restartRequired: true |
| Solicitud de IA | Solicitud exitosa con clave proporcionada por el usuario |
⚠️ Errores comunes
Errores de seguridad
- Registro de secretos: Nunca registrar claves API, ni siquiera parcialmente. Asegurarse de que todas las declaraciones de registro redacten los valores sensibles.
// ❌ Incorrecto logger.info(`Using API key: ${apiKey}`);// ✅ Correcto logger.debug(
Using API key for provider: ${provider}); - Fuga de mensajes de error: Los errores de validación pueden exponer la estructura de la clave. Enmascarar claves en todas las respuestas de error.
// ❌ Incorrecto throw new Error(`Invalid key: ${providedKey}`);// ✅ Correcto throw new Error(
Invalid key format. Key must match pattern for ${provider}); - Retención en memoria: Las claves API en memoria deben borrarse después de su uso cuando sea posible. Considerar el uso de patrones de cadenas seguras.
Problemas específicos de la plataforma
| Plataforma | Problema | Solución |
|---|---|---|
| macOS | Acceso a Keychain denegado | Solicitar derecho en el perfil de aprovisionamiento |
| Windows | Respaldo de Credential Manager | Asegurar que la API safeStorage de Electron esté disponible |
| Linux | libsecret no disponible | Implementar respaldo con almacenamiento de archivos cifrados |
| Móvil (iOS) | Limitaciones de Secure Enclave | Usar ASAuthorizationManager para acceso a keychain |
| Móvil (Android) | Cifrado de Keystore | Requerir API 23+ para almacenamiento respaldado por hardware |
Errores de experiencia de usuario
- Responsabilidad de facturación poco clara: Los usuarios deben entender que se les factura directamente por el proveedor de IA. El aviso de facturación es obligatorio.
- Sin advertencia de rotación de claves: Advertir a los usuarios que cambiar las claves API requiere reiniciar la puerta de enlace.
- Sin manejo de expiración de claves: Algunos proveedores (Azure) tienen expiración de claves. Implementar advertencias proactivas.
Errores de configuración
// ❌ Error: Sobrescribir credenciales incluidas
// Si la puerta de enlace tiene credenciales incluidas Y BYOK, ¿cuál tiene prioridad?
// ✅ Solución: Prioridad explícita
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');
};
🔗 Funciones y errores relacionados
Implementación relacionada
- #221 - BYOK de puerta de enlace local: Implementación existente de BYOK a través del asistente de configuración. El BYOK de puerta de enlace en la nube debe compartir componentes centrales mientras maneja los requisitos de seguridad específicos de la nube.
- Sistema de rotación de secretos: Mejora planificada para rotación automatizada de claves API (ver roadmap).
- Respaldo de múltiples proveedores: Permitir configurar múltiples proveedores con failover automático.
Opciones de configuración relacionadas
| Configuración | Descripción | Valor predeterminado |
|---|---|---|
AI_PROVIDER | Proveedor de IA activo | openai |
USE_BUNDLED_CREDENTIALS | Respaldo a claves incluidas | true |
BYOK_ENABLED | Habilitar marca de función BYOK | true |
KEYCHAIN_SERVICE | Identificador de servicio keychain | openclaw.byok |
Documentación relacionada
- Descripción general de la función BYOK
- Arquitectura de almacenamiento de secretos
- Configuración del proveedor OpenAI
- Configuración del proveedor Anthropic
- Preguntas frecuentes sobre facturación BYOK
Consideraciones futuras
- Claves con alcance: Soporte para claves con alcance del proveedor (por ejemplo, claves específicas del endpoint de Azure) con validación mejorada.
- BYOK de equipo: Función empresarial para administración de claves API compartidas por equipo con registro de auditoría.
- Estimación de costos: Integración con APIs de proveedores para mostrar estimaciones de uso antes de las solicitudes.