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-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.
  • IdentitiesOnly par 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-async pour 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 Match est 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 -J en exigeant la confiance certifiée — cela singulariserait -J pour 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 verdict HostTrust::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 en 0640. 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.