fix(deployer) : le dépôt rend sa racine au service qui écrit chez lui #112

Merged
foXaCe merged 1 commit from passe-20261018-091602-racineresident into main 2026-10-18 09:20:48 +02:00
Owner

La PR #111 a donné au déploiement un garde qui constate qu'un service ne peut pas graver
chez lui. Celle-ci enlève la cause, et la cause était une mécanique, pas un accident.

Le défaut, nommé

Le dépôt laissait la racine de la destination au propriétaire inscrit dans l'archive.

tar -xzf tourne en root dans le conteneur, et l'archive contient ./ : tar restaure donc
l'uid du poste qui a fabriqué le tar — 1000, depotmaj dans le conteneur. ordre_pose_droits
chownait tout sauf ./, sur la foi d'un commentaire (« ./ — la destination elle-même — est
écarté : elle existe avant le dépôt et n'en fait pas partie ») faux sur le mécanisme : l'archive
la dépose, elle.

⇒ Chaque déploiement re-cassait la racine de tvexchange. Le service tourne en nuvio-tvx
(uid 997), grave ses sessions de la cabine dans ce dossier, et le dossier ne lui appartenait plus.
Trois CABINE-SESSIONS-NON-GRAVEES PermissionError dans son journal, et cabine_sessions.json
n'existait pas du tout au lieu d'exister vide : toute session tombe à la première relance.

La mesure, sur la machine réelle

Chaîne entière rejouée dans le LXC 125 : archive fabriquée sur le poste (uid 1000), extraite
en root dans le conteneur, puis le fragment que ordre_pose_droits produit vraiment — joué
dans ses deux natures. Le témoin vit dans /tmp : /opt/nuvio-tvexchange n'a pas été ouvert.

archive   : drwxr-xr-x stephane/stephane 0 ./   <- l'entrée './' porte l'uid du POSTE
           uid 1000 = depotmaj | nuvio-tvx = 997

départ          : nuvio-tvx:nuvio-tvx 755     sentinelle www-data:www-data 640
après le tar    : depotmaj:depotmaj 755       <- la racine a été REPOSÉE par le tar

── fragment joué avec une nature VIDE (le comportement d'avant) ──
DROITS-POSES:3
racine          : depotmaj:depotmaj 755
écriture service: REFUSEE                      <- ce que le garde de #111 constatait

── fragment joué avec la nature RESIDENT (le correctif) ──
DROITS-POSES:3
RACINE-AU-SERVICE:nuvio-tvx
racine          : nuvio-tvx:nuvio-tvx 755
écriture service: POSSIBLE
sentinelle      : www-data:www-data 640        <- non déposée, intacte
fichiers déposés : nuvio-tvx:nuvio-tvx 644

C'est la même archive, le même tar, le même root, le même runuser : la seule différence est la
nature
, et elle change l'écriture du service de REFUSEE à POSSIBLE.

Le correctif

  • pour une cible resident — la seule qui a un processus qui écrit chez lui — la racine reçoit
    le propriétaire du service, le DOSSIER SEUL, jamais -R ;
  • la nature se lit dans la table des cibles (4e colonne) : la fonction ne la suppose pas, et un
    oubli d'argument (nature vide) n'élargit pas le périmètre ;
  • un second mot de passage positif, RACINE-AU-SERVICE:<propriétaire>, et le dépôt le LIT :
    une pose manquée est un ÉCHEC, jamais l'absence d'erreur ;
  • le refus nomme la réparation et son piège : « le dossier SEUL, jamais -R ». -R est le geste
    qui a mis la cabine hors service onze heures le 2026-08-18, et c'est celui qu'un opérateur pressé
    tapera.

Le périmètre ne s'élargit pas. Une cible statique (la cabine, servie en lecture par nginx)
et une cible lot (timers sous root) ne voient pas leur racine touchée. Ce qui a tué la cabine n'est
pas « avoir touché un dossier », c'est -R : il avait retiré à .htpasswd le groupe dont nginx
avait besoin.

La pose ne remplace pas la mesure. Poser la racine prouve qu'une commande a rendu 0, pas que
le service écrit : un ACL, un bit immuable, une monture en lecture seule, un dossier parent non
traversable, et le chown réussit sur un dossier où le noyau refusera l'écriture. preuve_resident
(#111) continue de demander au noyau, en tant que le service — les deux se surveillent.

Les preuves

Quatre tests, éprouvés par réinjection :

ce qui est mesuré comment
la racine revient au service le fragment est joué sur l'arborescence témoin, et le chemin chowné est lu
la racine seule, sans -R .htpasswd et config.js vivent dans ce dossier : ils doivent rester intacts
une cible statique n'est pas touchée même fragment, nature statique : aucun chemin racine, aucun mot
le manque fait rougir le vrai fragment amputé de l'étape de la racine — pas une ligne réécrite à la main

Le mot de passage est lu côté dépôt (test_le_depot_exige_la_racine_au_service_avant_de_rendre_vert),
et la réparation y est nommée avec son piège.

Porte

./run_tests.sh -q : garde d'octets VERT, parité conforme, 2131 passés, 14 sautés, 0 échec —
référence main 2127, mesurée sur le même banc avant d'écrire cette ligne (+4).

Ce que cette PR ne porte pas

  • Les récits et comptes de JOURNAL.md / CLAUDE.md : la référence de porte se remesure sur le
    banc en vigueur
    (RG78), ce n'est pas à cette PR de choisir le banc. À la demande.
  • Aucun déploiement n'a été joué sur la cible vivante depuis cette branche : la destination est
    saine, et cette branche ne touche aucun fichier du service (tvlogin/ inchangé). Après
    fusion, le prochain --cible tvexchange posera la racine et l'écrira. À ta demande, je le joue
    avant : le contenu déposé serait identique octet pour octet, seul le déployeur change.
La PR #111 a donné au déploiement un garde qui **constate** qu'un service ne peut pas graver chez lui. Celle-ci enlève la **cause**, et la cause était une mécanique, pas un accident. ## Le défaut, nommé Le dépôt laissait la **racine** de la destination au propriétaire **inscrit dans l'archive**. `tar -xzf` tourne **en root** dans le conteneur, et l'archive contient `./` : tar restaure donc l'uid du poste qui a fabriqué le tar — `1000`, **`depotmaj`** dans le conteneur. `ordre_pose_droits` chownait tout **sauf `./`**, sur la foi d'un commentaire (« `./` — la destination elle-même — est écarté : elle existe avant le dépôt et n'en fait pas partie ») **faux sur le mécanisme** : l'archive la dépose, elle. ⇒ **Chaque déploiement re-cassait la racine de `tvexchange`.** Le service tourne en `nuvio-tvx` (uid 997), grave ses sessions de la cabine dans ce dossier, et le dossier ne lui appartenait plus. Trois `CABINE-SESSIONS-NON-GRAVEES PermissionError` dans son journal, et `cabine_sessions.json` n'existait pas **du tout** au lieu d'exister vide : toute session tombe à la première relance. ## La mesure, sur la machine réelle Chaîne entière rejouée dans le **LXC 125** : archive fabriquée **sur le poste** (uid 1000), extraite **en root** dans le conteneur, puis le fragment que `ordre_pose_droits` produit **vraiment** — joué dans ses deux natures. Le témoin vit dans `/tmp` : `/opt/nuvio-tvexchange` n'a **pas** été ouvert. ``` archive : drwxr-xr-x stephane/stephane 0 ./ <- l'entrée './' porte l'uid du POSTE uid 1000 = depotmaj | nuvio-tvx = 997 départ : nuvio-tvx:nuvio-tvx 755 sentinelle www-data:www-data 640 après le tar : depotmaj:depotmaj 755 <- la racine a été REPOSÉE par le tar ── fragment joué avec une nature VIDE (le comportement d'avant) ── DROITS-POSES:3 racine : depotmaj:depotmaj 755 écriture service: REFUSEE <- ce que le garde de #111 constatait ── fragment joué avec la nature RESIDENT (le correctif) ── DROITS-POSES:3 RACINE-AU-SERVICE:nuvio-tvx racine : nuvio-tvx:nuvio-tvx 755 écriture service: POSSIBLE sentinelle : www-data:www-data 640 <- non déposée, intacte fichiers déposés : nuvio-tvx:nuvio-tvx 644 ``` C'est la même archive, le même tar, le même root, le même `runuser` : **la seule différence est la nature**, et elle change l'écriture du service de `REFUSEE` à `POSSIBLE`. ## Le correctif - pour une cible `resident` — la seule qui a un **processus qui écrit chez lui** — la racine reçoit le propriétaire du service, le **DOSSIER SEUL**, jamais `-R` ; - la nature se lit dans la **table des cibles** (4e colonne) : la fonction ne la suppose pas, et un oubli d'argument (`nature` vide) **n'élargit pas** le périmètre ; - un second mot de passage positif, **`RACINE-AU-SERVICE:<propriétaire>`**, et le dépôt le **LIT** : une pose manquée est un **ÉCHEC**, jamais l'absence d'erreur ; - le refus nomme la réparation **et son piège** : « le dossier SEUL, jamais -R ». `-R` est le geste qui a mis la cabine hors service onze heures le 2026-08-18, et c'est celui qu'un opérateur pressé tapera. **Le périmètre ne s'élargit pas.** Une cible `statique` (la cabine, servie en **lecture** par nginx) et une cible `lot` (timers sous root) ne voient pas leur racine touchée. Ce qui a tué la cabine n'est pas « avoir touché un dossier », c'est `-R` : il avait retiré à `.htpasswd` le groupe dont nginx avait besoin. **La pose ne remplace pas la mesure.** Poser la racine prouve qu'une **commande** a rendu 0, pas que le service **écrit** : un ACL, un bit immuable, une monture en lecture seule, un dossier parent non traversable, et le `chown` réussit sur un dossier où le noyau refusera l'écriture. `preuve_resident` (#111) continue de demander au noyau, **en tant que le service** — les deux se surveillent. ## Les preuves Quatre tests, éprouvés **par réinjection** : | ce qui est mesuré | comment | |---|---| | la racine revient au service | le fragment est **joué** sur l'arborescence témoin, et le chemin chowné est lu | | la racine seule, sans `-R` | `.htpasswd` et `config.js` vivent **dans** ce dossier : ils doivent rester intacts | | une cible `statique` n'est pas touchée | même fragment, nature `statique` : aucun chemin racine, aucun mot | | le manque fait rougir | le **vrai fragment amputé** de l'étape de la racine — pas une ligne réécrite à la main | Le mot de passage est **lu** côté dépôt (`test_le_depot_exige_la_racine_au_service_avant_de_rendre_vert`), et la réparation y est nommée avec son piège. ## Porte `./run_tests.sh -q` : garde d'octets `VERT`, parité conforme, **2131 passés, 14 sautés, 0 échec** — référence `main` **2127**, mesurée sur le même banc avant d'écrire cette ligne (+4). ## Ce que cette PR ne porte pas - Les récits et comptes de `JOURNAL.md` / `CLAUDE.md` : la référence de porte se **remesure sur le banc en vigueur** (`RG78`), ce n'est pas à cette PR de choisir le banc. À la demande. - **Aucun déploiement n'a été joué sur la cible vivante** depuis cette branche : la destination est saine, et cette branche ne touche **aucun** fichier du service (`tvlogin/` inchangé). Après fusion, le prochain `--cible tvexchange` posera la racine et l'écrira. À ta demande, je le joue avant : le contenu déposé serait **identique octet pour octet**, seul le déployeur change.
fix(deployer): le dépôt rend sa racine au service qui écrit chez lui
All checks were successful
Porte / porte (pull_request) Successful in 54s
566683350b
Le dépôt laissait la racine de la destination au propriétaire INSCRIT DANS L'ARCHIVE.

`tar -xzf` tourne EN ROOT dans le conteneur, et l'archive contient `./` : tar restaure donc
l'uid du poste qui a fabriqué le tar — `1000`, `depotmaj` dans le conteneur. `ordre_pose_droits`
chownait tout SAUF `./`, sur la foi d'un commentaire (« elle existe avant le dépôt et n'en fait
pas partie ») faux sur le mécanisme : l'archive la dépose, elle.

MESURÉ le 2026-10-18 sur `/opt/nuvio-tvexchange` : racine à `10001`, un uid SANS NOM, puis
reposée à `1000` par un déploiement. Le service tourne en `nuvio-tvx` et grave ses sessions de
la cabine dans ce dossier : trois `CABINE-SESSIONS-NON-GRAVEES PermissionError` dans son
journal, et le fichier n'existait pas du tout au lieu d'exister vide. Chaque déploiement
re-cassait la racine, et rien ne le disait.

Ce que le dépôt fait maintenant :

  · pour une cible `resident` — la seule qui a un processus qui ÉCRIT chez lui — la racine
    reçoit le propriétaire du service, le DOSSIER SEUL, jamais `-R` ;
  · la nature se lit dans la table des cibles (4e colonne) : la fonction ne la suppose pas ;
  · un second mot de passage positif, `RACINE-AU-SERVICE:<propriétaire>`, et le dépôt le LIT :
    une pose manquée est un ÉCHEC, jamais l'absence d'erreur ;
  · le refus nomme la réparation ET son piège — `-R` est le geste qui a mis la cabine hors
    service onze heures le 2026-08-18.

Le périmètre ne s'élargit pas : une cible `statique` (la cabine, servie en LECTURE par nginx)
et une cible `lot` (timers sous root) ne voient pas leur racine touchée.

La pose ne remplace pas la mesure : elle prouve qu'une COMMANDE a rendu 0, pas que le service
écrit. `preuve_resident` demande toujours au noyau, EN TANT QUE le service — un ACL, un bit
immuable ou une monture en lecture seule feraient réussir le chown sur un dossier où l'écriture
est refusée.

Tests : 4 de plus, éprouvés par réinjection — le fragment réel AMPUTÉ de l'étape de la racine
doit faire rougir le test de la racine, et le cas `statique` mesure l'absence. Porte : 2131
passés, 14 sautés, 0 échec (référence `main` : 2127, mesurée avant d'écrire cette ligne).
foXaCe merged commit cf06aa5474 into main 2026-10-18 09:20:48 +02:00
foXaCe deleted branch passe-20261018-091602-racineresident 2026-10-18 09:20:48 +02:00
Sign in to join this conversation.
No reviewers
No labels
montee-majeure
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
foXaCe/backend!112
No description provided.