ADR-133 — Quota de signatures du monitor air-sshd : borner l’oracle de signature d’un enfant pré-auth compromis
Statut : Accepté (2026-07-29). RFC de structure (ADR-015).
Catégorie : durcissement de sécurité couche 2 (air-sshd). Aucun
descellement de couche 0/1 — la surface scellée est inchangée ; le changement est
interne au démon. Aucun tag de sceau.
Nature : défense en profondeur sur la frontière SIGN du privsep
(ADR-122,
ADR-124 §D4). Répond à une
observation de la revue de sécurité adverse de la PR #511 (V2.6b), non bloquante
mais tracée : la fenêtre de signature offerte à un enfant compromis n’était pas
bornée.
Contexte — l’oracle de signature, non borné
Sous séparation de privilèges, le monitor (root) détient la clé d’hôte privée et
signe le hash d’échange que l’enfant pré-auth lui présente par RPC. La clé privée
ne franchit jamais la frontière : un enfant compromis n’obtient donc rien qui lui
survive. Mais tant qu’il vit, il obtient des signatures — et la validation du monitor
est structurelle (§D4, tranché par le BDFL) : longueurs 32/48/64 o, cohérence des
KEXINIT, égalité du blob K_S, sans recompute de H (impossible sans le secret
DH, qui reste dans l’enfant par ADR-122).
Conséquence exacte, relevée par la revue #511 : un enfant compromis dispose d’un oracle de signature Ed25519 sous la clé d’hôte sur un hash arbitraire de 32/48/64 octets. Ce n’est pas un flux de signatures pour de vrais handshakes : c’est un oracle de message quasi arbitraire, donc de l’usurpation de la clé d’hôte face à d’autres clients (MITM). V2.6b (rekey à travers le proxy) n’a pas créé cet oracle — il préexiste à ADR-122/124 — mais il en a allongé la durée (toute la session au lieu du seul pré-auth), en gardant le canal RPC monitor ouvert pour re-signer aux rekeys.
Verrou d’accès : obtenir l’oracle exige d’abord une exécution de code dans l’enfant, soit une faille mémoire dans du Rust safe — très improbable. Le durcissement est donc de la défense en profondeur, pas la fermeture d’une brèche exploitable.
Modèle d’attaque à retenir : l’enfant compromis ne passe pas par un vrai rekey
pour récolter. Il spamme directement des trames SIGN sur le RPC ; le débit de
récolte est borné par le seul round-trip RPC (des milliers/s), pas par la cadence de
rekey légitime. Toute contre-mesure doit donc borner le débit, pas seulement le
cumul.
Décision
Le monitor applique, par connexion, un double quota fail-closed sur SIGN,
dans HostKeySignPolicy::sign (crates/air-sshd/src/monitor/sign.rs). HostKeySignPolicy
est déjà instancié une fois par connexion (le privsep monte un monitor par accept)
et compte déjà signed/refused ; le quota s’y greffe.
Ordre d’évaluation (le plus grossier et le moins cher d’abord), chaque étape rendant
MonitorResponse::SignRefused muet (la raison reste dans le monitor, jamais sur le
fil — cohérent avec l’anti-énumération existante) :
- Plafond absolu de secours —
if signed >= HARD_CAP { refused += 1; return SignRefused }. Valeur ratifiée :HARD_CAP = 1024signatures par connexion. - Token-bucket (débit) —
if !bucket.try_take(now) { refused += 1; return SignRefused }. Valeurs ratifiées : rafale (burst) = 8, recharge = 2 jetons/s. Réutilise le motif deConnectionThrottle(#439,crates/air-sshd/src/throttle.rs) : token-bucket sur horloge monotoneInstant,refill(rate_per_sec, burst). Le monitor est root non confiné ⇒Instant::now()y est disponible (à la différence de l’enfant en cage, d’où le rekey-par-le-temps hors périmètre en V2.6b — cette limitation ne concerne PAS le monitor). - Validation structurelle existante (
validate_sign_request) — inchangée. - Signature —
signed += 1;SignOk.
Justification des valeurs
- Légitime, jamais gêné : une connexion émet 1 signature au KEX initial, puis 1 par rekey. Le rekey est rare (RFC 4253 §9, seuil de volume ~1 GiB ; le rekey-par-le-temps n’existe pas sous cage) et très espacé devant 2/s. La rafale de 8 absorbe le KEX initial et quelques rekeys rapprochés ; le plafond de 1024 couvre des sessions de très longue durée à très gros volume. Aucun usage normal ne touche l’un ou l’autre.
- Récolte, choke : un enfant compromis qui spamme
SIGNpasse de « milliers/s » à ≤ 2/s + rafale 8, et ≤ 1024 au total pour la vie de la connexion — après quoi toute demande est refusée jusqu’à la fin de la connexion. La fenêtre reste, mais son débit et son cumul sont bornés d’un à deux ordres de grandeur. - Fail-closed : un refus de quota ne signe pas ; il ne relâche jamais la clé, ne poursuit jamais sans signature. Sous abus, la session finit par ne plus pouvoir rekeyer et meurt — comportement voulu (indisponibilité de la session compromise, pas de compromis de la clé).
Portée
Couche 2 uniquement (air-sshd). Aucun symbole de couche 0/1 touché, aucun re-sceau,
aucun tag. Constantes exposées pub const pour l’observabilité et les tests
(HOST_KEY_SIGN_HARD_CAP, HOST_KEY_SIGN_BURST, HOST_KEY_SIGN_REFILL_PER_SEC).
Constructeur with_sign_quota(...) pub(crate) — réservé aux tests, qui doivent
pouvoir mesurer la fenêtre sans quota puis la fenêtre avec. Il n’est pas exposé hors
du crate : u32::MAX y désactiverait le quota, et une fenêtre non bornée reste
exactement ce que la preuve n° 1 démontre comme dangereux. Hors du crate, les valeurs
ci-dessus ne sont pas un défaut mais la seule possibilité (new,
with_authorizer). Un réglage futur, s’il advient, passera par un point d’entrée dédié
et borné en amont — jamais par une passe-plat vers ces trois paramètres.
Preuves exigées (règle d’honnêteté — chaque preuve rouge sans son correctif)
- Mesure de la fenêtre non bornée (le finding) : un harnais d’enfant simulé compromis parlant le codec RPC obtient, sans le quota, un nombre de signatures ≫ 1024 en rafale — la preuve de l’observation #511.
- Plafond absolu : au-delà de
HARD_CAP,SIGNest refusé fail-closed ; rouge sans l’étape 1 du quota. - Débit : une rafale au-delà de
burstsur un intervalle court est refusée, puis ré-autorisée après recharge (temps injecté, comme les tests dethrottle.rs) ; rouge sans l’étape 2. - Non-régression légitime : une session réelle avec KEX + un rekey (voire quelques-uns) n’est jamais refusée (le compteur reste très sous les seuils).
- Mutisme du refus : un refus de quota reste
SignRefusednu, la raison ne franchit pas la frontière.
Conséquences
Borner le quota de signatures ferme un oracle : un enfant pré-authentifié compromis ne peut plus faire signer arbitrairement par le monitor.
La borne doit rester au-dessus de l’usage légitime — un client qui présente plusieurs clés — sinon elle casserait l’authentification normale.
Alternatives rejetées
- Plafond absolu seul : borne le cumul mais pas le débit — 1024 signatures récoltées en < 1 s restent possibles. Insuffisant pour un oracle.
- Token-bucket seul : borne le débit mais laisse un cumul non borné sur une session très longue. Le plafond de secours ferme cette traîne. D’où le double quota (défense en profondeur, cohérent avec « sécurité > performance »).
- Recompute de
Hdans le monitor : fermerait l’oracle à la racine, mais exige le secret DH dans le privilège — refusé par ADR-122.
Suites
Cette décision clôt l’observation #511. Les 8 axes privsep de la campagne de sécurité (dont B2, la mesure de cet oracle) restent différés à la 2ᵉ campagne complète (fin de vague 2, décision BDFL 2026-07-29) ; la mesure exigée en preuve n° 1 ci-dessus en est le fragment mené dès maintenant.