Compatibilidad con BYOK para puertas de enlace en la nube - Support BYOK (Bring Your Own Key) for Cloud Gateways
Guía de implementación para permitir a los usuarios proporcionar sus propias claves API de proveedores de IA (Anthropic, OpenAI) para puertas de enlace desplegadas en la nube, habilitando gestión personalizada de facturación y cuotas.
🔍 Síntomas
Escenarios de Uso que Indican la Necesidad de Soporte BYOK
Los usuarios encuentran las siguientes situaciones al operar puertas de enlace implementadas en la nube:
Escenario 1: Centralización de Facturación
Los usuarios quieren que los cargos por uso de la API de IA aparezcan bajo su propia cuenta corporativa en lugar del sistema de facturación de la aplicación.
# 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.
Escenario 2: Gestión de Cuotas
Los usuarios empresariales necesitan aprovechar sus límites de tasa y asignaciones de cuota de API existentes en lugar de estar sujetos a los límites agrupados de la aplicación.
# Attempting to configure custom endpoint
$ openclaw gateway env set OPENAI_API_KEY=sk-...
Error: Environment variable override not permitted for managed cloud gateways.
Escenario 3: Entorno Multi-Proveedor
Las organizaciones que requieren acceso simultáneo a múltiples proveedores de IA (Anthropic para Claude, OpenAI para modelos GPT) no pueden separar la facturación de uso entre sus propias cuentas.
# Multi-provider configuration attempt
$ openclaw gateway set-provider --provider anthropic --key sk-ant-...
ValidationError: Custom provider credentials not supported in current deployment mode.
Escenario 4: Requisitos de Cumplimiento y Auditoría
Las empresas con mandatos estrictos de cumplimiento requieren que todas las llamadas a la API se realicen bajo sus propias credenciales para fines de seguimiento de auditoría.
# Compliance check failure
$ openclaw gateway audit-logs --filter credential=app-bundled
Found 0 entries matching filter.
All requests used bundled application credentials.
🧠 Causa raíz
Análisis de Brechas Arquitectónicas
La ausencia de soporte BYOK para puertas de enlace en la nube proviene de tres limitaciones arquitectónicas fundamentales:
1. Modelo de Inyección de Credenciales
Los despliegues actuales de puertas de enlace en la nube utilizan un modelo de credenciales integradas donde las claves de API se incrustan en el momento del contenedor/compilación. El pipeline de despliegue inyecta credenciales compartidas de la aplicación durante el proceso de CI/CD:
# Current deployment architecture
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Build Time │───▶│ Baked-in Keys │───▶│ Runtime │
│ Config │ │ (immutable) │ │ Gateway │
└─────────────┘ └──────────────────┘ └─────────────┘
# No runtime override capability
El gateway-config.yaml carece de un campo credential_mode que permitiría la inyección de claves en tiempo de ejecución.
2. Restricciones de la Capa de Seguridad
La arquitectura de seguridad actual no incluye una capa de integración de gestión de claves o secretos para despliegues en la nube:
# lib/gateway/security/index.ts
interface GatewaySecurityConfig {
// Current state - no BYOK support
bundledCredentials: true;
runtimeOverride: false; // ← This is the gap
}
La inyección de secretos ocurre en el despliegue, no en tiempo de ejecución, lo que impide a los usuarios actualizar credenciales después del despliegue.
3. Esquema de Configuración del Proveedor
El esquema de configuración de la puerta de enlace solo soporta definiciones estáticas de proveedores:
# 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. Brecha en Componentes de UI/UX
La interfaz de configuración carece de los componentes necesarios para capturar y validar claves de API proporcionadas por el usuario:
# ui/components/settings/api-key-manager.tsx
// Current implementation - missing BYOK UI components
export function ApiKeyManager() {
return null; // Not implemented
}
🛠️ Solución paso a paso
Hoja de Ruta de Implementación para Soporte BYOK
Fase 1: Actualizaciones del Esquema de Configuración
Paso 1.1: Actualizar el esquema de configuración de la puerta de enlace para soportar inyección de credenciales en tiempo de ejecución.
# 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"
}
}
}
Paso 1.2: Crear una interfaz de proveedor de credenciales:
# 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'
}
Fase 2: Implementación de Almacenamiento Seguro
Paso 2.1: Implementar almacenamiento seguro del lado del cliente (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;
}
}
Paso 2.2: Implementar almacenamiento seguro del lado de la puerta de enlace (inyección de variables de entorno):
# 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);
}
}
Fase 3: Implementación de UI de Configuración
Paso 3.1: Crear el componente de gestión de claves de 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>
);
}
Paso 3.2: Agregar componente de mensaje 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>
);
}
Fase 4: Soporte de Runtime de la Puerta de Enlace
Paso 4.1: Actualizar la puerta de enlace para soportar inyección de credenciales en tiempo de ejecución:
# 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];
}
}
Paso 4.2: Implementar endpoint de parches de configuración:
# 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'
});
});
🧪 Verificación
Procedimientos de Prueba para Implementación BYOK
Caso de Prueba 1: Almacenamiento y Recuperación de Clave de 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
Caso de Prueba 2: Parche de Configuración de la Puerta de Enlace
# 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"
}
Caso de Prueba 3: Validación de Componentes de 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
Caso de Prueba 4: Flujo BYOK de Extremo a Extremo
# 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
⚠️ Errores comunes
Casos Extremos y Trampas de Implementación
- Rotación de Claves Sin Reinicio de la Puerta de Enlace: Cuando los usuarios rotan su clave de API, asegúrese de que la puerta de enlace detecte el cambio sin requerir un redespliegue completo. Implemente un mecanismo de recarga basado en señales con bloqueo adecuado.
- Diferencias en Validación de Formato de Clave: Los formatos de clave de API varían entre proveedores y pueden cambiar. Cree un servicio de validación que pueda actualizarse sin lanzar nuevas versiones de la aplicación.
- Inyección de Variables de Entorno en Contenedores: Algunas plataformas de orquestación de contenedores (por ejemplo, AWS ECS, GKE) manejan la inyección de secretos de manera diferente. Pruebe la puerta de enlace en todas las plataformas soportadas.
- Cifrado en Reposo para Almacenamiento de Credenciales: Las claves almacenadas en Keychain/Keystore deben estar cifradas. Verifique que el cifrado cumpla con los requisitos de seguridad especificados en la documentación de cumplimiento.
- Exposición de Memorias en Registros: Asegúrese de que las claves de API nunca aparezcan en los registros de la aplicación. Implemente una capa de redacción que oculte las claves en cualquier salida de registro.
- Escenarios Multi-Cuenta: Los usuarios con múltiples cuentas para el mismo proveedor (por ejemplo, cuentas de desarrollo y producción de OpenAI) necesitan una UI clara para gestionar ambas claves sin confusión.
- Ambigüedad en Comportamiento de Reserva: Cuando una clave proporcionada por el usuario falla (expirada, revocada), la puerta de enlace debe indicar claramente el error en lugar de volver silenciosamente a las credenciales integradas.
- Ruta de Migración para Despliegues Existentes: Los usuarios con puertas de enlace en la nube existentes necesitan una ruta de migración para habilitar BYOK sin perder su configuración actual.
Consideraciones de Seguridad
# 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"
}
🔗 Errores relacionados
Problemas Conextados Contextualmente e Contexto Histórico
| Referencia | Descripción | Relación |
|---|---|---|
| #221 | Soporte BYOK de puerta de enlace local a través del asistente de configuración | Implementación previa que esta característica extiende. La puerta de enlace local ya tiene los patrones de UI y almacenamiento necesarios; este problema los adapta para despliegues en la nube. |
| #189 | Especificación de almacenamiento seguro de claves de API | Define la arquitectura de almacenamiento seguro (Keychain/Keystore) que BYOK debe aprovechar para la gestión de credenciales del lado del cliente. |
| #215 | Esquema de configuración de puerta de enlace v2 | Actualiza el esquema de configuración para soportar modos de inyección de credenciales en tiempo de ejecución requeridos para BYOK. |
| #98 | Soporte de puerta de enlace multi-proveedor | Trabajo fundacional para manejar múltiples proveedores de IA; BYOK se basa en esto para permitir la gestión de claves por proveedor. |
| #178 | Inyección de secretos para servicios gestionados | Proporciona la infraestructura de gestión de secretos que BYOK utiliza para el almacenamiento seguro del lado de la puerta de enlace. |
Patrones Similares en Documentación
docs/cloud-gateways/security.md— Arquitectura de seguridad para despliegues en la nube (incluye requisitos BYOK)docs/providers/anthropic-setup.md— Configuración de clave específica del proveedordocs/providers/openai-setup.md— Configuración de clave específica del proveedordocs/settings/byok-settings-reference.md— Especificación de UI de configuración para componentes BYOK