docs(runbook): piege cle virtuelle Nabil-Key vs cles dediees Hermes (ticket infra-2026-09-014)

This commit is contained in:
Gemini
2026-09-04 21:51:26 +01:00
parent 0281c51a48
commit da376a630e
2 changed files with 124 additions and 0 deletions
@@ -0,0 +1,120 @@
# Piège Clé Virtuelle Nabil-Key vs Clé Dédiée Consommateur Bifrost (04/09/2026)
## 1. Contexte & Problématique
Chaque instance Hermes et consommateur de la passerelle **Bifrost** dispose d'une clé virtuelle dédiée (Virtual Key ou VK) associée à une politique budgétaire propre dans la table `governance_budgets` :
- `vk-hermes-tt` $\rightarrow$ budget TT
- `vk-hermes-nyora` $\rightarrow$ budget Nyora
- `vk-hermes-perso` $\rightarrow$ budget Perso
- `Nabil-Key` $\rightarrow$ budget personnel Nabil (plafond strict : 8 \$/mois)
### Le Piège
Lors de configurations initiales ou de migrations manuelles, la valeur secrète de **`Nabil-Key`** (`bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d`) a été copiée par erreur dans `/opt/data/config.yaml` d'instances Hermes (`hermes-nyora`, `hermes-perso`).
### Symptômes
1. **Facturation croisée** : L'ensemble des coûts de tokens (appels LLM principaux et/ou appels `auxiliary.vision` / `auxiliary.approval`) est débité du budget personnel de 8 \$/mois au lieu de la sphère appropriée.
2. **Risque de coupure inopinée** : Lorsque le budget de 8 \$ est atteint, Bifrost rejette toutes les requêtes portant cette clé avec une erreur HTTP 429 / 401 (`budget exhausted`), bloquant à la fois l'usage personnel et l'instance Hermes concernée.
---
## 2. Procédure de Diagnostic
### Étape 1 : Récupérer les clés virtuelles officielles depuis Bifrost
Sur l'hôte hébergeant Bifrost (actuellement VPS Contabo `100.94.90.119` ou snapshot de base) :
```bash
python3 -c "
import sqlite3
conn = sqlite3.connect('/home/dsh-agent/bifrost/data/config.db')
c = conn.cursor()
c.execute('SELECT name, value, is_active FROM governance_virtual_keys;')
for name, val, active in c.fetchall():
print(f'{name}: {val[:12]}... (actif={active})')
"
```
*Correspondances attendues :*
- `vk-hermes-nyora` : commence par `sk-bf-90d6...`
- `vk-hermes-perso` : commence par `sk-bf-bb06...`
- `vk-hermes-tt` : commence par `sk-bf-2669...`
- `Nabil-Key` : commence par `bfk-0cd1...` (à proscrire dans les conteneurs d'agents)
### Étape 2 : Auditer `config.yaml` dans chaque instance Hermes
Sur le NAS Synology :
```bash
for inst in hermes-agent-tt hermes-agent-nyora hermes-agent-perso; do
echo "=== $inst ==="
docker exec $inst python3 -c '
import yaml
cfg = yaml.safe_load(open("/opt/data/config.yaml"))
m_key = cfg.get("model", {}).get("api_key", "")
print(" model.api_key:", m_key[:12] if m_key else "vide")
for k, v in cfg.get("auxiliary", {}).items():
if isinstance(v, dict) and v.get("api_key"):
print(f" auxiliary.{k}.api_key:", v["api_key"][:12])
'
done
```
**Alerte immédiate** si une clé commence par `bfk-0cd1...`.
---
## 3. Procédure de Correction Chirurgicale
Ne jamais réécrire l'intégralité du fichier `config.yaml` (risque d'écraser des réglages runtime ou de corrompre l'indentation YAML). Utiliser un remplacement chirurgical par `sed`.
### 1. Sauvegarde préalable obligatoire
```bash
docker exec <nom_conteneur> cp /opt/data/config.yaml /opt/data/config.yaml.bak-$(date +%Y%m%d)-cle-perso
```
### 2. Remplacement chirurgical de la valeur
```bash
# Exemple pour hermes-agent-perso avec sa clé vk-hermes-perso
docker exec <nom_conteneur> sed -i 's/bfk-0cd1fba7d440ca2eefd30b28a7349b7d28422dcbc49e982d/<VRAIE_CLE_VK>/g' /opt/data/config.yaml
```
### 3. Contrôle du diff
```bash
docker exec <nom_conteneur> diff -u /opt/data/config.yaml.bak-$(date +%Y%m%d)-cle-perso /opt/data/config.yaml
```
*Le diff ne doit comporter strictement que les lignes d'affectation de clé modifiées.*
### 4. Redémarrage propre du conteneur
```bash
docker restart <nom_conteneur>
```
---
## 4. Validation Post-Correction par Inférence Réelle
Exécuter un appel réel depuis l'intérieur du conteneur pour valider le bon fonctionnement de bout en bout :
```bash
docker exec <nom_conteneur> python3 -c '
import yaml, urllib.request, json, time
cfg = yaml.safe_load(open("/opt/data/config.yaml"))
m = cfg.get("model", {})
base_url = m.get("base_url", "http://bifrost-proxy/v1").rstrip("/")
api_key = m.get("api_key", "")
url = base_url + "/chat/completions"
payload = {
"model": m.get("default", "mimo-v2.5"),
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 10
}
req = urllib.request.Request(
url,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
},
data=json.dumps(payload).encode()
)
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read().decode())
print("Inférence OK :", data.get("model"), "en", data.get("extra_fields", {}).get("latency"), "ms")
'
```
*Attendu : `Inférence OK : mimo-v2.5 en <latence> ms`.*