ADR-118 — Config binaire SSH : capacités étendues (Match, options serveur, IdentitiesOnly/ForwardAgent, keepalive) — en binaire, pas de grammaire texte
Statut : Accepté (2026-07-26, décision BDFL). RFC de direction
(ADR-015).
S’appuie sur ADR-073 (doctrine config
binaire), ADR-040 (artefact AIRCFGv1),
ADR-099 (config client air-ssh),
ADR-097 (serveur), ADR-089
(SecurityManager — politique), ADR-117 (forwarding —
options associées), ADR-029 (nommage).
Catégorie : Modèle de configuration (couche 1 schéma + couche 2 évaluation). Vague 2 #6.
Contexte
OpenSSH configure via des fichiers texte (ssh_config/sshd_config) à grammaire riche :
blocs Match, AllowUsers, ForceCommand, IdentitiesOnly, etc. Air a fait le choix
opposé — configuration binaire (ADR-073, AIRCFGv1 ADR-040) — et a déjà oublié la
grammaire texte serveur (bloc A du triage). Mais il manque les capacités que ces options
apportent : conditionnalité (Match), contrôle d’accès, forçage de commande, keepalive… côté
client et serveur.
Directive BDFL (2026-07-26). Comme pour le bloc A : « la config est binaire, pas du texte.
On implémente les capacités (Match-like, ProxyJump, IdentitiesOnly, ForwardAgent…) dans notre
modèle binaire — pas de parseur de ~/.ssh/config texte. » Et : « IdentitiesOnly mérite
d’être le défaut de fait. »
Décision
1. Capacités, pas grammaire texte
On implémente les capacités de ssh_config/sshd_config dans le modèle de config
binaire (ADR-073/ADR-040) — aucun parseur de ~/.ssh/config ou /etc/ssh/sshd_config
texte. La source de vérité reste l’artefact binaire scellé ; le modèle gagne des champs,
pas une grammaire.
2. Blocs Match (client et serveur)
Un modèle conditionnel : des règles sélectionnées par critères (utilisateur, groupe, hôte,
adresse IP source…) qui surchargent les réglages de base. C’est une structure de données
binaire (liste ordonnée de conditions → réglages), évaluée à la connexion (couche 2, dans
air-ssh/air-sshd), pas une grammaire à parser. Sémantique déterministe (première/dernière
correspondance définie explicitement).
3. Options serveur (sous-ensemble sécurité)
Ajoutées au modèle binaire serveur : AllowUsers/AllowGroups (contrôle d’accès),
ForceCommand (forçage), PermitRootLogin, AllowTcpForwarding,
PermitOpen/PermitListen (bornage du forwarding ADR-117), Banner,
MaxSessions. Différés : ChrootDirectory (plus tard), GatewayPorts (avec -R,
ADR-117). L’application de ces politiques est médiée par le SecurityManager (ADR-089).
4. Options client
Ajoutées au modèle binaire client (ADR-099) : IdentitiesOnly, ForwardAgent
(ADR-116), CanonicalizeHostname, plus les cibles de forwarding/ProxyJump (ADR-117).
IdentitiesOnly est le défaut de fait : sans lui, le client épuise le plafond de 6
essais (ADR-094) avant d’atteindre la bonne clé (constaté en test). On n’offre donc que les
identités explicitement désignées.
5. Keepalive ClientAliveInterval
Détection de pair mort via des sondes périodiques : réutilise les timers d’air-async
(ADR-092/ADR-098, couche 2) — pas de nouveau mécanisme. Réglages (ClientAliveInterval,
ClientAliveCountMax) dans le modèle binaire serveur ; analogue client (ServerAliveInterval).
Conséquences
Positives.
- Parité fonctionnelle (conditionnalité, contrôle d’accès, forçage, keepalive) sans réintroduire une grammaire texte — cohérent ADR-073.
IdentitiesOnlypar défaut : corrige un vrai piège d’épuisement d’essais (ADR-094).- Politique centralisée : les options d’accès/forwarding s’appliquent via
SecurityManager(ADR-089), pas éparpillées. - Réutilise
air-asyncpour le keepalive — pas de nouveau mécanisme.
Négatives / coûts assumés.
- Modèle binaire plus riche : plus de champs = plus de schéma à versionner (ADR-040) et à
tester ; l’évaluateur
Matchest du code à couvrir à 100 %. - Pas d’interop texte : un utilisateur migrant depuis OpenSSH ne peut pas copier son
ssh_config— il passe par l’outil de config Air. Assumé (doctrine ADR-073). - Sémantique
Match= source de surprises si mal spécifiée : ordre d’évaluation et surcharge définis explicitement + tests exhaustifs.
Mise en œuvre (référence, hors décision). Incréments : (a) extension du schéma binaire
client/serveur (ADR-040) ; (b) évaluateur Match (couche 2) ; (c) application des options via
SecurityManager (ADR-089) ; (d) IdentitiesOnly défaut + keepalive sur timers air-async.
Non engagés par cet ADR.
Amendement — clôture du volet client : trois options refusées, une reportée (2026-08-05)
Décidé par le BDFL le 2026-08-05, à la clôture de la campagne « config client » (incréments
c1 ForwardAgent [#633], c2 keepalive client [#636], c3 ProxyJump [#638]). Le volet serveur
avait été clos en huit incréments ; le volet client de cet ADR (§2 côté client, §4) n’avait
alors, lui, aucune ligne de code — constat du 2026-08-04, au moment de passer l’ADR à fait.
Cet amendement consigne ce que la mise en œuvre a refusé de faire, et pourquoi. Un ADR qu’on n’applique pas intégralement sans le dire devient un document dont on ne sait plus ce qu’il engage.
A1 — IdentitiesOnly n’est pas implémenté : il est déjà vrai par construction
Le §4 justifie cette option ainsi : « sans lui, le client épuise le plafond de 6 essais (ADR-094) avant d’atteindre la bonne clé (constaté en test) ».
Ce constat porte sur le client OpenSSH, pas sur air-ssh. Dans les suites d’interop de ce
dépôt, -o IdentitiesOnly=yes est passé au vrai ssh, jamais à air-ssh.
air-ssh n’offre jamais plus d’une identité. resolve_identity rend exactement une
ClientIdentity ; et devant un agent porteur de plusieurs clés sans sélecteur,
select_identity refuse pour ambiguïté au lieu d’en essayer plusieurs. C’est plus strict
que ce que IdentitiesOnly obtiendrait.
Décision. Le bouton n’est pas ajouté. L’invariant est verrouillé par un test nommé, et écrit dans le guide client. Un réglage qui ne change rien est pire que son absence : il ment sur ce que le système fait.
A2 — CanonicalizeHostname : hors périmètre
resolve_host fait une correspondance exacte (les jokers OpenSSH sont différés). Canonicaliser
un nom via DNS avant cette correspondance placerait une résolution réseau sur le chemin d’une
décision de configuration — ce que le Principe 11 (entrée stricte, sortie lisible) borne
explicitement — pour un gain nul tant qu’il n’y a pas de jokers.
Décision. Non implémenté. À reconsidérer si et seulement si les motifs à jokers arrivent, et alors avec la question DNS instruite pour elle-même.
A3 — Blocs Match côté client : hors périmètre
Le §2 annonce Match « client et serveur ». Côté serveur, l’évaluateur est fait (inc.3).
Côté client, le modèle conditionnel existe déjà : c’est HostBlock (motif → surcharges).
Ce que le Match client d’OpenSSH ajoute par-dessus, ce sont des critères — dont exec, qui
est une porte d’exécution pilotée par un fichier de configuration. C’est précisément ce que
refusent l’amendement D6 d’ADR-046 (« plus d’exécution non confinée ») et
ADR-150.
Décision. Le modèle conditionnel client reste
HostBlock, et on ne lui ajoute aucun critère exécutant. Les critères non exécutants (originalhost…) ne sont pas refusés sur le fond ; ils n’ont simplement pas trouvé d’usage qui les justifie.
A4 — ProxyCommand : reporté, en attente de son propre arbitrage
ADR-117 §2 décrit ProxyJump et ProxyCommand. c3 n’a livré que le premier.
ProxyJump est purement interne : aucun processus lancé, aucun exec — un canal
direct-tcpip portant le transport, soit une quatrième implémentation de SshByteStream.
ProxyCommand, lui, spawne une commande arbitraire désignée par un fichier de configuration :
c’est la même porte d’exécution qu’en A3, et elle relève du même arbitrage.
Décision. Non implémenté, pas même partiellement. L’absence est consignée dans le schéma, le module et le guide, pour se lire comme un choix et non comme un oubli.
A5 — Une correction de fait sur le §5 (keepalive)
Le §5 annonce que le keepalive « réutilise les timers d’air-async ». C’est vrai côté
client (air-ssh porte un vrai Runtime : le keepalive y est une future sur une échéance
io_uring) et faux côté serveur — le keepalive serveur vit dans le worker confiné, qui n’a
aucun réacteur ; l’inc.8 a dû l’implémenter comme une échéance de ppoll.
Ce n’est pas une divergence de style mais la conséquence de l’endroit où chacun vit. La
décision est partagée (KeepalivePolicy/KeepaliveTracker, quatre points d’intégration) ;
seul l’ordonnanceur diffère.
A6 — Une limite à ne pas laisser croire dépassée
La vérification de clé d’hôte à chaque saut, que c3 met en œuvre, n’a d’effet que si l’hôte
présente un certificat d’une CA de confiance (ADR-109). Devant une clé d’hôte nue,
air-ssh ne vérifie rien aujourd’hui : l’épinglage known_hosts/TOFU n’est pas implémenté, le
verdict HostTrust::PlainKey est produit puis jeté.
C’est un manque global au client, antérieur à cette campagne et qui ne concerne pas que les rebonds. Il est consigné ici parce que c3 est la fonctionnalité qui place un intermédiaire semi-fiable sur le chemin, et qu’une revendication de sécurité non adossée au code est plus dangereuse que son absence.
Décision (BDFL, 2026-08-05). Les textes de c3 sont rectifiés pour énoncer la condition, et l’épinglage de clé d’hôte devient un chantier à part entière. Écarté : durcir
-Jen exigeant la confiance certifiée — cela singulariserait-Jpour une faiblesse globale et rendrait la fonctionnalité inutilisable là où aucune CA n’est déployée.
Clôture (2026-08-07). Le chantier a été mené :
air-sshépingle les clés d’hôte nues (air_sshd::known_hosts), et le verdictHostTrust::PlainKey— celui qu’on produisait puis jetait — n’existe plus comme valeur qu’un appelant puisse laisser tomber. Deux magasins sont consultés, l’administré de la machine (chemin fixe, lecture seule, son verdict l’emporte) puis le personnel du compte (knownHostsPath, écrit à la première vue). Une clé changée ou révoquée est un refus sans appel ; un hôte jamais vu demande une confirmation explicite sur un terminal, et est refusé à défaut. Le nom d’hôte est validé avant d’être mis en motif (la virgule est un séparateur dans ce format : un nom qui en porte couvrirait d’autres hôtes une fois mémorisé). Les deux magasins sont gardés à la lecture (propriétaire attendu, pas de lien symbolique, fichier ordinaire, non inscriptible par autrui) ; le personnel est écrit en0640. Chaque saut d’un rebond est épinglé sous son propre motif d’hôte.Ce que la clôture ne prétend pas : la toute première connexion à un hôte reste non protégée — c’est une limite du modèle de confiance à la première utilisation, pas un reste de travail. Seule la voie certificat (ADR-109) la couvre, parce qu’une autorité y a tranché d’avance. Ce qui est acquis est la détection du changement de clé, c’est-à-dire de tout attaquant qui n’était pas déjà en place à l’instant zéro.
Alternatives rejetées
Aucune alternative n’a été consignée lors de l’instruction.