API 密钥遭到泄露。团队成员离开。安全策略需要定期轮换。在不停机的情况下在实时 MCP 服务器上轮换密钥需要双密钥验证窗口,其中旧密钥和新密钥在过渡期间同时工作。本教程为封装 Scadio API 的 MCP 服务器实现密钥轮换,并提供优雅的弃用警告、可配置的重叠周期和自动轮换计划。
前置条件
- 已安装 Python 3.9+
- 请求已安装库
- 来自 scavio.dev 的 Scavio API 密钥
- 现有 MCP 服务器部署
操作指南
步骤 1: 设计密钥轮换状态机
定义关键状态(活动、轮换、弃用、撤销)以及它们之间的转换规则。在轮换期间,新旧密钥都必须有效。
Python
import os, requests, json, time, secrets
from datetime import datetime, timedelta
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'}
KEY_FILE = 'mcp_keys.json'
def generate_key() -> str:
return f'mcp_{secrets.token_hex(16)}'
def load_keys() -> dict:
if os.path.exists(KEY_FILE):
with open(KEY_FILE) as f:
return json.load(f)
return {'keys': {}}
def save_keys(data: dict):
with open(KEY_FILE, 'w') as f:
json.dump(data, f, indent=2)
# Key states: active -> rotating -> deprecated -> revoked
VALID_TRANSITIONS = {
'active': ['rotating'],
'rotating': ['deprecated'],
'deprecated': ['revoked'],
'revoked': [],
}
print('Key states:', list(VALID_TRANSITIONS.keys()))步骤 2: 实施轮换工作流程
创建函数来启动轮换(生成新密钥,将旧密钥标记为轮换)、完成轮换(将旧密钥标记为已弃用)以及撤销旧密钥。
Python
def create_key(name: str) -> str:
data = load_keys()
key = generate_key()
data['keys'][key] = {
'name': name,
'state': 'active',
'created': datetime.now().isoformat(),
'rotated_at': None,
'deprecated_at': None,
}
save_keys(data)
print(f'Created key: {name} ({key[:12]}...)')
return key
def initiate_rotation(old_key: str) -> str:
data = load_keys()
if old_key not in data['keys'] or data['keys'][old_key]['state'] != 'active':
raise ValueError('Can only rotate active keys')
# Mark old key as rotating
data['keys'][old_key]['state'] = 'rotating'
data['keys'][old_key]['rotated_at'] = datetime.now().isoformat()
# Create replacement key
new_key = generate_key()
data['keys'][new_key] = {
'name': data['keys'][old_key]['name'],
'state': 'active',
'created': datetime.now().isoformat(),
'replaces': old_key[:12],
}
save_keys(data)
print(f'Rotation started: {old_key[:12]}... -> {new_key[:12]}...')
print(f'Both keys are now valid. Update clients to use the new key.')
return new_key
def complete_rotation(old_key: str):
data = load_keys()
if data['keys'].get(old_key, {}).get('state') != 'rotating':
raise ValueError('Key is not in rotating state')
data['keys'][old_key]['state'] = 'deprecated'
data['keys'][old_key]['deprecated_at'] = datetime.now().isoformat()
save_keys(data)
print(f'Rotation complete: {old_key[:12]}... is now deprecated')
# Demo
key1 = create_key('production-v1')
key2 = initiate_rotation(key1)
complete_rotation(key1)步骤 3: 通过旋转感知验证密钥
构建一个验证函数,该函数接受活动密钥和轮换密钥,对已弃用的密钥发出警告,并拒绝已撤销的密钥。
Python
def validate_with_rotation(api_key: str) -> dict:
data = load_keys()
key_config = data['keys'].get(api_key)
if not key_config:
return {'valid': False, 'error': 'Unknown key'}
state = key_config['state']
if state == 'active':
return {'valid': True, 'warning': None}
elif state == 'rotating':
return {'valid': True, 'warning': 'This key is being rotated. Please switch to the new key.'}
elif state == 'deprecated':
return {'valid': False, 'error': 'Key deprecated. Use the replacement key.', 'deprecated_at': key_config.get('deprecated_at')}
elif state == 'revoked':
return {'valid': False, 'error': 'Key revoked.'}
return {'valid': False, 'error': 'Invalid key state'}
def secure_search_with_rotation(api_key: str, query: str) -> dict:
check = validate_with_rotation(api_key)
if not check['valid']:
return {'error': check['error']}
resp = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'query': query, 'country_code': 'us', 'num_results': 3})
result = {'results': resp.json().get('organic_results', [])}
if check.get('warning'):
result['warning'] = check['warning']
return result
# Test with the active key
result = secure_search_with_rotation(key2, 'test query')
print(f'Results: {len(result.get("results", []))}, Warning: {result.get("warning", "none")}')Python 示例
Python
import os, requests, json, secrets
from datetime import datetime
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'}
keys = {}
def create_key(name):
key = f'mcp_{secrets.token_hex(16)}'
keys[key] = {'name': name, 'state': 'active', 'created': datetime.now().isoformat()}
return key
def rotate_key(old_key):
keys[old_key]['state'] = 'rotating'
new_key = create_key(keys[old_key]['name'])
print(f'Rotated: {old_key[:12]}... -> {new_key[:12]}...')
return new_key
def validate(key):
k = keys.get(key, {})
if k.get('state') in ('active', 'rotating'):
resp = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'query': 'test', 'country_code': 'us', 'num_results': 1})
print(f'[{k["state"]}] Search OK')
else:
print(f'Key rejected: {k.get("state", "unknown")}')
k1 = create_key('prod')
k2 = rotate_key(k1)
validate(k1) # Still works (rotating)
validate(k2) # Works (active)JavaScript 示例
JavaScript
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const crypto = require('crypto');
const keys = {};
function createKey(name) {
const key = `mcp_${crypto.randomBytes(16).toString('hex')}`;
keys[key] = { name, state: 'active', created: new Date().toISOString() };
return key;
}
function rotateKey(oldKey) {
keys[oldKey].state = 'rotating';
const newKey = createKey(keys[oldKey].name);
console.log(`Rotated: ${oldKey.slice(0, 12)}... -> ${newKey.slice(0, 12)}...`);
return newKey;
}
async function validate(key) {
const k = keys[key];
if (!k || !['active', 'rotating'].includes(k.state)) {
console.log(`Key rejected: ${k?.state || 'unknown'}`);
return;
}
const resp = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ query: 'test', country_code: 'us', num_results: 1 })
});
console.log(`[${k.state}] Search OK`);
}
const k1 = createKey('prod');
const k2 = rotateKey(k1);
validate(k1).then(() => validate(k2));预期输出
JSON
Key states: ['active', 'rotating', 'deprecated', 'revoked']
Created key: production-v1 (mcp_a1b2c3d4...)
Rotation started: mcp_a1b2c3d4... -> mcp_e5f6g7h8...
Both keys are now valid. Update clients to use the new key.
Rotation complete: mcp_a1b2c3d4... is now deprecated
Results: 3, Warning: none
[rotating] Search OK
[active] Search OK