Files
nas-runbooks/common/piege-virtual-key-nabil-key-bifrost-hermes-20260904.md
T

4.6 KiB

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) :

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 :

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

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

# 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

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

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 :

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.