Montages Défenseurs en lecture-écriture alors que la doc dit lecture seule #36

Closed
opened 2026-09-30 00:23:59 +00:00 by maximus · 1 comment
Owner

Constat (sonde du 2026-09-30, agent-defenseurs#131)

docker inspect du conteneur u8000gsg044wsk0oo0w884ok-232803574172 :

/data/defenseurs -> /data/defenseurs rw=true
/home/defenseur/defenseurs/reports -> /data/defenseurs/reports rw=true
/data/hosts -> /data/hosts rw=true

Le CLAUDE.md du dépôt décrit ces montages Défenseurs en lecture seule : c'est faux. Le MASTERPLAN.md d'agent-defenseurs dit « lecture-écriture » : c'est lui qui a raison.

Analyse (/analyze)

Prémisse vérifiée

Sonde refaite après le déploiement de 523d90b, sur le conteneur u8000gsg044wsk0oo0w884ok-004930023045 : les trois montages sont en RW=true (Mode="rw"). Le compose généré par Coolify les écrit sans mode, sous la forme '/data/defenseurs:/data/defenseurs'.

Nuance absente du constat : aucune écriture n'est possible aujourd'hui. Le conteneur tourne en uid 1000 (node, sans groupe supplémentaire), et tout le contenu Défenseurs appartient à defenseur (uid 1001), en 755 ou 775 pour les dossiers et 664 pour les fichiers. Dans le conteneur, test -w échoue sur les six chemins Défenseurs et réussit sur /data/hosts. Côté code, index.js ne fait que lire les chemins Défenseurs ; toutes ses écritures visent HOSTS_DIR (lignes 408-416 et 453).

Le :ro ajoute donc une seconde couche de défense, il ne ferme pas une exposition active. Il compte si un attaquant obtient root dans le conteneur : le montage rw lui ouvrirait alors findings-state.json et les rapports, que le Sergent relit sous l'utilisateur defenseur, membre du groupe docker sur l'hôte.

Historique

Personne n'a choisi le rw. Le :ro a été tenté deux fois par custom_docker_run_options (le 2026-04-28, puis le 2026-07-15 en #10), et Coolify l'a ignoré en silence les deux fois. Les montages ont ensuite été posés sans mode : /data/defenseurs par un INSERT SQL dans local_persistent_volumes, reports par l'UI. La mention « Lecture seule » du CLAUDE.md décrit l'usage de l'API, pas le mode du montage.

Faisabilité

La table local_persistent_volumes de Coolify beta.463 n'a pas de colonne lecture seule. Mais Coolify recopie la source et la destination telles quelles dans le compose : un suffixe :ro dans la destination doit produire '/data/defenseurs:/data/defenseurs:ro', que Docker lit comme un montage en lecture seule. Ce suffixe n'a jamais été essayé, et la validation de l'UI reste inconnue (la lecture du code Coolify en production a été refusée par le classifieur du mode auto).

Décisions (Max, via /analyze)

  • Approche : suffixe :ro dans la destination des deux montages Défenseurs, par l'UI Coolify. Repli si Coolify refuse ou ignore le suffixe : documenter l'état réel (rw + droits Unix) et consigner l'impossibilité dans coolify-ops.md.
  • Test : espion sur les fonctions d'écriture de node:fs, dans la même PR que la doc.
  • Vérification de Coolify : essai direct dans l'UI, prouvé par docker inspect.
  • Options écartées : garder le rw en documentant les droits Unix (pas de moindre privilège, et la protection tient au umask du Sergent et à USER node) ; passer l'app en build pack Docker Compose (re-provisionnement disproportionné pour une seconde couche).

Travail à faire

  • Coolify (UI, par Max) : destinations /data/defenseurs:ro et /data/defenseurs/reports:ro, /data/hosts inchangé. Redéployer.
  • Vérifier docker inspect, les quatre routes avec contenu, puis le battement suivant du cron host-agent.
  • Doc du dépôt : décrire les deux couches (mode du montage, droits Unix) et la commande de vérification.
  • Test : espion sur les fonctions d'écriture de node:fs, posé avant un require frais (même montage que l'espion timingSafeEqual, __tests__/auth.test.js:178-188). Démarrage, quatre routes de lecture, puis POST /hosts/<id> en témoin positif : chaque chemin écrit doit être sous HOSTS_DIR. Rouge prouvé en ajoutant une écriture sous REPORTS_DIR. Un test par chmod 555 ne prouverait rien, la CI tournant en root.
  • Docs hors dépôt, dans la même session et avec le résultat réel : la-compagnie-maximus/docs/coolify-ops.md:356-361 (procédure lecture seule), defenseurs/MASTERPLAN.md:169, ~/.claude/skills/diag-auth-drift/SKILL.md:284.

Fichiers concernés

  • CLAUDE.md:44-46 : « Lecture seule pour cette API ». Donner le mode réel et la façon de l'obtenir.
  • CLAUDE.md:81 : « contrairement aux montages /data/defenseurs ». Droits Unix aujourd'hui, :ro ensuite.
  • CLAUDE.md:86 : gotcha des volumes Coolify. Ajouter la technique :ro, ou son échec.
  • README.md:143 : même ambiguïté.
  • .env.example:7-8 : « parent /data/defenseurs/ is already mounted » est faux, reports est un montage imbriqué distinct depuis /home/defenseur/defenseurs/reports. .env.example:13 : même ambiguïté que CLAUDE.md:81.
  • __tests__/ : nouveau test d'écriture.
  • Aucun changement de code runtime.

Pièges

  • Ne pas monter les fichiers un par un : le Sergent écrit par rename atomique, et un bind de fichier reste accroché à l'ancien inode.
  • Docker ne peut pas créer le point de montage imbriqué reports dans un parent en lecture seule. Il existe côté hôte (/data/defenseurs/reports, vrai dossier root en 755 créé le 2026-07-16) : ne pas le supprimer, même s'il paraît vide.
  • Le :ro ne vaut que pour la vue du conteneur ; le Sergent continue d'écrire côté hôte.
  • Un montage manquant répond 200 no_data : vérifier le contenu des routes, pas seulement le code HTTP.
  • Le changement exige un redéploiement manuel (pas d'auto-deploy).

Critères d'acceptation

  • docker inspect du nouveau conteneur : /data/defenseurs et /data/defenseurs/reports en RW=false, /data/hosts en RW=true.
  • /defenseurs, /defenseurs/state, /defenseurs/findings?project=<p> et /reports/scans?date=<dernier scan> répondent 200 avec du contenu réel, pas no_data.
  • Le battement suivant du cron host-agent reçoit 204.
  • Docs du dépôt et hors dépôt alignées sur l'état obtenu.
  • npm test vert ; rouge de l'espion prouvé par mutation.

Complexité

Simple : un réglage Coolify, de la doc, un test. Moyenne si Coolify refuse le suffixe.

Hors périmètre

defenseurs/MASTERPLAN.md:112, :469 et :546 affirment qu'un push Forgejo déclenche un rebuild Coolify automatique. C'est faux (source_id=null, déploiement manuel) ; à corriger dans agent-defenseurs.

## Constat (sonde du 2026-09-30, agent-defenseurs#131) `docker inspect` du conteneur `u8000gsg044wsk0oo0w884ok-232803574172` : ``` /data/defenseurs -> /data/defenseurs rw=true /home/defenseur/defenseurs/reports -> /data/defenseurs/reports rw=true /data/hosts -> /data/hosts rw=true ``` Le `CLAUDE.md` du dépôt décrit ces montages Défenseurs en lecture seule : c'est faux. Le `MASTERPLAN.md` d'agent-defenseurs dit « lecture-écriture » : c'est lui qui a raison. ## Analyse (/analyze) ### Prémisse vérifiée Sonde refaite après le déploiement de `523d90b`, sur le conteneur `u8000gsg044wsk0oo0w884ok-004930023045` : les trois montages sont en `RW=true` (`Mode="rw"`). Le compose généré par Coolify les écrit sans mode, sous la forme `'/data/defenseurs:/data/defenseurs'`. Nuance absente du constat : aucune écriture n'est possible aujourd'hui. Le conteneur tourne en uid 1000 (`node`, sans groupe supplémentaire), et tout le contenu Défenseurs appartient à `defenseur` (uid 1001), en 755 ou 775 pour les dossiers et 664 pour les fichiers. Dans le conteneur, `test -w` échoue sur les six chemins Défenseurs et réussit sur `/data/hosts`. Côté code, `index.js` ne fait que lire les chemins Défenseurs ; toutes ses écritures visent `HOSTS_DIR` (lignes 408-416 et 453). Le `:ro` ajoute donc une seconde couche de défense, il ne ferme pas une exposition active. Il compte si un attaquant obtient root dans le conteneur : le montage rw lui ouvrirait alors `findings-state.json` et les rapports, que le Sergent relit sous l'utilisateur `defenseur`, membre du groupe `docker` sur l'hôte. ### Historique Personne n'a choisi le rw. Le `:ro` a été tenté deux fois par `custom_docker_run_options` (le 2026-04-28, puis le 2026-07-15 en #10), et Coolify l'a ignoré en silence les deux fois. Les montages ont ensuite été posés sans mode : `/data/defenseurs` par un `INSERT` SQL dans `local_persistent_volumes`, `reports` par l'UI. La mention « Lecture seule » du `CLAUDE.md` décrit l'usage de l'API, pas le mode du montage. ### Faisabilité La table `local_persistent_volumes` de Coolify beta.463 n'a pas de colonne lecture seule. Mais Coolify recopie la source et la destination telles quelles dans le compose : un suffixe `:ro` dans la destination doit produire `'/data/defenseurs:/data/defenseurs:ro'`, que Docker lit comme un montage en lecture seule. Ce suffixe n'a jamais été essayé, et la validation de l'UI reste inconnue (la lecture du code Coolify en production a été refusée par le classifieur du mode auto). ## Décisions (Max, via /analyze) - Approche : suffixe `:ro` dans la destination des deux montages Défenseurs, par l'UI Coolify. Repli si Coolify refuse ou ignore le suffixe : documenter l'état réel (rw + droits Unix) et consigner l'impossibilité dans `coolify-ops.md`. - Test : espion sur les fonctions d'écriture de `node:fs`, dans la même PR que la doc. - Vérification de Coolify : essai direct dans l'UI, prouvé par `docker inspect`. - Options écartées : garder le rw en documentant les droits Unix (pas de moindre privilège, et la protection tient au umask du Sergent et à `USER node`) ; passer l'app en build pack Docker Compose (re-provisionnement disproportionné pour une seconde couche). ## Travail à faire - [ ] Coolify (UI, par Max) : destinations `/data/defenseurs:ro` et `/data/defenseurs/reports:ro`, `/data/hosts` inchangé. Redéployer. - [ ] Vérifier `docker inspect`, les quatre routes avec contenu, puis le battement suivant du cron `host-agent`. - [ ] Doc du dépôt : décrire les deux couches (mode du montage, droits Unix) et la commande de vérification. - [ ] Test : espion sur les fonctions d'écriture de `node:fs`, posé avant un `require` frais (même montage que l'espion `timingSafeEqual`, `__tests__/auth.test.js:178-188`). Démarrage, quatre routes de lecture, puis `POST /hosts/<id>` en témoin positif : chaque chemin écrit doit être sous `HOSTS_DIR`. Rouge prouvé en ajoutant une écriture sous `REPORTS_DIR`. Un test par `chmod 555` ne prouverait rien, la CI tournant en root. - [ ] Docs hors dépôt, dans la même session et avec le résultat réel : `la-compagnie-maximus/docs/coolify-ops.md:356-361` (procédure lecture seule), `defenseurs/MASTERPLAN.md:169`, `~/.claude/skills/diag-auth-drift/SKILL.md:284`. ## Fichiers concernés - `CLAUDE.md:44-46` : « Lecture seule pour cette API ». Donner le mode réel et la façon de l'obtenir. - `CLAUDE.md:81` : « contrairement aux montages `/data/defenseurs` ». Droits Unix aujourd'hui, `:ro` ensuite. - `CLAUDE.md:86` : gotcha des volumes Coolify. Ajouter la technique `:ro`, ou son échec. - `README.md:143` : même ambiguïté. - `.env.example:7-8` : « parent /data/defenseurs/ is already mounted » est faux, `reports` est un montage imbriqué distinct depuis `/home/defenseur/defenseurs/reports`. `.env.example:13` : même ambiguïté que `CLAUDE.md:81`. - `__tests__/` : nouveau test d'écriture. - Aucun changement de code runtime. ## Pièges - Ne pas monter les fichiers un par un : le Sergent écrit par `rename` atomique, et un bind de fichier reste accroché à l'ancien inode. - Docker ne peut pas créer le point de montage imbriqué `reports` dans un parent en lecture seule. Il existe côté hôte (`/data/defenseurs/reports`, vrai dossier root en 755 créé le 2026-07-16) : ne pas le supprimer, même s'il paraît vide. - Le `:ro` ne vaut que pour la vue du conteneur ; le Sergent continue d'écrire côté hôte. - Un montage manquant répond 200 `no_data` : vérifier le contenu des routes, pas seulement le code HTTP. - Le changement exige un redéploiement manuel (pas d'auto-deploy). ## Critères d'acceptation - [ ] `docker inspect` du nouveau conteneur : `/data/defenseurs` et `/data/defenseurs/reports` en `RW=false`, `/data/hosts` en `RW=true`. - [ ] `/defenseurs`, `/defenseurs/state`, `/defenseurs/findings?project=<p>` et `/reports/scans?date=<dernier scan>` répondent 200 avec du contenu réel, pas `no_data`. - [ ] Le battement suivant du cron `host-agent` reçoit `204`. - [ ] Docs du dépôt et hors dépôt alignées sur l'état obtenu. - [ ] `npm test` vert ; rouge de l'espion prouvé par mutation. ## Complexité Simple : un réglage Coolify, de la doc, un test. Moyenne si Coolify refuse le suffixe. ## Hors périmètre `defenseurs/MASTERPLAN.md:112`, `:469` et `:546` affirment qu'un push Forgejo déclenche un rebuild Coolify automatique. C'est faux (`source_id=null`, déploiement manuel) ; à corriger dans agent-defenseurs.
maximus added the
status:triage
type:security
source:analyste
labels 2026-09-30 00:23:59 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-09-30 01:22:41 +00:00
Author
Owner

Changement Coolify et vérification (2026-09-30 UTC)

Changement (Max, UI Coolify)

  • Volume vps-health-defenseurs : destination /data/defenseurs remplacée par /data/defenseurs:ro.
  • Montage reports : le Directory Mount n'est pas modifiable dans l'UI. Il a été supprimé sans cocher la suppression du dossier sur le serveur, puis recréé en Volume Mount (Source Path /home/defenseur/defenseurs/reports, destination /data/defenseurs/reports:ro).
  • /data/hosts inchangé.
  • Sauvegarde prise avant la suppression : ~/backups/defenseurs-reports/reports-20260930T012915Z.tgz sur le VPS (1947 fichiers, identique au disque). Restauration si besoin : sudo tar xzpf <archive> -C /home/defenseur/defenseurs.
  • Redéploiement : finished à 01:33:39 UTC, commit 523d90b, conteneur u8000gsg044wsk0oo0w884ok-013300798249.

Vérification

docker inspect :

bind /data/defenseurs -> /data/defenseurs RW=false Mode="ro"
bind /data/hosts -> /data/hosts RW=true Mode="rw"
bind /home/defenseur/defenseurs/reports -> /data/defenseurs/reports RW=false Mode="ro"

Vue du noyau dans le conteneur (/proc/mounts) : /data/defenseurs ro, /data/defenseurs/reports ro, /data/hosts rw.

Compose généré par Coolify :

- '/data/defenseurs:/data/defenseurs:ro'
- '/data/hosts:/data/hosts'
- '/home/defenseur/defenseurs/reports:/data/defenseurs/reports:ro'

Coolify beta.463 recopie donc tel quel le suffixe :ro de la destination dans le compose : c'est la technique à documenter.

Routes de lecture, appelées depuis le conteneur avec son propre token (aucune ne répond no_data) :

Route Code Contenu
/defenseurs 200 status.json complet (agents, summary, topFindings)
/defenseurs/state 200 findings-state.json complet
/defenseurs/findings?project=agents 200 rapport defenseur-agents du 2026-09-30 00:09 UTC, 8 constats
/reports/scans?date=2026-09-30 200 1 rapport

Dossier des rapports intact après la suppression du Directory Mount : 1947 fichiers. Le point de montage /data/defenseurs/reports (root, 755) est toujours présent côté hôte.

Ingestion des postes : le battement host-agent de 01:35:02 UTC, premier après le redéploiement, a reçu 204.

Reste à faire

Doc du dépôt, espion sur les écritures node:fs et docs hors dépôt, par /fix-issue 36.

## Changement Coolify et vérification (2026-09-30 UTC) ### Changement (Max, UI Coolify) - Volume `vps-health-defenseurs` : destination `/data/defenseurs` remplacée par `/data/defenseurs:ro`. - Montage `reports` : le Directory Mount n'est pas modifiable dans l'UI. Il a été supprimé sans cocher la suppression du dossier sur le serveur, puis recréé en Volume Mount (Source Path `/home/defenseur/defenseurs/reports`, destination `/data/defenseurs/reports:ro`). - `/data/hosts` inchangé. - Sauvegarde prise avant la suppression : `~/backups/defenseurs-reports/reports-20260930T012915Z.tgz` sur le VPS (1947 fichiers, identique au disque). Restauration si besoin : `sudo tar xzpf <archive> -C /home/defenseur/defenseurs`. - Redéploiement : `finished` à 01:33:39 UTC, commit `523d90b`, conteneur `u8000gsg044wsk0oo0w884ok-013300798249`. ### Vérification `docker inspect` : ``` bind /data/defenseurs -> /data/defenseurs RW=false Mode="ro" bind /data/hosts -> /data/hosts RW=true Mode="rw" bind /home/defenseur/defenseurs/reports -> /data/defenseurs/reports RW=false Mode="ro" ``` Vue du noyau dans le conteneur (`/proc/mounts`) : `/data/defenseurs ro`, `/data/defenseurs/reports ro`, `/data/hosts rw`. Compose généré par Coolify : ``` - '/data/defenseurs:/data/defenseurs:ro' - '/data/hosts:/data/hosts' - '/home/defenseur/defenseurs/reports:/data/defenseurs/reports:ro' ``` Coolify beta.463 recopie donc tel quel le suffixe `:ro` de la destination dans le compose : c'est la technique à documenter. Routes de lecture, appelées depuis le conteneur avec son propre token (aucune ne répond `no_data`) : | Route | Code | Contenu | |---|---|---| | `/defenseurs` | 200 | `status.json` complet (`agents`, `summary`, `topFindings`) | | `/defenseurs/state` | 200 | `findings-state.json` complet | | `/defenseurs/findings?project=agents` | 200 | rapport `defenseur-agents` du 2026-09-30 00:09 UTC, 8 constats | | `/reports/scans?date=2026-09-30` | 200 | 1 rapport | Dossier des rapports intact après la suppression du Directory Mount : 1947 fichiers. Le point de montage `/data/defenseurs/reports` (root, 755) est toujours présent côté hôte. Ingestion des postes : le battement `host-agent` de 01:35:02 UTC, premier après le redéploiement, a reçu `204`. ### Reste à faire Doc du dépôt, espion sur les écritures `node:fs` et docs hors dépôt, par `/fix-issue 36`.
maximus added
status:in-progress
and removed
status:ready
labels 2026-09-30 01:42:11 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-09-30 02:07:33 +00:00
maximus added
status:needs-fix
and removed
status:review
labels 2026-09-30 23:49:18 +00:00
maximus added
status:review
and removed
status:needs-fix
labels 2026-10-01 00:00:23 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-01 00:18:32 +00:00
maximus removed the
status:approved
label 2026-10-01 00:27:50 +00:00
Sign in to join this conversation.
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: maximus/vps-health-api#36
No description provided.