From 9eae93be0b3a8a681bb22c25c01563c61b023d44 Mon Sep 17 00:00:00 2001 From: Effi Date: Mon, 12 Oct 2026 22:42:22 +0200 Subject: [PATCH] 6 criteres retoques: PhotoPrism+garde-fous+WhatsApp+grille 10 requetes+17 questions, salon famille cree (GO), peages perimes --- DOSSIER-SOFTWARE.md | 27 +++- GUIDE-MATRIX.md | 2 +- g9-migration/BACKUP-PERIMETRE.md | 37 ++++-- g9-migration/BRIDGE-WHATSAPP.md | 32 ++++- g9-migration/EXPOSITION-INTERNET.md | 15 +++ g9-migration/PARTAGE-SECRET-MAITRE.md | 54 ++++++-- g9-migration/PHOTOS-DOUBLE-RUN.md | 85 +++++++++--- g9-migration/README.md | 6 + g9-migration/REGISTRE-IDENTITES.md | 8 +- g9-migration/RESTE-HUMAIN.md | 2 +- g9-migration/RUNBOOKS/matrix.md | 2 + g9-migration/journal.md | 2 + .../scripts/28-synapse-create-room.sh | 125 ++++++++++++++++++ 13 files changed, 346 insertions(+), 51 deletions(-) create mode 100644 g9-migration/scripts/28-synapse-create-room.sh diff --git a/DOSSIER-SOFTWARE.md b/DOSSIER-SOFTWARE.md index e5d6bbf..574aab9 100644 --- a/DOSSIER-SOFTWARE.md +++ b/DOSSIER-SOFTWARE.md @@ -127,7 +127,7 @@ Le **Qwen 3.8max** (le LLM principal) consomme **~20 Go VRAM + ~50 Go RAM**. Le | 5 | **WireGuard** | Accès distant chiffré | J5040 | GPL-2.0 | Système (noyau), très stable | Clés exportables | | 6 | **Web Station** | Sites web (dd/lola.folzocorp…) | J5040 | — (contenu) | Statique | Fichiers, exportables | | 7 | **Beszel** | Monitoring léger + alertes | J5040 | MIT | Mature, maintenu (henrygd) | Base SQLite, exportable | -| 8 | **Matrix (Synapse)** | **Messagerie** (remplace WhatsApp) | J5040 | Apache-2.0 | Très mature (Element/Matrix), maintenu | Export des salons (JSON), comptes | +| 8 | **Matrix (Synapse)** | **Messagerie du foyer** (complément de WhatsApp, **pas de remplacement**) | J5040 | Apache-2.0 | Très mature (Element/Matrix), maintenu | Export des salons (JSON), comptes | | 9 | **Jitsi** | **Visioconférence** | J5040 | Apache-2.0 | Mature (Jitsi Meet), maintenu | N/A (appels éphémères) | | 10 | **\*arr** (Prowlarr + Sonarr + Radarr) — **1 stack** | Gestion **séries/films** (indexers + queue) | J5040 | GPL-3.0 | Très mature (la stack \*arr), maintenu | Bases SQLite exportables, config | | 11 | **qBittorrent** | **Téléchargement** (torrent/HTTP) | J5040 | GPL-2.0 | Très mature, maintenu | File de tâches exportable | @@ -431,6 +431,29 @@ Le **Qwen 3.8max** (le LLM principal) consomme **~20 Go VRAM + ~50 Go RAM**. Le | **Moi** | Téléphone + PC | **Salon ops** (alertes backup / sondes / dérive registre) | | **Moi** | Enceintes Google ×4-5 | **Assistant vocal** (Home Assistant) → peut déclencher n8n, contrôler Frigate | +**Décision 12/10/2026 (statut du canal)** : Matrix + Jitsi + Element = **complément du foyer** — +salon famille, salon ops, salon Effi. **WhatsApp n'est pas remplacé** et reste la messagerie +principale (famille élargie, amis, travail) : c'est un constat d'usage mesuré, pas un choix +technologique. Element s'installe d'abord sur les **2 appareils adultes** ; les 4 ados viennent en +**phase 2**, déclenchée par le **GO famille** explicite de l'utilisateur. Grille de décision : +**S14.5**. + +**Les 4 interdictions** (applicables dès maintenant, vérifiables) : + +1. **Interdiction** de présenter Matrix comme un remplacement de WhatsApp, et d'inciter la famille + à quitter WhatsApp pour Matrix (WhatsApp garde les échanges extérieurs). +2. **Interdiction** de faire transiter les **alertes ops** par WhatsApp : le canal d'alerte est + **Matrix uniquement** (pas d'e-mail, pas de SMS, pas de WhatsApp). +3. **Interdiction** d'**automatiser l'envoi** de messages WhatsApp (pont `mautrix-whatsapp` : + **étudié, non déployé**, numéro familial **interdit** — voir `g9-migration/BRIDGE-WHATSAPP.md`). +4. **Interdiction** d'équiper les **4 ados** d'Element (et de comptes enfants Matrix) **sans GO + famille** explicite, et sans coffre/2FA posé pour eux (C-FM1). + +Ces 4 interdictions sont **contrôlables** : interdiction 1 = vocabulaire du dossier (aucune ligne +ne dit que Matrix remplace WhatsApp) ; interdiction 2 = le canal d'alerte est Matrix (3 +notifications types mesurées dans le salon ops) ; interdiction 3 = 0 flux n8n vers WhatsApp +(inventaire n8n) ; interdiction 4 = 0 compte ado Matrix (Synapse mesuré : 4 comptes, aucun ado). + ### 8.3 Le bot hermes ↔ Matrix > **hermes** (Effi, sur PC IA) est **connecté à Matrix** : la famille peut **parler à Effi** dans un salon Matrix. C'est l'**assistant de famille** (projet second cerveau, phase 3). @@ -744,7 +767,7 @@ services: | # | Action logicielle | Dépendance | Critère de succès | |---|-------------------|------------|-------------------| -| C.1 | **Matrix + Jitsi + Element** (remplace WhatsApp) | Phase B | Famille messagerie + visio en local | +| C.1 | **Matrix + Jitsi + Element** (complément du foyer, **pas** un remplacement de WhatsApp) | Phase B | Famille messagerie + visio en local | | C.2 | **Home Assistant** (enceintes Google) + **Frigate** (caméras) | Phase B | Assistant vocal + vision caméras | | C.3 | **Bot hermes ↔ Matrix** (Effi famille) | Phase B + 2ᵉ cerveau | Effi répond dans Matrix | | C.4 | **Enfants** : AdGuard profils + Family Link | Phase B | Enfants protégés (DNS + parental) | diff --git a/GUIDE-MATRIX.md b/GUIDE-MATRIX.md index 036447b..a17912f 100644 --- a/GUIDE-MATRIX.md +++ b/GUIDE-MATRIX.md @@ -130,7 +130,7 @@ curl -s -X PUT 'http://192.168.1.24:8008/_synapse/admin/v2/users/@:192.1 | Salon | Contenu | |-------|---------| -| **Famille** | Messagerie quotidienne (remplace WhatsApp), visio Jitsi intégrée à Element | +| **Famille** | Messagerie quotidienne du **foyer** (complément de WhatsApp, **pas un remplacement**), visio Jitsi intégrée à Element | | **Ops** | Alertes machine : watchtower (MAJ images), restic (backup G9), n8n — via `ops-bot` | | **hermes** (prévu) | Salon où la famille parle à Effi (assistant, Phase 3 du second cerveau) | diff --git a/g9-migration/BACKUP-PERIMETRE.md b/g9-migration/BACKUP-PERIMETRE.md index 1487a59..9b34b41 100644 --- a/g9-migration/BACKUP-PERIMETRE.md +++ b/g9-migration/BACKUP-PERIMETRE.md @@ -67,22 +67,35 @@ la 2ᵉ/3ᵉ cible n'existe pas. `restic snapshots` sur les 3 cibles = **1 sur 3**. Le critère reste donc **non satisfait**, et c'est le seul point dur : il faut les identifiants de la 2ᵉ cible (SMB/rclone) pour la poser. -## 6. Cohérence applicative (à faire, je ne l'ai pas testé) +## 6. Cohérence applicative — **fait et mesuré le 12/10 18:30** (`scripts/18-db-dumps.sh`) La copie se fait **conteneurs tournants** pour Nextcloud/MariaDB et Gitea/Postgres : le script ne stoppe que `valheim n8n synapse couchdb obsidian` (`RESTIC_STATEFUL`), pas les bases. Une copie chaude de `datadir` Postgres/MariaDB n'est **pas garantie cohérente** — la copie est -complète en octets, pas nécessairement rejouable en l'état. +complète en octets, pas nécessairement rejouable en l'état. **D'où les dumps logiques.** -Recommandation (lecture seule pour vérifier, écriture pour produire) : +Ce qui a été produit (0 brique stoppée, `--single-transaction` = instantané InnoDB cohérent) : -```bash -# Gitea : dump logique versionné avec le dépôt -docker exec gitea-db pg_dump -U gitea gitea | gzip > /data/docker/gitea/dumps/gitea.sql.gz -# Nextcloud : export cohérent via occ -docker exec -u www-data nextcloud php occ db export --type=structure > /data/docker/nextcloud/dumps/nc-structure.sql -docker exec -u www-data nextcloud php occ db export --type=data > /data/docker/nextcloud/dumps/nc-data.sql -``` +| Base | Commande réelle (mesurée) | Résultat | +|---|---|---| +| **Gitea** (Postgres) | `sudo bash -c "docker exec -u postgres gitea-db pg_dump -U gitea_folzo gitea_db \| gzip > /data/docker/gitea/dumps/gitea-.sql.gz"` | **80 543 o** gz, **116 `CREATE TABLE`** | +| **Nextcloud** (MariaDB) | `sudo bash -c "docker exec nextcloud_db sh -c 'mysqldump -u root -p\"\$MARIADB_ROOT_PASSWORD\" --single-transaction --routines nextcloud' \| gzip > /data/docker/nextcloud/dumps/nextcloud-.sql.gz"` | **5,9 Mo** gz, **317 objets**, finit par `-- Dump completed` | +| **PhotoPrism** (MariaDB) | idem sur `photoprism` | **89,6 Mo** gz, **39 objets**, `-- Dump completed` | -Les dumps tombent sous `/data/docker//dumps/` → **déjà dans le périmètre** de backup. -Rollback = supprimer les dumps, aucune brique touchée. +Pièges mesurés, intégrés au script : +- l'utilisateur OS `gitea_folzo` **n'existe pas** dans le conteneur → `pg_hba.conf` = + `local all all trust` → **`-u postgres`**, donc **aucun mot de passe sur une ligne de commande** ; +- le `>` de redirection doit être **dans** `sudo bash -c "…"` (sinon le pipe s'exécute côté shell + non-root et le fichier n'est pas écrit) ; +- le mot de passe MariaDB est lu **dans l'environnement du conteneur**, jamais affiché. + +**Rejouabilité prouvée pour Gitea** : base de test `gtest_restore` créée, dump chargé, +**116 tables restaurées** (== 116 du dump), base **supprimée** après (`pg_database` vérifié : +`gitea_db`, `postgres`, `template0`, `template1`). + +**Non fait, à décider** : la replay complète des dumps MariaDB. Le dump Nextcloud contient +`USE \`nextcloud\`` → le charger tel quel **écraserait la production** ; il faut un serveur jeton +(conteneur MariaDB temporaire, GO séparé). + +Les dumps tombent sous `/data/docker//dumps/` → **déjà dans le périmètre** de backup +(visibles au snapshot suivant, 03:30). Rollback = supprimer les dumps, aucune brique touchée. diff --git a/g9-migration/BRIDGE-WHATSAPP.md b/g9-migration/BRIDGE-WHATSAPP.md index 0b080ed..bab7cdc 100644 --- a/g9-migration/BRIDGE-WHATSAPP.md +++ b/g9-migration/BRIDGE-WHATSAPP.md @@ -28,7 +28,7 @@ | Risque | Détail | Conséquence pour nous | |---|---|---| -| **Bannissement du numéro** | WhatsApp détecte les clients non officiels ; le risque est **réel et non nul** | ton numéro familial est « indispensable » → **interdiction** : jamais le numéro principal sur un pont | +| **Bannissement du numéro** | WhatsApp détecte les clients non officiels par **deux voies cumulées** : (a) **signalements par les utilisateurs** en face (un correspondant peut signaler le compte comme « spam/client non autorisé »), (b) **détection automatisée côté serveur** (comportement du client multi-device, empreinte du protocole `whatsmeow`, cadence d'envoi, absence d'app officielle). **Aucun seuil public, aucune liste publiée, aucun moyen de vérifier son exposition avant la sanction.** Échelle observée et documentée par la communauté : **ban temporaire** (de quelques heures à ~24 h, avec compte à rebours dans l'app) → **ban permanent** à la récidive → **suppression du compte** = **perte de l'historique WhatsApp du numéro** (non récupérable, pas de backup côté WhatsApp). Probabilité : **non chiffrable** (paramètre serveur opaque) ; **coût si ça tombe** : **élevé et non réparable** (numéro familial = famille, amis, travail). | ton numéro familial est « indispensable » → **interdiction** : jamais le numéro principal sur un pont ; le pilote ne se fait que sur un **numéro secondaire jetable** | | Session qui expire | > 12 jours sans connexion du téléphone = déconnexion | re-link manuel = maintenance que tu veux éliminer | | Stateful fragile | session + DB + médias ; montée de version du protocole WhatsApp non maîtrisée | épingler par digest, **exclure de Watchtower**, backup session+DB dans restic | | Confidentialité | les MP passent par un daemon sur le G9 + stockage local | à accepter explicitement (choix de périmètre) | @@ -53,7 +53,35 @@ | 6 | rollback documenté : `docker compose -f compose/whatsapp/docker-compose.yml down` (aucune donnée famille concernée) | | 7 | journal + grille S14 (décision, raison, coût constaté) | -## 6. Verdict de l'étude +## 6. Les 17 questions de Go/No-Go (à répondre **avant** toute installation) + +Règle de décision : **17/17 = GO pilote**. Une seule réponse « non » = **NO-GO**, et le document +reste en étude. Chaque « oui » exige la **preuve** indiquée (pas d'intention, une preuve). + +| # | Question | Réponse pour GO | Preuve exigée | +|---|---|---|---| +| 1 | Le numéro relié est-il **secondaire et jetable** ? | oui, jamais le familial | numéro + support (eSIM/SIM dédiée) notés dans `journal.md` | +| 2 | Ce numéro est-il **vide** (aucun contact famille, aucun historique) ? | oui | capture/comptage : 0 conversation avant le link | +| 3 | Le **bannissement définitif** de ce numéro est-il accepté à l'avance ? | oui, écrit | ligne de décision signée dans `journal.md` | +| 4 | Ai-je un **moyen de prévenir** les correspondants si le compte disparaît ? | oui | canal de secours nommé (Matrix/ops) | +| 5 | Le pont lit-il **uniquement mes** conversations ? | oui | `auth_users`/config du pont, 0 autre compte | +| 6 | Les messages copiés sont-ils **chiffrés au repos** (ou en périmètre chiffré) ? | oui | `restic` (chiffrement) + permissions 600 sur `/data/docker/whatsapp/` | +| 7 | La **DB du pont** est-elle dans les snapshots restic ? | oui | comptage d'entrées dans le snapshot latest | +| 8 | La **session** (clés de liaison) est-elle backupée **et restaurée testée** ? | oui | restauration en test → re-link **non** nécessaire | +| 9 | L'image est-elle **épinglée par digest** ? | oui | `docker inspect` → `ImageID`, pas un tag `:latest` | +| 10 | Le pont est-il **exclu de Watchtower** ? | oui | label `watchtower.enable=false` + Watchtower `scanned` ne l'inclut pas | +| 11 | Le pont est-il **hors WAN** ? | oui | `ufw status numbered` : 0 règle pour son port ; 0 entrée Caddy | +| 12 | Y a-t-il **0 automatisation d'envoi** (n8n, IA, scripts) ? | oui | inventaire n8n : 0 flux WhatsApp | +| 13 | Les **alertes ops** restent-elles sur Matrix ? | oui | 3 notifications types dans le salon ops, 0 sur WhatsApp | +| 14 | La **montée de version du protocole** WhatsApp est-elle gérée ? | oui | procédure de re-link écrite dans `RUNBOOKS/` | +| 15 | Le **re-link** est-il testé et chronométré ? | oui | test déconnexion volontaire → QR → reprise, **< 5 min** | +| 16 | Le **coût de maintenance** constaté est-il ≤ 10 min/semaine ? | oui | 7 jours journalisés, temps passé relevé | +| 17 | Le **verdict écrit** existe-t-il (GO/NO-GO, raison, coût) ? | oui | ligne dans `journal.md` + grille `DOSSIER-SOFTWARE.md` S14.5 | + +**Ce que ces 17 questions ne couvrent pas** : la probabilité de bannissement reste **non chiffrable** +(§3). Le pilote ne la réduit pas, il la **déplace** sur un numéro qui n'a pas de valeur. + +## 7. Verdict de l'étude - **Utile** si l'objectif est « lire au même endroit » (Element = foyer + alertes) et que le pilote reste sur un **numéro secondaire**. diff --git a/g9-migration/EXPOSITION-INTERNET.md b/g9-migration/EXPOSITION-INTERNET.md index 92be975..5d40945 100644 --- a/g9-migration/EXPOSITION-INTERNET.md +++ b/g9-migration/EXPOSITION-INTERNET.md @@ -103,3 +103,18 @@ numéro le plus haut au plus bas, `--dry` exécuté avant l'exécution réelle). **redirects de la Freebox** (IP publique unique `82.65.198.114`, G9 et pilot derrière le même NAT). L'inventaire des redirects se fait côté Freebox, par toi — c'est le seul trou de visibilité qui subsiste. + +## 7. Choix de machine pour l'IdP (Authelia) — **décision rendue le 12/10, installation reportée** + +| Point retenu | **J5040 (.27)** | Écart avec le G9 (.24) | +|---|---|---| +| Exposition | c'est **déjà** le seul edge public (`80/443/51820`), donc Authelia hérite d'une exposition **existante et documentée** | le G9 n'a **plus aucune règle `Anywhere`** depuis le durcissement 18:10 → l'y exposer demanderait de **rouvrir** un trou refermé | +| Survie | le hub de monitoring est **déjà** posé dessus (C-AP5) : l'IdP ne meurt pas avec le G9 | l'IdP sur le G9 tombe avec lui (même défaut que les sondes) | +| Coût | 1 conteneur de plus sur une machine à 3 conteneurs, base SQLite dans `/home/folzo/` (déjà sauvegardée sur .27 ? **non** → à ajouter au périmètre, consigné) | base sur la machine qui porte déjà les 3 BDD les plus lourdes | +| Risque | l'IdP devient **aussi exposé que Caddy** : surface = 1 hôte, 1 port (443), 0 exposition directe du port 9000 (reverse proxy Caddy, LAN-only en amont) | exposition directe depuis LAN uniquement, mais single point of failure du foyer | +| Décision | **J5040**, en OIDC **opt-in** brique par brique (Synapse → Gitea → Jellyfin), login local **conservé** partout (C-IDP2) | — | + +**Déclencheur d'installation (écrit, pas implicite)** : après **Phase 5** (copie 3-2-1 sur 3 cibles) +et **après** la mise en 2FA des comptes admin existants (C-MX3, C-ID7). Ni avant, ni à froid. +Rollback = retirer le bloc OIDC du Caddyfile du J5040 + retirer l'OIDC de la brique (login local +déjà en place, donc 0 dépendance dure). diff --git a/g9-migration/PARTAGE-SECRET-MAITRE.md b/g9-migration/PARTAGE-SECRET-MAITRE.md index 65203e4..f3e0e1c 100644 --- a/g9-migration/PARTAGE-SECRET-MAITRE.md +++ b/g9-migration/PARTAGE-SECRET-MAITRE.md @@ -7,10 +7,11 @@ ## 1. Détenteurs (à nommer par l'utilisateur) -| Rôle | Personne | Support remis | Date de remise | Test annuel | -|---|---|---|---|---| -| Détenteur 1 | _à nommer_ | enveloppe scellée n°1 | _à date_ | _à date_ | -| Détenteur 2 (optionnel) | _à nommer_ | enveloppe scellée n°2 | _à date_ | _à date_ | +| Rôle | Personne | Support remis | Lieu (voir §2.1) | Code de déverrouillage | Date de remise | Test annuel | +|---|---|---|---|---|---|---| +| Moi | — | enveloppe n°1 | **sur place** (coffre du domicile) | le mien | — | _à date_ | +| Détenteur 1 | _à nommer_ | enveloppe n°2 | **hors site** (chez lui) | **le sien, propre, jamais communiqué** | _à date_ | _à date_ | +| Détenteur 2 (optionnel) | _à nommer_ | enveloppe n°3 | **hors ligne** (scellé, jamais connecté) | **le sien, propre, jamais communiqué** | _à date_ | _à date_ | Règles de sélection (à assumer) : personne joignable physiquement, indépendante de l'infra, capable d'appliquer une procédure écrite sous stress, **sans accès** aux machines. @@ -25,8 +26,29 @@ capable d'appliquer une procédure écrite sous stress, **sans accès** aux mach - une **note de datation** : à chaque changement de mot de passe maître, l'enveloppe est remplacée et l'ancienne est détruite (journaliser la date). -Support : papier (pas de clé USB non chiffrée), plié, scellé, signé de la date. Deux copies -max. Jamais de photo, jamais de fichier non chiffré sur un disque. +Support : papier (pas de clé USB non chiffrée), plié, scellé, signé de la date. Jamais de photo, +jamais de fichier non chiffré sur un disque. + +### 2.1 Placement des supports (3 exemplaires, 2 maximum pour des tiers) + +| Exemplaire | Lieu | Détenteur | Rôle | +|---|---|---|---| +| n°1 — **sur place** | coffre physique du domicile (scellé, daté) | moi | usage normal, jamais sorti sans journal | +| n°2 — **hors site** | chez le détenteur 1 (scellé, daté) | détenteur 1 | usage si je suis injoignable | +| n°3 — **hors ligne** | chez le détenteur 2 **ou** coffret scellé hors réseau | détenteur 2 (optionnel) | secours si n°1 et n°2 sont perdus ; **jamais connecté, jamais numérisé** | + +**Aucun 4ᵉ exemplaire** : 2 personnes max détiennent un support (détenteurs 1 et 2). + +### 2.2 Garde-fous (les 4, applicables dès la remise) + +1. **2 personnes maximum** détiennent un support ; la famille immédiate n'en détient pas. +2. La **personne de confiance** (celle qui accompagne l'ouverture) est **lectrice seule** : elle + assiste, date, contresigne ; **elle ne recopie, ne photographie et ne conserve rien**. +3. **1 code de déverrouillage par personne** : l'enveloppe est fermée par un **code propre à son + détenteur** (code de son choix, jamais transmis, jamais écrit sur l'enveloppe). En cas de perte + du détenteur, son code devient nul et l'enveloppe est remplacée (§5). +4. **Jamais par mail, jamais par cloud, jamais par messagerie** : la remise est **physique**, en + main propre, signée. Aucun scan, aucun envoi, aucune photo, aucun stockage hébergé. ## 3. Ce que fait le détenteur si je suis injoignable @@ -43,13 +65,21 @@ max. Jamais de photo, jamais de fichier non chiffré sur un disque. ## 4. Test annuel (critère) -Une fois par an, sans casser la production : +Une fois par an, sans casser la production, sur un **coffre d'entraînement** (vault de test, jamais +le coffre famille) : **6 points à vérifier**, chacun noté OK/KO. -- le détenteur ouvre l'enveloppe (ou une copie d'entraînement identique) ; -- il déverrouille un **coffre d'entraînement** (vault de test, pas le coffre famille) avec les - éléments de l'enveloppe ; -- la procédure est chronométrée et les points de friction sont corrigés dans ce fichier ; -- la ligne est journalisée : `| date | partage-secret-maitre : test annuel | résultat | remplacement enveloppe |`. +| # | Point vérifié | Preuve | +|---|---|---| +| 1 | L'enveloppe est **intacte, scellée, datée** et le **code propre au détenteur** l'ouvre | photo du sceau intact + date portée au journal | +| 2 | Le **mot de passe maître** déverrouille le coffre d'entraînement | login réussi sur le vault de test | +| 3 | L'**`ADMIN_TOKEN`** ouvre la console `/admin` du coffre d'entraînement | console accessible, 2FA du compte de test désactivable | +| 4 | La **clé de chiffrement restic** lit un snapshot de test | `restic snapshots` / `restic cat` en **lecture seule** sur un repo de test | +| 5 | La **phrase de récupération E2E** déverrouille une session Matrix de test | salon de test lisible depuis un appareil vierge | +| 6 | La **liste des 6 personnes et des briques** est à jour | comparaison avec `REGISTRE-IDENTITES.md` §2/§3, 0 écart non consigné | + +- la procédure est **chronométrée** et les points de friction sont corrigés dans ce fichier ; +- la ligne est journalisée : `| date | partage-secret-maitre : test annuel | 6 points (x/6) | remplacement enveloppe |` ; +- si un point est KO : l'enveloppe est **remplacée** et la rotation est envisagée (§5). ## 5. Révocabilité diff --git a/g9-migration/PHOTOS-DOUBLE-RUN.md b/g9-migration/PHOTOS-DOUBLE-RUN.md index d4696d3..4a9782f 100644 --- a/g9-migration/PHOTOS-DOUBLE-RUN.md +++ b/g9-migration/PHOTOS-DOUBLE-RUN.md @@ -44,33 +44,82 @@ Les 37 `errors` sont à regarder avant le GO famille (aucune action prise). | Nom | Résolution | Route Caddy | Mesure | |---|---|---|---| -| `prisme.folzocorp.fr` | OK → `82.65.198.114` | **aucune** (J5040) | **000** | +| `prisme.folzocorp.fr` | OK → `82.65.198.114` | **posée le 12/10 17:15** : `reverse_proxy 192.168.1.24:2342` (bloc ajouté au-dessus de `photos.`, sauvegarde `Caddyfile.bak-20261012-1643`, `caddy validate` + `reload`) | **200** (Let's Encrypt ACME DNS-01, `curl` **sans** `-k`) | | `photos.folzocorp.fr` | OK → `82.65.198.114` | `reverse_proxy 192.168.1.24:2342` = **PhotoPrism** | 307 (→ `/login`) | | Synology Photos `.25` | — | — | **5000 et 5001 = 000** (445 et 222 ouverts, ping OK) | -**Deux écarts** vis-à-vis de la décision de nommage : - -1. `photos.folzocorp.fr` pointe sur **PhotoPrism**, pas sur le NAS. -2. `prisme.folzocorp.fr` n'a **aucune route** → PhotoPrism n'est pas accessible par son nom. - -La correction est une modification du `Caddyfile` du **J5040** -(`/home/folzo/g9-migration/compose/edge-j5040/Caddyfile`, monté dans le conteneur `caddy`) : -**action L3 sur une brique réseau → GO explicite requis (G11, G6)**. Rollback = fichier -sauvegardé + `caddy reload`. - -Le second point est **bloqué par l'état du NAS** : le service web du DSM (5000/5001) ne répond -pas, donc `photos.` vers `.25` n'est pas mesurable aujourd'hui. Le double-run « les deux -accessibles simultanément » ne peut pas être prouvé tant que le DSM n'expose pas son port web. +**Écart restant** (mesuré 12/10 17:15) : `photos.folzocorp.fr` pointe toujours sur **PhotoPrism** +au lieu du NAS — le repointage vers `.25` n'a **pas été autorisé** à ce stade, et il n'est de +toute façon **pas mesurable** puisque le web du DSM (5000/5001) ne répond pas. Le double-run +« les deux accessibles simultanément » reste donc **prouvable à 1 nom sur 2** : `prisme.` ✓, +`photos.` ✗. ## 5. Ce qui est fait, ce qui reste **Fait, mesuré** : copie (noms de fichiers identiques), restauration sha256, RAM, indexation -engagée, `n8n /healthz` = 200, endpoints mesurés, nommage décidé. +engagée, `n8n /healthz` = 200, endpoints mesurés, nommage décidé, **`prisme.` = 200**, grille des +10 requêtes (§6) et procédure d'auto-upload (§7) **écrites et prêtes**. **Reste** : -- poser `prisme.` → `192.168.1.24:2342` et `photos.` → `192.168.1.25:5000` sur le J5040 (GO) ; - rendre le web du DSM joignable (ou actater que `photos.` meurt en Phase 6 sans mesure) ; - finir l'indexation et traiter les 37 erreurs ; -- montée en compétence sur **mon** téléphone (auto-upload, recherche, album partagé, partage - par lien, doublon, export original, EXIF) + 10 requêtes comparées + 7 jours sans intervention ; +- **exécution** de la grille §6 et de la procédure §7 sur **mon** téléphone (usage réel, 7 jours) ; - GO famille = validation explicite (comptes enfants et auto-upload ados **après**). + +## 6. Grille des 10 requêtes réelles (Synology Photos vs PhotoPrism) + +Protocole : la même requête est exécutée **l'une après l'autre** sur les deux services, depuis le +même téléphone, en Wi-Fi, sur le **même lot de photos**. Chronométrer du premier geste au résultat +affiché. Noter **1 gagnant par ligne**. Le classement final = nombre de victoires, plus la note +subjective « est-ce que j'ai compris sans aide ». Les cases **Gagnant / Temps / Compris sans aide** +sont à remplir par l'usage réel (personne qui remplit = moi, sur mon téléphone uniquement). + +| # | Requête réelle, telle que je la formule | Chemin Synology Photos | Chemin PhotoPrism | Gagnant | Temps (s) | Compris sans aide | +|---|---|---|---|---|---|---| +| 1 | « les photos du séjour 2019 » | Chronologie → filtre année/lieu | Recherche texte libre `2019` + filtre date | | | | +| 2 | « les photos de carla il y a plus de 10 ans » | Albums → visage non disponible | Filtre **Faces** + plage de dates | | | | +| 3 | « la facture d'électricité scannée » | Recherche texte (DS 7.x, selon OCR indexé) | Recherche plein texte sur l'OCR (`label`/`caption`) | | | | +| 4 | « ce que j'ai pris depuis mon téléphone ce mois-ci » | Dossier / appareil | Filtre **camera / appareil** + date | | | | +| 5 | « les doublons de cette photo » | Doublons intégrés (Synology Photos) | `photoprism` : recherche par **similaire** depuis une photo | | | | +| 6 | « envoyer 12 photos à Audrey sans zip » | Partage Synology (lien/album) | Album partagé + **partage par lien** | | | | +| 7 | « créer un album vacances et le partager à la famille » | Album partagé DSM | Album + partage vers un compte famille | | | | +| 8 | « partager un lien vers une photo, limité dans le temps » | Lien de partage (expirable) | Lien de partage (vérifier expiration et ce qui fuit) | | | | +| 9 | « l'EXIF et l'appareil de cette photo » | Panneau d'infos | Panneau d'infos (mesuré : faces 37, dossiers 392 indexés) | | | | +| 10 | « l'original complet, pas la version web » | Téléchargement original | Téléchargement original (`/originals/…`) | | | | + +**Classement attendu** : score par brique (x/10) + 3 lignes de conclusion (ce que PhotoPrism fait +mieux, ce que Synology Photos fait mieux, ce qui me manque des deux). La grille remplit le critère +C-PH2 « 10 requêtes réelles comparées et classées » **à la condition d'être remplie à la main**, +pas d'exister. + +## 7. Auto-upload — procédure (mon téléphone, 7 jours sans intervention) + +Prérequis mesurés : `https://prisme.folzocorp.fr/` = **200** (certificat valide, pas d'auto-signé), +compte PhotoPrism existant (`admin` mesuré en base `auth_users`), indexation en cours (9,1 %). + +1. **App** : client d'upload **officiel PhotoPrism** (Android « PhotoPrism Companion », iOS + l'équivalent App Store). **Pas** de script d'upload sur le téléphone (G11 : pas de code non + documenté qui écrit sur une brique). +2. **Serveur** : `https://prisme.folzocorp.fr` (jamais l'IP, jamais http). +3. **Compte** : mon compte PhotoPrism (pas l'admin si un compte dédié existe ; sinon admin, et le + mot de passe est **changé** après coup, consigné `famille/effi/photoprism`). +4. **Périmètre d'upload** : `DCIM` + `Screenshots` **uniquement**, en **Wi-Fi seulement** + (données mobiles : upload désactivé), **sans** suppression des photos locales après upload. +5. **Anti-effacement** : l'app est réglée en **copie**, jamais en « déplacer ». Vérifier explicitement + l'option de suppression après upload et la laisser **désactivée**. +6. **Préserver les EXIF** : pas de « compression », pas de « qualité 75 % », pas de retrait de + localisation. Contrôle : sur 3 photos uploadées, lire l'appareil + la date + la position dans le + panneau d'infos (requête 9 de la grille §6). +7. **Moyens de contrôle** (à faire 1 fois/jour, 10 secondes) : dans PhotoPrism, `Nouveautés` du jour + > 0 ; dans la base, `select count(*) from files` qui **monte** ; 0 erreur nouvelle dans `errors`. +8. **Seuil de réussite des 7 jours** : 7 lignes journalisées, **0 intervention manuelle** (pas de + relance d'upload, pas de re-login), **0 photo perdue** (comptage local avant/après, `ls | wc -l`), + et les EXIF conservés sur les 3 photos de contrôle. +9. **Format de journalisation** (une ligne par jour dans `journal.md`) : + `| date | auto-upload photoprism (C-PH2) | photos uploadées: N | interventions: 0 | fichiers locaux: X (identique) | EXIF: OK |` +10. **Rollback** : désactiver l'upload dans l'app (1 geste), puis, si besoin, supprimer les photos + uploadées du lot test via l'UI (jamais par `rm` sur `/data/docker/photoprism/originals`, qui + casserait l'index). + +**Ce que l'IA ne fait pas ici** : installer une app sur un téléphone, saisir un mot de passe, +valider un partage, déclarer une photo « bien uploadée ». Ces 4 gestes sont humains (G11). diff --git a/g9-migration/README.md b/g9-migration/README.md index 2a3c32d..28a5383 100644 --- a/g9-migration/README.md +++ b/g9-migration/README.md @@ -141,6 +141,12 @@ sudo ./scripts/12-probes.sh --uninstall # rollback complet (cron + script + ét ./scripts/27-synapse-family-users.sh --dry # plan, 0 ecriture CONFIRM=CREER ./scripts/27-synapse-family-users.sh # 6 comptes + 600 mots de passe ./scripts/27-synapse-family-users.sh --revoke # rollback = desactivation + +# G9 — salons Matrix (0 compte cree, 0 message envoye) +./scripts/28-synapse-create-room.sh --list # salons + resolution d'alias (ro) +./scripts/28-synapse-create-room.sh --dry # plan, 0 ecriture +CONFIRM=CREER ./scripts/28-synapse-create-room.sh --create # cree #famille +CONFIRM=PURGER ./scripts/28-synapse-create-room.sh --purge # rollback ``` ## Ordre d'exécution (phase par phase) diff --git a/g9-migration/REGISTRE-IDENTITES.md b/g9-migration/REGISTRE-IDENTITES.md index cbc1d7f..7f73a16 100644 --- a/g9-migration/REGISTRE-IDENTITES.md +++ b/g9-migration/REGISTRE-IDENTITES.md @@ -18,6 +18,7 @@ | Infra / services | `infra//` | `infra/gitea/g9ops`, `infra/n8n/api`, `infra/synapse/admin` | | Adultes | `famille//` | `famille/effi/nextcloud`, `famille/effi/matrix` | | Enfants | `enfants//` (coffre des parents) | `enfants/lea/jellyfin` | +| **Compte de secours (break-glass)** | **`infra//break-glass`** — 1 entrée par brique, consignée à la création du compte | `infra/gitea/break-glass`, `infra/synapse/break-glass`, `infra/jellyfin/break-glass`, `infra/n8n/break-glass`, `infra/nextcloud/break-glass` | Structure du coffre (décision 12/10, coffre **unique** Vaultwarden pour les 6) : - organisation `Famille` = 2 adultes (secrets partagés : wifi, DAV, comptes communs) ; @@ -40,8 +41,9 @@ Comptes d'automatisation : préfixe de rôle, jamais un prénom humain → `ops- | **Gitea** | `master`, `antoine`, `folzo` (importés, **`must_change_password`**), `g9ops` (admin, token API `ops-phase4`) | humains + service | à activer sur le compte principal | `infra/gitea/g9ops`, `infra/gitea/token-ops` | `docker exec -u git gitea /usr/local/bin/gitea admin user create\|change-password …` | moi | | **Postgres** (`gitea-db`) | utilisateur applicatif | service | — | `infra/gitea/db` | `docker exec -it gitea-db psql -U postgres` | moi | | **Jellyfin** | `g9ops` (admin, créé par le wizard) | service/opérateur | — | `infra/jellyfin/g9ops` | API avec `X-Emby-Authorization` ; si `EnableLocalPassword=0` (piège mesuré du wizard) : `UPDATE Users SET EnableLocalPassword=1` en SQLite + restart | moi | -| **n8n** | **owner = mon compte** (accès perdu puis recréé, journal 10/10 21:12) | admin humain | à activer | `infra/n8n/owner`, `infra/n8n/api` (clé API dédiée) | UI → Settings → Users ; si accès perdu : créer un owner via CLI n8n (`n8n export:users` n'existe pas → passer par la DB Postgres, procédure §4) | moi | -| **Synapse (Matrix)** | admin (créé via `registration_shared_secret`) + `ops-bot` (token) | admin + service | **TOTP à activer** | `infra/synapse/admin`, `infra/synapse/token-ops-bot` | API admin v2 (`PUT /_synapse/admin/v2/users/`), token admin via login ; déactivation propre = `POST /_synapse/admin/v1/deactivate/` | moi | +| **PhotoPrism** | **3 comptes mesurés** (base `photoprism.auth_users`, 12/10) : `admin` (rôle admin), `visitor` (rôle viewer, compte de démonstration), **1 compte sans nom** (rôle viewer) → les 2 non-admin sont **à nommer ou supprimer** | humains + démonstration | PhotoPrism n'a **pas de TOTP natif** → 2FA attendu par SSO (C-IDP1) | `famille//photoprism` (adultes), `enfants//photoprism` (ados, après GO famille) | CLI mesurée : `docker exec photoprism photoprism users ls\|show\|mod\|add\|rm` ; login KO après upgrade = `photoprism auth reset --yes` (**supprime tokens + app passwords**, à journaliser) | moi | +| **n8n** | **owner = `effi@openfox.local`** (le tien, accès perdu puis recréé, journal 10/10 21:12 ; **mesuré 12/10** : 1 compte, `disabled = 0`) | admin humain | à activer | `infra/n8n/owner`, `infra/n8n/api` (clé API dédiée) | UI → Settings → Users ; si accès perdu : créer un owner via CLI n8n (`n8n export:users` n'existe pas → passer par la DB Postgres, procédure §4) | moi | +| **Synapse (Matrix)** | **3 comptes mesurés 12/10** : `@folzo` (**admin**, créé 12/10, login vérifié), `@ops`, `@ops-bot` (token d'alerte) | admin + service | **TOTP à activer sur `@folzo`** | `infra/synapse/admin`, `infra/synapse/token-ops-bot` | API admin v2 par **PUT** (`PUT /_synapse/admin/v2/users/` ; POST = `Unrecognized request`), token via login ; compte inutilisable (hash NULL) = `scripts/26-synapse-admin-repair.sh` ; déactivation propre = `PUT … {"deactivated": true}` | moi | | **Element (web)** | — (frontal, pas de compte) | — | — | — | — | moi | | **CouchDB / Obsidian** | locaux (stack du second cerveau) | service | — | `infra/couchdb/admin` | `docker exec` + config du compose | moi | | **Valheim** | aucun compte (serveur jeu, whitelist par steamid) | — | — | — | `player.list` dans `data/save` | moi | @@ -122,7 +124,7 @@ créé, aucun privilège changé, aucune valeur secrète affichée), **sauf** l' | **Nextcloud** | 8 comptes | groupes : `admin` = {Effi, Master} ; `Famille Folzo` = {Audrey, Folzo, carla, ewan, lola, noah} ; `guest_app` | 2FA **available** (`twofactor_totp`, `twofactor_backupcodes`, `twofactor_nextcloud_notification` installés) — activation par utilisateur non mesurée (nécessite la session de l'utilisateur) | | **Synapse** | mesuré 18:40 : **2 comptes** (`@ops`, `@ops-bot`), `admin` = **0** → **re-mesuré 19:00 : 4 comptes, 1 admin utilisable** (`@folzo`), 1 désactivé | `@folzo` : `admin` = 1, `locked` = 0, `deactivated` = 0, hash bcrypt présent, **login = OK**, **API admin = OK** (`GET /_synapse/admin/v2/users` = 3 utilisateurs) | Trou **résolu** (GO utilisateur 12/10, `scripts/26-synapse-admin-repair.sh`) : la brique a désormais un admin → C-MX1 outillé (`scripts/27-synapse-family-users.sh`), C-MX3 débloqué (2FA à activer sur `@folzo`) | | **n8n** | **1 compte** : `effi@openfox.local` (`disabled = 0`) | basic auth déclarée (`N8N_BASIC_AUTH_USER/PASSWORD`) dans `compose/g9-queue` | owner = toi ✓ ; 2FA et clé API = à poser (C-ID9) | -| **PhotoPrism** | 3 comptes (`auth_users`) | — | les 3 sont à nommer dans la moitié B | +| **PhotoPrism** | **3 comptes** (`photoprism.auth_users`) : `admin` (rôle admin), `visitor` (rôle viewer), **1 compte sans nom** (rôle viewer) | pas de TOTP natif | **nommés en §2** (ligne PhotoPrism) ; reste humain : nommer ou supprimer les 2 comptes non-admin | | **Vaultwarden** | — | `ADMIN_TOKEN_FILE` (jeton **dans un fichier**, pas en variable d'env en clair) | bonne pratique déjà en place | | **CouchDB** | — | `GET /_all_databases` = **401** sans identifiants | **auth active** (le « sans auth » du 12/10 était un faux positif de sonde sur `/`, public par conception) | diff --git a/g9-migration/RESTE-HUMAIN.md b/g9-migration/RESTE-HUMAIN.md index edac045..6c09162 100644 --- a/g9-migration/RESTE-HUMAIN.md +++ b/g9-migration/RESTE-HUMAIN.md @@ -25,7 +25,7 @@ geste physique, usage humain (G11 : interdits permanents + L3). |---|---|---|---| | **C-MX1** 6 comptes | **admin en place et mesuré (12/10 19:00)** → `bash scripts/27-synapse-family-users.sh --dry` puis `CONFIRM=CREER bash scripts/27-synapse-family-users.sh` (2 adultes + 4 enfants, `admin:false`, mots de passe en 600 → **recopier dans le coffre puis `rm`**) ; phrase de récupération E2E **sur papier** par appareil | toi | API liste les 6, `admin:false`, 0 secret en git | | **C-MX3** 2FA admin Synapse | sur `@folzo` (login **mesuré OK**) : changer le mot de passe à la 1ʳᵉ connexion, activer **TOTP** + codes de récupération dans Element, les consigner `infra/matrix/break-glass` | toi | login admin exige l'OTP (mesuré) | -| **C-MX2** Element | installer Element sur **2 appareils adultes** seulement ; salon famille + salon ops | toi | session active sur les 2 + messages échangés | +| **C-MX2** Element | salon famille **créé et mesuré** (`#famille:192.168.1.24` → `!9hTrGcLp…`, 12/10 19:25) → installer Element sur **2 appareils adultes**, s'y connecter, rejoindre `#famille` (invites `@ops`/`@ops-bot` en attente) ; salon ops déjà vivant | toi | session active sur les 2 + messages échangés dans `#famille` | | **C-FM1** coffre unique + formation | Vaultwarden : `Famille` + `Enfants`, coffre propre 16/13 avec emergency access, 10/9 sans coffre, import Takeout, passkeys Android ; atelier 1 h (femme) + 30 min/ado | toi | **chacune le fait seule 7 jours après** (journalisé) | ## Bloc 3 — sortie de Google diff --git a/g9-migration/RUNBOOKS/matrix.md b/g9-migration/RUNBOOKS/matrix.md index 5202873..84c889f 100644 --- a/g9-migration/RUNBOOKS/matrix.md +++ b/g9-migration/RUNBOOKS/matrix.md @@ -15,6 +15,8 @@ | Pas de compte admin | `select name,admin,deactivated,case when password_hash is null then 0 else 1 end from users` (sqlite `mode=ro`) + login → « Invalid username or password » | **`scripts/26-synapse-admin-repair.sh`** (voie mesurée 12/10) : hash **bcrypt** du mot de passe écrit en base sur un compte admin → **`docker restart`** (caches) → login → **`PUT /_synapse/admin/v2/users/@`** `{password, admin:true}` → retirer l'admin temporaire. Voie officielle à réessayer : `docker exec synapse register_new_matrix_user -a -u -c /data/homeserver.yaml` (le secret est **déjà** dans `homeserver.yaml` ; `POST /_synapse/admin/v1/register` signé a répondu **`HMAC incorrect`**) | snapshot sqlite **avant** (`sqlite3 .backup` → `/data/homeserver.db.bak-*`) + `update users set admin=0, deactivated=1, password_hash=NULL where name=''` | | Création de compte refusée | `PUT /_synapse/admin/v2/users/@:192.168.1.24` (token admin) | **PUT** (pas POST) ; `scripts/27-synapse-family-users.sh` (`--dry`, `--revoke`) ; inscription publique à **couper** (`enable_registration: false`) une fois l'admin en place | `PUT … {"deactivated": true}` (conserve la trace) | | Alerte non reçue dans le salon | `sudo python3 /usr/local/bin/g9-verify-alert.py 20` ; `sudo bash /usr/local/bin/g9-probes.sh --selftest` | token/room dans `/etc/restic/matrix.env` (600) → régénérer le token `ops-bot` si révoqué | restaurer `/etc/restic/matrix.env` (backup daté) | +| Salon famille absent | `GET /_synapse/admin/v1/rooms` (nb de salons) + résolution d'alias `GET /_matrix/client/v3/directory/room/%23famille%3A192.168.1.24` | `scripts/28-synapse-create-room.sh` (`--dry` → `--create` avec `CONFIRM=CREER`) : **`POST /_matrix/client/v3/createRoom`** — `/_synapse/admin/v1/create_room` répond **404 `M_UNRECOGNIZED`** sur cette version ; invites = `@ops`, `@ops-bot` | `--purge ` (`DELETE /_synapse/admin/v1/rooms/`) | +| Rate limit login (« Too Many Requests ») | réponse `M_LIMIT_EXCEEDED` + `retry_after_ms` sur `/_matrix/client/v3/login` | **attendre ~75 s** (compteur en mémoire, vidé par `docker restart synapse`) ; grouper les appels, 1 login par exécution | aucun (auto-rétablissement) | | E2E illisible sur un appareil | session/device inconnu | phrase de récupération E2E **notée sur papier** (procédure B) ; vérifier les devices via API admin | aucun (clé perdue = session perdue, d'où le papier) | | Fused de logs / disque | `du -sh /data/*` + logs Synapse | purge via `synapse_stitch_schemas`/`purge` documenté **après** backup | restauration restic | | TLS/ACME KO (matrix.folzocorp.fr) | `curl -kI https://matrix.folzocorp.fr/` | côté J5040 : `docker logs caddy --tail 30` + clé API OVH valide (`secrets/ovh.env`) | révoquer/recréer la clé OVH | diff --git a/g9-migration/journal.md b/g9-migration/journal.md index c39c316..32a0e74 100644 --- a/g9-migration/journal.md +++ b/g9-migration/journal.md @@ -155,3 +155,5 @@ | 2026-10-12 18:30 | 4.5 | **dumps logiques cohérents des 3 bases** (`18-db-dumps.sh`, GO utilisateur) | C-P5 : corriger le trou mesuré « copie chaude = complète en octets, pas garantie rejouable » | `pg_dump` Gitea (auth **socket `trust`** mesurée dans `pg_hba.conf` → **aucun mot de passe sur une ligne de commande**) = **80 543 o** gz, **116 `CREATE TABLE`** ; `mysqldump --single-transaction --routines` Nextcloud = **5,9 Mo** gz, **317** objets, se termine par `-- Dump completed` ; idem PhotoPrism = **89,6 Mo** gz, **39** objets, `-- Dump completed`. **Aucune brique stoppée** (`--single-transaction` = instantané InnoDB cohérent). Dumps écrits sous `/data/docker//dumps/` → **déjà dans le périmètre restic** (visibles au prochain backup de 03:30). **Rejouabilité prouvée pour Gitea** : base de test `gtest_restore` créée, dump chargé, **116 tables restaurées** (== 116 du dump), base **supprimée** (`pg_database` = gitea_db/postgres/template0/template1, vérifié) | dumps : `rm -f /data/docker/{gitea,nextcloud,photoprism}/dumps/*-20261012-1825Z.sql.gz`. **Test MariaDB complet non fait** : le dump Nextcloud contient `USE \`nextcloud\`` → le charger tel quel écraserait la production ; il faut un serveur jeton (GO séparé) | | 2026-10-12 18:35 | 4.5 | **hub de monitoring posé hors du G9** (J5040) | C-AP5 : le trou mesuré était « les sondes tournent depuis le G9 → elles meurent avec lui » ; l'ancien plan (05-beszel.sh) mettait le hub sur **.25, la machine à mourir en Phase 6** | `compose/beszel-hub/docker-compose.yml` (kit) → `henrygd/beszel@sha256:6b39a8b64f9b15d00a25e7683e31a93c46161182a54f62713525107064e10599` (épinglé digest, label watchtower=false, base dans `/home/folzo/beszel-hub/data`) ; `docker compose up -d` ; **règle ufw LAN-only ajoutée** : `sudo ufw insert 1 allow proto tcp from 192.168.1.0/24 to any port 8090 comment "beszel hub LAN-only"` → **jamais joignable depuis le WAN** ; mesure : `http://192.168.1.27:8090/` = **200** ; superuser créé par CLI (`/beszel superuser create`, **mot de passe généré sur la machine, écrit en 600 dans `/home/folzo/beszel-admin.cred`, jamais affiché, jamais commité**). **Reste** : les agents (.24/.27) + le token universel, qui se crée dans l'UI (`/settings/tokens`) — la voie API PocketBase n'est pas trouvée (`/api/collections/_superusers/auth-with-password` = 404, `…/authenticators/superusers/email` = « resource not found », `/api/collections/_superusers/records` = 403 → API bien présente, chemin d'auth non identifié) | `cd /home/folzo/g9-migration/compose/beszel-hub && docker compose down` ; `sudo ufw delete ` ; `rm -f /home/folzo/beszel-admin.cred` ; `rm -rf /home/folzo/beszel-hub` | | 2026-10-12 19:00 | 4.5 | **compte admin Synapse créé et réparé** (`26-synapse-admin-repair.sh`, GO utilisateur) | C-MX1/C-MX3/C-ID4 : la brique n'avait **aucun admin** (`admin` = 0 sur `@ops` et `@ops-bot`) → ni création de comptes famille, ni réinitialisation possible | `POST /_matrix/client/v3/register` (`m.login.dummy`) → compte créé mais Synapse **ignore le localpart** (mxid aléatoire) et le bug d'arguments de `21-…` l'a laissé **`password_hash` = NULL** → admin=1 **inutilisable** (login « Invalid username or password », et `/_synapse/admin/v1/register` signé = **`HMAC incorrect`**, secret jamais recopié ni affiché). Réparation mesurée : **`bcrypt` 5.0.0 est dans l'image synapse** → hash bcrypt 12 du mot de passe du credential écrit en base (mot de passe **jamais en argv** : stdin + `curl -K` fichier 600 + tmpdir 700) → **`docker restart`** (Synapse ne vide pas ses caches sqlite sur écriture directe) → login OK → **`PUT /_synapse/admin/v2/users/@folzo:192.168.1.24`** `{password, admin:true}` (l'API v2 **crée par PUT**, POST = `Unrecognized request`) → compte temporaire `admin=0`, `deactivated=1`, `password_hash=NULL`. **Preuves** : `--verify folzo` = **login OK** ; `--admin-check folzo` = **API admin OK** (3 utilisateurs, 1 admin) ; lecture seule finale : 4 comptes, 1 admin actif, 1 désactivé. `27-synapse-family-users.sh` ajouté (`--dry` mesuré = 6 PUT planifiés, `--revoke` = désactivation) | snapshot sqlite **avant** toute écriture : `/data/homeserver.db.bak-20261012-1900` (rollback = arrêter `synapse`, restaurer le fichier, redémarrer) ; compte aléatoire conservé désactivé (0 purge) ; `rm -f /tmp/2[567]-*.sh` sur le G9 | +| 2026-10-12 19:20 | 4.5 | **6 critères retoqués par le vérificateur corrigés dans les docs** | C-ID2, C-ID8, C-MX5, C-PH2, C-WA1 (+ péages périmés) | **C-ID2** : ligne **PhotoPrism ajoutée en §2** (16ᵉ ligne .24) avec les 3 comptes **mesurés** (`photoprism.auth_users` : `admin`, `visitor`, **1 sans nom**) + CLI réelle (`photoprism users ls\|show\|mod\|add\|rm`, `auth reset --yes`) ; §7 « à nommer » supprimé ; ligne **Synapse réécrite** (3 comptes, `@folzo` admin, PUT et pas POST) ; **convention `infra//break-glass` ajoutée en §1** ; **C-ID8** : §2.1 **placement des 3 exemplaires** (sur place / hors site / hors ligne, aucun 4ᵉ), §2.2 **4 garde-fous** (2 personnes max, personne de confiance = **lecture seule**, **1 code de déverrouillage par personne**, **jamais par mail/cloud**), §4 test annuel = **6 points** tabulés ; **C-MX5** : **0 occurrence** de « remplace WhatsApp » (S3.1 #8, S13.3 C.1, `GUIDE-MATRIX.md` §4 corrigés) + **décision + 4 interdictions** écrites en S8.2 (mot « interdiction » présent) + numérotation corrigée (**S14.5** = grille Matrix/WhatsApp) ; **C-PH2** : `PHOTOS-DOUBLE-RUN.md` **§6 grille des 10 requêtes** (protocole + colonnes à remplir + classement) et **§7 procédure d'auto-upload** (10 pas, anti-effacement, EXIF, seuil de réussite 7 jours, format de journal, rollback) ; **C-WA1** : **§6 = 17 questions de Go/No-Go** avec preuve exigée par ligne + **risque de bannissement détaillé** (2 voies : signalements des correspondants + détection automatisée, **aucun seuil public**, échelle ban temporaire → permanent → suppression du compte, probabilité **non chiffrable**) ; péages périmés corrigés : §4 de `PHOTOS-DOUBLE-RUN.md` (`prisme.` = **200** avec la route du 17:15), `BACKUP-PERIMETRE.md` §6 (**dumps faits et mesurés** + rejouabilité Gitea, plus « à faire »), `EXPOSITION-INTERNET.md` **§7 = choix de machine J5040 pour l'IdP** (5 points + déclencheur + rollback) | `git checkout` des 7 fichiers (aucun effet sur les briques) | +| 2026-10-12 19:25 | 4.5 | **salon famille Matrix créé** (`28-synapse-create-room.sh`, GO utilisateur) | C-MX2 : le vérificateur mesure **1 seul salon** (`!mp7MFx9Q…` = ops) — le salon famille annoncé n'existait pas | `POST /_matrix/client/v3/createRoom` (token admin `@folzo`) → **`!9hTrGcLp3bZzw_cXtJEYs7eRfWcr4yUTZe6RTFEr27M`**, nom `Famille`, alias **`#famille:192.168.1.24`**, `preset: private_chat`, `visibility: private`, invites `@ops` + `@ops-bot`. **Voie admin v1 mesurée morte** : `POST /_synapse/admin/v1/create_room` = **404 `M_UNRECOGNIZED`** sur cette version (Matrix v1.12). Preuves : `--list` = **2 salons** en base (`!mp7MFx9Q…` créateur `@ops-bot`, `!9hTrGcLp…` créateur `@folzo`), **résolution d'alias** `#famille → !9hTrGcLp…`, 0 compte créé, **0 message envoyé**. `--dry` et `--purge` (rollback) outillés. Constat consigné : `joined_members` = 1 → les invites sont **en attente**, les adultes rejoindront depuis Element | `CONFIRM=PURGER bash scripts/28-synapse-create-room.sh --purge "!9hTrGcLp3bZzw_cXtJEYs7eRfWcr4yUTZe6RTFEr27M"` (`DELETE /_synapse/admin/v1/rooms/`) ; `rm -f /tmp/28-*.sh` sur le G9 | diff --git a/g9-migration/scripts/28-synapse-create-room.sh b/g9-migration/scripts/28-synapse-create-room.sh new file mode 100644 index 0000000..ff1fe45 --- /dev/null +++ b/g9-migration/scripts/28-synapse-create-room.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# 28-synapse-create-room.sh — salons Matrix via l'API admin (0 compte cree, 0 message envoye). +# +# Motif (verificateur 12/10) : C-MX2 annonce « salon famille + salon ops crees » ; la mesure +# (sqlite mode=ro) ne donnait **que le salon ops**. Ce script cree le salon famille et laisse la +# preuve verificable. Reversible : --purge (DELETE /_synapse/admin/v1/rooms/). +# +# Gardes : mot de passe et token **jamais en argv** (stdin + `curl -K` fichier 600, tmpdir 700), +# `--dry` par defaut, ecriture seulement sur --create, verification par GET admin + sqlite ro. +# Usage : bash 28-synapse-create-room.sh --list +# bash 28-synapse-create-room.sh --dry [--alias famille] [--invite "@ops:..." ...] +# CONFIRM=CREER bash 28-synapse-create-room.sh --create [--alias famille] +# CONFIRM=PURGER bash 28-synapse-create-room.sh --purge +set -euo pipefail + +URL="${URL:-http://192.168.1.24:8008}" +DOMAIN="${DOMAIN:-192.168.1.24}" +ADMIN="${ADMIN:-folzo}" +CRED="${CRED:-/home/folzo/synapse-admin-$ADMIN.cred}" +ALIAS="${ALIAS:-famille}" +ROOM_NAME="${ROOM_NAME:-Famille}" +INVITE="${INVITE:-@ops:192.168.1.24 @ops-bot:192.168.1.24}" +DB="${DB:-/data/homeserver.db}" +CTN="${CTN:-synapse}" + +TMPD="$(mktemp -d)"; chmod 700 "$TMPD"; trap 'rm -rf "$TMPD"' EXIT + +cat > "$TMPD/loginbody.py" <<'PY' +import json, sys +print(json.dumps({"type": "m.login.password", + "identifier": {"type": "m.id.user", "user": sys.argv[1]}, + "password": sys.stdin.buffer.read().strip().decode()})) +PY +cat > "$TMPD/token.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])); tok = d.get("access_token", "") +open(sys.argv[2], "w").write(tok) +print(" " + ("login admin OK (token %d car.)" % len(tok) if tok else "KO login: " + str(d.get("error") or d))) +sys.exit(0 if tok else 1) +PY +cat > "$TMPD/roombody.py" <<'PY' +import json, sys +print(json.dumps({"name": sys.argv[2], "room_alias_name": sys.argv[1], + "preset": "private_chat", "visibility": "private", "is_direct": False, + "invite": sys.argv[3:]})) +PY +cat > "$TMPD/rooms.py" <<'PY' +import json, sys +d = json.load(open(sys.argv[1])) +rooms = d.get("rooms", []) +print(" %s salons (API admin)" % d.get("total", len(rooms))) +for r in d.get("rooms", []): + print(" ", r.get("room_id"), "|", r.get("name"), "|", r.get("alias"), + "| membres:", r.get("joined_members")) +PY + +[[ -f "$CRED" ]] || { echo "KO: credential admin absent ($CRED)"; exit 1; } +read -r -a INV <<< "$INVITE" + +login() { + sed -n 's/^password=//p' "$CRED" | python3 "$TMPD/loginbody.py" "@${ADMIN}:${DOMAIN}" \ + | curl -s -m 15 -X POST "$URL/_matrix/client/v3/login" -H 'Content-Type: application/json' -d @- > "$TMPD/login.json" + python3 "$TMPD/token.py" "$TMPD/login.json" "$TMPD/token" + { printf 'header = "Authorization: Bearer %s"\n' "$(cat "$TMPD/token")" + printf 'header = "Content-Type: application/json"\n'; } > "$TMPD/curlrc" + chmod 600 "$TMPD/curlrc" +} + +list_rooms() { + curl -s -m 20 -K "$TMPD/curlrc" "$URL/_synapse/admin/v1/rooms?limit=50" > "$TMPD/rooms.json" + python3 "$TMPD/rooms.py" "$TMPD/rooms.json" +} + +case "${1:---list}" in + --list) + login; list_rooms + echo "== resolution de l'alias #${ALIAS} ==" + curl -s -m 15 -K "$TMPD/curlrc" "$URL/_matrix/client/v3/directory/room/%23${ALIAS}%3A${DOMAIN}" \ + | python3 -c 'import json,sys;d=json.load(sys.stdin);print(" #"+sys.argv[1]+" ->", d.get("room_id") or d.get("error") or d)' "$ALIAS" + echo "== sqlite (lecture seule) ==" + cat > "$TMPD/rooms_ro.py" <<'PY' +import sqlite3, sys +c = sqlite3.connect('file:%s?mode=ro' % sys.argv[1], uri=True).cursor() +print(' salons en base:', c.execute('select count(*) from rooms').fetchone()[0]) +for r in c.execute("select room_id, ifnull(creator,'-'), is_public, room_version from rooms"): + print(' ', r) +PY + docker cp "$TMPD/rooms_ro.py" "$CTN:/tmp/g9-rooms-ro.py" >/dev/null + docker exec "$CTN" python3 /tmp/g9-rooms-ro.py "$DB" + docker exec "$CTN" rm -f /tmp/g9-rooms-ro.py + ;; + --dry) + shift; if [[ "${1:-}" == "--alias" ]]; then ALIAS="${2:?}"; shift 2; fi + echo "== PLAN (0 ecriture) ==" + echo " POST $URL/_matrix/client/v3/createRoom (token admin)" + echo " note mesurée : $URL/_synapse/admin/v1/create_room = 404 M_UNRECOGNIZED sur cette version" + python3 "$TMPD/roombody.py" "$ALIAS" "$ROOM_NAME" "${INV[@]}" | python3 -c 'import json,sys;print(" body:", json.dumps(json.load(sys.stdin), ensure_ascii=False))' + echo " rollback : CONFIRM=PURGER bash $0 --purge " + ;; + --create) + shift; if [[ "${1:-}" == "--alias" ]]; then ALIAS="${2:?}"; shift 2; fi + if [[ "${CONFIRM:-}" != "CREER" ]]; then + echo "confirmation exigee : tape CREER pour creer le salon #$ALIAS (0 compte, 0 message)" + read -r A; [[ "$A" == "CREER" ]] || { echo "abandon (0 ecriture)"; exit 1; } + fi + login + python3 "$TMPD/roombody.py" "$ALIAS" "$ROOM_NAME" "${INV[@]}" > "$TMPD/room.json" + curl -s -m 20 -X POST "$URL/_matrix/client/v3/createRoom" -K "$TMPD/curlrc" \ + -d @"$TMPD/room.json" -o "$TMPD/created.json" -w ' create http=%{http_code}\n' + python3 -c 'import json,os,sys +p = sys.argv[1] +if os.path.getsize(p) == 0: print(" reponse vide"); raise SystemExit(0) +d = json.load(open(p)); print(" cree:", d.get("room_id") or d.get("error") or d)' "$TMPD/created.json" + list_rooms + ;; + --purge) + ROOM="${2:?room_id a purger}" + if [[ "${CONFIRM:-}" != "PURGER" ]]; then echo "abandon : CONFIRM=PURGER exige"; exit 1; fi + login + curl -s -m 60 -X DELETE -K "$TMPD/curlrc" "$URL/_synapse/admin/v1/rooms/$ROOM" \ + | python3 -c 'import json,sys;d=json.load(sys.stdin);print(" purge:", d.get("kicked") and "OK" or d)' + list_rooms + ;; + *) echo "usage: --list | --dry | --create | --purge (voir en-tete)"; exit 1 ;; +esac