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
.capnppar l’outilcapnp(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_keysdans le registre génériqueair-config-schema(DOMAINS/lookup_domain), et lire viaair_config::AirConfig::open. - B — définir le schéma dans
air-sshd-schema(à côté desshd.capnp) et un lecteur dédié côtéair-sshd, réutilisant les primitives génériques d’artefact deair-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 :
- lit les octets du fichier via
air-filesystem::read_to_bytes(couche 1) — lesauthorized_keyssont petits, la copie est négligeable ; - vérifie l’intégrité via
air_config_compile::verify_checksum(+ARTIFACT_HEADER_LEN, couche 1) ; - 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— leHomeDirectoryest celui de l’utilisateur qui s’authentifie, résolu parair_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/etccodé 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 absent ⇒
is_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), viaair-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.rscustom_envelope_artifact/fnv1a64), sans dépendre de l’outilcapnpau 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_keysest 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_userauthet le cœurair-ssh-protorestent inchangés. air-config-schemareste 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 (champversion).Amendement (2026-07-28) — différé des options LEVÉ par ADR-128. Le schéma v2 porte
command=, les 5 restrictions nommées etrestrict(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_keysdansair-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. mmapdirect de l’artefact en couche 2.MmapRegionest couche 0 ; l’accès direct violecheck-layers(saut 2→0). La lecture parair-filesystem::read_to_bytes(couche 1) est layer-clean et suffisante (petits fichiers).authorized_keystexte (format OpenSSH). Exclue par la doctrine binaire (ADR-073) : jamais de config texte comme source de vérité vivante.