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-148 — Isolation par namespaces des cages pré-auth et forward-only d’air-sshd

Statut : Accepté (2026-08-02, ratifié BDFL). Durcissement défense-en-profondeur du modèle de séparation de privilèges (ADR-124, ADR-146), complémentaire du filtre seccomp d’ADR-147.

Catégorie : durcissement couche 2 qui exige des additifs couche 0 scellée (unshare, mount, pivot_root, éventuellement setns) — aucune primitive namespace n’existe aujourd’hui dans air-sys-syscall. C’est donc, contrairement à ADR-147, un chantier multi-couches et multi-incréments, avec re-sceau additif de la couche 0 à la clé. La portée exacte des additifs est le premier objet d’arbitrage.

Contexte

La campagne ADR-105 (2026-08-02) a relevé, en observant l’arbre privsep déployé, que la cage pre-auth (uid 124, zéro capability, seccomp) et le worker de session partagent tous les namespaces du listener :

mnt:[4026531832] net:[4026531833] pid:[4026531836] user:[4026531837]  ← identiques listener/cage/worker

Le confinement repose donc entièrement sur uid + capabilities + seccomp (+ Landlock pour la cage sftp). C’est solide, mais il manque une dimension que le noyau Linux offre et qu’Air revendique d’exploiter (choix Linux-only assumés, CLAUDE.md : « namespaces/ cgroups »). Concrètement, la cage pre-auth, qui exécute le KEX face à un pair non authentifié :

  • voit toute la pile réseau de l’hôte (net partagé) — une faille lui donnerait la capacité de créer des sockets vers le voisinage (bornée par seccomp si ADR-147 étend le filtre à la cage, mais pas isolée) ;
  • voit tout le système de fichiers (mnt partagé) — bornée par Landlock côté sftp, mais la cage pre-auth elle-même n’a pas de racine vidée ;
  • voit tous les PID de l’hôte (pid partagé) — surface d’information et cible de signaux.

Un network namespace vide + un mount namespace à racine minimale (pivot_root vers un répertoire quasi vide) transformeraient « la cage peut appeler seccomp-permis mais il n’y a rien à joindre » en garantie structurelle : défense-en-profondeur orthogonale à seccomp (seccomp borne quels syscalls ; le namespace borne ce qu’ils peuvent atteindre).

Décision (esquisse — à ratifier avant tout code)

Périmètre d’application, par rôle

  • Cage pre-auth — candidate idéale : elle ne fait aucun accès fichier et aucun accès réseau propre (le fd du client lui est passé déjà connecté). On peut donc, après le drop d’identité et avant le KEX :
    • entrer un network namespace neuf et vide (CLONE_NEWNET) — le socket client, déjà ouvert, reste utilisable ; toute nouvelle tentative réseau n’a aucune interface ;
    • entrer un mount namespace (CLONE_NEWNS) et pivot_root vers un répertoire vide dédié (/run/air-sshd/empty, 0555 root:root), démontant le reste ;
    • optionnellement un pid namespace (CLONE_NEWPID) — plus intrusif (exige un fork supplémentaire pour devenir PID 1 du namespace) ; différé, à évaluer.
  • Worker forward-only (-N) — même logique que la cage pré-auth : network namespace vide (la mediation ADR-146 garantit qu’il n’ouvre aucun socket lui-même ; le fd cible lui vient par SCM_RIGHTS), mnt à racine vide (il n’ouvre aucun fichier). Fortement complémentaire d’ADR-147 : seccomp interdit socket(), le netns garantit qu’il n’y a de toute façon rien à joindre.
  • Worker shell / execexclu. execve du shell utilisateur exige la vraie vue mnt (/bin, /lib, $HOME) et souvent la vraie vue net (l’utilisateur a droit au réseau). Y mettre des namespaces d’isolation casserait la session. C’est la limite dure : l’isolation par namespace vise le code exposé sans exec, pas la session utilisateur légitime.
  • Worker sftpnet vide envisageable (le SFTP ne fait pas de réseau) ; mnt reste contraint par Landlock déjà en place — un mount namespace y ajouterait peu. Différé.

Additifs couche 0 nécessaires (nouveau descellement)

Amendé le 2026-09-01 — cette section est reprise par ADR-175. Elle déclarait que « la portée exacte des additifs est le premier objet d’arbitrage » : cet arbitrage a eu lieu. ADR-175 porte le descellement couche-0-v1.17 pour deux consommateurs — les cages décrites ici, et le jalon C d’ADR-168 (air-launchd en PID 1) — et tranche quatre points laissés ouverts ci-dessous :

  • mount est exposé par une fonction par usage, jamais par un appel générique : son argument data est une chaîne d’options que le noyau analyserait. Ce qu’ADR-148 réclame ici (MS_REC|MS_PRIVATE) devient remount_private_recursive ;
  • les namespaces s’entrent par unshare après le fork, et non par des flags de clone3 — la question « à trancher » ci-dessous est donc tranchée ;
  • setns est hors périmètre et inscrit dans docs/UNSUPPORTED.md, comme cette section le suggérait elle-même ;
  • le re-sceau visé est couche-0-v1.17, ce qui suppose un couche-0-v1.16 régularisant fchmodat2 (ADR-144), entré dans la couche scellée le 2026-07-31 sans re-sceau.

Le texte d’origine est conservé ci-dessous, tel qu’il a été ratifié.

Aucune de ces primitives n’existe : à créer dans air-sys-syscall (+ types dans air-sys-types), chacune fonction dédiée typée (ADR-021), Result<…, Errno>, SAFETY documenté, 100 % couverture + fuzz des chemins d’octets :

  • unshare(flags) — ou usage de clone3 avec CLONE_NEWNET|CLONE_NEWNS déjà partiellement câblés pour thread::spawn ; à trancher : unshare post-fork vs flags de clone3.
  • mount(source, target, fstype, flags, data) — au minimum pour MS_REC|MS_PRIVATE et le remontage de la racine.
  • pivot_root(new_root, put_old).
  • setns(fd, nstype)peut-être pas nécessaire si l’on ne fait qu’entrer des namespaces neufs ; à exclure du périmètre si non utilisé (UNSUPPORTED.md).

Ces additifs relèvent du re-sceau additif couche 0 (modèle ADR-126 / ADR-144) — autorisation BDFL explicite requise, jamais un raccourci.

Couche 1

Ces primitives doivent être bindées en couche 1 (règle de layering : air-sshd ne touche jamais la couche 0 en direct) — vraisemblablement une extension d’air-sandbox (un AirNamespacePolicy à côté d’AirLandlockPolicy / AirSandboxProfile), appliqué par le même SandboxApplier, dans l’ordre : namespaces → Landlock → seccomp.

Conséquences

  • Gain : la cage pré-auth (code face à l’attaquant non authentifié) et le worker forward-only passent d’« isolés par politique » à « isolés par le noyau, sans réseau ni système de fichiers à atteindre ». Dimension orthogonale à seccomp/caps.
  • Coût — le plus élevé des deux pistes : descellement couche 0 (primitives + tests + fuzz + re-sceau), extension couche 1, câblage couche 2, et un /run/air-sshd/empty provisionné par le packaging. Plusieurs incréments.
  • Risque : les namespaces interagissent avec le drop d’identité (ordre unshare vs setuid), avec PDEATHSIG/pidfd du monitor (un pid namespace change la sémantique parent-mort), et avec io_uring (déjà non hérité, ADR-146 — à re-vérifier sous netns). Chaque interaction = un test dédié.
  • Périmètre négatif explicite : shell/exec jamais isolé par namespace ; sftp et pid-namespace différés. À journaliser (pas de cap silencieux).

Alternatives rejetées

  • Tout worker dans des namespaces, shell compris. Casse les sessions interactives (pas de /bin, pas de réseau). Rejeté — l’isolation vise le code sans exec.
  • Se contenter de seccomp (ADR-147) sans namespaces. Seccomp borne quels appels ; il ne retire pas ce qu’ils atteignent (un socket() permis par erreur voit encore le réseau). Les deux sont complémentaires, pas substituables — d’où deux ADR distincts.
  • Réutiliser une brique de conteneur tierce (runc/…). Contraire à la règle des 80 % (ADR-024) et au périmètre : Air pose ses propres primitives noyau typées. Rejeté.
  • chroot au lieu de mount-namespace + pivot_root. chroot est contournable et ne cache pas la pile de montages ; pivot_root en mnt neuf est la forme correcte. Rejeté.

Suite

Ne pas implémenter avant ratification : ce chantier engage un descellement couche 0. Séquence proposée si accepté : (1) additifs couche 0 (unshare/mount/pivot_root) + re-sceau couche-0-v1.17 ; (2) AirNamespacePolicy couche 1 (air-sandbox) ; (3) pose sur la cage pré-auth seule, mesurée et testée ; (4) extension au worker forward-only (après ADR-147). sftp et pid-namespace évalués séparément. Chaque étape, sa PR et ses tests d’interaction (drop d’identité, PDEATHSIG, io_uring).