Campagne de tests de sécurité d’air-sshd — journal de résultats (ADR-105)
Journal vivant de la campagne définie par
ADR-105. Chaque axe : méthode,
résultat mesuré, critère coché ou correctif ouvert. La campagne est menée après la
clôture fonctionnelle d’air-sshd (SFTP S.5, re-sceau couche-1-v2.4).
Topologie
- Attaquant :
carbon(air-test-host, 8 c / 16 Gio), atteint depuis speedy viassh carbon.net(192.168.1.21 port 52222). Outils hors dépôt :nmap,ncrack,hydra,hping3,ssh-audit(~/sectools-venv),Terrapin-Scanner(~/.local/bin). - Cible :
air-sshdlinux-air (*-unknown-linux-air, static-pie, 0 glibc, vérifiéreadelf -d→ aucunNEEDED), service systemdair-sshd.serviceactif sur192.168.1.43:2222(IP interne ; ⚠️speedy.netrésout côté carbon vers une IP publique — ne pas l’utiliser). Réseau interne uniquement.
Axe 1 — Audit non-intrusif du transport (2026-07-26)
Méthode : nmap -sV, ssh-audit, Terrapin-Scanner depuis carbon.
| Critère | Verdict | Preuve |
|---|---|---|
| Terrapin (CVE-2023-48795) | ✅ NON vulnérable | Terrapin-Scanner : strict key exchange true, ChaCha20-Poly1305 true, CBC-EtM false — « can establish connections that are NOT VULNERABLE to Terrapin » |
| Profil crypto moderne-only | ✅ conforme | ssh-audit : KEX curve25519-sha256(@libssh.org) + kex-strict-s-v00@openssh.com ; hôte ssh-ed25519 ; chiffres AEAD chacha20-poly1305@openssh.com + aes256-gcm@openssh.com ; compression none ; aucun RSA/DSA/CBC/SHA-1 |
| Bannière | ℹ️ | SSH-2.0-Air_0.1.0 (serveur Air, pas un OpenSSH déguisé) |
| Débit de connexions pré-auth | ⚠️ finding → Axe 2 | ssh-audit : ~2890–4302 conn/s acceptées sans throttling par source (heuristique DHEat, CVE-2002-20001) |
Axe 2 — DoS pré-auth : throttling par source (2026-07-26) — CLOS
Finding de l’Axe 1 : aucune limite de connexions pré-auth par source — un flood force
autant de KEX. Correctif (couche 2 pure, air-sshd::throttle) :
- Concurrence par source (16) et globale (256) — PR #438 ;
- Débit par source (token bucket : 10 conn/s soutenu, rafale 20) — PR #439 ;
- au-delà d’une borne : refus avant tout KEX (fermeture du fd accepté) ; crédit de concurrence rendu à l’authentification (une session établie ne pèse plus).
Validation empirique (carbon → cible redéployée) :
| Test | Attendu | Mesuré |
|---|---|---|
| 24 connexions concurrentes d’une source | ≤ 16 admises | 16 admises / 8 refusées avant KEX |
| 300 handshakes KEX en rafale d’une source | fortement bornés | 27 admises / 273 refusées (~91 % coupées) |
| Handshake légitime complet, source distincte, pendant le churn KEX | latence ≈ baseline | OK, clé récupérée, ~0.05 s (baseline 0.04–0.07 s) |
Critère ADR-105 « un DoS pré-auth n’empêche pas une connexion légitime » : ✅ ATTEINT, prouvé au niveau du KEX complet (pas seulement l’admission).
Nuance consignée (transparence) : ssh-audit continue d’afficher l’avertissement
DHEat — son test compte les connexions acceptées au niveau TCP (noyau), or notre limite
agit après l’accept ; ssh-audit ne peut structurellement pas la voir, et son propre
message admet que « même PerSourceMaxStartups 1 yield a false positive ». De plus,
air-sshd n’offre que curve25519 (pas de DH à corps fini) : le vecteur DHEat au sens
strict (coût de l’exponentiation modulaire) ne s’applique pas. Un throttling au niveau
pare-feu (nftables limit) ferait taire l’heuristique, mais relève du déploiement,
hors périmètre du démon.
Axe 3 — dudect : temps constant de la comparaison de tag (2026-07-26) — CLOS
Méthode (ADR-105 niveau 1 : local, cycle-précis) : dudect (Reparaz et al.). On mesure
AirHmac::verify (la comparaison de tag constant-time subtle sur laquelle reposent les
vérifications d’air-sshd) sur deux classes de même longueur — tag correct
(correspondance complète) vs tag au premier octet inversé (divergence précoce) — et on
applique un test t de Welch. Timing cycle-précis (rdtsc), 300 000 échantillons/
classe, ordre entrelacé aléatoire, cropping d’outliers (p99/p95). Harnais in-tree :
crates/air-crypto/tests/dudect_constant_time.rs (gaté AIR_DUDECT=1, hors gate CI).
| Critère | Verdict | Preuve |
|---|---|---|
| Comparaison de tag constant-time | ✅ prouvé | ` |
Un memcmp à court-circuit aurait donné |t| ≫ 10 sur la classe « divergence précoce ».
Portée : la même primitive subtle sous-tend la comparaison finale d’Argon2id
(verify_password) et de Poly1305 (ssh_aead) — couvertes par extension. La vérification
de signature Ed25519 est constant-time par construction (crate ed25519). La résolution
de login (anti-énumération) est un temps bout-en-bout : elle relève de l’Axe 4 (réseau).
Axe 4 — anti-énumération de comptes (2026-07-26) — CLOS
Méthode : niveau 1 (dudect local) + corroboration réseau (niveau 2).
Niveau 1 — dudect sur is_authorized (crates/air-sshd/tests/dudect_authorization.rs,
gaté AIR_DUDECT=1) : temps d’autorisation d’un compte existant (mauvaise clé) vs
inexistant (même longueur de nom), rdtsc, 400 000/classe.
| Backend | Verdict | Preuve |
|---|---|---|
InMemoryAuthorizedKeys | ✅ aucune fuite détectable | ` |
FileAuthorizedKeys (production) | ⚠️ différence au niveau fonction, non exploitable | par analyse : un compte existant déclenche une lecture de fichier en plus (son magasin par-utilisateur) qu’un compte inexistant n’a pas — écart ~µs |
Niveau 2 — corroboration réseau (carbon → démon de production, 30 essais/classe, espacés
sous le rate-limit) : temps d’échec d’auth pour thierry (existant) vs zznouser
(inexistant), clé non autorisée.
| Compte | min | p10 | médiane | p90 |
|---|---|---|---|---|
| existant | 23.2 | 24.7 | 33.8 | 36.8 ms |
| inexistant | 24.4 | 26.0 | 32.0 | 36.4 ms |
Distributions indistinguables (quantiles bas à ~1 ms près ; plancher KEX ~24 ms). Le
sur-coût d’I/O de FileAuthorizedKeys (~µs) est noyé sous le coût réseau+KEX.
Critère ADR-105 « user-existant vs -inexistant indistinguable » : ✅ ATTEINT (réponse
uniforme U.4 + timing indistinguable au réseau). Limitation consignée (pas exploitable) :
FileAuthorizedKeys n’est pas constant-time par construction (I/O supplémentaire pour un
compte existant) ; un durcissement (I/O uniforme) est possible mais à rendement décroissant
(l’écart est déjà sous le jitter, et des canaux cache/inode subsisteraient) — différé,
décision ouverte.
Axe 5 — brute-force (2026-07-26) — CLOS
air-sshd est publickey-only (ADR-094) : la surface de brute-force par mot de passe
est nulle. Tests depuis le vrai client OpenSSH :
| Test | Verdict | Preuve |
|---|---|---|
| Surface password | ✅ nulle | PreferredAuthentications=password → « Authentications that can continue: publickey » puis « No more authentication methods » |
| Plafond d’essais publickey/connexion (U.4) | ✅ 6 | 7 clés non autorisées offertes → exactement 6 offres puis déconnexion (DEFAULT_MAX_AUTH_FAILURES = 6, cf. MaxAuthTries OpenSSH) |
| Débit de brute-force effectif | ✅ borné | 6 essais/connexion × ≤ 10 connexions/s/source (rate-limit, axe 2) = ≤ 60 essais/s/source — infaisable contre Ed25519 (2¹²⁸) ; une source légitime distincte reste servie |
Axe 6 — SYN flood (2026-07-26) — CLOS
Méthode : hping3 --flood -S (raw socket, root) depuis carbon contre
192.168.1.43:2222, 8 s.
| Critère | Verdict | Preuve |
|---|---|---|
| Défense noyau | ✅ | net.ipv4.tcp_syncookies = 1 (SYN cookies actifs) |
| Joignabilité pendant le flood | ✅ | handshake légitime complet 0.05 s (×3) pendant les 8 s de flood |
| Récupération après | ✅ | service active, handshake post-flood OK |
Le SYN flood est absorbé par la pile TCP du noyau (SYN cookies) ; air-sshd ne voit que les
connexions établies, protégées en amont par le throttling par source (axe 2).
Conclusion — campagne ADR-105 close (2026-07-26)
air-sshd (livrable linux-air, 0 glibc) a été confronté à toutes les classes
d’attaques SSH connues ciblées par l’ADR : downgrade/Terrapin, DoS pré-auth (concurrence
- débit), fuite de temps (comparaison de secret, énumération de comptes), brute-force, SYN flood. Tous les critères de réussite sont atteints.
Le seul finding (débit de connexions pré-auth non borné, axe 1) a ouvert un
correctif tracé (throttling par source, #438/#439) validé empiriquement contre un
vrai attaquant (axe 2). Une limitation non exploitable reste consignée (axe 4 :
FileAuthorizedKeys non constant-time par construction, écart ~µs noyé sous ~24 ms de
réseau) — durcissement I/O-uniforme différé (rendement décroissant, décision ouverte).
Méthode à deux niveaux honorée : les preuves constant-time reposent sur le niveau 1
dudect (cycle-précis, local) ; le niveau 2 réseau corrobore sans prétendre au
constant-time (jitter). Les harnais niveau 1 sont in-tree et gatés AIR_DUDECT=1 (hors
gate CI) ; les attaques réseau sont menées depuis carbon (outillage hors dépôt).
Chantier air-sshd clos : shell + exec + forwarding + SFTP (S.5) + durcissement
sécurité (ADR-105). Prochaine décision BDFL : durcissement FileAuthorizedKeys (optionnel)
ou passage à la suite.