ADR-129 — Conception de V2.5 : transition post-auth (worker + proxy chiffré, options, from=)
Statut : Accepté (conception, 2026-07-29) — détaille le comment de la
transition post-auth du privsep air-sshd, dont le quoi est ratifié par
ADR-122 §5 et
ADR-124 (D1/D6/§D4). Companion :
ADR-128 (options), ADR-127 (AirTemporaryIdentity), ADR-112 (air-sandbox),
ADR-069 (SCM_RIGHTS). Un point sécurité nouveau est remonté au BDFL (§Points remontés).
Catégorie : Conception d’architecture couche 2 (air-sshd). Aucun code : fixe la
mécanique de V2.5 et son découpage en sous-incréments V2.5a/b/c avant toute implémentation.
Contexte — ce que V2.5 doit livrer
Après un AUTHORIZE accordé (V2.4 : le monitor rend AuthorizeAllow{forced_command, permissions}), le privsep doit servir la session sans jamais donner à l’étage confiné
les privilèges du compte cible. Restent, hérités des incréments précédents :
- Transition post-auth par proxy (ADR-124 §D6, non implémentée) : l’enfant pré-auth
garde
Transport<S>(état de chiffrement) et devient proxy ; un worker setuid parle ssh-connection en clair. - Application des options (ADR-128) :
command=,no-pty,no-*-forwarding… sont décodées/résolues mais rien ne les exécute — c’est le worker. - Corollaire §D4 : l’enfant a besoin de
K_Spublic (pour sonKEXINIT/reply), que le proto RPC V2.1 ne transmet pas. from=: reporté par ADR-128 (exige l’adresse du pair de confiance).- Contrat mono-fil d’
AirTemporaryIdentity(ADR-127, avertissement A2) : à honorer au câblage du démon.
Décisions de conception
D1 — Le monitor forke le worker post-auth ; cage selon le service
À l’ALLOW final, le monitor (root borné) fork_process un worker :
AirPrivilegeDrop::for_user(compte) → drop_privileges (setuid irréversible vers le
compte cible). La cage dépend du service demandé (ADR-122 §7) :
- shell / exec : drop-only (largage de privilèges, sans cage seccomp/Landlock —
un shell interactif a besoin d’une surface syscall large ; le confinement est l’identité
non-root +
no_new_privs) ; - SFTP : cage complète
sftp-worker(seccompsftp-worker+ Landlock du sous-arbre autorisé), posée avant de servir le moindre octet SFTP.
Le type de service n’est connu qu’après l’auth (requête de canal ssh-connection). Le
worker démarre donc drop-only, lit la première requête de canal/subsystem, puis pose la
cage sftp-worker si et seulement si c’est du SFTP, avant tout traitement de données.
D2 — Transition par proxy chiffré + passage de fd par SCM_RIGHTS
L’enfant pré-auth conserve Transport<S> et devient un proxy : réseau→clair vers le
worker, clair→réseau. Le worker ne voit jamais de clé de session (ADR-124 §D6).
Mécanique du tuyau clair : le monitor crée un socketpair(AF_UNIX, SOCK_STREAM) ;
il en donne un bout au worker (hérité au fork), et envoie l’autre bout à l’enfant
pré-auth par SCM_RIGHTS sur le canal RPC déjà établi (ADR-069, disponible). L’enfant
relaie alors entre le réseau (via Transport) et ce tuyau clair. Le worker n’a pas le
socket réseau ; l’enfant n’a aucun privilège.
D3 — K_S public : hérité du listener, jamais le privé dans l’enfant
L’enfant reçoit K_S public par héritage du listener (matérialisé avant le fork
de l’enfant ; une clé publique n’est pas un secret). La clé privée reste chargée dans
le monitor, post-fork enfant (§D4 amendé). Pas de message HOSTKEY ajouté au proto :
l’héritage du public suffit et évite un aller-retour RPC.
D4 — from= : adresse du pair fournie par le LISTENER, jamais par l’enfant
from= est ajouté au schéma (fromPatterns, champ additif @id neuf sur
AuthorizedKeyOptions — un magasin sans lui = « aucune restriction de source »). L’enforcement
vit dans le monitor, contre l’adresse du pair que le LISTENER a obtenue à l’accept()
et transmise au monitor (héritée au fork du monitor). Le monitor n’utilise JAMAIS une
adresse fournie par l’enfant (un enfant compromis mentirait pour satisfaire from=). Aucun
champ d’adresse n’est donc ajouté à AuthorizeRequest : le monitor la détient déjà, de source
fiable. La CLI cesse de refuser from= une fois ce chemin en place.
D5 — Application des options par le worker
Le worker reçoit AuthorizeConstraint{forced_command, permissions} (hérité au fork) et
l’applique dans le traitement ssh-connection : command= ⇒ exécute la commande forcée
(en ignorant celle demandée) ; no-pty ⇒ refuse pty-req ; no-{port,agent,X11}-forwarding
⇒ refuse les canaux/requêtes de forwarding correspondants ; no-user-rc ⇒ pas de ~/.ssh/rc.
Fail-closed : un bit de permission non reconnu a déjà fait rejeter la réponse (ADR-128).
D6 — Contrat mono-fil honoré au câblage
Le monitor reste une boucle bloquante mono-fil (ADR-124 §D2) ; AirTemporaryIdentity
(!Send, ADR-127) y est créée/détruite synchroniquement. Le câblage du démon ne place
pas le monitor dans un exécuteur async multi-fil. À vérifier par construction.
Découpage en sous-incréments
- V2.5a — Socle de transition :
K_Spublic hérité (D3) ;socketpairclair + passageSCM_RIGHTSau proxy (D2) ; fork du worker (setuid, drop-only) ; round-trip clair prouvé de bout en bout (un octet traverse réseau→proxy→worker→proxy→réseau), sans vraie session. Tests root. - V2.5b — Service + options : le worker parle réellement ssh-connection (shell/exec/SFTP)
sur le tuyau clair ; cage
sftp-workerposée pour SFTP (D1) ; application des options (D5). Vraie sessionssh/sftpde bout en bout via le proxy. - V2.5c —
from=: champ schéma additif (D4) ; adresse pair de confiance listener→monitor ; enforcement au monitor ; CLI acceptefrom=.
Chaque sous-incrément = branche + PR + revue (air-security-reviewer) + CI + barrière.
Points remontés au BDFL
- Modèle de confiance de
from=(D4) : décidé « adresse fournie par le listener, jamais par l’enfant ». C’est le seul choix sûr (un enfant compromis mentirait), mais il suppose que le listener conserve/transmette l’adresse du pair jusqu’au monitor — à confirmer comme contrainte d’architecture du câblage démon (V2.5c). Aucun autre point nouveau : D1-D3/D5/D6 découlent d’ADR-122/124 déjà ratifiés.
Conséquences
La transition post-authentification devient un point de bascule explicite : avant, l’enfant est confiné et sans secret ; après, le worker sert la session sous l’identité de l’utilisateur.
Le proxy chiffré ajoute une copie sur le chemin de données, contrepartie assumée de l’isolation.
Alternatives rejetées
- Transfert de l’état de chiffrement au worker (re-exec façon OpenSSH). Rejeté (ADR-124 §D6) : sérialiser l’état cipher à travers un privilège est fragile ; le proxy garde les clés dans l’enfant confiné.
- Message
HOSTKEYdans le proto pourK_Spublic. Rejeté (D3) : l’héritage du public suffit, sans aller-retour ni surface de proto en plus. - Adresse du pair dans
AuthorizeRequest(fournie par l’enfant). Rejeté (D4) : un enfant compromis falsifieraitfrom=. La source fiable est le listener. - Cage seccomp complète pour le shell. Rejeté (ADR-122 §7) : un shell interactif exige une
surface syscall trop large ; drop-only + non-root +
no_new_privsest la posture retenue.
Suite
- Sur confirmation du point
from=(D4) : démarrer V2.5a. Enregistrement SUMMARY/registre/ suivi. Clôture V2.5b/c + V2.6 (durcissement + campagne de tests de sécurité) = clôture vague 1 → cycle de livraison.