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-010 — Format .airapp et .airservice, entitlements déclaratifs signés, sandbox capability-based

Statut : Accepté — document fondateur. Édition directe autorisée en phase de design pré-ouverture publique ; immuable après ouverture publique (toute évolution via RFC, ADR-015). Catégorie : Architecture (couche 5 — distribution et bac à sable).

Amendé deux fois, toutes deux en phase de design :

  • 2026-08-03, par ADR-150 D2 : le manifeste et les entitlements sont des artefacts binaires, non du TOML.
  • 2026-08-12, ici même : « déplaçable » ne veut pas dire exécutable en place, et la disposition sur disque est fixée (§ Décision).

Contexte

Distribuer une application suppose de trancher trois questions liées : sous quelle forme elle est livrée, ce à quoi elle a le droit d’accéder, et comment on vérifie qu’elle n’a pas été altérée.

Les répondre séparément produit les systèmes où les permissions sont demandées après coup, à un utilisateur hors d’état de juger.

Décision

Format standard de distribution : bundle .airapp (répertoire structuré : manifeste et entitlements en artefacts binaires, signatures détachées, binaires, frameworks embarqués, ressources, schémas AirCom publiés). Autonome et déplaçable comme forme de distribution.

Amendement du 2026-08-12 : déplaçable ne veut pas dire exécutable en place. Un bundle ne s’exécute que depuis une racine que la porte d’administration contrôle (/usr/lib/air/bundles/, /var/lib/air/applications/, /var/lib/air/services/). La raison est structurelle : un répertoire à la fois inscriptible par l’utilisateur et exécutable annulerait tout le confinement — c’est la forme même de l’attaque que la règle EXECUTE d’office cherche à fermer. Et la politique de montage l’avait déjà tranché de son côté : les supports amovibles sont montés nosuid,nodev,noexec, si bien qu’un .airapp posé sur une clé USB ne s’exécutait de toute façon pas.

Ce que « déplaçable » continue de dire : le bundle est autonome — il ne dépend d’aucun état hors de lui —, il se copie, se transporte et s’inspecte tel quel. Ce qu’il ne dit plus : qu’on puisse le lancer depuis n’importe où. La disposition sur disque fait autorité : docs/notes/instruction-disposition-bundle-fr.md.

Disposition sur disque (décision BDFL du 2026-08-12) :

<racine>/<bundleId>/
├── manifest.aircfg              artefact du domaine air.manifest
├── manifest.signature.aircfg    artefact du domaine air.signature
├── executables/
├── libraries/
└── resources/

Le répertoire installé ne porte pas le suffixe .airapp : kind vit dans l’artefact précisément pour ne pas se déduire d’un chemin, plus facile à influencer. La racine porte la distinction, et c’est la porte d’administration qui la choisit.

Le manifeste et les entitlements sont des artefacts BINAIRES — schéma Cap’n Proto sous enveloppe AIRCFGv1 —, avec un codec texte fourni aux développeurs pour la rédaction et la relecture. Le lanceur ne lit que le binaire. C’est le Principe 11 (« Entrée stricte, sortie lisible ») : on ne met pas un parseur généraliste sur le chemin de décision de privilèges. Fait autorité : ADR-150 D2.

Format secondaire .airservice pour les composants système (drivers utilisateur, services réseau, agents système avec accès eBPF, etc.) avec modèle d’entitlements étendu et installation nécessitant privilèges administrateur. Distinction visuelle dans le shell.

Entitlements déclaratifs en deny-by-default, signés. Modèle hybride : entitlements par chemins explicites pour le filesystem + entitlements thématiques pour les patterns courants. Quatre familles : filesystem (mappés sur Landlock), network (mappés sur namespace réseau + filtrage), AirCom (capabilities aux services), devices.

Trois niveaux de signature : développeur (toujours, self-signed possible), notarization (modèle ouvert : plusieurs autorités possibles), politique locale (Gatekeeper-like).

Le manifeste ne confère rien — il demande. Ce que la machine accorde est un artefact distinct, l’octroi, signé par la clé de la machine, émis à l’installation et vivant hors du bundle. Le présent ADR ne décrit que le côté demande ; l’octroi, la procédure d’arbitrage à l’exec et la révocation font autorité dans ADR-150.

Conséquences

air-launchd (couche 2 — décision BDFL du 2026-08-04, cf. docs/notes/air-launchd-couche2-roadmap-fr.md ; il lie air-sandbox/air-process en couche 1 et AirCom en couche 2, sans saut de couche) applique les entitlements au lancement : namespaces, Landlock, seccomp, distribution des capabilities AirCom initiales par FD passing. Pas d’autorité ambiante (POLA strict).

Alternatives rejetées

Aucune alternative n’a été consignée lors de l’instruction.

Amendement — les programmes qui ne sont pas des ELF : scripts et interprètes (2026-08-21)

Décision BDFL préalable, qui ferme le cas ELF : le sort des binaires de provenance étrangère est très clair — poubelle. On ne permet pas qu’ils puissent s’exécuter. Un développeur travaillant sur un poste Air a donc la responsabilité d’empaqueter son exécutable avec les bons entitlements, et de passer par les commandes Air pour se générer les octrois nécessaires. À ces conditions seulement, il peut tester son programme sur la machine.

Cette décision est nette pour un ELF. Elle ouvre en grand le cas des langages de script — shell, Perl, Python, Ruby, JavaScript, awk — où le programme n’est pas le fichier qui s’exécute.

Le piège : l’interprète comme porte dérobée universelle

python3 mon_script.py. Le script n’est pas un ELF ; seul python3 en est un. Si python3 porte un octroi couvrant le réseau et $HOME, alors tout script Python hérite de cet octroi. Ce n’est pas une faille : c’est la conséquence logique d’avoir doté l’interprète pour le pire de ses usages. Un unique octroi large, adossé à un binaire universel, annule le modèle de capacités pour tout ce qu’il exécute.

Le raisonnement vaut pour tout ce qui exécute du code reçu en donnée : les shells en premier lieu, mais aussi un binaire qui charge des greffons, ou une machine virtuelle.

D-a — Le sujet de l’octroi est le programme, jamais l’outil qui l’exécute

Le manifeste peut déclarer un point d’entrée qui n’est pas un ELF, accompagné de l’interprète requis, désigné par son bundleId — jamais par un chemin, plus facile à influencer (même raison que pour kind, cf. la disposition sur disque ci-dessus).

L’arbitre résout alors le script comme sujet : il trouve son bundle, en dérive la cage, vérifie que l’interprète déclaré est lui-même empaqueté et approuvé, puis exécute l’interprète dans la cage du script. Le python3 chargé est une dépendance du programme, pas le sujet de la décision.

C’est la généralisation directe d’ADR-150 D10 — la cage vient du bundle qui déclare la cible résolue — à une cible qui se trouve être du texte. La cage suit le programme, jamais l’outil qui le fait tourner.

D-b — L’octroi propre d’un interprète est minimal

Décision BDFL : le python3 déployé sur Air ouvre un fichier de script et le lit, sur la $HOME de l’utilisateur seulement. Rien d’autre : pas de réseau, pas d’écriture, aucune exécution de tiers.

C’est ce que l’interprète obtient quand il est lui-même le sujet — un python3 lancé sans script déclaré, c’est-à-dire une boucle interactive. Ce n’est pas un plafond imposé au script mené par son bundle : dans ce cas, l’arbitre pose la cage du script, en un seul exec, et l’octroi propre de l’interprète n’entre pas dans le calcul.

L’inversion mérite d’être dite, parce qu’elle est contre-intuitive et qu’elle est le cœur de la décision : les systèmes existants livrent leurs interprètes dotés de l’union de tous les besoins imaginables. Air les livre dotés de presque rien, et fait porter le besoin par le programme qui en a un.

D-c — On ne filtre jamais sur argv

La tentation est de faire dire à l’arbitre : « si la cible est un interprète, regarde son premier argument ». C’est un filtre qui échoue ouvert, et il est écarté d’emblée :

python3 -c 'import os; os.system("…")'
python3 -m un_module_arbitraire
python3 < script.py
cat script.py | python3 -

Un interprète a une infinité de manières de recevoir du code. Toute analyse d’argv serait un parseur tolérant sur le chemin d’entrée d’une décision de privilège, ce que le Principe 11 proscrit, et un parseur dont l’échec ne serait pas un refus mais une autorisation par défaut.

Tout ce que l’interprète peut faire, le script peut le faire. Le sujet doit donc être établi avant que l’interprète démarre, et jamais déduit de ce qu’on lui passe.

Conséquence pratique assumée : python3 mon_script.py continue de fonctionner, mais donne la cage minimale de python3 — le script échoue, et il échoue explicitement (D11 d’ADR-150 : l’absence de bundle est un refus explicite). La forme qui marche est ./mon_script.py, le fichier portant le bit d’exécution et son bundle déclarant son interprète. C’est un changement d’habitude, et il se documente ; il ne se contourne pas.

D-d — air-run : demander, jamais négocier — et l’obstacle qu’il doit franchir

La forme envisagée (BDFL, 2026-08-21) :

air-run --id-octroi abcdef01 -- python3 mon_script.py

L’obstacle, et il est dur. Le confinement est monotone, et ce n’est pas une politique : les filtres seccomp sont hérités à travers fork et execve et se composent par intersection ; les jeux de règles Landlock s’héritent et s’empilent. Un processus ne peut donc pas élargir la cage de son enfant — c’est un fait du noyau, rappelé par ADR-150 D5.

air-run, exécuté dans la cage du shell, ne peut donc pas lancer un script dans une cage plus large que celle du shell. S’il doit accorder un listen réseau que le shell n’a pas, il en est incapable, quel que soit son entitlement. Écrire l’inverse dans cet ADR décrirait un mécanisme que le noyau refuse.

Deux issues, et elles ne s’excluent pas :

(A) Le shell de session est la racine de la chaîne de l’utilisateur. Sa cage vaut ce que l’utilisateur a le droit de faire ; tout ce qu’il lance rétrécit. air-run ne fait alors jamais qu’étrécir, ce qui est toujours possible. Conséquence — et elle est saine — un script ne peut jamais excéder l’enveloppe de l’utilisateur qui le lance.

(B) Le lancement est médié par un service hors de la cage. air-run n’exécute pas : il demande au lanceur, qui pose la cage et lui rend les descripteurs. Ce n’est pas un motif neuf — ADR-147 a tranché exactement ce problème pour air-sshd : le worker de session, toujours confiné, ne fait plus d’execve, et le lancement est médié par le monitor. On rejoue ce motif, on ne l’invente pas.

RECTIFICATION du 2026-08-22 (BDFL). Ce paragraphe disait : « (B) est la voie retenue lorsqu’un script doit obtenir ce que son lanceur n’a pas ; (A) suffit dans tous les autres cas et doit rester le chemin ordinaire ». C’est (B) qui est le modèle, pas l’exception.

Le raisonnement qui manquait : sous (A), la cage de la session plafonne tout ce que l’utilisateur lance, y compris une application dont l’octroi — signé par l’administrateur — dit davantage. Une session étroite rend donc inapplicables des octrois légitimes, et une session large rend son enveloppe décorative. (A) n’était pas « le chemin ordinaire », c’était un chemin qui obligeait à choisir entre les deux.

Sous (B), le lanceur pose la cage depuis l’octroi du programme, et la session peut rester étroite sans rien empêcher. Décidé et chiffré dans ADR-150 D12 et D13, avec les deux vérifications que la médiation rend obligatoires — le lanceur ne fait confiance à aucun demandeur, et il vérifie que cet utilisateur peut invoquer cet octroi.

(B) est le modèle. (A) reste vrai comme propriété du noyau — un enfant lancé directement par la session ne sera jamais plus large qu’elle —, mais ce n’est pas par là que passe le lancement d’un programme empaqueté.

Et la correction de vocabulaire, qui n’est pas cosmétique : air-run ne négocie rien. S’il négociait, le compromettre donnerait une cage arbitraire — et il vit précisément là où le code hostile arrive en premier, dans la session de l’utilisateur. Il nomme un octroi ; le lanceur vérifie que cet octroi existe, qu’il couvre ce script, que l’empreinte concorde, et que cet utilisateur a le droit de l’invoquer. Un identifiant d’octroi est une référence, pas une créance : le connaître n’accorde rien.

C’est la même règle qu’ADR-150 D7 — l’appelant ne nomme jamais la cage — appliquée ici : il nomme un octroi, et c’est tout ce qu’il lui est permis de nommer.

D-e — Corollaire d’exploitation

Un interprète en accès libre sur une machine de production est un shell sous un autre nom. Sur un poste de développement, c’est normal et attendu. Sur un serveur, python3 accessible sans octroi doit être traité comme ce qu’il est : une porte d’exécution générale, à octroyer comme telle ou à ne pas installer.

Le développement, qui est le vrai risque

Un développeur contraint de ré-empaqueter et de ré-octroyer à chaque enregistrement désactivera la sécurité dans l’heure — et c’est ainsi qu’on perd un modèle, pas par une faille. Il faut donc un régime de développement, mais un vrai octroi, pas une exception :

  • émis par l’autorité locale (ADR-162) ;
  • borné à un répertoire que le développeur nomme ;
  • daté d’expiration ;
  • tracé au journal comme tout octroi.

Ce n’est pas une porte dérobée, et la raison est précise : un tel octroi ne peut rien accorder que le développeur ne puisse déjà accorder — sur une machine partagée, un utilisateur ordinaire ne peut rien accorder du tout. Il est étroit et daté là où doter python3 largement serait large et permanent : les deux « laissent le script tourner », un seul se referme tout seul.

Ce que cet amendement ne tranche pas

  1. La forme exacte du champ de manifeste qui déclare un point d’entrée non-ELF et son interprète — schéma Cap’n Proto, donc à instruire avec le schéma, pas ici.
  2. Le protocole entre air-run et le lanceur — AirCom, avec passage de descripteurs (ADR-151) ; la surface reste à écrire.
  3. Le mode graphique, où le rôle du shell est tenu par le gestionnaire de session. La mécanique est la même ; le lanceur diffère.

Licence du document : MPL 2.0