Ed25519 signing-key rotatie
Emergency-action tokens (playbook runs, isolation commands, agent self-update wrappers) zijn Ed25519-gesigned door de hub. De agent verifieert tegen een gepinde public key. Voor rotatie zonder downtime:
Het probleem
Naïeve rotatie = nieuwe key + alle agents direct verifiëren tegen nieuwe key → in-flight tokens met de oude key worden plots ongeldig. Bij compromise wil je dat WEL (instant revocation), maar bij geplande rotatie WIL je grace.
Hoe het werkt
De hub houdt een set van hub_signing_keys per tenant. Elke key heeft is_active=true, optioneel expires_at. Bij rotatie:
- Nieuwe Ed25519 keypair genereert
- Public part
INSERTinhub_signing_keysmetis_active=true,expires_at=NULL - Bestaande active keys krijgen
expires_at = NOW() + grace_days × INTERVAL '1 day' - Private key wordt ÉÉN keer in de response getoond, daarna nooit meer
De agent fetched periodiek GET /api/v1/agents/:id/signing-keys/active (alleen non-expired actives). Tijdens grace heeft de agent dus BEIDE keys in zijn trust set; tokens gesigned met óf de oude óf de nieuwe key valideren.
Na expires_at van de oude key valt deze automatisch uit de trust set.
Hoe je het in de UI doet
/settings → Signing keys tab:
- Geef een reden in (“annual rotation”, “suspected compromise”, …)
- Kies grace days (default 7, max 90)
- Klik Rotate now
- Copy de getoonde private_hex direct — wordt nergens opgeslagen, alleen getoond
- Plak die in je hub deployment config (env var
MONSYS_EMERGENCY_PRIVATE_KEY_HEXof secrets manager) - Restart hub. Vanaf dat moment signt de hub met de nieuwe key; oude tokens blijven
grace_daysvalideren.
Tabel onder de rotate-button toont alle keys: ACTIVE (geen expiry), EXPIRES <date> (in grace), of RETIRED.
Compromise scenario
Bij vermoeden van compromise:
- Rotate met
grace_days=0(verkort grace tot ~nu) - Plak nieuwe private key in deploy
- Restart hub
- Alle oude tokens vallen direct ongeldig
Dit triggert wel: in-flight playbook-runs die nog niet door de agent zijn ontvangen kunnen falen. Daarom alleen bij ÉCHTE compromise — niet voor planned maintenance.
API
GET /api/v1/signing-keys (admin only)POST /api/v1/signing-keys/rotate (admin, rate-limit 5/u)GET /api/v1/agents/:id/signing-keys/active (agent-auth)Body POST /rotate:
{ "reason": "annual rotation 2026", "grace_days": 7}Response (ONE TIME):
{ "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
Elke rotation logt naar audit_log met resource_type='signing_key', resource_id=<new_key_id>, IP, user, reason. Reviewable via /audit?resource_type=signing_key.