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-096 — air-sshd U.3b : magasin authorized_keys binaire par-utilisateur (schéma bespoke air-sshd-schema + lecteur dédié)

Statut : Proposé (2026-07-23, draft pour ratification BDFL). Met en œuvre le sous-incrément U.3b d’ADR-094 (phase 2 ssh-userauth), au-dessus de U.3a (trait AuthorizedKeys + events AirCom, main PR #365). Tranche l’option d’architecture laissée ouverte par ADR-094 §3 (« emplacement figé par l’incrément », « schéma .capnp à concevoir »). Décision d’architecture B actée par le BDFL (2026-07-23). S’appuie sur la doctrine de configuration binaire ADR-073/ADR-040/ADR-041, la résolution de comptes ADR-067, et le cadrage de la note d’orientation sauvegarde/restauration.

Catégorie : Architecture (couche 2, service réseau). Toute évolution structurante passe par un RFC (ADR-015).

⛔ Note de faisabilité (tooling). L’implémentation de U.3b exige la génération du code Rust du nouveau schéma .capnp par l’outil capnp (C++) — opération mainteneur (cf. regenerate.sh, ADR-040 addendum), absente de la machine de production courante. Cet ADR est ratifiable indépendamment ; le code attend la disponibilité de l’outil (installer, ou déléguer la régénération).

Contexte

U.3a a posé la frontière d’autorisation : un trait AuthorizedKeys que serve_userauth consulte (SUCCESS = signature Ed25519 valide ET clé autorisée), avec des backends en mémoire (InMemoryAuthorizedKeys/DenyAll). U.3b fournit le backend réel : un magasin authorized_keys binaire (jamais texte, ADR-073), par-utilisateur, lu dans le HomeDirectory.

Deux architectures étaient possibles pour le schéma binaire :

  • A — enregistrer un domaine sshd_authorized_keys dans le registre générique air-config-schema (DOMAINS/lookup_domain), et lire via air_config::AirConfig::open.
  • B — définir le schéma dans air-sshd-schema (à côté de sshd.capnp) et un lecteur dédié côté air-sshd, réutilisant les primitives génériques d’artefact de air-config-compile.

Décision BDFL : B. Motif : air-config-schema est la façade de configuration système générique ; y coder un schéma propre à un service la transforme en dépotoir de schémas applicatifs, brouillant la frontière. Le couplage schéma ↔ service doit rester dans le domaine du service (air-sshd).

Décision

1. Schéma sshd_authorized_keys — dans air-sshd-schema

Nouveau fichier crates/air-sshd-schema/schema/sshd-authorized-keys.capnp, auto-versionné (pas d’enveloppe air-config générique) :

struct SshdAuthorizedKeys {
  version @0 :UInt32;            # version de format (évolution additive)
  entries @1 :List(AuthorizedKey);
}
struct AuthorizedKey {
  algorithm @0 :Text;           # "ssh-ed25519" (moderne-only U.2/U.3)
  keyBlob   @1 :Data;           # blob de clé publique ssh-ed25519 (RFC 4253 §6.6)
  comment   @2 :Text;           # libre (traçabilité)
  # options (from=, command=, no-pty…) : DIFFÉRÉES (List différable, §6).
}

Code Rust généré committé (sshd_authorized_keys_capnp.rs, include!, exclu du fmt), régénéré par l’outil capnp mainteneur (regenerate.sh) — jamais en CI.

2. Format d’artefact — en-tête réutilisé, capnp bespoke

L’artefact = en-tête d’artefact d’air-config-compile (magie + checksum FNV, 16 octets, ARTIFACT_HEADER_LEN) suivi du message Cap’n Proto de SshdAuthorizedKeys. On réutilise la vérification d’intégrité générique (air_config_compile::verify_checksum) sans le registre de domaines d’air-config-schema (qui refuserait un domaine bespoke). Corruption ⇒ erreur, jamais lecture silencieuse (ADR-040).

3. Lecteur dédié — couche-clean, sans mmap

Le lecteur vit dans air-sshd (couche 2). Il NE mappe PAS l’artefact (MmapRegion est couche 0, air-sys-syscall — un accès direct depuis la couche 2 violerait check-layers). Il :

  1. lit les octets du fichier via air-filesystem::read_to_bytes (couche 1) — les authorized_keys sont petits, la copie est négligeable ;
  2. vérifie l’intégrité via air_config_compile::verify_checksum (+ ARTIFACT_HEADER_LEN, couche 1) ;
  3. parse le message Cap’n Proto après l’en-tête via le reader généré d’air-sshd-schema (capnp, déjà dépendance transitive).

Arêtes de couche : 2→1 (air-account, air-config-compile, air-filesystem) + capnp. Toutes licites ; check-layers passe. Aucun mmap en couche 2.

4. Emplacement — figé

  • Par-utilisateur : <HomeDirectory>/.config/air/sshd/authorized_keys — le HomeDirectory est celui de l’utilisateur qui s’authentifie, résolu par air_account::lookup_user_by_name(user).dir (ADR-067), pas la cascade XDG du démon.
  • Système (global) : <racine>/etc/air/sshd/authorized_keys (racine paramétrée, jamais /etc codé en dur), complète la politique par-utilisateur (clés d’administration). L’emplacement racine exact suit la note d’orientation (orienté /etc/air, ratification à part).

5. Backend FileAuthorizedKeys — implémente le trait U.3a

Nouvelle struct air_sshd::FileAuthorizedKeys implémentant AuthorizedKeys (U.3a) :

#![allow(unused)]
fn main() {
impl AuthorizedKeys for FileAuthorizedKeys {
    fn is_authorized(&self, user: &[u8], algorithm: &[u8], key_blob: &[u8]) -> bool {
        // 1. résout le home de `user` via air-account (absent → non autorisé) ;
        // 2. lit + vérifie l'artefact par-utilisateur (et l'artefact système) ;
        // 3. cherche (algorithm, key_blob) dans les entrées.
    }
}
}

serve_userauth (U.3a) ne change pas : il consomme le trait. Utilisateur absentis_authorized = false (jamais d’erreur remontée) ⇒ anti-énumération préservée (U.4 : même USERAUTH_FAILURE, quelle que soit l’existence du compte).

6. Production d’artefacts

  • Runtime Air : un verbe de la CLI cliente/administrative (air-ssh config… — cf. ADR-095 §2) écrit l’artefact binaire (schéma + en-tête magie/checksum), via air-config-compile. Import/export texte read-only possible (ADR-073), jamais source de vérité.
  • Tests : un helper fabrique l’artefact à la main (message capnp + en-tête + checksum FNV), sur le gabarit des tests d’air-config (reader.rs custom_envelope_artifact / fnv1a64), sans dépendre de l’outil capnp au runtime.

7. Dépendances air-sshd (à ajouter)

air-account (2→1), air-config-compile (2→1, verify_checksum/ARTIFACT_HEADER_LEN), air-filesystem (2→1, read_to_bytes), air-sshd-schema (2→2, étendu du nouveau schéma).

Conséquences

  • Le magasin authorized_keys est binaire par-construction ; jamais de fichier texte (doctrine ADR-073 matérialisée côté HomeDirectory).
  • Le trait AuthorizedKeys (U.3a) est le seul point d’extension : U.3b = une impl ; serve_userauth et le cœur air-ssh-proto restent inchangés.
  • air-config-schema reste générique (registre de config système, non pollué par un schéma de service) — bénéfice central de l’option B.
  • Additif couche 1 possible : si le parsing sans-registre exige un helper générique d’air-config-compile (p. ex. open_artifact_payload(bytes) -> Reader, non spécifique à sshd), cet additif couche 1 sera folé dans le re-sceau différé couche-1-v2.2 (décidé BDFL 2026-07-23 : la couche 1 reste scellée jusqu’à la fin d’air-ssh, puis re-sceau unique avec F2 contributory X25519 et les autres additifs).
  • Différés : options authorized_keys (from=, command=, no-pty…), rotation, artefact système multi-administrateurs — évolution additive du schéma (champ version).

    Amendement (2026-07-28) — différé des options LEVÉ par ADR-128. Le schéma v2 porte command=, les 5 restrictions nommées et restrict (champ explicite), en ajout (AuthorizedKey.options @3) : un magasin v1 reste lisible. from= reste différé — livré en V2.5 avec son enforcement et l’adresse du pair de confiance, plutôt qu’affiché sans être honoré. Rotation et artefact système multi-administrateurs restent différés. (Note de lecture : plusieurs textes citaient « ADR-096 §6 » pour ce différé ; la §6 traite de la production d’artefacts — la clause exacte est celle-ci.)

Alternatives rejetées

  • A — domaine sshd_authorized_keys dans air-config-schema. Couple le registre de configuration système générique à une notion propre à un service, contraire à la vocation de façade d’air-config. Rejetée par le BDFL au profit de B.
  • mmap direct de l’artefact en couche 2. MmapRegion est couche 0 ; l’accès direct viole check-layers (saut 2→0). La lecture par air-filesystem::read_to_bytes (couche 1) est layer-clean et suffisante (petits fichiers).
  • authorized_keys texte (format OpenSSH). Exclue par la doctrine binaire (ADR-073) : jamais de config texte comme source de vérité vivante.