April 20, 2026 • 版本: v1.2.0+

[云网关支持自带密钥] - Support BYOK (Bring Your Own Key) for Cloud Gateways

实现指南:用于在云部署的OpenClaw网关上启用自定义AI提供商API密钥管理,具备安全存储和计费透明度功能。

🔍 概述

本指南涵盖为云部署的 OpenClaw 网关添加自选密钥(BYOK)支持的实施要求。用户必须能够提供自己的 AI 提供商 API 密钥(OpenAI、Anthropic、Google AI 等),而不是依赖应用程序捆绑的凭证。

功能范围

云网关的 BYOK 系统需要在三个层面进行实施:

  • 客户端 UI:用于输入、查看和管理每个提供商的 API 密钥的设置界面
  • 安全存储:桌面客户端的密钥链集成,移动端的加密保管库
  • 网关配置:云部署网关的环境变量注入机制

现有实现参考

本地网关已通过设置向导实现 BYOK(参见 PR #221)。云网关实现应与既定模式保持一致,同时解决云特定的问题:


本地网关 BYOK 架构:
┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│ 设置向导    │────▶│ 安全存储     │────▶│ .env.local  │
│   (UI)      │     │   (Keychain) │     │   (文件)    │
└─────────────┘     └──────────────┘     └─────────────┘

云网关 BYOK 架构:
┌─────────────┐     ┌──────────────┐     ┌──────────────────┐     ┌─────────────┐
│ 设置 UI     │────▶│ 安全保管库   │────▶│ 配置 API 补丁    │────▶│ 环境变量    │
│   (云端)    │     │  (加密)      │     │   (网关)         │     │ 注入        │
└─────────────┘     └──────────────┘     └──────────────────┘     └─────────────┘

🧠 技术要求

架构组件

1. API 密钥提供商注册表

系统必须维护支持的 AI 提供商及其配置要求的注册表:


// 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. 安全存储规范

客户端存储(密钥链/安全飞地)

// src/storage/secure-keychain.ts
interface SecureKeyStorage {
  // 使用提供商标识符存储 API 密钥
  setApiKey(provider: string, key: string): Promise<boolean>;
  
  // 检索 API 密钥(如果未找到则返回 null)
  getApiKey(provider: string): Promise<string | null>;
  
  // 列出已配置的提供商(不暴露密钥)
  listProviders(): Promise<string[]>;
  
  // 删除 API 密钥
  deleteApiKey(provider: string): Promise<boolean>;
  
  // 存储前验证密钥格式
  validateKeyFormat(provider: string, key: string): ValidationResult;
}

interface ValidationResult {
  valid: boolean;
  error?: string;
  maskedKey?: string; // 例如 "sk-...xyz"
}
密钥格式验证模式

// 按提供商划分的验证模式
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

云网关需要安全的配置补丁机制:


// 网关配置 API 端点
// POST /api/v1/gateway/config/patch

interface ConfigPatchRequest {
  operation: 'set' | 'remove';
  target: 'env' | 'secret';
  key: string;
  value?: string; // 设置操作时必填
  metadata?: {
    provider?: string;
    createdAt: string;
    expiresAt?: string;
  };
}

interface ConfigPatchResponse {
  success: boolean;
  appliedAt: string;
  restartRequired: boolean;
  error?: string;
}

🛠️ 实施步骤

阶段 1:设置 UI 组件

步骤 1.1:创建 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[]>([]);
  
  // 挂载时加载已配置的提供商
  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;
    }

    // 安全存储
    await KeychainStorage.setApiKey(selectedProvider, apiKey);
    
    // 同步到云网关
    await syncKeyToGateway(selectedProvider, apiKey);
    
    // 清除输入并刷新列表
    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>
  );
}

步骤 1.2:创建计费免责声明组件


// src/components/Settings/BillingDisclaimer.tsx

export function BillingDisclaimer() {
  return (
    <div className="billing-disclaimer">
      <div className="disclaimer-icon">💳</div>
      <div className="disclaimer-content">
        <h4>计费责任</h4>
        <p>
          当您提供自己的 API 密钥时,所有使用费用将直接计费到您
          的 AI 提供商账户。OpenClaw 不会处理、加价或了解您的 API 使用情况或账单。
        </p>
        <ul>
          <li>您需要负责提供商的使用限制和配额</li>
          <li>API 密钥会安全传输,绝不会以明文形式存储在服务器上</li>
          <li>您可以随时从提供商的仪表板撤销访问权限</li>
        </ul>
        <a href="../../../docs/byok/billing-faq" target="_blank">
          了解更多关于 BYOK 计费的信息 →
        </a>
      </div>
    </div>
  );
}

阶段 2:安全存储实现

步骤 2.1:密钥链存储服务


// 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}`;
    
    // 平台特定实现
    if (Platform.OS === 'ios' || Platform.OS === 'android') {
      return this.setSecureItem(service, key);
    }
    
    // 桌面:使用 electron-store 加密或原生 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[]> {
    // 返回存储了密钥的提供商列表(不暴露密钥本身)
    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 {
    // 显示前 3 个和后 4 个字符
    if (key.length <= 10) return '***';
    return `${key.slice(0, 3)}...${key.slice(-4)}`;
  }
}

阶段 3:网关配置同步

步骤 3.1:云网关 API 客户端


// 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', // 对于 API 密钥使用 secret,而不是 env
        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();
  }
}

步骤 3.2:网关端密钥管理

在云网关端,实现密钥轮换和安全注入:


# 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> {
    // 从安全存储加载所有 BYOK 密钥
    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);
  }

  // 当调用 AI 提供商时调用
  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;
  }
}

阶段 4:配置迁移

之前(使用捆绑凭证)


# gateway/.env (由 OpenClaw 管理)
AI_PROVIDER=openai
OPENAI_API_KEY=sk-org-managed-key-12345
ANTHROPIC_API_KEY=sk-ant-org-managed-key-67890

之后(用户 BYOK 及备用方案)


# gateway/.env (部分,非敏感)
AI_PROVIDER=openai
USE_BUNDLED_CREDENTIALS=false
BYOK_ENABLED=true

# 密钥单独存储(永不提交到仓库)
# 运行时从安全密钥管理器加载
# OPENAI_API_KEY=sk-user-provided-key (从密钥注入)

🧪 验证

验证清单

执行以下测试以验证 BYOK 实现:

测试 1:密钥存储验证


// 单元测试:密钥链存储操作
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');
  });
});

测试 2:网关配置同步


// 集成测试:网关配置同步
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 () => {
    // 使用无效令牌设置
    const sync = new GatewayConfigSync('unauthorized-gateway');
    
    await expect(sync.syncApiKey('openai', 'sk-test'))
      .rejects.toThrow('Failed to sync API key');
  });
});

测试 3:端到端 BYOK 流程


# E2E 测试脚本
#!/bin/bash

GATEWAY_ID="test-e2e-gateway"
PROVIDER="openai"
TEST_KEY="sk-test-$(date +%s)"

echo "=== BYOK 端到端测试 ==="

# 1. 本地存储密钥
echo "1. 在 Keychain 中存储密钥..."
# 客户端操作(伪代码)
client.setApiKey --provider $PROVIDER --key $TEST_KEY

# 2. 同步到网关
echo "2. 同步到云网关..."
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. 验证网关有密钥
echo "3. 验证网关配置..."
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'
# 预期:true

# 4. 测试 AI 请求使用用户密钥
echo "4. 使用 BYOK 凭证测试 AI 请求..."
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"}]}'

# 验证 X-Billing-Mode 头表示 BYOK
# 预期:X-Billing-Mode: byok

echo "=== 测试完成 ==="

预期测试输出

测试预期结果
Key Storage密钥可检索,掩码显示格式正确
Format Validation无效密钥被拒绝并显示描述性错误
Gateway Sync200 OK,restartRequired: true
AI Request使用用户提供的密钥请求成功

⚠️ 常见陷阱

安全陷阱

  • 日志记录密钥:绝不记录 API 密钥,即使部分记录也不行。确保所有日志语句都编辑敏感值。
    // ❌ 错误
    logger.info(`Using API key: ${apiKey}`);
    

    // ✅ 正确 logger.debug(Using API key for provider: ${provider});

  • 错误消息泄露:验证错误可能暴露密钥结构。在所有错误响应中掩码密钥。
    // ❌ 错误
    throw new Error(`Invalid key: ${providedKey}`);
    

    // ✅ 正确 throw new Error(Invalid key format. Key must match pattern for ${provider});

  • 内存保留:内存中的 API 密钥应在可能的情况下使用后清除。考虑使用安全字符串模式。

平台特定问题

平台问题解决方案
macOSKeychain 访问被拒绝在配置文件中请求权限
WindowsCredential Manager 回退确保 Electron 的 safeStorage API 可用
Linuxlibsecret 不可用实现使用加密文件存储的回退
移动端 (iOS)Secure Enclave 限制使用 ASAuthorizationManager 访问密钥链
移动端 (Android)Keystore 加密需要 API 23+ 以支持硬件支持存储

用户体验陷阱

  • 计费责任不明确:用户必须了解他们直接由 AI 提供商计费。计费免责声明是必填项。
  • 无密钥轮换警告:警告用户更改 API 密钥需要重启网关。
  • 缺少密钥过期处理:某些提供商(Azure)有密钥过期时间。实施主动警告。

配置陷阱

// ❌ 陷阱:覆盖捆绑凭证
// 如果网关同时有捆绑凭证和 BYOK,哪个优先?

// ✅ 解决方案:明确优先级
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');
};

🔗 相关功能与错误

相关实现

  • #221 - 本地网关 BYOK:通过设置向导实现 BYOK 的现有实现。云网关 BYOK 应共享核心组件,同时处理云特定的安全要求。
  • 密钥轮换系统:计划中的自动 API 密钥轮换增强功能(参见路线图)。
  • 多提供商回退:允许配置多个提供商并实现自动故障转移。

相关配置选项

设置描述默认值
AI_PROVIDER活跃的 AI 提供商openai
USE_BUNDLED_CREDENTIALS回退到捆绑密钥true
BYOK_ENABLED启用 BYOK 功能标志true
KEYCHAIN_SERVICEKeychain 服务标识符openclaw.byok

相关文档

未来考虑

  • 范围限定密钥:支持提供商范围限定的密钥(例如 Azure 端点特定密钥),并改进验证。
  • 团队 BYOK:团队共享 API 密钥管理及审计日志记录的企业功能。
  • 成本估算:与提供商 API 集成,在请求前显示使用估算。

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。