Bascule du jalon B — air-sshd sous air-launchd
Ce guide décrit un geste d’administration, pas une procédure automatisée. Il n’existe aucun script qui l’exécute, et c’est délibéré (ADR-121) : un système qui installe seul son propre superviseur est un système dont personne ne relit l’installation.
Ce que le jalon exige, exactement
ADR-168 D2 :
une machine où l’unité
air-sshd.serviceest retirée, le service tournant sous le lanceur
C’est un état de machine. La preuve en test — le harnais sous_air_launchd — est
nécessaire et ne suffit pas.
Le chemin : réversible d’abord (ADR-168 D2.2)
Le critère décrit l’état final, pas le chemin. Retirer l’unité avant d’avoir vérifié laisserait la machine sans démon SSH Air pendant qu’on diagnostique.
⚠️ Corrigé le 2026-10-10 — le troisième port n’existe pas. Ce tableau faisait servir le lanceur sur 2223 pendant la cohabitation, et l’étape 2 octroyait
--bind-port 2223. Or le plafond ne nomme que 22 et 2222, aux deux endroits qui comptent :bindPorts = [22, 2222]danscrates/air-sshd/bundle/manifest.toml, etbind_ports: vec![22, 2222]danscrates/air-bundle/src/ceiling.rs:677.2223n’a jamais figuré dans le manifeste (git log -S'2223': aucun commit). Un octroi pour 2223 excède donc la demande, et le lanceur écarte le bundle.Il n’y a donc pas de cohabitation par un troisième port. Ce qui borne le risque est ailleurs, et c’est suffisant : un accès d’administration indépendant des deux démons, et un retour en arrière d’une commande. Élargir le plafond d’un service de sécurité pour le confort d’une manœuvre de transition serait payer longtemps un gain d’un soir.
| # | Geste | Ce qu’on vérifie avant de continuer |
|---|---|---|
| 0 | S’assurer d’un accès indépendant des deux démons | il écoute, et on s’y connecte AVANT de toucher au reste |
| 1 | Empaqueter et installer le bundle | le manifeste scellé porte l’empreinte du binaire livré |
| 2 | Émettre l’octroi, sur le port réellement servi (22 ou 2222) | il est signé par la clé de la machine, et tient sous le plafond de service |
| 3 | Installer air-launchd.service | l’unité démarre, et le journal nomme les services supervisés |
| 4 | Arrêter l’ancienne unité, démarrer le lanceur | le port est repris, air-sshd est un ENFANT du lanceur |
| 5 | Ouvrir une session sur le port servi | de bout en bout : poignée de main, authentification, shell |
| 6 | Retirer le fichier d’unité, daemon-reload | l’ancienne unité est partie |
| 7 | Mettre le suivi et le journal à jour | le jalon est franchi |
Le retour en arrière est possible à chaque étape : tant que l’étape 5 n’est pas faite,
systemctl stop air-launchd rend la machine à son état d’avant.
⚠️ L’accès d’administration ne doit dépendre d’aucun des deux. Sur carbon, il passe par
OpenSSH sur le port 52222 — un troisième port, servi par un troisième démon. Cela borne le
risque ; cela ne dispense pas de vérifier avant de retirer, parce qu’un jalon franchi en
laissant un service cassé n’est pas un jalon franchi.
Et
systemctl is-active ssh.servicene répond PAS à cette question. Sur speedy, le 2026-10-10, ce service étaitinactive— et le port 22 était pourtant tenu, parsystemdlui-même (users:(("systemd",pid=1,fd=100))) : activation par socket, OpenSSH démarrant à la demande. En conclure « cette machine n’a pas d’autre accès SSH » était faux. Ce qu’il faut regarder est qui écoute (ss -lntp) — et mieux : s’y connecter.
Étape 1 — empaqueter et installer
Le manifeste versionné (crates/air-sshd/bundle/manifest.toml) est un gabarit : son
empreinte vaut soixante-quatre zéros. C’est air-bundle install --seal-binary qui la pose,
sur le binaire réellement construit.
⚠️ Corrigé le 2026-10-10. Cette étape commandait
cargo xtask package --package air-sshd. Cette sous-commande n’existe pas — relevé sur les 34 entrées du dispatch dextask/src/main.rs, où"package"ne figure pas une seule fois. C’était la PREMIÈRE étape du guide, celle qui produit l’artefact dont tout le reste dépend, et elle a arrêté une préparation de redéploiement au titre de la règle NO GO n° 1.
cargo xtask deb # construit, SCELLE, puis empaquette
xtask deb est ce qui scelle : il appelle air-bundle-gnu install --manifest <gabarit> --key <clé> --root <étape>/var/lib/air/services/<bundleId> --seal-binary <exécutable>
(xtask/src/deb.rs). Sans clé ni ephemere, la construction échoue — « un paquet qui
livrerait un manifeste non signé ne serait pas incomplet, il mentirait sur ce qu’il affirme ».
Pour re-sceller en place sur une machine, sans repasser par un paquet, c’est la même commande à la main :
air-bundle install \
--manifest crates/air-sshd/bundle/manifest.toml \
--key <clé développeur> \
--root /var/lib/air/services/org.air-desktop.sshd \
--seal-binary /var/lib/air/services/org.air-desktop.sshd/air-sshd
Deux clés, et ce ne sont pas les mêmes : install signe le manifeste avec une clé
développeur (air-bundle developer-key create), l’octroi de l’étape 2 est signé par la
clé de la machine. Et install ne déploie pas : « le répertoire et ses exécutables
doivent déjà être là ; cette commande constate et scelle ».
Le bundle est déposé sous /var/lib/air/services/org.air-desktop.sshd/ : l’exécutable, son
manifest.aircfg et sa manifest.signature.aircfg.
Vérifier avant de poursuivre — un manifeste qui ne demande pas ce dont le démon a besoin produit un service qui ne peut jamais démarrer, l’octroi ne pouvant excéder la demande :
air-bundle show --bundle org.air-desktop.sshd
La sortie doit porter ioUring, le port en écoute, et les trois chemins de lecture
(/etc/air/sshd.aircfg, /etc/air/sshd, /etc/air/keystore).
Étape 2 — émettre l’octroi
L’octroi est signé par la clé de la machine, qui vit dans son keystore et ne le quitte pas (ADR-150 D16). Il n’accorde que ce qui est nommé :
air-bundle grant --bundle org.air-desktop.sshd \
--io-uring \
--bind-port 2222 \
--read-path /etc/air/sshd.aircfg \
--read-path /etc/air/sshd \
--read-path /etc/air/keystore \
--aircom-publish org.air-desktop.sshd \
--session-program /bin/bash \
--session-program /usr/sbin/nologin
Les noms d’options sont relevés dans crates/air-bundle-cli/src/lib.rs, pas reconstitués :
c’est --read-path et non --read, --session-program et non --session. Un guide dont on
copie les commandes doit porter celles qui existent.
Il est déposé sous /etc/air/grants/.
Étape 3 — installer l’unité du lanceur
Le fichier versionné est crates/air-launchd/units/air-launchd.service. Son en-tête explique
pourquoi Delegate=yes n’est pas décoratif : sans lui, le lanceur ne peut pas distribuer
pids/memory à ses services, et toutes ses bornes tombent en silence.
sudo cp crates/air-launchd/units/air-launchd.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now air-launchd.service
Ce qu’il faut lire ensuite, et ne pas survoler :
systemctl status air-launchd.service
journalctl -u air-launchd.service -n 50
Un bundle écarté est nommé sur la sortie d’erreur, avec son motif. Un lanceur qui démarre
sans rien superviser rend un succès — un système sans service installé n’est pas une
panne — donc active (running) ne prouve pas que le démon tourne. C’est le journal qui le
dit, pas le statut de l’unité.
Étapes 4 et 5 — reprendre le port, puis la vérification qui compte
L’ancienne unité tient le port : elle s’arrête AVANT que le lanceur démarre, sans quoi le
démon du lanceur échoue en EADDRINUSE et brûle ses relances (maxRestarts = 3).
sudo systemctl disable --now air-sshd.service # le port est libéré
sudo systemctl enable --now air-launchd.service # le lanceur le reprend
journalctl -u air-launchd.service -n 50 # ce qu'il supervise, et ce qu'il écarte
ps -eo pid,ppid,comm | grep -E 'air-launchd|air-sshd' # air-sshd doit être ENFANT du lanceur
ssh -p <port servi> -o StrictHostKeyChecking=no <utilisateur>@<machine>
Le fichier d’unité n’est pas encore retiré, et c’est délibéré : systemctl enable --now air-sshd.service rend la machine à son état d’avant tant qu’il est là. On ne le retire qu’une
fois la session ouverte.
Une session ouverte prouve la chaîne entière : arbitrage, cage Landlock, seccomp avec la
famille io_uring, écoute liée, publication du nom au registre.
Si elle échoue, le diagnostic est dans cet ordre :
| Symptôme | Cause probable |
|---|---|
| le processus sort avec 125 | la cage n’a pas pu être posée ; l’étape fautive et l’errno sont sur sa sortie d’erreur (CHILD_CONFINE_FAILURE) |
| tué par SIGSYS | un appel système hors de la cage seccomp — famille d’octroi manquante |
| « octroi refusé » au journal | l’octroi excède le manifeste, ou le manifeste excède le plafond de service |
| connexion refusée, service actif | le port accordé n’est pas celui que la configuration sert |
| journal : « supervision interrompue : landlock_path_open », précédé de « N service(s) supervisé(s), 0 écarté(s) » | le déploiement de la machine est PÉRIMÉ — et c’est le premier soupçon, avant toute autre hypothèse. Le bundle et l’octroi sont bons : le lanceur les a acceptés et a lancé le démon. Mais son binaire ne sait pas poser les chemins que le manifeste demande aujourd’hui ({home} d’ADR-180, la liste /etc d’ADR-183 D8, les périphériques, ADR-185, ADR-187). Ne PAS regarder la date du fichier : le mtime de /usr/sbin/air-launchd est l’horodatage déterministe des builds reproductibles (ADR-025) — 2026-07-24 sur toutes les machines, à jour comprises. La version de paquet est 0.7.0 partout. Les deux contrôles qui discriminent : sha256sum /usr/sbin/air-launchd, comparé à une machine où le service tourne ; et la date d’installation réelle, zgrep ' air-launchd' /var/log/dpkg.log*. Constaté sur speedy le 2026-10-11 : installé le 2026-08-14, contre le 2026-09-28 sur carbon et neon. Un lanceur d’avant #974 (2026-09-25) tombe en entier sur l’échec d’un seul service — ce que « supervision interrompue » dit exactement. |
Étapes 6 et 7 — le retrait de l’unité
Seulement après qu’une session s’est ouverte sous le lanceur :
sudo cp /etc/systemd/system/air-sshd.service /root/air-sshd.service.sauvegarde # au cas où
sudo rm /etc/systemd/system/air-sshd.service
sudo systemctl daemon-reload
Il n’y a pas de « retour sur 2222 » : le lanceur sert dès l’étape 4 le port que la configuration et l’octroi nomment — celui que l’ancienne unité servait. Le port ne change jamais pendant la manœuvre, puisque le plafond n’en autorise pas de troisième.
Après la bascule
- mettre
docs/etat-avancement.mdà jour : le jalon B n’est franchi qu’à l’étape 7, et son critère est un état de machine — donc machine par machine, jamais « le parc » ; - journaliser la campagne dans
docs/JOURNAL.md; - ce qui reste ouvert pour le jalon C est dans ADR-168 D2.
Ce que ce guide a appris le 2026-10-10
Une tentative sur speedy a échoué, et ses quatre enseignements sont désormais dans le corps du guide plutôt que dans la mémoire de quelqu’un :
- Deux de ses sept étapes étaient inexécutables — une sous-commande
xtaskqui n’existe pas, et un port que le plafond refuse. Un guide se relit contre le code, pas contre le souvenir qu’on a de lui ; c’est ce que son propre paragraphe sur les noms d’options exige, et il ne se l’appliquait qu’aux options. - Un guide exact sur une machine périmée échoue quand même. Le bundle et l’octroi de
speedy étaient bons ; son lanceur avait été installé le 14 août, celui de carbon et de
neon le 28 septembre. L’empreinte du binaire et le journal de
dpkgsont désormais le premier contrôle du tableau de diagnostic. - Le retour en arrière a marché, et c’est la seule raison pour laquelle l’échec n’a rien coûté : unité restaurée, session rouverte, machine rendue à son état d’avant. Garder l’ancienne unité jusqu’à la session de vérification n’est pas une précaution de style.
- Le contrôle qu’on y avait d’abord mis ne mesurait rien — corrigé le 2026-10-11. Le
tableau disait « vérifier la DATE de
/usr/sbin/air-launchd», et cette date est l’horodatage déterministe des builds reproductibles : identique sur les trois machines. Un diagnostic fondé sur elle aurait conclu « déploiement périmé » sur n’importe quelle machine, y compris à jour — un générateur de faux positifs placé en tête du tableau. Un instrument qui rend la même valeur partout ne distingue rien, et c’est muet, non bruyant : il se lit sans soupçon.