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

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.service est 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] dans crates/air-sshd/bundle/manifest.toml, et bind_ports: vec![22, 2222] dans crates/air-bundle/src/ceiling.rs:677. 2223 n’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.

#GesteCe qu’on vérifie avant de continuer
0S’assurer d’un accès indépendant des deux démonsil écoute, et on s’y connecte AVANT de toucher au reste
1Empaqueter et installer le bundlele 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
3Installer air-launchd.servicel’unité démarre, et le journal nomme les services supervisés
4Arrêter l’ancienne unité, démarrer le lanceurle port est repris, air-sshd est un ENFANT du lanceur
5Ouvrir une session sur le port servide bout en bout : poignée de main, authentification, shell
6Retirer le fichier d’unité, daemon-reloadl’ancienne unité est partie
7Mettre le suivi et le journal à jourle 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.service ne répond PAS à cette question. Sur speedy, le 2026-10-10, ce service était inactive — et le port 22 était pourtant tenu, par systemd lui-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 de xtask/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ômeCause probable
le processus sort avec 125la 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 SIGSYSun appel système hors de la cage seccomp — famille d’octroi manquante
« octroi refusé » au journall’octroi excède le manifeste, ou le manifeste excède le plafond de service
connexion refusée, service actifle 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 :

  1. Deux de ses sept étapes étaient inexécutables — une sous-commande xtask qui 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.
  2. 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 dpkg sont désormais le premier contrôle du tableau de diagnostic.
  3. 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.
  4. 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.