Les clés API sont compromises. Des membres de l'équipe partent. Les politiques de sécurité exigent une rotation régulière. Faire pivoter les clés sur un serveur MCP en direct sans temps d'arrêt nécessite une fenêtre de validation à double clé où les anciennes et les nouvelles clés fonctionnent simultanément pendant la période de transition. Ce tutoriel implémente la rotation des clés pour un serveur MCP qui encapsule l'API Scavio, avec des avertissements de dépréciation gracieux, des périodes de chevauchement configurables et une planification automatisée de la rotation.
Prérequis
- Python 3.9+ installé
- bibliothèque requests installée
- Une clé API Scavio depuis scavio.dev
- Un déploiement de serveur MCP existant
Parcours
Étape 1: Concevoir la machine d'état de rotation des clés
Définir les états des clés (active, en rotation, dépréciée, révoquée) et les règles de transition entre eux. Pendant la rotation, les anciennes et les nouvelles clés doivent fonctionner.
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()))Étape 2: Implémenter le workflow de rotation
Créer des fonctions pour initier la rotation (générer une nouvelle clé, marquer l'ancienne comme en rotation), compléter la rotation (marquer l'ancienne comme dépréciée) et révoquer les anciennes clés.
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)Étape 3: Valider les clés avec une conscience de la rotation
Construire une fonction de validation qui accepte les clés actives et en rotation, avertit sur les clés dépréciées et rejette les clés révoquées.
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")}')Exemple 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)Exemple 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));Sortie attendue
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