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-033 — Modèle de configuration : source typée, compilation validée, artefact binaire

Statut : Accepté (2026-06-12). Décline les Principes d’ingénierie 4 (validation en amont, « parse, don’t validate »), 3 (zéro présomption sur les entrées) et 9 (budgets sur matériel modeste) en un modèle transverse de configuration système. Cohérent avec ADR-025 (builds reproductibles) et ADR-005 (systemd socle pour V1).

Catégorie : Architecture (transverse — couche 1 air-base-lib à couche 5 outils d’administration).

Contexte

Air s’adresse à deux publics qu’il refuse d’opposer : l’utilisateur qui ne veut pas savoir ce qu’il y a sous le capot et veut que « ça marche », et le développeur/technicien qui connaît sa machine et veut la régler finement. Les deux doivent être servis par le même système de configuration, sans que le premier puisse se tirer une balle dans le pied ni que le second soit bridé.

Le modèle Unix traditionnel — des fichiers texte éditables à la main sous /etc — a une vertu (lisibilité, grep, diff, versionnable) et un vice symétrique : une faute de frappe brique le système. Un grub.cfg erroné et la machine ne boote plus ; un fichier de /etc corrompu et un pan entier de la sécurité tombe, silencieusement. La validité n’est vérifiée qu’au runtime, trop tard, par le consommateur, de façon non uniforme.

À l’inverse, un magasin binaire canonique sans source lisible (le Registre Windows) échange ces vices contre d’autres : opacité, fragilité à la corruption, impossibilité de diff/versionner, point de défaillance monolithique.

Air veut le meilleur des deux : la sûreté et la rapidité d’un format de données vérifié, et la lisibilité et la maîtrise du texte — sans le piège de l’un ni de l’autre.

Décision

La configuration Air a pour source de vérité une source typée validée à l’écriture par un compilateur de configuration, qui en dérive un artefact binaire reproductible que le runtime consomme. Le texte lisible (JSON/YAML/TOML) est une projection d’import/export, jamais la forme runtime ; le binaire est un artefact de build, jamais la source de vérité.

Sept règles gravées :

  1. La source typée fait foi ; le binaire est dérivé. La source (forme texte typée, ou entrée structurée d’une GUI) est canonique, versionnable, diffable. Le binaire est un artefact reproductible qu’on peut détruire et reconstruire à l’identique (même esprit qu’ADR-025). Jamais l’inverse : inverser, c’est réinventer le Registre.

  2. La compilation valide en amont (Principe 4). Le compilateur vérifie schéma, types et invariants avant de produire le binaire, refuse une entrée invalide, et émet des diagnostics de qualité compilateur (ligne/colonne, message actionnable, suggestion). Le runtime ne voit jamais de configuration invalide.

  3. Le compilateur est tenu à la barre couches 0/1. Il ingère de la donnée humaine = donnée hostile (Principe 3) : fuzzing obligatoire, property-testing, couverture 100 %. La confiance se déplace du « sysadmin soigneux » vers cet outil, qui devient un composant sécurité-critique.

  4. Artefact binaire schema-first à évolution intégrée. Le format doit gérer la compatibilité avant/arrière par construction (numérotation de champs, optionnalité — famille Cap’n Proto / FlatBuffers / protobuf, à auditer sous la règle des 80 %, ADR-024). Il porte version de schéma + checksum : une corruption est détectée, jamais lue en silence. Lecture zéro-copie / mmap privilégiée (budget runtime, Principe 9).

  5. Projections texte round-trippables. Import/export JSON/YAML/TOML pour le développeur et le technicien ; outil de diff lisible entre deux générations. Ces formes alimentent ou exportent la source, mais ne sont pas lues par le runtime.

  6. Sûreté système par générations + switch atomique + rollback. Une configuration valide peut exprimer une mauvaise intention (couper le réseau, mauvais paramètre kernel) : la validité ne suffit pas. Le filet anti-brick réel est le modèle générationnel — conserver la dernière configuration compilée known-good, basculer atomiquement (air-filesystem::write_atomic), offrir un retour à la génération précédente (y compris une entrée de secours au boot pour le cas pré-userspace type bootloader).

  7. Backends de compilation = le joint d’indépendance. Le compilateur émet vers une cible enfichable. Pour V1 sur systemd (ADR-005), la cible est l’ensemble unit files + drop-ins /etc générés et validés : l’humain édite la source Air, jamais l’unit file. Le jour où une brique systemd est remplacée par une brique Air native, on change le backend (émettre le binaire natif), sans toucher la source de l’utilisateur. Cf. la note de stratégie de remplacement progressif ci-dessous.

Conséquences

  • AirConfig (spec air-base-lib services, aujourd’hui « parseur TOML derrière une feature ») est étendu vers ce modèle : un cœur validant unique, air-config, partagé par tous les frontends.
  • Deux publics, un cœur. Frontend texte typé piloté par le schéma (autocomplétion, doc inline, erreurs précises) pour le développeur ; GUI contrainte par le schéma — l’état invalide devient inexprimable — pour l’utilisateur lambda. Même validation, mêmes générations, même rollback.
  • Couches touchées : air-base-lib (couche 1, le cœur air-config), air-systemd (couche 2, backend unit files pour V1), les outils d’administration (couche 5), et les consommateurs comme air-device.
  • Articulation principes/ADRs : Principe 4 (valider en amont, parse don’t validate), Principe 3 (entrée hostile → fuzzing), Principe 9 (lecture binaire économe), ADR-025 (artefact reproductible), ADR-019 (les erreurs de compilation sont des AirError riches et contextualisées).

Alternatives rejetées

  • Statu quo Unix (texte /etc canonique, édité à la main). Lisible et versionnable, mais validité tardive et « typo qui brique » — le problème même qu’on résout.
  • Binaire canonique sans source texte (modèle Registre). Rapide mais opaque, fragile, non diffable, non versionnable.
  • Format binaire maison sans politique d’évolution. Dette de schéma garantie sur l’horizon 20 ans ; d’où l’exigence schema-first (règle 4).

Hors périmètre (à traiter ailleurs)

L’authentification/intégrité des exécutables, bibliothèques et pilotes présents sur la machine (signature, contrôle de ce qui « doit » être là, exceptions développeur) est un second pilier distinct — adjacent au package management et au confinement OS — qui fera l’objet de son propre ADR. Ce document ne couvre que la configuration.

Amendement — les paramètres du noyau : l’équivalent Air de sysctl (2026-08-22)

Origine. Question du BDFL, posée en cherchant le domicile du manager d’air-config :

Ce qui n’a pas encore été abordé dans les ADRs mais qu’il va bien falloir traiter à un moment, c’est l’équivalent de sysctl — et je pensais naturellement confier cette tâche à AirConfigManager.

Puis, sur la forme de la relation :

Il y a air-system, le contrôleur qui sait comment parler au système, qui connaît les paramètres du système. Puis AirConfigManager, celui qui manage ce contrôleur… Maintenant la relation pourrait être inversée.

Ce que cet ADR disait déjà, sans en faire un objet. La règle 6 nomme le « mauvais paramètre kernel » comme l’exemple type d’une configuration valide qui exprime une mauvaise intention. Le sujet était donc vu — comme un risque, jamais comme un objet que le modèle sait porter. Cet amendement le fait entrer dans le périmètre.

Rectification préalable — deux affirmations que le code ne tient pas (2026-08-22)

Vérifié avant d’écrire, parce qu’un ADR qui se croit outillé et ne l’est pas est pire qu’un ADR muet :

  • La règle 7 annonce un backend systemd de couche 2, et les « Conséquences » nomment une crate air-systemd. Elle n’existe pas. Les quatre backends de projection vivent dans air-config/src/backend/accounts.rs, fstab.rs, resolv.rs, systemd.rs — donc en couche 1, à l’intérieur du cœur. L’intention de la règle 7 (le backend est le joint d’indépendance, on le change sans toucher la source de l’utilisateur) est tenue ; sa localisation annoncée ne l’est pas.
  • Les « Conséquences » situent le cœur dans air-base-lib. Il vit dans une crate dédiée, air-config (couche 1), que dix crates consomment.

D-a — Un paramètre du noyau est de la configuration, et il entre dans ce modèle

La valeur voulue d’un paramètre du noyau est une donnée de configuration comme une autre : elle vit dans la source typée, la compilation la valide, et l’artefact binaire la porte. Rien dans les sept règles ne change pour elle.

Ce qui est nouveau, c’est que la valeur effective ne vit pas dans un fichier qu’Air écrit : elle vit dans le noyau en fonctionnement, et disparaît au redémarrage. Un paramètre du noyau a donc deux existences — celle qu’Air décide et celle que la machine porte à cet instant — et c’est cette dualité, absente partout ailleurs dans cet ADR, qui justifie les décisions qui suivent.

D-b — C’est la cinquième surface de projection — et le rollback y a une limite honnête

air-config projette déjà la configuration d’Air sur des surfaces étrangères qu’il ne possède pas : /etc/passwd, /etc/fstab, /etc/resolv.conf, les drop-ins systemd. /proc/sys est la cinquième, et le geste est le même. Générations, bascule atomique et retour à la génération précédente (règle 6) s’y appliquent.

Mais le filet anti-brick n’y est pas complet, et l’écrire est le seul moyen de ne pas mentir. Revenir en arrière sur un paramètre du noyau, c’est y réécrire l’ancienne valeur — ce qui suppose qu’elle soit réécrivable. Certains paramètres ne le sont pas : le noyau documente kernel.unprivileged_bpf_disabled comme à sens unique — mis à 1 ou 2, il ne se remet pas sans redémarrer. (Propriété documentée du noyau ; elle n’a pas été vérifiée sur nos cibles, et la vérifier demanderait de desserrer la sécurité d’une machine réelle.)

Conséquence gravée : un paramètre à sens unique se déclare comme tel dans le schéma, et son application n’est pas couverte par la promesse de rollback. Une génération qui en contient un franchit une frontière que le modèle générationnel ne sait pas défaire.

D-c — Air ne connaît que les paramètres que son schéma énumère

Jamais « écris cette valeur à ce chemin sous /proc/sys ». Chaque paramètre est un champ nommé et typé du schéma, portant son chemin, son type, le sens dans lequel il durcit, et son caractère à sens unique éventuel.

Deux raisons, et chacune suffirait :

  1. Principe 11. Un écrivain de chemin générique est exactement le parseur tolérant sur le chemin d’entrée d’une décision que le principe proscrit : il accepte ce qu’il ne comprend pas, et deux lecteurs en tirent deux conclusions.

  2. Sans le schéma, la garde de D-e est impossible — le sens du durcissement n’est pas dérivable de la valeur. Mesuré sur carbon le 2026-08-22 :

    ParamètreLuSens qui durcit
    kernel.dmesg_restrict1monter
    kernel.kptr_restrict1monter
    kernel.yama.ptrace_scope1monter
    kernel.randomize_va_space2monter
    fs.protected_symlinks1monter
    vm.mmap_min_addr65536monter
    kernel.perf_event_paranoid4monter — et l’échelle descend à −1 : le champ est signé
    kernel.unprivileged_bpf_disabled2monter — et à sens unique (cf. D-b)
    net.ipv4.ip_forward1descendre

    ip_forward est le contre-exemple qui ferme la porte : « plus grand = plus sûr » est faux. Aucun moteur générique ne peut juger ; seul le schéma le sait.

  3. Un paramètre n’est même pas toujours une échelle. Relevé le 2026-08-22 sur les deux arches tier-1 (carbon x86_64, raspi-srv-2 aarch64) : les dix-neuf paramètres candidats existent sur les deux, mais ils ne se comparent pas de la même façon. Le schéma distingue donc trois espèces, et la garde de D-e s’énonce dans chacune :

    EspèceCe que « desserrer » veut direExemple
    Échellefranchir la valeur dans le sens qui affaiblit — sens porté par le champkernel.dmesg_restrict (monter durcit), net.ipv4.ip_forward (descendre durcit)
    Seuildescendre sous le plancher en vigueurvm.mmap_min_addr65536 sur x86_64, 32768 sur aarch64 : le plancher sûr dépend de l’architecture et ne se grave pas
    Masque de bitsallumer un bit éteintkernel.sysrq = 176 sur les deux exécuteurs, soit 0b10110000 — ni une échelle ni un seuil

    Un masque de bits enterre définitivement l’idée d’une comparaison numérique unique : desserrer sysrq de 176 à 180 ajoute une fonction, alors que 180 > 176 comme 176 > 172. La comparaison est par espèce, jamais générique.

    Deux autres valeurs diffèrent entre les exécuteurs sans que l’une soit plus sûre dans l’absolu — fs.suid_dumpable (0 / 2) et net.ipv4.ip_forward (1 / 0) — ce qui confirme qu’Air décrit un sens, jamais une valeur universelle attendue.

D-d — Le savoir est un contrôleur interne d’air-system ; la décision est au AirConfigManager

La question posée était « qui manage qui ». Le test d’ADR-149 D4 n’est pas la hiérarchie, c’est le contrat : un manager est la surface publique que les toits appellent ; un contrôleur interne est le secret d’implémentation, « destiné à évoluer, changer, disparaître ».

Les deux contrats sont déjà tenus, et l’inversion en casserait un : air-libc-system (src/lib.rs:35) bind déjà AirSystemManager pour gethostname/sethostname ; air-config est consommé depuis la couche 2 par air-sshd, air-launchd, air-bundle et six autres. air-system et air-config restent tous deux des managers.

Ce qui est authentiquement un contrôleur, c’est la table des paramètres — chemin, type, sens du durcissement, sens unique. C’est du savoir système pur, qui évolue avec les versions du noyau : la définition littérale du contrôleur interne. ADR-149 D6 l’autorise sans réserve (« une crate peut regrouper 1, 2, 3 ou N contrôleurs ») : air-system ne devient pas un contrôleur, il en gagne un, et garde son manager.

Elle vit dans air-system, pas dans air-config. air-sys-syscall lit déjà /proc/sys/kernel/apparmor_restrict_unprivileged_userns (l. 4052) et /proc/sys/kernel/yama/ptrace_scope (l. 4859) dans ses harnais : les futurs consommateurs de cette table ne sont pas le domaine de la configuration. Si elle y vivait, air-sandbox devrait dépendre de la configuration pour demander « ce noyau autorise-t-il les userns non privilégiés ? ». ADR-149 D7 autorise explicitement la consommation intra-couche entre contrôleurs, qui est le chemin retenu.

Et c’est le caractère interne qui rend la garde incontournable. ADR-149 D9 interdit à la couche 2 de voir un contrôleur : un service qui veut toucher un paramètre ne peut passer que par AirConfigManager, donc par la garde de D-e. Si la table était publique, n’importe quel service desserrerait kernel.dmesg_restrict sans qu’aucune politique soit consultée. La porte n’est pas incontournable parce que les appelants sont vertueux : elle l’est par construction.

Le partage est donc :

Ce qu’il détient
Contrôleur interne d’air-system vit le paramètre, de quel type, dans quel sens il durcit, s’il est à sens unique. Mécanisme. Libre d’évoluer avec le noyau.
AirSystemManagerInchangé : l’identité système à syscall dédié (hostname, nombre de processeurs).
AirConfigManagerQuelle valeur, d’où elle vient, quand elle s’applique, et ce qui se passe si elle desserre. Politique — donc il porte la garde.

D-e — La garde porte sur le desserrage, jamais sur le durcissement

C’est la monotonie, encore — la même forme qu’à ADR-112 et ADR-150 D3 : durcir un paramètre ne peut pas affaiblir la machine ; le desserrer, si. Beaucoup de ces paramètres sont les défenses du noyau.

L’opération soumise au SecurityManager (ADR-089) est donc le desserrage, et elle porte le paramètre, sa valeur actuelle et la valeur demandée — jamais un simple « on écrit ». Ce que « desserrer » signifie se lit dans l’espèce du paramètre (D-c) : franchir dans le mauvais sens pour une échelle, descendre sous le plancher pour un seuil, allumer un bit pour un masque. Une politique peut alors dire ce qu’aucune n’a pu dire jusqu’ici :

Sur cette machine, aucun paramètre du noyau ne se desserre — quel que soit le motif invoqué.

Le durcissement n’est pas gardé, et c’est délibéré : une politique qui refuserait de durcir ferait de la garde le vecteur qu’elle prétend fermer.

D-e-bis — La projection sur /proc/sys ne peut pas être atomique

Les quatre backends existants écrivent par air_filesystem::AirFileSystem::write_atomic — fichier temporaire puis rename, ce qui rend la bascule indivisible (règle 6). procfs ne supporte pas rename : la cinquième surface ne peut pas emprunter ce mécanisme. Une génération qui touche N paramètres les écrit donc un par un, et une interruption au milieu laisse la machine dans un état intermédiaire qu’aucun rename ne rattrape.

C’est la seconde limite de D-b, et elle a la même cause : cette surface n’est pas un système de fichiers, c’est une interface du noyau qui y ressemble. L’écrire ici évite qu’un lecteur suppose l’atomicité par analogie avec les quatre autres backends.

D-f — Volatile et durable sont deux opérations, jamais un drapeau

sysctl a deux visages : l’application immédiate et non persistante (-w), et l’inscription qui survit au redémarrage (/etc/sysctl.d). Air garde les deux, séparés :

  • l’inscription modifie la source typée, se recompile, et prend effet par une génération ;
  • l’application volatile touche le noyau maintenant, ne survit pas au redémarrage, et se journalise comme telle.

Elles ne sont jamais deux modes d’un même appel distingués par un drapeau. Une machine où l’on ne distingue pas « appliqué » de « inscrit » est une machine qui ment à son administrateur — et elle ne lui ment qu’au redémarrage suivant, c’est-à-dire au pire moment.

Hors périmètre de cet amendement

Qui applique les paramètres au démarrage — PID 1, le lanceur, ou une unité dédiée — n’est pas tranché ici. C’est une question d’amorçage, adjacente à ADR-005 et au lanceur, et l’inventer au passage serait exactement le raccourci que ce dépôt refuse.


Licence du document : MPL 2.0 Statut : Accepté. Édition directe autorisée en phase de design pré-ouverture publique ; après ouverture publique, toute évolution passe par RFC (ADR-015).