Rotation des clés Ed25519
Les emergency-action tokens (playbook runs, isolation commands, agent self-update wrappers) sont signés Ed25519 par la hub. L’agent vérifie contre une public key pinée. Pour la rotation sans downtime :
Le problème
Rotation naïve = nouvelle key + tous les agents vérifient immédiatement contre la nouvelle key → les tokens in-flight signés avec l’ancienne deviennent soudain invalides. En cas de compromise vous VOULEZ ça (révocation instantanée), mais pour une rotation planifiée vous voulez une grace.
Comment ça marche
La hub maintient un set de hub_signing_keys par tenant. Chaque key a is_active=true, optionnellement expires_at. À la rotation :
- Génère une nouvelle keypair Ed25519
- Public part
INSERTdanshub_signing_keysavecis_active=true,expires_at=NULL - Les active keys existantes reçoivent
expires_at = NOW() + grace_days × INTERVAL '1 day' - La private key est montrée UNE FOIS dans la response, puis plus jamais
L’agent fetche périodiquement GET /api/v1/agents/:id/signing-keys/active (uniquement non-expired actives). Pendant la grace l’agent a donc LES DEUX keys dans son trust set ; les tokens signés avec l’ancienne OU la nouvelle valident.
Après expires_at de l’ancienne key elle tombe automatiquement du trust set.
Comment le faire dans l’UI
/settings → onglet Signing keys :
- Donnez une raison (“annual rotation”, “suspected compromise”, …)
- Choisissez grace days (default 7, max 90)
- Cliquez Rotate now
- Copiez la private_hex affichée immédiatement — pas stockée, montrée une fois
- Collez dans la config de déploiement de la hub (env var
MONSYS_EMERGENCY_PRIVATE_KEY_HEXou secrets manager) - Redémarrez la hub. À partir de là, la hub signe avec la nouvelle key ; les anciens tokens restent valides pendant
grace_days.
Le tableau sous le bouton montre toutes les keys : ACTIVE (pas d’expiry), EXPIRES <date> (en grace), ou RETIRED.
Scénario compromise
En cas de suspicion de compromise :
- Rotate avec
grace_days=0(raccourcit grace à ~maintenant) - Collez la nouvelle private key dans le deploy
- Redémarrez la hub
- Tous les anciens tokens deviennent immédiatement invalides
Ça déclenche : les playbook-runs in-flight pas encore reçus par l’agent peuvent échouer. Donc à utiliser UNIQUEMENT pour un VRAI compromise — pas pour une maintenance planifiée.
API
GET /api/v1/signing-keys (admin only)POST /api/v1/signing-keys/rotate (admin, rate-limit 5/h)GET /api/v1/agents/:id/signing-keys/active (agent-auth)Body POST /rotate :
{ "reason": "annual rotation 2026", "grace_days": 7}Response (UNE FOIS) :
{ "id": "uuid", "public_hex": "abc…64", "private_hex": "def…128", "expires_grace_days": 7, "expires_at": "2026-05-17T...Z", "warning": "Save the private key now — it is shown only this once."}Audit
Chaque rotation log dans audit_log avec resource_type='signing_key', resource_id=<new_key_id>, IP, user, reason. Reviewable via /audit?resource_type=signing_key.