Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Plafond absolu de secoursif signed >= HARD_CAP { refused += 1; return SignRefused }. Valeur ratifiée : HARD_CAP = 1024 signatures par connexion.
  2. 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 de ConnectionThrottle (#439, crates/air-sshd/src/throttle.rs) : token-bucket sur horloge monotone Instant, 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).
  3. Validation structurelle existante (validate_sign_request) — inchangée.
  4. Signaturesigned += 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 SIGN passe 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)

  1. 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.
  2. Plafond absolu : au-delà de HARD_CAP, SIGN est refusé fail-closed ; rouge sans l’étape 1 du quota.
  3. Débit : une rafale au-delà de burst sur un intervalle court est refusée, puis ré-autorisée après recharge (temps injecté, comme les tests de throttle.rs) ; rouge sans l’étape 2.
  4. 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).
  5. Mutisme du refus : un refus de quota reste SignRefused nu, 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 H dans 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.