Compare commits
2
Commits
2f98c32da1
...
4b26049b8f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b26049b8f | ||
|
|
ca64eaaf77 |
+44
-7
@@ -411,23 +411,24 @@ Le **Qwen 3.8max** (le LLM principal) consomme **~20 Go VRAM + ~50 Go RAM**. Le
|
|||||||
|
|
||||||
## S8 — Communication (Matrix + Jitsi + assistant vocal)
|
## S8 — Communication (Matrix + Jitsi + assistant vocal)
|
||||||
|
|
||||||
> **Statut** : ✅ **Tranché** (05/10/2026) — **Matrix + Jitsi + Element** (remplace WhatsApp) + **assistant vocal** via Home Assistant.
|
> **Statut** : ✅ **Tranché** (05/10/2026), **révisé 12/10/2026** — **Matrix + Jitsi + Element** = **complément du foyer** (messagerie familiale + canal d'alerte + assistant), **pas un remplacement de WhatsApp**. WhatsApp reste la messagerie principale (famille, amis, travail) — réalité d'usage mesurée, voir `g9-migration/BRIDGE-WHATSAPP.md`.
|
||||||
|
|
||||||
### 8.1 La stack communication
|
### 8.1 La stack communication
|
||||||
|
|
||||||
| Brique | Rôle | Machine | Licence |
|
| Brique | Rôle | Machine | Licence |
|
||||||
|--------|------|---------|---------|
|
|--------|------|---------|---------|
|
||||||
| **Matrix (Synapse)** | **Messagerie** (remplace WhatsApp) | J5040 | Apache-2.0 |
|
| **Matrix (Synapse)** | **Messagerie du foyer** + **canal d'alerte ops** (complément de WhatsApp) | J5040 | Apache-2.0 |
|
||||||
| **Jitsi** | **Visioconférence** | J5040 | Apache-2.0 |
|
| **Jitsi** | **Visioconférence** | J5040 | Apache-2.0 |
|
||||||
| **Element** | **Client** (Android) | Téléphone | GPL-3.0 |
|
| **Element** | **Client** (Android) — 2 adultes d'abord, ados en phase 2 | Téléphone | GPL-3.0 |
|
||||||
| **Home Assistant** | **Assistant vocal** (enceintes Google) | Freebox | Apache-2.0 |
|
| **Home Assistant** | **Assistant vocal** (enceintes Google) | Freebox | Apache-2.0 |
|
||||||
|
|
||||||
### 8.2 L'usage famille (résilience = usage)
|
### 8.2 L'usage famille (résilience = usage)
|
||||||
|
|
||||||
| Utilisateur | Appareil | Usage quotidien |
|
| Utilisateur | Appareil | Usage quotidien |
|
||||||
|-------------|----------|-----------------|
|
|-------------|----------|-----------------|
|
||||||
| **Famille** | Téléphone (Element) | **Messagerie** (remplace WhatsApp) |
|
| **Famille** | Téléphone (Element) | **Messagerie du foyer** (salon famille) — **WhatsApp garde** les échanges extérieurs (amis, travail, famille élargie) |
|
||||||
| **Famille** | Téléphone + TV | **Visio** (Jitsi, via Element) |
|
| **Famille** | Téléphone + TV | **Visio** (Jitsi, via Element) |
|
||||||
|
| **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 |
|
| **Moi** | Enceintes Google ×4-5 | **Assistant vocal** (Home Assistant) → peut déclencher n8n, contrôler Frigate |
|
||||||
|
|
||||||
### 8.3 Le bot hermes ↔ Matrix
|
### 8.3 Le bot hermes ↔ Matrix
|
||||||
@@ -461,7 +462,7 @@ Le **Qwen 3.8max** (le LLM principal) consomme **~20 Go VRAM + ~50 Go RAM**. Le
|
|||||||
| Élément | Détail |
|
| Élément | Détail |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| **Domaine** | **folzocorp.fr** (principal) + **sous-domaines** par service |
|
| **Domaine** | **folzocorp.fr** (principal) + **sous-domaines** par service |
|
||||||
| **Sous-domaines** | `cloud.folzocorp.fr` (Nextcloud) · `photos.folzocorp.fr` (PhotoPrism) · `media.folzocorp.fr` (Jellyfin) · `matrix.folzocorp.fr` (Matrix) · `git.folzocorp.fr` (Gitea) · `vault.folzocorp.fr` (Vaultwarden) · `ai.folzocorp.fr` (OpenWebUI) |
|
| **Sous-domaines** | `cloud.folzocorp.fr` (Nextcloud) · `prisme.folzocorp.fr` (**PhotoPrism**) · `photos.folzocorp.fr` (**Synology Photos** — LAN-only, **temporaire**, disparaît avec le xpenology en Phase 6) · `media.folzocorp.fr` (Jellyfin) · `matrix.folzocorp.fr` (Matrix) · `git.folzocorp.fr` (Gitea) · `pass.folzocorp.fr` (Vaultwarden) · `n8n.folzocorp.fr` (n8n) · `ai.folzocorp.fr` (OpenWebUI) |
|
||||||
| **TLS** | **Caddy** génère + renouvelle **automatiquement** les certificats Let's Encrypt (pas de script). |
|
| **TLS** | **Caddy** génère + renouvelle **automatiquement** les certificats Let's Encrypt (pas de script). |
|
||||||
| **Exposition** | **Seulement** les services **nécessaires** à l'extérieur passent par Caddy. Le **reste** est **LAN-only** (accès via **WireGuard**). |
|
| **Exposition** | **Seulement** les services **nécessaires** à l'extérieur passent par Caddy. Le **reste** est **LAN-only** (accès via **WireGuard**). |
|
||||||
|
|
||||||
@@ -555,7 +556,9 @@ Le **Qwen 3.8max** (le LLM principal) consomme **~20 Go VRAM + ~50 Go RAM**. Le
|
|||||||
| **Docker Compose** | **Déclaration** des services (fichiers `docker-compose.yml`) | toutes | Apache-2.0 |
|
| **Docker Compose** | **Déclaration** des services (fichiers `docker-compose.yml`) | toutes | Apache-2.0 |
|
||||||
| **Gitea** | **Versioning** des `docker-compose.yml` + config (source de vérité) | G9 | MIT |
|
| **Gitea** | **Versioning** des `docker-compose.yml` + config (source de vérité) | G9 | MIT |
|
||||||
| **Watchtower** | **Mises à jour** automatiques **sélectives** (stateless auto / stateful exclu + manuel) | J5040 + G9 + PC IA | Apache-2.0 |
|
| **Watchtower** | **Mises à jour** automatiques **sélectives** (stateless auto / stateful exclu + manuel) | J5040 + G9 + PC IA | Apache-2.0 |
|
||||||
| **Beszel** | **Monitoring** léger + historique + **alertes** | J5040 (hub) + agents | MIT |
|
| **Beszel** | **Monitoring** léger + historique + **alertes** (métriques + état conteneurs) | J5040 (hub) + agents | MIT |
|
||||||
|
| **Registre** (`11-registry.sh` → `REGISTRE-BRIQUES.md`) | **Inventaire généré** (images épinglées, labels, endpoints, état) + **détection de dérive** par `git diff` | pilot → .24/.27 (lecture seule) | MIT (kit) |
|
||||||
|
| **Sondes applicatives** (`12-probes.sh`) | **`up ≠ utilisable`** : sonde la **fonction** (occ status, discovery, healthz, login) + **alertes JSON** + **1 digest/jour** | G9 (cron) | MIT (kit) |
|
||||||
| **n8n** | **Alertes** + **automatisation** (workflows) | G9 | ⚠️ fair-code |
|
| **n8n** | **Alertes** + **automatisation** (workflows) | G9 | ⚠️ fair-code |
|
||||||
|
|
||||||
### 11.2 Pourquoi pas Portainer / PaaS
|
### 11.2 Pourquoi pas Portainer / PaaS
|
||||||
@@ -827,7 +830,7 @@ Phase A (Fondations logicielles) ──► Phase B (Cœur famille) ──► Pha
|
|||||||
| Fédéré / auto-hébergé | ✅ | ❌ | 🟡 |
|
| Fédéré / auto-hébergé | ✅ | ❌ | 🟡 |
|
||||||
| **Verdict** | **✅ Recommandé** | ❌ (GAFAM) | 🟡 (pas d'auto-hébergement facile) |
|
| **Verdict** | **✅ Recommandé** | ❌ (GAFAM) | 🟡 (pas d'auto-hébergement facile) |
|
||||||
|
|
||||||
> **Recommandation** : **Matrix (Synapse) + Jitsi + Element** (remplace WhatsApp).
|
> **Recommandation** : **Matrix (Synapse) + Jitsi + Element** en **complément du foyer** (messagerie familiale + alertes + assistant). **Révisé 12/10/2026** : WhatsApp **n'est pas remplaçable** dans notre usage (famille, amis, travail) → il reste la messagerie principale ; Element couvre le **foyer** et le **canal d'alerte**. Le pont `mautrix-whatsapp` est **étudié, non déployé** : numéro familial **interdit**, risque de bannissement, coût de session (QR / 12 jours). Détail : `g9-migration/BRIDGE-WHATSAPP.md`.
|
||||||
|
|
||||||
### 14.6 MDM enfants
|
### 14.6 MDM enfants
|
||||||
|
|
||||||
@@ -840,6 +843,39 @@ Phase A (Fondations logicielles) ──► Phase B (Cœur famille) ──► Pha
|
|||||||
|
|
||||||
> **Recommandation** : **Google Family Link** (maintenant, usage parental) + **MDM OSS** (MicroMDM/Android Enterprise) en **option future**.
|
> **Recommandation** : **Google Family Link** (maintenant, usage parental) + **MDM OSS** (MicroMDM/Android Enterprise) en **option future**.
|
||||||
|
|
||||||
|
### 14.7 Coffre famille (décision 12/10/2026)
|
||||||
|
|
||||||
|
| Critère | **Vaultwarden unique (6 personnes)** ✅ | Nextcloud Passwords |
|
||||||
|
|---------|----------------------------------------|---------------------|
|
||||||
|
| Dépendance | ✅ indépendant des autres briques | ❌ le coffre vit dans la brique qu'il protège (Nextcloud HS = plus les accès pour réparer Nextcloud) |
|
||||||
|
| Reset admin d'un compte enfant | ✅ sans effet sur le coffre | ❌ clés dérivées du mot de passe → coffre potentiellement illisible |
|
||||||
|
| Modèle familial (organisations, emergency access) | ✅ | ❌ |
|
||||||
|
| Coût pour 6 (import, backup, reset, formation) | ✅ 1 chemin | ❌ 2 chemins (registre doublé) |
|
||||||
|
| **Verdict** | **✅ Retenu** | ❌ Non retenu (pilote adulte possible, non déclenché) |
|
||||||
|
|
||||||
|
### 14.8 Identité (décision 12/10/2026, ordre validé)
|
||||||
|
|
||||||
|
| Étape | Décision | Statut |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **Registre d'identités** (moitié générée + moitié humaine) — répond à « quel compte a servi ? » | ✅ posé (`g9-migration/REGISTRE-IDENTITES.md`) |
|
||||||
|
| 2 | **Étude exposition internet** avant tout IdP | ✅ étude écrite, 5 décisions à rendre |
|
||||||
|
| 3 | **Authelia** en cible, pilote 3 briques (Synapse, Gitea, Jellyfin), **opt-in + break-glass** | 🟡 après étude + Phase 5 |
|
||||||
|
| — | Authentik / Keycloak / Samba AD / FreeIPA / Nextcloud-IdP | ❌ écartés (poids, sur-ingénierie, SPOF sur la brique la plus stateful) |
|
||||||
|
|
||||||
|
### 14.9 Suivi applicatif (décision 12/10/2026)
|
||||||
|
|
||||||
|
| Critère | **Sondes + registre + Diun** ✅ | Grafana/Prometheus (+cAdvisor) | Netdata | Uptime Kuma |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Couvre `up ≠ utilisable` | ✅ (occ, discovery, healthz, login) | ❌ (métriques) | 🟡 | ✅ |
|
||||||
|
| Briques ajoutées | **0** (scripts du kit) | 4 | 1 (lourd) | 1 (stateful) |
|
||||||
|
| Chevauchement avec Beszel | **complémentaire** | redondant (InfluxDB déjà écarté) | redondant | partiel |
|
||||||
|
| Charge de maintenance | faible (cron + 1 digest/jour) | élevée | moyenne | moyenne + fatigue d'alertes |
|
||||||
|
| **Verdict** | **✅ Retenu** | ❌ | ❌ | 🟡 reporté (déclencheur écrit, Phase 6 + 2 semaines d'usage) |
|
||||||
|
|
||||||
|
### 14.10 Autonomie de l'assistante IA (décision 12/10/2026)
|
||||||
|
|
||||||
|
> **Paliers G11** : L0 lecture seule → L1 propose → L2 exécute le **stateless documenté** → L3 stateful **sur GO explicite**. Portes chiffrées (≥ 90 % de propositions pertinentes sur 30 jours ; sinistre simulé sans perte). Interdits permanents : destructif, suppression de données, réseau (G6), wipe, création de privilège, lecture de secrets hors coffre. Détail : `g9-migration/README.md` (charte G11).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## S15 — Critères d'acceptance (AC1–AC10)
|
## S15 — Critères d'acceptance (AC1–AC10)
|
||||||
@@ -867,6 +903,7 @@ Phase A (Fondations logicielles) ──► Phase B (Cœur famille) ──► Pha
|
|||||||
|
|
||||||
| Date | Version | Changement |
|
| Date | Version | Changement |
|
||||||
|------|---------|------------|
|
|------|---------|------------|
|
||||||
|
| 12/10/2026 | **v1.6** | **Identité + suivi applicatif + communication (décisions 12/10)** : **S8 révisé** — Matrix/Jitsi/Element = **complément du foyer**, **pas** remplacement de WhatsApp (Element : 2 adultes d'abord, ados phase 2) ; **S9.2** sous-domaines alignés sur le réel (`pass.folzocorp.fr` = Vaultwarden, **`prisme.folzocorp.fr` = PhotoPrism**, `photos.folzocorp.fr` = Synology Photos **temporaire**, meurt Phase 6) ; **S11** enrichi de deux briques d'ops sans conteneur de plus : **registre généré** (`11-registry.sh` → `REGISTRE-BRIQUES.md`, dérive par `git diff`) et **sondes applicatives** (`12-probes.sh`, `up ≠ utilisable`, alertes **JSON** + 1 digest/jour) ; **S14.7→S14.10** : coffre **unique Vaultwarden** pour 6 (Nextcloud Passwords écarté : boucle de verrouillage + reset admin + pas d'emergency access), identité = **registre → étude exposition → Authelia** (Authentik/Keycloak/Samba AD/FreeIPA écartés), suivi = **sondes + registre + Diun** (Grafana/Prometheus/Netdata écartés, Uptime Kuma reporté avec déclencheur écrit), **G11 paliers d'autonomie IA**. Artéfacts : `g9-migration/{REGISTRE-IDENTITES,REGISTRE-BRIQUES,PARTAGE-SECRET-MAITRE,RESTORE-NAS-USB,EXPOSITION-INTERNET,BRIDGE-WHATSAPP}.md`, `RUNBOOKS/` ×7, charte **G11**. |
|
||||||
| 10/10/2026 | **v1.5** | **S10.2 — copie 2 (Freebox) EN PLACE sur le G9** : restic 0.19.1 + rclone 1.75.2, repo `rclone:freeboxftp:Freebox/backup/g9` (FTP local 21), snapshot 4.18 GiB, **append-only prouvé**, **restore testée** (sha256), cron 03:30/04:00/09:15 + alerte Matrix ; **mont CIFS abandonné** (restic/`chmod`, FUSE) → backend rclone, **persistance post-reboot sans montage**. **Phase 5 planifiée** (non réalisée) : VM Freebox + `rest-server --append-only` (autostart à valider), `restic copy` → 218play, identifiants `NoAccess`, chroot durci. |
|
| 10/10/2026 | **v1.5** | **S10.2 — copie 2 (Freebox) EN PLACE sur le G9** : restic 0.19.1 + rclone 1.75.2, repo `rclone:freeboxftp:Freebox/backup/g9` (FTP local 21), snapshot 4.18 GiB, **append-only prouvé**, **restore testée** (sha256), cron 03:30/04:00/09:15 + alerte Matrix ; **mont CIFS abandonné** (restic/`chmod`, FUSE) → backend rclone, **persistance post-reboot sans montage**. **Phase 5 planifiée** (non réalisée) : VM Freebox + `rest-server --append-only` (autostart à valider), `restic copy` → 218play, identifiants `NoAccess`, chroot durci. |
|
||||||
| 06/10/2026 | **v1.4** | **Cohérence inter-dossiers (source unique)** : (1) **S10.2** — le schéma 3-2-1 ne **re-déclare plus** le tableau → **pointeur** vers `DOSSIER-RECREATION.md` §R4 (source unique) + note outil (restic, quotidien/hebdo) ; (2) **S4.3** — le modèle 2 couches ne **re-déclare plus** le principe → **pointeur** vers `PROJET-SECOND-CERVEAU.md` S4.1 (source unique) + tableau **mapping briques/machines** (spécificité du dossier logiciel) ; (3) **S3.1** — le tableau est **cadré autour des briques fonctionnelles** : la stack \*arr #10 = **1 brique**, les apps Nextcloud 13-15 marquées **↳ apps du socle NC #12** ; note de comptage précisée (**28 lignes = 25 briques fonctionnelles**, dans la cible ≤ ~20 briques de consolidation). |
|
| 06/10/2026 | **v1.4** | **Cohérence inter-dossiers (source unique)** : (1) **S10.2** — le schéma 3-2-1 ne **re-déclare plus** le tableau → **pointeur** vers `DOSSIER-RECREATION.md` §R4 (source unique) + note outil (restic, quotidien/hebdo) ; (2) **S4.3** — le modèle 2 couches ne **re-déclare plus** le principe → **pointeur** vers `PROJET-SECOND-CERVEAU.md` S4.1 (source unique) + tableau **mapping briques/machines** (spécificité du dossier logiciel) ; (3) **S3.1** — le tableau est **cadré autour des briques fonctionnelles** : la stack \*arr #10 = **1 brique**, les apps Nextcloud 13-15 marquées **↳ apps du socle NC #12** ; note de comptage précisée (**28 lignes = 25 briques fonctionnelles**, dans la cible ≤ ~20 briques de consolidation). |
|
||||||
| 06/10/2026 | **v1.3** | **Correction mainteneur hermes** : S3.1 #24 — « maintenu (Nous Research) » → « **maintenu (nesquena — `ghcr.io/nesquena/hermes-webui`)** », aligné sur l'image réellement en production (snapshot `ETAT-DE-LART-2026-09-30.md` §2.1). |
|
| 06/10/2026 | **v1.3** | **Correction mainteneur hermes** : S3.1 #24 — « maintenu (Nous Research) » → « **maintenu (nesquena — `ghcr.io/nesquena/hermes-webui`)** », aligné sur l'image réellement en production (snapshot `ETAT-DE-LART-2026-09-30.md` §2.1). |
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# BRIDGE WHATSAPP ↔ MATRIX (T-W — étude, **0 installation**)
|
||||||
|
|
||||||
|
> Contexte décidé 12/10 : **WhatsApp est indispensable** (famille, amis, travail) et ne sera **pas**
|
||||||
|
> remplacé. Matrix = complément du foyer. Ce document dit ce qu'un pont apporte, ce qu'il ne donne
|
||||||
|
> pas, et ce qui est **interdit**. Aucune installation sur le numéro familial.
|
||||||
|
|
||||||
|
## 1. L'outil
|
||||||
|
|
||||||
|
| Point | Fait |
|
||||||
|
|---|---|
|
||||||
|
| Projet | `mautrix-whatsapp` (Tulir / mautrix), basé sur `whatsmeow` |
|
||||||
|
| Licence | **AGPL-3.0** (compatible avec la règle « OSS + chemin de sortie » de la table rase) |
|
||||||
|
| Mécanisme | protocole **WhatsApp Web multi-device** : le serveur tient une session liée par **QR code** |
|
||||||
|
| Contrainte téléphone | WhatsApp impose que le **téléphone se connecte au moins tous les 12 jours**, sinon la session est coupée → **re-link** (QR) à refaire |
|
||||||
|
| État | DB (messages, préférences) + **session** (clés de liaison) + médias = **brique stateful** |
|
||||||
|
| Chiffrement | côté Matrix : E2EE gérable ; **côté WhatsApp : la session standard**, tes correspondants ne voient **que** WhatsApp |
|
||||||
|
|
||||||
|
## 2. Ce que le pont apporte / ce qu'il ne donne pas
|
||||||
|
|
||||||
|
| Apporte | Ne donne pas |
|
||||||
|
|---|---|
|
||||||
|
| Miroir des conversations WhatsApp dans Element (MP, groupes, médias), historique importable | un canal commun : un groupe WhatsApp et un salon Matrix ne « se parlent » pas proprement (les membres WhatsApp ne voient pas Matrix) |
|
||||||
|
| Répondre depuis Element | la fin de WhatsApp : **WhatsApp reste la source** |
|
||||||
|
| Lecture centralisée (1 client pour la famille + les alertes ops) | la garantie de disponibilité : pont cassé = **perte de l'affichage**, pas des messages |
|
||||||
|
| Base pour qu'Effi (assistante) **lisse/résume** des MP si tu l'autorises | la conformité : un client non officiel est **contraire aux ToS WhatsApp** |
|
||||||
|
|
||||||
|
## 3. Risques à nommer (le point qui décide tout)
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
| 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) |
|
||||||
|
| Surface d'attaque | brique de plus, exposée = non (interne), mais lit un compte de communication | pas d'exposition WAN ; pas d'automatisation d'**envoi** |
|
||||||
|
|
||||||
|
## 4. Interdits écrits (T-W.3)
|
||||||
|
|
||||||
|
1. **Numéro familial principal** : jamais relié à un pont.
|
||||||
|
2. **Alertes ops sur WhatsApp** : le canal d'alerte reste Matrix (S8.4), pas WhatsApp.
|
||||||
|
3. **Aucune automatisation qui envoie** des messages WhatsApp (n8n/IA) : lecture seulement, si pilote validé.
|
||||||
|
4. Aucune exposition WAN du pont ; aucune ouverture ufw pour lui.
|
||||||
|
|
||||||
|
## 5. Si un jour GO utilisateur (pilote borné)
|
||||||
|
|
||||||
|
| Étape | Contenu |
|
||||||
|
|---|---|
|
||||||
|
| 1 | **numéro secondaire isolé** (numero dédié, vide, hors famille) |
|
||||||
|
| 2 | conteneur épinglé par digest, label `watchtower.enable=false`, DB + session dans `/data/docker/whatsapp/` |
|
||||||
|
| 3 | session + DB **dans les snapshots restic** (vérifié) |
|
||||||
|
| 4 | **7 jours de miroir stable** (pas de re-link, pas de perte de messages) |
|
||||||
|
| 5 | re-link testé (déconnexion volontaire → QR → reprise) |
|
||||||
|
| 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
|
||||||
|
|
||||||
|
- **Utile** si l'objectif est « lire au même endroit » (Element = foyer + alertes) et que le pilote
|
||||||
|
reste sur un **numéro secondaire**.
|
||||||
|
- **Pas utile** si l'objectif est de quitter WhatsApp : le pont ne fait pas ça.
|
||||||
|
- **Coût de maintenance** : session (QR/12 jours) + DB + médias = exactement le type de charge que
|
||||||
|
tu veux supprimer. Le pont **ajoute** de la maintenance pour un gain de confort.
|
||||||
|
- Recommandation : **étude validée, pilote non déclenché** ; on en reparle quand Element tourne
|
||||||
|
pour le foyer et que les alertes ops sont stables (tâche MATRICE FAMILLE), jamais avant.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# ÉTUDE EXPOSITION INTERNET (T-D — étude, **aucune règle posée**)
|
||||||
|
|
||||||
|
> Périmètre : documenter, pas agir. **0 règle ufw modifiée, 0 changement réseau** (G6).
|
||||||
|
> Faits mesurés le 12/10/2026 en lecture seule (`ufw status numbered`, `ss -ltnu`, Caddyfile, journal).
|
||||||
|
|
||||||
|
## 1. Exposition mesurée aujourd'hui
|
||||||
|
|
||||||
|
| Machine | Exposée au WAN (pare-feu) | Observations |
|
||||||
|
|---|---|---|
|
||||||
|
| **G9 (.24)** | `22/tcp` **Anywhere**, `443/tcp` **Anywhere** (+ v6) | 22 ouvert au WAN = **risque si la Freebox forward** ; 443 = l'ancien edge (TLS **interne**, CA auto-signé) → un endpoint chiffré non reconnu exposé publiquement |
|
||||||
|
| **G9 (.24)** LAN-only | 2342, 3000, 3100, 5678, 5984, 8008, 8009, 8080, 8081, 8096, 9980, 2456-2458, SMB (137-139/445) | tout ce qui est « brique » est correctement restreint à `192.168.1.0/24` |
|
||||||
|
| **J5040 (.27)** | `80/tcp`, `443/tcp`, `51820/udp` (**Anywhere**) | le **vrai** edge : Caddy + Let's Encrypt (7/7 certs DNS-01) + WireGuard. 80 = redirect ACME |
|
||||||
|
| **J5040 (.27)** LAN-only | `53` (DNS AdGuard), `3000` (hub Beszel), `8081` (UI AdGuard) | UI AdGuard = **login local** (bcrypt), pas de 2FA |
|
||||||
|
| **Pilot (.11)** | `tailscale0` actif (100.111.222.30) | **voie d'accès distante non documentée** dans le plan S9 → à nommer explicitement (acceptée ou coupée) |
|
||||||
|
| **Freebox** | IP publique `82.65.198.114` (`folzocorp.fr`) | les **redirects Freebox** (443 → G9, Valheim UDP 2456-2458) ne sont **pas lisibles** depuis l'infra : inventaire à faire côté Freebox par l'utilisateur |
|
||||||
|
|
||||||
|
## 2. Grille par brique (actuelle / nécessaire / dangereuse)
|
||||||
|
|
||||||
|
| Brique | Publique aujourd'hui | Publique **nécessaire** | Danger si exposée | Public visé |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Nextcloud (`cloud`) | oui, via J5040 (443) | **oui** (usage famille hors maison) | compte unique = porte d'entrée des fichiers ; 2FA obligatoire | famille |
|
||||||
|
| Vaultwarden (`pass`) | oui, via J5040 | **oui** (coffre = besoin nomade) | exposition du coffre = cible n°1 ; 2FA + mot de passe maître fort | famille |
|
||||||
|
| Matrix/Synapse + Element (`matrix`, 8009) | matrix via J5040 ; Element web LAN | **non** (Element = client, passe par le homeserver) | 8009 exposé = frontal gratuit ; 8008 exposé = API admin exposée | famille |
|
||||||
|
| Gitea (`git`) | oui, via J5040 | **non** (usage interne + pushes) | dépôt du kit + vault second cerveau = **fuites de plans** ; comptes `must_change_password` | moi |
|
||||||
|
| Jellyfin (`media`) | oui, via J5040 | **optionnel** (usage hors maison = désirabilité) | compte opérateur `g9ops` exposé ; pas de 2FA | famille |
|
||||||
|
| PhotoPrism (`prisme`) | route prévue, pas posée | **optionnel** | 10 ans de photos = **donnée la plus sensible** de la maison | famille |
|
||||||
|
| n8n (`n8n`) | oui, via J5040 | **non** | UI d'automatisation = exécution de commandes ; **pas de SSO** (payant) ; owner unique | moi |
|
||||||
|
| Beszel hub (3000 .27) | non | non | vue de l'inventaire + alertes | moi |
|
||||||
|
| AdGuard UI (8081 .27) | non | non | contrôle du DNS de toute la maison | moi |
|
||||||
|
| Collabora (9980) | non | non | WOPI interne | interne |
|
||||||
|
| CouchDB (5984) | non | non | **danger LAN** : CouchDB sans auth = lecture/écriture directe | interne |
|
||||||
|
| SMB (137-139/445) | non | non | partage famille, invité `guest ok` → **LAN-only strict** | famille |
|
||||||
|
| SSH G9 (22) | **oui au pare-feu** | **non** | exposition SSH = 22 en clair sur internet si forward | moi |
|
||||||
|
| Edge G9 (443) | **oui au pare-feu** | **non** ( remplacé par J5040 ) | TLS auto-signé exposé, double porte d'entrée | — |
|
||||||
|
| Valheim (2456-2458) | non (redirect Freebox UDP existant) | **oui** (amis hors LAN) | jeu, sans donnée sensible | amis |
|
||||||
|
| WireGuard (51820 .27) | oui | **oui** (c'est LA porte d'entrée distante) | clé privée = accès total LAN | moi |
|
||||||
|
|
||||||
|
## 3. Ce que ça change pour l'identité (lien direct avec la tâche Authelia)
|
||||||
|
|
||||||
|
| Constat | Conséquence |
|
||||||
|
|---|---|
|
||||||
|
| La **forward-auth ne protège que ce qui passe par Caddy** (les 6 sous-domaines) | les ports LAN directs (3000, 5678, 5984, 8009, 8081, 9980, 2342) **restent en login local** : un IdP ne les couvre pas |
|
||||||
|
| Tout ce qui est **LAN-only** n'a pas besoin d'un IdP pour être sûr | si la posture reste « LAN + WireGuard », l'apport d'Authelia est **confort** (1 login, 1 2FA) et non sécurité |
|
||||||
|
| Tout ce qui est **exposé** (Nextcloud, Vaultwarden, Matrix, Gitea, Jellyfin, n8n) est concerné | plus l'exposition est large, plus l'IdP + 2FA central devient indispensable |
|
||||||
|
| `22/tcp` et l'edge G9 sont ouverts au WAN **sans nécessité** | ce sont les 2 décisions les moins chères à prendre (fermer = 2 règles à retirer, pas à poser) |
|
||||||
|
| Tailscale sur le pilot | voie distante réelle et silencieuse : à **nommer** (acceptée = documentée ; refusée = désactivée) |
|
||||||
|
|
||||||
|
## 4. Playbook de durcissement (classé, **non implémenté**)
|
||||||
|
|
||||||
|
| Rang | Action | Coût | Effet | Réversibilité |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | Retirer `22/tcp Anywhere` du G9 (SSH = LAN + WireGuard) | 1 règle | supprime l'expo SSH publique | immédiate |
|
||||||
|
| 2 | Retirer `443/tcp` du G9 après la bascule bridge (edge J5040 seul) | 1 règle + démontage | une seule porte d'entrée | immédiate |
|
||||||
|
| 3 | 2FA sur Nextcloud + Vaultwarden + Gitea + Synapse (S9.1) | ~1 h | la vraie protection des comptes exposés | par compte |
|
||||||
|
| 4 | WireGuard = **seul** chemin distant pour Gitea/n8n/Beszel/AdGuard UI | config Caddy/ufw | 4 briques sortent de la surface publique | immédiate |
|
||||||
|
| 5 | Forward-auth Authelia sur les 6 sous-domaines + break-glass | 1 brique | 1 login, 1 2FA, déprovisionnement en 1 point | opt-in par brique |
|
||||||
|
| 6 | CouchDB : auth ou bind 127.0.0.1 (l'appli n'a besoin que du réseau Docker) | 1 ligne compose | ferme un trou LAN silencieux | immédiate |
|
||||||
|
| 7 | Inventaire des redirects Freebox (443, UDP 2456-2458) + suppression de ce qui est mort | côté Freebox, utilisateur | cohérence réel/plan | immédiate |
|
||||||
|
| 8 | Nommer Tailscale (accepté/documenté, ou désactivé) | 1 décision | supprime une voie non documentée | immédiate |
|
||||||
|
|
||||||
|
## 5. Décisions à rendre (ta main)
|
||||||
|
|
||||||
|
1. Fermer `22` WAN sur le G9 ? (rang 1)
|
||||||
|
2. Démonter l'edge G9 à la bascule bridge ? (rang 2)
|
||||||
|
3. Garder Gitea et n8n exposés, ou les réserver à WireGuard ? (rang 4)
|
||||||
|
4. Tailscale : documenté et assumé, ou coupé ? (rang 8)
|
||||||
|
5. PhotoPrism et Jellyfin exposés hors maison, oui/non ? (grille)
|
||||||
|
|
||||||
|
**Tant que ces 5 réponses ne sont pas données, aucune règle n'est posée** (G6).
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# PARTAGE-SECRET-MAITRE — procédure de secours (T-A.9)
|
||||||
|
|
||||||
|
> Décision 12/10 : aujourd'hui **moi seul** connais le mot de passe maître du coffre (Vaultwarden)
|
||||||
|
> et l'`ADMIN_TOKEN`. Objectif : **1 à 2 amis de confiance** capables de déverrouiller le coffre
|
||||||
|
> si je suis injoignable. Ce fichier ne contient **aucun secret** : il décrit le contenant, les
|
||||||
|
> détenteurs, et ce qu'ils font.
|
||||||
|
|
||||||
|
## 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è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.
|
||||||
|
|
||||||
|
## 2. Contenu de l'enveloppe (hors-ligne, scellée)
|
||||||
|
|
||||||
|
- le **mot de passe maître** du coffre (la passphrase, pas les mots de passe du coffre) ;
|
||||||
|
- l'**`ADMIN_TOKEN`** en clair (console Vaultwarden) ;
|
||||||
|
- la **phrase de récupération E2E** de Matrix (au moins pour les 2 adultes) ;
|
||||||
|
- la **clé de chiffrement du repo restic** (`repo.key`) — copie, pas l'originale ;
|
||||||
|
- la liste des 6 personnes et des briques concernées (copie de `REGISTRE-IDENTITES.md` §2/§3) ;
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
## 3. Ce que fait le détenteur si je suis injoignable
|
||||||
|
|
||||||
|
1. Ouvrir l'enveloppe **devant témoin** (famille), noter la date.
|
||||||
|
2. Déverrouiller le coffre : app Bitwarden → URL du coffre = `https://pass.folzocorp.fr`
|
||||||
|
(si le domaine n'est pas joignable, l'accès LAN = `http://192.168.1.24:8081`) →
|
||||||
|
identifiant = mon compte → **mot de passe maître** de l'enveloppe.
|
||||||
|
3. Si l'app refuse (2FA) : utiliser la console admin `https://pass.folzocorp.fr/admin` avec
|
||||||
|
l'`ADMIN_TOKEN` de l'enveloppe → désactiver le 2FA du compte (mesure de dernier recours,
|
||||||
|
à journaliser).
|
||||||
|
4. Transmettre aux adultes de la famille les entrées nécessaires (organisation `Famille`),
|
||||||
|
**sans** diffuser les secrets hors famille.
|
||||||
|
5. Journaliser dans `journal.md` : date, ce qui a été ouvert, ce qui a été transmis.
|
||||||
|
|
||||||
|
## 4. Test annuel (critère)
|
||||||
|
|
||||||
|
Une fois par an, sans casser la production :
|
||||||
|
|
||||||
|
- 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 |`.
|
||||||
|
|
||||||
|
## 5. Révocabilité
|
||||||
|
|
||||||
|
| Événement | Action |
|
||||||
|
|---|---|
|
||||||
|
| Détenteur injoignable/decédé | retirer son accès, remplacer l'enveloppe, journaliser |
|
||||||
|
| Perte d'une enveloppe | **changer le mot de passe maître** + régénérer l'`ADMIN_TOKEN`, refaire les enveloppes |
|
||||||
|
| Doute sur la confidentialité | idem : rotation complète (mot de passe maître + ADMIN_TOKEN + clés restic) |
|
||||||
+36
-2
@@ -28,7 +28,7 @@ xpenology → G9, phase par phase, quand le G9 sera branché (IP temporaire `.24
|
|||||||
|-----------|---------------------|
|
|-----------|---------------------|
|
||||||
| G1 | Scripts idempotents, en-têtes documentant chaque modification. |
|
| G1 | Scripts idempotents, en-têtes documentant chaque modification. |
|
||||||
| G2 | `06-restic-backup.sh` : backup + **test de restauration** avant toute migration. |
|
| G2 | `06-restic-backup.sh` : backup + **test de restauration** avant toute migration. |
|
||||||
| G3 | `06b-usb-media.sh`, `08-decommission.sh` : drapeau `--i-understand` + saisie du nom du disque / `WIPE XPENOLOGY`. |
|
| G3 | `06b-usb-media.sh` : drapeau `--i-understand` + saisie du nom du disque. `08-decommission.sh` : saisie `ETEINDRE XPENOLOGY` — **aucun wipe exécuté par le kit** (décision 12/10 : débrancher, disques intacts, récupération `RESTORE-NAS-USB.md`). |
|
||||||
| G4 | `07a`–`07d` (+ `07-migrate-stateful.sh` générique) : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
|
| G4 | `07a`–`07d` (+ `07-migrate-stateful.sh` générique) : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
|
||||||
| G5 | README (ordre d'exécution) + `08-decommission.sh` : IP `.24`→`.25` uniquement au décommissionnement. |
|
| G5 | README (ordre d'exécution) + `08-decommission.sh` : IP `.24`→`.25` uniquement au décommissionnement. |
|
||||||
| G6 | `01-hardening.sh` : ufw LAN-only (`192.168.1.0/24`), pas d'exposition WAN. |
|
| G6 | `01-hardening.sh` : ufw LAN-only (`192.168.1.0/24`), pas d'exposition WAN. |
|
||||||
@@ -36,6 +36,7 @@ xpenology → G9, phase par phase, quand le G9 sera branché (IP temporaire `.24
|
|||||||
| G8 | `journal()` de chaque script → `journal.md` (6 colonnes : date, phase, action, raison, résultat, rollback). |
|
| G8 | `journal()` de chaque script → `journal.md` (6 colonnes : date, phase, action, raison, résultat, rollback). |
|
||||||
| G9 | `.gitignore` (`*.env`, `secrets/`, configs remplis) + `03b-secrets.sh` (chmod 600, non loggué). |
|
| G9 | `.gitignore` (`*.env`, `secrets/`, configs remplis) + `03b-secrets.sh` (chmod 600, non loggué). |
|
||||||
| G10 | Scripts ne modifient que le G9 ; xpenology en lecture seule (SSH `Master@192.168.1.25:222`, clé SSH — compte `effi` bloqué par Synology 10/2026). |
|
| G10 | Scripts ne modifient que le G9 ; xpenology en lecture seule (SSH `Master@192.168.1.25:222`, clé SSH — compte `effi` bloqué par Synology 10/2026). |
|
||||||
|
| G11 | **Autonomie de l'assistante IA par paliers** (décision 12/10) : **L0** lecture seule → **L1** propose, l'humain exécute → **L2** exécute les actions **stateless documentées** des `RUNBOOKS/` → **L3** actions **stateful** uniquement sur **GO explicite** de l'utilisateur. Portes : L1→L2 = 20 propositions journalisées dont **≥ 90 % pertinentes sur 30 jours** ; L2→L3 = sinistre simulé sur une brique stateful **sans perte de données** + rollback vérifié. Interdits permanents : opérations destructives (G3), suppression de données, modification réseau impactant d'autres appareils (G6), wipe, création de compte ou de privilège, lecture de secrets hors coffre. Traçabilité : chaque action de l'IA = ligne de journal (quoi, pourquoi, résultat, rollback) + notification dans le salon ops. **Aucune exécution L2/L3 sans validation utilisateur tracée.** |
|
||||||
|
|
||||||
## Arborescence
|
## Arborescence
|
||||||
|
|
||||||
@@ -63,7 +64,14 @@ g9-migration/
|
|||||||
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
|
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
|
||||||
│ ├── 09-j5040-dhcp.sh ← E3 : DHCP dnsmasq sur J5040 (pré-bascule bridge)
|
│ ├── 09-j5040-dhcp.sh ← E3 : DHCP dnsmasq sur J5040 (pré-bascule bridge)
|
||||||
│ ├── 10-verify-restore-photo.sh ← critère 3 : preuve restore photo (sha256 vs .25)
|
│ ├── 10-verify-restore-photo.sh ← critère 3 : preuve restore photo (sha256 vs .25)
|
||||||
│ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF]
|
│ ├── 11-registry.sh ← T-A.1/T-A.5 : génère REGISTRE-BRIQUES.md + --check --alert (dérive)
|
||||||
|
│ ├── registry_parse.py ← helper lecture seule de 11-registry.sh (docker ps → lignes + constats)
|
||||||
|
│ ├── registry_inputs.py ← helper : compose du kit + Caddyfile → declarations & endpoints
|
||||||
|
│ ├── 12-probes.sh ← T-C.1/C.2/C.5 : sondes APPLICATIVES + alertes JSON + digest (cron G9)
|
||||||
|
│ ├── g9-notify.py ← poste un payload JSON dans le salon ops (install /usr/local/bin)
|
||||||
|
│ ├── g9-verify-alert.py ← vérifie la conformité des payloads JSON reçus (C-AP2)
|
||||||
|
│ ├── lib-notify.sh ← côté pilot : notifie via g9-notify.py (aucun secret dans le kit)
|
||||||
|
│ └── 08-decommission.sh ← T6.4 : arrêt + débranchement xpenology [0 wipe — disques intacts]
|
||||||
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
|
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
|
||||||
│ ├── vaultwarden/ ← T4.1
|
│ ├── vaultwarden/ ← T4.1
|
||||||
│ ├── nextcloud/ ← T4.2
|
│ ├── nextcloud/ ← T4.2
|
||||||
@@ -80,6 +88,32 @@ g9-migration/
|
|||||||
└── restic.env.example ← template restic (T3.1) — REMPLIR les secrets
|
└── restic.env.example ← template restic (T3.1) — REMPLIR les secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docs d'identité, d'observabilité et d'études (Phase 4.5+)
|
||||||
|
|
||||||
|
| Fichier | Rôle | Nature |
|
||||||
|
|---|---|---|
|
||||||
|
| `REGISTRE-BRIQUES.md` | **Moitié technique** du registre : briques, images épinglées, labels Watchtower, endpoints, état, backup. | **Générée** par `scripts/11-registry.sh` — ne pas éditer |
|
||||||
|
| `REGISTRE-IDENTITES.md` | **Moitié humaine** : quel compte sur quelle brique, type, 2FA, référence d'entrée du coffre, **comment réinitialiser**, responsable. | Versionnée, **0 secret** |
|
||||||
|
| `RUNBOOKS/` | `nextcloud` · `matrix` · `jellyfin` · `photoprism` · `vaultwarden` · `registre` · `digest` : symptôme → diagnostic → action → rollback | matière première de l'assistante IA (G11) |
|
||||||
|
| `PARTAGE-SECRET-MAITRE.md` | Qui peut déverrouiller le coffre si je suis injoignable : détenteurs, contenu de l'enveloppe, procédure, **test annuel** | 0 secret |
|
||||||
|
| `RESTORE-NAS-USB.md` | Lire les disques DSM hors boîtier (mdadm/LVM, **lecture seule**), procédure + contrôles + interdits | test = critère Phase 6 |
|
||||||
|
| `EXPOSITION-INTERNET.md` | Étude : exposition mesurée, grille par brique, lien avec l'IdP, playbook classé | **0 règle posée** |
|
||||||
|
| `BRIDGE-WHATSAPP.md` | Étude `mautrix-whatsapp` : apports, limites, risque de bannissement, interdits, pilote borné | **0 installation** |
|
||||||
|
|
||||||
|
### Régler le registre et les sondes (une fois, puis ça tourne)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# machine de pilotage — lecture seule sur .24/.27
|
||||||
|
./scripts/11-registry.sh # génère REGISTRE-BRIQUES.md
|
||||||
|
./scripts/11-registry.sh --check --alert # dérive -> payload JSON dans le salon ops (rc=1)
|
||||||
|
|
||||||
|
# G9 (root) — sondes applicatives + digest
|
||||||
|
sudo ./scripts/12-probes.sh --install # /usr/local/bin/g9-probes.sh + cron */15 + digest 08:30
|
||||||
|
sudo ./scripts/12-probes.sh --selftest # provocation d'un KO -> NOTIFY_OK 200
|
||||||
|
sudo ./scripts/g9-verify-alert.py 20 # payloads JSON conformes ? -> VERIFY_OK
|
||||||
|
sudo ./scripts/12-probes.sh --uninstall # rollback complet (cron + script + état)
|
||||||
|
```
|
||||||
|
|
||||||
## Ordre d'exécution (phase par phase)
|
## Ordre d'exécution (phase par phase)
|
||||||
|
|
||||||
| Phase | Script / action | Garde-fou |
|
| Phase | Script / action | Garde-fou |
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# REGISTRE-BRIQUES — moitié technique (générée)
|
||||||
|
|
||||||
|
> Source : `scripts/11-registry.sh` (`docker ps` + `docker inspect` en **lecture seule** sur .24 et .27,
|
||||||
|
> compose du kit, Caddyfile pour les endpoints). **Ne pas éditer à la main** : la moitié humaine
|
||||||
|
> (comptes, 2FA, procédure de réinitialisation, responsable, break-glass) vit dans
|
||||||
|
> `REGISTRE-IDENTITES.md`.
|
||||||
|
>
|
||||||
|
> Périmètre (T-A.10) : **.24 (G9) et .27 (J5040) uniquement**. Le xpenology **.25 est hors périmètre** :
|
||||||
|
> il est décrit dans `INVENTAIRE-STATEFUL.md` et ses briques sont **à mourir en Phase 6**.
|
||||||
|
>
|
||||||
|
> Couverture backup : g9=restic (sources /data /home/folzo /etc, cron 03:30) ; j5040=non (à couvrir Phase 5)
|
||||||
|
|
||||||
|
<!-- GÉNÉRÉ PAR scripts/11-registry.sh — ne pas éditer à la main. ts=2026-10-12 14:17 UTC ; 21 conteneurs -->
|
||||||
|
|
||||||
|
| Machine | Conteneur | Projet / service (compose) | Image | État | Épinglée | Watchtower | Type | Dans le kit | Endpoint (Caddy) |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| g9 | `collabora` | nextcloud / collabora | `collabora/code:26.04.5.1.1` | running | oui | false | stateful | oui | — |
|
||||||
|
| g9 | `couchdb` | g9-queue / couchdb | `couchdb:3.3` | running | oui | false | stateful | NON | — |
|
||||||
|
| g9 | `edge` | edge / edge | `caddy:2` | running | oui | true | stateless | oui | — |
|
||||||
|
| g9 | `element` | matrix / element | `vectorim/element-web:latest` | running | non (latest) | false | stateful | NON | — |
|
||||||
|
| g9 | `gitea` | gitea / gitea | `gitea/gitea:1.27.3` | running | oui | false | stateful | oui | git.folzocorp.fr |
|
||||||
|
| g9 | `gitea-db` | gitea / gitea-db | `postgres:16` | running | oui | false | stateful | oui | — |
|
||||||
|
| g9 | `jellyfin` | jellyfin / jellyfin | `jellyfin/jellyfin:10.9` | running | oui | false | stateful | oui | media.folzocorp.fr |
|
||||||
|
| g9 | `n8n` | g9-queue / n8n | `docker.n8n.io/n8nio/n8n:latest` | running | non (latest) | false | stateful | NON | n8n.folzocorp.fr |
|
||||||
|
| g9 | `nextcloud` | nextcloud / nextcloud | `nextcloud:35.0.1` | running | oui | false | stateful | oui | cloud.folzocorp.fr |
|
||||||
|
| g9 | `nextcloud_db` | nextcloud / db | `mariadb:10.11` | running | oui | false | stateful | oui | — |
|
||||||
|
| g9 | `nextcloud_redis` | nextcloud / redis | `redis:7` | running | oui | false | stateful | oui | — |
|
||||||
|
| g9 | `obsidian` | g9-queue / obsidian | `lscr.io/linuxserver/obsidian:latest` | running | non (latest) | false | stateful | NON | — |
|
||||||
|
| g9 | `photoprism` | photoprism / photoprism | `photoprism/photoprism:260919` | running | oui | false | stateful | oui | photos.folzocorp.fr |
|
||||||
|
| g9 | `photoprism_db` | photoprism / db | `mariadb:10.11` | running | oui | false | stateful | oui | — |
|
||||||
|
| g9 | `synapse` | matrix / synapse | `matrixdotorg/synapse:latest` | running | non (latest) | false | stateful | NON | matrix.folzocorp.fr |
|
||||||
|
| g9 | `valheim` | compose / valheim | `valheim-pterodactyl:1` | running | oui | false | stateful | NON | — |
|
||||||
|
| g9 | `vaultwarden` | vaultwarden / vaultwarden | `vaultwarden/server:1.37.4` | running | oui | false | stateful | oui | pass.folzocorp.fr |
|
||||||
|
| g9 | `watchtower` | g9-queue / watchtower | `45b54f6dc152` | running | oui (digest) | true | stateless | NON | — |
|
||||||
|
| j5040 | `adguard` | adguard / adguard | `aba9e3bf0613` | running | oui (digest) | true | stateless | oui | — |
|
||||||
|
| j5040 | `caddy` | edge-j5040 / caddy | `502996b5b60a` | running | oui (digest) | true | stateless | oui | — |
|
||||||
|
| j5040 | `sweet_feynman` | - / - | `437a490f7afc` | running | oui (digest) | ABSENT | stateless | NON | — |
|
||||||
|
|
||||||
|
## Constats automatiques (détection de dérive)
|
||||||
|
|
||||||
|
- g9/valheim: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/n8n: image non épinglée (docker.n8n.io/n8nio/n8n:latest) — S11.4 pinning
|
||||||
|
- g9/n8n: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/element: image non épinglée (vectorim/element-web:latest) — S11.4 pinning
|
||||||
|
- g9/element: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/synapse: image non épinglée (matrixdotorg/synapse:latest) — S11.4 pinning
|
||||||
|
- g9/synapse: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/watchtower: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/couchdb: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- g9/obsidian: image non épinglée (lscr.io/linuxserver/obsidian:latest) — S11.4 pinning
|
||||||
|
- g9/obsidian: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
- j5040/sweet_feynman: label watchtower absent — S11.4 sélectivité
|
||||||
|
- j5040/sweet_feynman: conteneur réel non déclaré dans le kit (compose)
|
||||||
|
|
||||||
|
## Utiliser ce fichier
|
||||||
|
|
||||||
|
- `./11-registry.sh` : régénère (à lancer après chaque déploiement / MAJ d'image).
|
||||||
|
- `./11-registry.sh --check --alert` : régénère, compare à la version commitée, **alerte en payload JSON**
|
||||||
|
si le réel a dérivé du registre (label perdu, image non épinglée, conteneur hors kit, état ≠ running).
|
||||||
|
- Un `git diff` sur ce fichier est, par construction, la détection de dérive.
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# REGISTRE-IDENTITES — moitié humaine (comptes, reset, responsables, break-glass)
|
||||||
|
|
||||||
|
> **Règle** : ce fichier ne contient **aucun secret**. Il dit **quel compte** sert sur quelle brique,
|
||||||
|
> **qui en est responsable**, **où vit le secret** (référence d'entrée du coffre) et **comment on réinitialise**.
|
||||||
|
> Les secrets vivent dans **Vaultwarden** (`db.sqlite3` du G9, déjà dans les snapshots restic).
|
||||||
|
> La moitié technique (briques, images épinglées, labels Watchtower, endpoints, état) est **générée**
|
||||||
|
> par `scripts/11-registry.sh` dans `REGISTRE-BRIQUES.md` — ne pas la dupliquer ici.
|
||||||
|
>
|
||||||
|
> Périmètre (T-A.10) : **.24 (G9)** et **.27 (J5040)**. Le xpenology **.25 est hors périmètre** :
|
||||||
|
> inventaire dans `INVENTAIRE-STATEFUL.md`, briques **à mourir en Phase 6**.
|
||||||
|
>
|
||||||
|
> Source des faits : `journal.md` (lignes Phase 4, 2026-10-11) + `PHASE4-RECOLLEMENT.md` §0.
|
||||||
|
|
||||||
|
## 1. Convention du coffre (T-A.3)
|
||||||
|
|
||||||
|
| Portée | Chemat d'entrée Vaultwarden | Exemples |
|
||||||
|
|---|---|---|
|
||||||
|
| Infra / services | `infra/<brique>/<compte>` | `infra/gitea/g9ops`, `infra/n8n/api`, `infra/synapse/admin` |
|
||||||
|
| Adultes | `famille/<pseudo>/<brique>` | `famille/effi/nextcloud`, `famille/effi/matrix` |
|
||||||
|
| Enfants | `enfants/<pseudo>/<brique>` (coffre des parents) | `enfants/lea/jellyfin` |
|
||||||
|
|
||||||
|
Structure du coffre (décision 12/10, coffre **unique** Vaultwarden pour les 6) :
|
||||||
|
- organisation `Famille` = 2 adultes (secrets partagés : wifi, DAV, comptes communs) ;
|
||||||
|
- organisation `Enfants` = 16 et 13 ans : coffre propre + **emergency access** vers les parents ;
|
||||||
|
- 10 et 9 ans : **pas de coffre**, identifiants tenus par les parents sous `enfants/<pseudo>/` ;
|
||||||
|
- `ADMIN_TOKEN` (console Vaultwarden) : consigné + copie hors-ligne (voir `PARTAGE-SECRET-MAITRE.md`).
|
||||||
|
|
||||||
|
Comptes d'automatisation : préfixe de rôle, jamais un prénom humain → `ops-bot` (Matrix),
|
||||||
|
`g9ops` (Gitea/Jellyfin), `n8n-bot` (à créer si des workflows doivent pousser des logs).
|
||||||
|
|
||||||
|
## 2. Registre des comptes (.24 G9)
|
||||||
|
|
||||||
|
| Brique | Compte utilisé | Type | 2FA | Entrée coffre | Réinitialisation (break-glass) | Responsable |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **Vaultwarden** | 1 utilisateur (coffre) | admin humain | **TOTP actif** (importé de la source, `twofactor enabled=1`) | `infra/vaultwarden/maitre` | `ADMIN_TOKEN` (argon2id) → console `/admin` ; emergency access famille activée (`EMERGENCY_ACCESS_ALLOWED=true`) | moi |
|
||||||
|
| **Nextcloud** | 8 utilisateurs importés | humains | par utilisateur (à activer) | `famille/<pseudo>/nextcloud` | `docker exec nextcloud php occ user:set-password <uid>` (ou `occ user:resetpassword`) | moi |
|
||||||
|
| **MariaDB** (`nextcloud_db`) | root + utilisateur applicatif | service | — | `infra/nextcloud/db` | `docker exec -it nextcloud_db mariadb -u root -p` (mot de passe dans `secrets/`) | moi |
|
||||||
|
| **Redis** (`nextcloud_redis`) | sans mot de passe (LAN/bridge) | service | — | — | aucun compte ; accès = réseau Docker uniquement | moi |
|
||||||
|
| **Collabora** | mot de passe console (`secrets/collabora.env`, `env_file`) | service | — | `infra/collabora/console` | régénérer `secrets/collabora.env` (600) + restart conteneur | moi |
|
||||||
|
| **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/<uid>`), token admin via login ; déactivation propre = `POST /_synapse/admin/v1/deactivate/<uid>` | 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 |
|
||||||
|
| **Watchtower** (G9) | aucun (socket Docker) | service | — | — | retirer le compose | moi |
|
||||||
|
| **edge** (Caddy G9) | aucun (443, TLS interne) | service | — | — | retirer le compose (démontage prévu après bascule J5040) | moi |
|
||||||
|
|
||||||
|
## 3. Registre des comptes (.27 J5040)
|
||||||
|
|
||||||
|
| Brique | Compte utilisé | Type | 2FA | Entrée coffre | Réinitialisation (break-glass) | Responsable |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **AdGuard Home** | utilisateurs bcrypt repris de la VM Freebox (UI 8081) | admin humain | — | `infra/adguard/ui` | redéfinir le hash bcrypt dans `AdGuardHome.yaml` + restart | moi |
|
||||||
|
| **Caddy OVH** (edge) | aucun ; **clé API OVH** (`secrets/ovh.env`, 600, hors git) | service | — | `infra/ovh/api-key` | recréer une clé dans le manager OVH (droits legacy `/domain/zone/folzocorp.fr/*`), révoquer l'ancienne | moi |
|
||||||
|
| **WireGuard** | clés (config hors git, 600) | service | — | `infra/wireguard/` | régénérer `wg genkey` + redéclarer un peer | moi |
|
||||||
|
| **dnsmasq (DHCP, E3)** | aucun | service | — | — | `dnsmasq --test` puis restart | moi |
|
||||||
|
| **Beszel hub** | utilisateur local Beszel | admin humain | — | `infra/beszel/hub` | `docker exec` + reset dans l'UI ; hub = J5040 (décision C-AP5) | moi |
|
||||||
|
|
||||||
|
## 4. Procédures de récupération (les 4 qui ont déjà fait mal)
|
||||||
|
|
||||||
|
1. **n8n owner perdu** (subi le 10/10) : `docker exec -it n8n sh` → CLI `n8n export:users` n'existe pas ;
|
||||||
|
la récupération passe par Postgres : créer un utilisateur (bcrypt) + le lier au rôle owner,
|
||||||
|
puis **consigner** l'identifiant dans `infra/n8n/owner` et créer une **clé API dédiée** (Settings → n8n API).
|
||||||
|
2. **Gitea `must_change_password`** (subi le 11/10, bloque l'API) : `gitea admin user change-password -u <uid> --password <nouveau>`
|
||||||
|
avec `--must-change-password=false`. **Avant** : lister les remotes qui s'en servent
|
||||||
|
(`git remote -v` sur les clones, `~/.git-credentials`) → journal + rollback.
|
||||||
|
3. **Jellyfin `EnableLocalPassword=0`** (subi le 11/10) : `UPDATE Users SET EnableLocalPassword=1` en SQLite + restart ;
|
||||||
|
API = en-tête **`X-Emby-Authorization`** combiné (sinon 400).
|
||||||
|
4. **Synapse sans admin** : ajouter `registration_shared_secret` dans `homeserver.yaml` (sudo), restart,
|
||||||
|
`docker exec <synapse> register_new_matrix_user -a -u <admin>`, **retirer** le secret, restart.
|
||||||
|
|
||||||
|
## 5. Hygiène des secrets (T-A.5)
|
||||||
|
|
||||||
|
| Constat | Action |
|
||||||
|
|---|---|
|
||||||
|
| `~/.git-credentials` (600) + `credential.helper store` = **token en clair** sur la machine de pilotage | remplacer par un **token dédié** (scopes minima, nommé, révocable) consigné dans `infra/gitea/token-pilot`, helper non persistant en clair (SSH pour `git push`, ou `credential.helper cache`) |
|
||||||
|
| Secrets Docker (`secrets/*.env`, 600/644 root) | restent **hors git** (`.gitignore` : `*.env`, `*.cred`, `*.key`, `repo.key`, `matrix.env`) ; **contrôle** : `git ls-files` = 0 fichier de secret, `git grep` des valeurs = 0 |
|
||||||
|
| Token Matrix `ops-bot` | vit dans `/etc/restic/matrix.env` (600 root) ; jamais dans un script, jamais affiché |
|
||||||
|
| Clé API OVH | `secrets/ovh.env` (600, hors git) ; révocation dans le manager = procédure de rollback |
|
||||||
|
|
||||||
|
## 6. Ce que le registre ne remplace pas
|
||||||
|
|
||||||
|
Le registre dit **qui** et **comment**. Il ne donne **pas** le login unique (c'est la tâche
|
||||||
|
`IDENTITÉ UNIQUE — Authelia`), et ne supprime pas le besoin de coffre (c'est Vaultwarden).
|
||||||
|
Tant que l'IdP n'est pas posé, **chaque brique garde son compte local** : c'est voulu (break-glass).
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# RESTORE-NAS-USB — lire les disques du xpenology hors boîtier (T6.5)
|
||||||
|
|
||||||
|
> Décision 12/10 : le xpenology est **arrêté et débranché, disques conservés intacts**.
|
||||||
|
> Ce document rend cette promesse **exécutable**. Il est écrit avant la Phase 6 ; le **test**
|
||||||
|
> (1 fichier restauré) est un critère de la Phase 6.
|
||||||
|
|
||||||
|
## 1. Ce qu'il faut savoir avant de brancher quoi que ce soit
|
||||||
|
|
||||||
|
| Point | Réalité mesurée (journal/`INVENTAIRE-STATEFUL.md`) |
|
||||||
|
|---|---|
|
||||||
|
| Les volumes DSM ne sont **pas** des partitions lisibles telles quelles | DSM = **md (RAID)** + **LVM** + ext4/btrfs. Un disque seul monté directement = illisible. |
|
||||||
|
| Les partages vus dans la GUI sont des **vues** | Exemple mesuré 11/10 : `/volume1/docker-REC` est une **vue multi-volumes** du partage « docker-REC » ; la vérité terrain est `docker inspect` → `nextcloud_app` → **`/volume2/docker-REC/NextCloud/{data,config}`**, `vaultwarden-server-1` → `/volume1/docker/vaultwarden`. |
|
||||||
|
| Ordre des disques | Un RAID DSM se reconstitue **sans** respecter l'ordre affiché, mais **tous** les disques du même md doivent être présents. |
|
||||||
|
| Chiffrement | Les volumes du NAS ne sont **pas** chiffrés LUKS (le LUKS est la décision G9/backup). |
|
||||||
|
|
||||||
|
## 2. Matériel
|
||||||
|
|
||||||
|
- boîtier/USB-SATA **un disque à la fois** (ou dock multi-baies), **pas** de branchement sur le contrôleur interne ;
|
||||||
|
- machine de lecture = **machine de confiance** (le pilot .11), en **lecture seule** ;
|
||||||
|
- aucun disque du NAS branché sur le G9 en production (risque de montage automatique / `fstab`).
|
||||||
|
|
||||||
|
## 3. Procédure (lecture seule, par disque groupé)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 0) identifier les disques (ne rien monter)
|
||||||
|
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
|
||||||
|
sudo blkid
|
||||||
|
|
||||||
|
# 1) repérer les membres md (meta-data DSM en fin de partition)
|
||||||
|
sudo mdadm --examine /dev/sdX1 /dev/sdY1 /dev/sdZ1 # tous les disques du même volume
|
||||||
|
# -> noter la « Raid UUID » et le nom du md (ex /dev/md2 = volume 1 DSM)
|
||||||
|
|
||||||
|
# 2) assembler le md en lecture seule (mdadm assemble, pas create)
|
||||||
|
sudo mdadm --assemble --force /dev/md-X /dev/sdX1 /dev/sdY1 /dev/sdZ1
|
||||||
|
|
||||||
|
# 3) LVM au-dessus du md
|
||||||
|
sudo pvscan ; sudo vgscan ; sudo lvscan
|
||||||
|
sudo vgchange -ay -f # activation en lecture seule si possible
|
||||||
|
sudo lvscan
|
||||||
|
|
||||||
|
# 4) monter EN LECTURE SEULE uniquement
|
||||||
|
sudo mount -o ro,norecovery /dev/<vg>/<lv> /mnt/nas-ro
|
||||||
|
ls -la /mnt/nas-ro # doit montrer les partages DSM (docker/, photo/, …)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Ce qu'on vérifie (critère de test, Phase 6)
|
||||||
|
|
||||||
|
| Contrôle | Preuve attendue |
|
||||||
|
|---|---|
|
||||||
|
| md assemblé | `mdadm --detail /dev/md-X` = `State : clean`, nb disques = attendu |
|
||||||
|
| LVM actif | `lvs` liste les LV du volume |
|
||||||
|
| Montage RO | `mount | grep nas-ro` contient `ro` |
|
||||||
|
| 1 fichier restauré | un fichier de test (photo + sha256 connu) copié hors du montage → `sha256sum` identique à la référence |
|
||||||
|
| Démontage propre | `umount` + `vgchange -an` + `mdadm --stop` |
|
||||||
|
|
||||||
|
## 5. Interdits (écrits, à respecter)
|
||||||
|
|
||||||
|
- **jamais** `mdadm --create`, `pvcreate`, `vgcreate`, `mkfs`, `fdisk`, `parted -l` en écriture, `dd`, `wipefs` ;
|
||||||
|
- **jamais** de montage en lecture/écriture ;
|
||||||
|
- **jamais** de `fsck` sur un volume du NAS (peut aggraver un souci) ;
|
||||||
|
- si `mdadm --assemble` échoue : **s'arrêter**, journaliser, ne pas insister (les données sont
|
||||||
|
aussi dans les snapshots restic du G9 pour ce qui a été migré).
|
||||||
|
|
||||||
|
## 6. Rollback
|
||||||
|
|
||||||
|
Aucun : la procédure est purement lecture. Le seul état créé = des points de montage et des
|
||||||
|
périphériques md temporaires, tous démontables (`umount`, `vgchange -an`, `mdadm --stop`).
|
||||||
|
|
||||||
|
## 7. Statut
|
||||||
|
|
||||||
|
- [x] procédure écrite (ce fichier)
|
||||||
|
- [ ] **test réalisé** (1 fichier restauré, journalisé en Phase 6) — *non fait à ce jour*
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# RUNBOOK — Digest quotidien & chaîne d'alerte
|
||||||
|
|
||||||
|
> Le digest remplace le bruit : **1 message par jour** (briques OK/KO, versions épinglées, dernier
|
||||||
|
> backup, MAJ Watchtower) au lieu de N alertes. Les alertes immédiates restent réservées aux KO `high`.
|
||||||
|
|
||||||
|
## Chaîne
|
||||||
|
|
||||||
|
```
|
||||||
|
12-probes.sh (cron root G9, */15)
|
||||||
|
└─ KO → payload JSON → g9-notify.py → salon ops (Synapse .24:8008)
|
||||||
|
12-probes.sh --digest (cron root G9, 08:30)
|
||||||
|
└─ 1 message récapitulatif
|
||||||
|
11-registry.sh --check --alert (machine de pilotage)
|
||||||
|
└─ dérive registre/réel → même payload JSON
|
||||||
|
Beszel (hub J5040) ────────────────► métriques + état conteneurs (ne remplace pas les sondes)
|
||||||
|
restic-notify.sh (cron root G9) ───► backup OK/KO (prose, déjà en place)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contrôles
|
||||||
|
|
||||||
|
| Contrôle | Commande (G9, root) | Attendu |
|
||||||
|
|---|---|---|
|
||||||
|
| Passe de sondes | `/usr/local/bin/g9-probes.sh` | `13 OK / 0 KO` |
|
||||||
|
| Provocation non destructive | `/usr/local/bin/g9-probes.sh --selftest` | `NOTIFY_OK 200` |
|
||||||
|
| Conformité des payloads | `/usr/local/bin/g9-verify-alert.py 20` | `payloads JSON conformes: N`, `VERIFY_OK` |
|
||||||
|
| Digest | `/usr/local/bin/g9-probes.sh --digest` | 1 message `source=digest` |
|
||||||
|
| Cron | `sudo crontab -l \| grep -c g9-probes` | `2` |
|
||||||
|
|
||||||
|
## Symptômes → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Plus aucune alerte | `sudo crontab -l`, `/var/lib/g9-probes` | `./12-probes.sh --install` (idempotent) | `--uninstall` |
|
||||||
|
| `NOTIFY_KO HTTPError` | `sudo python3 /usr/local/bin/g9-verify-alert.py 5` (401 → token) | régénérer le token `ops-bot`, réécrire `/etc/restic/matrix.env` (600) | restaurer la copie datée du fichier |
|
||||||
|
| Alerte répétée chaque sonde | seuil anti-bruit (1 h) + sévérité | remonter la sévérité à `medium` si c'est structurel (ex. endpoint LAN-only) | rétrograder |
|
||||||
|
| Digest absent | logs cron, `date`/TZ | relancer `--digest` manuellement | aucun |
|
||||||
|
| G9 éteint (sinistre) | Beszel hub sur **J5040** doit alerter quand même | c'est le critère C-AP5 : le monitoring ne vit pas sur la machine surveillée | — |
|
||||||
|
|
||||||
|
## Ce que le digest ne dit pas (volontairement)
|
||||||
|
|
||||||
|
Pas de métriques fines (Beszel), pas de logs (aller voir `docker logs`), pas de propositions
|
||||||
|
d'action : les actions sont dans les `RUNBOOKS/`, et l'autonomie de l'assistante IA est bornée
|
||||||
|
par **G11** (paliers).
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# RUNBOOK — Jellyfin (+ stack *arr/qBittorrent sur .25 pour l'instant)
|
||||||
|
|
||||||
|
> Brique stateful, image épinglée `jellyfin/jellyfin:10.9`. Média servi par **référence**
|
||||||
|
> (`/mnt/usb-media` → `/media:ro`, **aucune copie**). Port 8096 LAN-only, `media.folzocorp.fr` via Caddy.
|
||||||
|
|
||||||
|
## Sonde
|
||||||
|
|
||||||
|
`GET :8096/System/Info/Public` (200 — `/system/info` normal renvoie 401 sans jeton : ne pas s'en servir).
|
||||||
|
|
||||||
|
## Symptômes → diagnostic → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| « Invalid username or password » | `docker exec jellyfin sqlite3 /config/jellyfin.db "SELECT Username,EnableLocalPassword FROM Users;"` | **piège mesuré du wizard** : `EnableLocalPassword=0` → `UPDATE Users SET EnableLocalPassword=1` + restart du conteneur | `UPDATE Users SET EnableLocalPassword=0` |
|
||||||
|
| API répond 400 « Value cannot be null » | appel sans en-tête | en-tête **combiné** `X-Emby-Authorization: Emby …` obligatoire (mesuré) | aucun |
|
||||||
|
| Médiathèque vide | `GET /Library/VirtualFolders` + `ls /media/films` | supprimer les médiathèques vides doublons (`DELETE /Library/VirtualFolders?name=`) puis recréer sur `/media/…` | recréer la folder |
|
||||||
|
| Média non listé après branchement USB | `mount | grep usb-media`, `06c-postboot-check.sh` | remonter (fstab) puis « Scan média library » | démonter |
|
||||||
|
| Accès WAN KO | `curl -kI https://media.folzocorp.fr/` | côté J5040 : `docker logs caddy` ; vérifier ufw `ALLOW IN 8096 from 192.168.1.0/24` | retirer la règle ajoutée |
|
||||||
|
|
||||||
|
## Comptes
|
||||||
|
|
||||||
|
`g9ops` = opérateur (créé par le wizard). Les comptes enfants (tâche SORTIE DE GOOGLE) :
|
||||||
|
rôles limités + restrictions de contenu, jamais le compte opérateur.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# RUNBOOK — Matrix (Synapse + Element)
|
||||||
|
|
||||||
|
> Brique stateful (base de messages). Image `matrixdotorg/synapse:latest` → **non épinglée**,
|
||||||
|
> signalé par `./11-registry.sh` (à corriger lors de la prochaine MAJ : épingler par digest).
|
||||||
|
> Usage décidé 12/10 : **complément du foyer** (salon famille + salon ops + Effi), pas de remplacement de WhatsApp.
|
||||||
|
|
||||||
|
## Sonde
|
||||||
|
|
||||||
|
`GET :8008/_matrix/client/versions` (200) ; Element `GET :8009/` (200) ; le salon ops reçoit les alertes.
|
||||||
|
|
||||||
|
## Symptômes → diagnostic → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Pas de compte admin | login admin → 403 | ajouter `registration_shared_secret` (sudo, `homeserver.yaml`) → restart → `docker exec <synapse> register_new_matrix_user -a -u <admin>` → **retirer** le secret → restart | retirer la ligne + restart |
|
||||||
|
| Création de compte refusée | `PUT /_synapse/admin/v2/users/@<uid>:192.168.1.24` (token admin) | créer avec `admin:false` ; inscription publique reste **coupée** (voulu) | `POST /_synapse/admin/v1/deactivate/<uid>` (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é) |
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
## Garde-fous
|
||||||
|
|
||||||
|
- Comptes famille : `admin:false`, 2FA sur les comptes admin.
|
||||||
|
- Suppression de compte = **désactivation** (propre), jamais purge destructive sans backup (G2/G3).
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# RUNBOOK — Nextcloud (+ MariaDB, Redis, Collabora)
|
||||||
|
|
||||||
|
> Brique stateful : **exclue de Watchtower**, MAJ manuelle (S11.5). Image épinglée `nextcloud:35.0.1`.
|
||||||
|
> Incident de référence : 06/10/2026 — Watchtower non sélectif → migration de schéma interrompue →
|
||||||
|
> **maintenance mode 503 alors que le conteneur était up** (leçon : `up ≠ utilisable`).
|
||||||
|
|
||||||
|
## Sonde
|
||||||
|
|
||||||
|
`./12-probes.sh` : `GET :8080/index.php/login` (2xx) + `occ status` doit contenir `installed: true`.
|
||||||
|
|
||||||
|
## Symptômes → diagnostic → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 503 / « Maintenance mode » | `docker exec nextcloud php occ status` → `maintenance: true` | backup en place (G2) puis `php occ maintenance:mode --off` ; si schéma inachevé → restaurer la base depuis restic | remettre `maintenance:mode --on` |
|
||||||
|
| Écran blanc / erreurs DB | `docker logs nextcloud --tail 50` ; `docker inspect nextcloud_db` (state) | relancer `nextcloud_db` puis `nextcloud` ; vérifier `secrets/dbpass` | `compose down` + `up -d` depuis le compose versionné |
|
||||||
|
| Fichiers disparus de la liste | `php occ files:scan --all` (compter, 0 erreur) | scan complet (long, pas de suppression de données) | aucun (scan = lecture/écriture du filecache) |
|
||||||
|
| Collabora « office indisponible » | `curl -s http://127.0.0.1:9980/hosting/discovery` (200) ; `php occ config:app:get richdocuments wopi_url` | discovery KO → relancer collabora ; wopi_url KO → `config:app:set richdocuments wopi_url --value=http://127.0.0.1:9980` | `occ config:app:set` avec l'ancienne valeur |
|
||||||
|
| Redirect en http / boucle de login | `curl -kI https://cloud.folzocorp.fr/` + `php occ config:system:get overwritehost` | aligner `g9-double-run.config.php` (`overwritehost`, `overwriteprotocol=https`, `trusted_proxies`) : source de vérité du kit, monté `:ro` | retirer le fichier monté |
|
||||||
|
| Login KO après import | `php occ user:list` (8 users attendus) | reset ciblé : `php occ user:set-password <uid>` | aucun (mot de passe consigné dans le coffre) |
|
||||||
|
|
||||||
|
## MAJ manuelle (procédure S11.5, jamais par Watchtower)
|
||||||
|
|
||||||
|
1. `restic daily` récent vérifié (`restic snapshots`) — **pas de MAJ stateful sans backup**.
|
||||||
|
2. Épingler la version cible dans le compose (pas de `:latest`), commit dans Gitea.
|
||||||
|
3. `docker compose up -d` → `php occ upgrade` → `php occ status` (`installed: true`, `maintenance: false`).
|
||||||
|
4. `./12-probes.sh` + `./11-registry.sh` (le registre doit rester propre).
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# RUNBOOK — PhotoPrism (+ MariaDB `photoprism_db`)
|
||||||
|
|
||||||
|
> Brique stateful, image épinglée `photoprism/photoprism:260919`. Photothèque **copiée** depuis
|
||||||
|
> `/volume1/photo` (433 941 fichiers / 567 Go, manifeste 0 écart, échantillon sha256 200/200) ;
|
||||||
|
> index progressif. Source .25 **jamais modifiée** (G4, lecture seule).
|
||||||
|
|
||||||
|
## Sonde
|
||||||
|
|
||||||
|
`GET :2342/` (200, suivre la redirection). Endpoint prévu : **`prisme.folzocorp.fr`**
|
||||||
|
(`photos.folzocorp.fr` = Synology Photos, temporaire, meurt en Phase 6).
|
||||||
|
|
||||||
|
## Symptômes → diagnostic → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| UI 502 via le domaine | `curl -s http://127.0.0.1:2342/` puis `docker logs caddy` (J5040) | la route Caddy pointe le bon port (2342) ; ajouter/aligner le site `prisme.folzocorp.fr` | retirer la route Caddy |
|
||||||
|
| Index bloqué / RAM | `docker stats --no-stream` (budget S2.4 : ~2-3 Go) | réduire la parallélisation d'indexation, relancer l'index ; ne pas toucher à la copie | aucun |
|
||||||
|
| Photos manquantes dans l'UI | comparer le manifeste : `find /data/photos -type f \| wc -l` vs manifeste source | relancer l'index sur le lot manquant ; **ne jamais supprimer depuis l'UI** (la copie est la copie, la source .25 est la vérité tant que Phase 6 n'est pas validée) | aucun |
|
||||||
|
| Base DB KO | `docker inspect photoprism_db`, logs | `compose up -d` depuis le compose versionné ; si corrompue → restaurer **la base** depuis restic (pas les photos) | restauration restic |
|
||||||
|
| Auto-upload Android ne monte pas | app : statut d'upload + journal | vérifier compte/URL (`https://prisme.folzocorp.fr`), stockage Android, batterie optimisée | aucun |
|
||||||
|
|
||||||
|
## Preuve de restauration
|
||||||
|
|
||||||
|
`./10-verify-restore-photo.sh` (snapshot restic → sha256 comparé à la source .25) — critère 3, déjà prouvé 12/10.
|
||||||
|
|
||||||
|
## Interdits
|
||||||
|
|
||||||
|
Pas de suppression de photos depuis l'UI avant la fin du double-run ; pas de `rm` sur `/data/photos`
|
||||||
|
sans backup vérifié (G2) ; pas de wipe du .25 (décision 12/10 : débrancher, disques intacts).
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# RUNBOOK — Registre & dérive (registre, watchtower, edge)
|
||||||
|
|
||||||
|
> Le registre est **généré** : `./11-registry.sh` (lecture seule : `docker ps` + `docker inspect`
|
||||||
|
> sur .24/.27 + compose du kit + Caddyfile). Un `git diff` sur `REGISTRE-BRIQUES.md` **est** la
|
||||||
|
> détection de dérive.
|
||||||
|
|
||||||
|
## Contrôles
|
||||||
|
|
||||||
|
| Contrôle | Commande | Attendu |
|
||||||
|
|---|---|---|
|
||||||
|
| Régénération | `./11-registry.sh` | rc=0, table complète |
|
||||||
|
| Dérive + alerte | `./11-registry.sh --check --alert` | rc=0 si propre ; rc=1 + `NOTIFY_OK 200` si dérive |
|
||||||
|
| Scope Watchtower | `docker run --rm -v /var/run/docker.sock:ro <watchtower@digest> --label-enable --monitor-only -R` | `scanned=2` (edge + watchtower), les stateful ignorés |
|
||||||
|
| Labels stateful | `docker inspect -f '{{.Name}} {{index .Config.Labels "com.centurylinklabs.watchtower.enable"}}' $(docker ps -q)` | `false` partout sauf edge/watchtower |
|
||||||
|
| Images non épinglées | section « Constats automatiques » du registre | 0 ligne `image non épinglée` |
|
||||||
|
|
||||||
|
## Symptômes → action
|
||||||
|
|
||||||
|
| Constat | Action | Rollback |
|
||||||
|
|---|---|---|
|
||||||
|
| `label watchtower absent` sur un stateful | poser le label via le compose (source de vérité) + `compose up -d` ; conteneurs pilotés hors compose = `docker update --label-add` (indisponible sur docker 24.0.2 DSM → clonage via API socket, procédure journalisée 10/10) | restaurer le compose `.bak` |
|
||||||
|
| `image non épinglée (:latest)` | épingler la version courante dans le compose, commit, `compose up -d` | revenir au compose précédent |
|
||||||
|
| `conteneur réel non déclaré dans le kit` | écrire le compose correspondant (c'est la production : tout dans git) | supprimer le conteneur **après** validation utilisateur (G3) |
|
||||||
|
| `edge` (Caddy G9) déclassé par le J5040 | garder tant que la bascule bridge n'est pas faite ; le retirer ensuite + retirer les règles ufw 443/80 du G9 | `ufw allow 443` + `compose up -d` |
|
||||||
|
|
||||||
|
## Anti-bruit
|
||||||
|
|
||||||
|
Une alerte par brique et par heure maximum (état dans `/var/lib/g9-probes`). Le digest quotidien
|
||||||
|
(08:30) porte l'état complet ; les alertes immédiates sont réservées aux KO `high`.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# RUNBOOK — Vaultwarden (coffre famille, brique la plus critique)
|
||||||
|
|
||||||
|
> Brique stateful, image épinglée `vaultwarden/server:1.37.4`. Coffre = `db.sqlite3` (8.8 Mo,
|
||||||
|
> 953 ciphers, 1 user), **déjà dans les snapshots restic**. 2FA TOTP actif (importé de la source).
|
||||||
|
> Console admin : `ADMIN_TOKEN` **haché argon2id**. `EMERGENCY_ACCESS_ALLOWED=true`, `INVITATIONS_ALLOWED=true`.
|
||||||
|
> **Règle durable** : le coffre ne doit pas dépendre d'une brique qu'il protège → il ne vit pas dans Nextcloud.
|
||||||
|
|
||||||
|
## Sonde
|
||||||
|
|
||||||
|
`GET :8081/alive` (200). WebVault = `GET :8081/` (200).
|
||||||
|
|
||||||
|
## Symptômes → diagnostic → action
|
||||||
|
|
||||||
|
| Symptôme | Diagnostic (lecture seule) | Action | Rollback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/alive` KO | `docker logs vaultwarden --tail 30` | `docker compose up -d` depuis le compose versionné | `compose down` |
|
||||||
|
| Login admin refusé | `POST /admin/register` (token) | token = entrée `infra/vaultwarden/admin-token` ; si perdu : régénérer un hash argon2id (`python3-argon2`, preset bitwarden) dans `secrets/admin` (600) + restart | restaurer `secrets/admin` + restart |
|
||||||
|
| Utilisateur bloqué (2FA) | table `twofactor` (`atype`, `enabled`, `last_used`) | **ne pas réinitialiser le TOTP à l'aveugle** : l'app d'authentification de l'utilisateur continue de marcher tant que la graine n'est pas régénérée | restaurer la ligne `twofactor` depuis backup |
|
||||||
|
| Coffre corrompu | `sqlite3 db.sqlite3 "PRAGMA integrity_check;"` (après **copy** du fichier) | restaurer `db.sqlite3` depuis restic ; **jamais** d'écriture directe sur la base en service | aucun |
|
||||||
|
| Emergency access à activer | UI admin → Emergency | créer la demande parent → enfant (16/13), valider côté enfant | supprimer la demande |
|
||||||
|
| Clients Bitwarden KO (ados) | `curl -kI https://pass.folzocorp.fr/` | DOMAIN conservé = `pass.folzocorp.fr` (les clients existants restent valides) — vérifier la route Caddy | aucun |
|
||||||
|
|
||||||
|
## Interdits
|
||||||
|
|
||||||
|
- Jamais de `rm` sur `/data/docker/vaultwarden` sans backup vérifié (G2) — c'est le coffre des 6.
|
||||||
|
- Jamais le mot de passe maître dans un fichier, un commit, un log (G9).
|
||||||
|
- La récupération en l'absence de l'opérateur passe par `PARTAGE-SECRET-MAITRE.md`, pas par l'admin console.
|
||||||
@@ -124,3 +124,10 @@
|
|||||||
| 2026-10-12 14:17 | 4 | migrate:photoprism : intégration PhotoPrism | copie de 530 Go à vérifier avant indexation | manifeste chemins+tailles identique (433941 fichiers, 567394182751 o) + échantillon sha256 200/200 | re-copie ciblée via tar/ssh |
|
| 2026-10-12 14:17 | 4 | migrate:photoprism : intégration PhotoPrism | copie de 530 Go à vérifier avant indexation | manifeste chemins+tailles identique (433941 fichiers, 567394182751 o) + échantillon sha256 200/200 | re-copie ciblée via tar/ssh |
|
||||||
| 2026-10-12 14:18 | 4 | migrate:photoprism : stack PhotoPrism déployée + indexation lancée | T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9) | image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale | docker compose down + rm -rf /data/docker/photoprism |
|
| 2026-10-12 14:18 | 4 | migrate:photoprism : stack PhotoPrism déployée + indexation lancée | T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9) | image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale | docker compose down + rm -rf /data/docker/photoprism |
|
||||||
| 2026-10-12 15:07 | 4 | test restauration photo depuis restic | Critère 3 : preuve restore du snapshot incluant les photos | snapshot 06a7e3b89ef3e97d5f5c1dc9098496fab8ad79d48f4455eabdde07255143e33b contient 505836 entrées sous /data/docker/photoprism/originals ; restore 1 fichier ; sha256 restauré == source .25 (00cbfed8da81…) ; chiffrement maintenu (clé locale G9) | rm -rf /tmp/restore-photo-test (fait) |
|
| 2026-10-12 15:07 | 4 | test restauration photo depuis restic | Critère 3 : preuve restore du snapshot incluant les photos | snapshot 06a7e3b89ef3e97d5f5c1dc9098496fab8ad79d48f4455eabdde07255143e33b contient 505836 entrées sous /data/docker/photoprism/originals ; restore 1 fichier ; sha256 restauré == source .25 (00cbfed8da81…) ; chiffrement maintenu (clé locale G9) | rm -rf /tmp/restore-photo-test (fait) |
|
||||||
|
| 2026-10-12 14:17 | 4.5 | registre : `11-registry.sh` génère la moitié technique | T-A.1 : inventaire **généré** (pas de saisie manuelle) | `REGISTRE-BRIQUES.md` : **21 conteneurs** (.24+.27), endpoints Caddy mappés, images/labels lus par `docker ps`/`inspect` en **lecture seule** ; **13 constats** réels (4 images `:latest` non épinglées : n8n, element, synapse, obsidian ; 6 conteneurs non déclarés dans le kit : n8n, element, synapse, couchdb, obsidian, valheim ; 1 conteneur inconnu `sweet_feynman` .27 **sans label Watchtower**) | `git rm REGISTRE-BRIQUES.md scripts/11-registry.sh scripts/registry_*.py` (aucun effet sur les briques) |
|
||||||
|
| 2026-10-12 14:27 | 4.5 | sondes applicatives : `12-probes.sh` installé sur le G9 (cron root) | T-C.1/C.2/C.5 : le trou mesuré est `up ≠ utilisable` (5 incidents de la semaine avec conteneur up) | install `/usr/local/bin/g9-probes.sh` + **2 lignes cron** (`*/15` + digest 08:30) ; passe = **13 OK / 0 KO** (nextcloud login + `occ status`, vaultwarden /alive, gitea /api/v1/version, jellyfin /System/Info/Public, photoprism /, collabora discovery, n8n healthz, synapse versions, element, edge TCP 443, cloud WAN 200, AdGuard UI) ; **`--selftest` → `NOTIFY_OK 200`** (KO provoqué sans toucher aucune brique) ; `g9-verify-alert.py 20` → **payloads JSON conformes: 1 / non conformes: 0, VERIFY_OK** | `sudo bash /usr/local/bin/g9-probes.sh --uninstall` (cron retiré, script + état supprimés) |
|
||||||
|
| 2026-10-12 14:40 | 4.5 | docs identité : `REGISTRE-IDENTITES.md` (moitié humaine) | T-A.2/A.3/A.4/A.8/A.9/A.11 : « quel compte a servi ? » | table par brique (.24 + .27) : compte, type, 2FA, **référence** d'entrée coffre, **comment réinitialiser**, responsable + 4 procédures de récupération (n8n owner, Gitea must_change_password, Jellyfin EnableLocalPassword, Synapse sans admin) + convention coffre `infra/`/`famille/`/`enfants/` + hygiène (`~/.git-credentials` en clair traité en T-A.5) ; **0 secret** (références seules) | `git rm REGISTRE-IDENTITES.md` |
|
||||||
|
| 2026-10-12 14:45 | 4.5 | docs : `RUNBOOKS/` ×7, `PARTAGE-SECRET-MAITRE.md`, `RESTORE-NAS-USB.md` | T-C.3 (runbooks = matière première de l'IA, G11) + T-A.9 + T6.5 | 7 runbooks (nextcloud, matrix, jellyfin, photoprism, vaultwarden, registre, digest) sur les **pièges mesurés** du journal ; procédure de partage du secret maître (détenteurs à nommer, enveloppe, test annuel — **test non réalisé**) ; procédure lecture disques DSM hors boîtier (mdadm/LVM, **lecture seule**, interdits — **test non réalisé**, critère Phase 6) | `git rm -r RUNBOOKS PARTAGE-SECRET-MAITRE.md RESTORE-NAS-USB.md` |
|
||||||
|
| 2026-10-12 14:50 | 4.5 | études : `EXPOSITION-INTERNET.md` + `BRIDGE-WHATSAPP.md` | T-D (étude, 0 règle) + T-W (étude, 0 install) | exposition **mesurée** (`ufw status numbered` .24/.27, `ss -ltnu` .27) : constats — **22/tcp et 443/tcp ouverts au WAN sur le G9 sans nécessité**, edge G9 en TLS auto-signé, Tailscale actif sur le pilot (voie non documentée), CouchDB 5984 LAN sans auth ; grille par brique + playbook classé 8 rangs + **5 décisions à rendre** ; **0 règle posée**. Pont WhatsApp : AGPL-3.0, session QR/12 jours, **risque de bannissement** → numéro familial interdit, **0 installation** | `git rm EXPOSITION-INTERNET.md BRIDGE-WHATSAPP.md` (aucun changement réseau) |
|
||||||
|
| 2026-10-12 14:55 | 4.5 | `08-decommission.sh` : **texte aligné** sur son comportement | T-A.6 : l'en-tête annonçait « ARRÊTE et WIPE » + saisie `WIPE XPENOLOGY` alors que le corps fait arrêt + consignes GUI/débranchement (wipe optionnel) | en-tête = « aucun wipe exécuté par le kit », prereq = « éteint et débranché, disques intacts, 0 effacement », confirmation = `ETEINDRE XPENOLOGY`, étape 3 = étiqueter/conserver + pointeur `RESTORE-NAS-USB.md`, journal rollback = « rebrancher (données intactes) ». **Corps du script inchangé** (mêmes commandes, même gates) | `git checkout scripts/08-decommission.sh` |
|
||||||
|
| 2026-10-12 15:00 | 4.5 | charte **G11** + docs `DOSSIER-SOFTWARE.md` v1.6 | G11 (paliers IA) + S8/S9.2/S11/S14 alignés sur les décisions 12/10 | README : G11 (L0→L3, portes chiffrées, interdits, traçabilité) + arborescence (11/12/g9-notify/g9-verify-alert/lib-notify) + docs d'identité/observabilité ; S8 = **complément** (plus « remplace WhatsApp »), S9.2 = `pass`/`prisme`/`photos` alignés, S11 = registre + sondes, S14.7→S14.10 (coffre, identité, suivi, IA) | `git checkout DOSSIER-SOFTWARE.md g9-migration/README.md` |
|
||||||
|
|||||||
@@ -4,12 +4,16 @@
|
|||||||
#
|
#
|
||||||
# Garde-fous : G3 (destructif — validation explicite), G4 (double-run terminé), G5 (IP .25), G7 (rollback), G8 (journalisation).
|
# Garde-fous : G3 (destructif — validation explicite), G4 (double-run terminé), G5 (IP .25), G7 (rollback), G8 (journalisation).
|
||||||
#
|
#
|
||||||
# ⚠️ OPÉRATION DESTRUCTIVE ET IRRÉVERSIBLE (garde-fous G3 + G4) :
|
# ⚠️ OPÉRATION IRRÉVERSIBLE CÔTÉ RÉSEAU (garde-fous G3 + G4) :
|
||||||
# Ce script ARRÊTE et WIPE le xpenology. Il ne s'exécute QUE si :
|
# Ce script ARRÊTE le xpenology (arrêt des conteneurs + consignes d'arrêt GUI + débranchement).
|
||||||
|
# **AUCUN WIPE n'est exécuté par ce script** — décision utilisateur 12/10 : disques conservés
|
||||||
|
# intacts, récupération documentée dans `RESTORE-NAS-USB.md`. Le wipe physique reste une
|
||||||
|
# décision séparée (réutilisation/vente), hors périmètre du kit.
|
||||||
|
# Il ne s'exécute QUE si :
|
||||||
# 1. Toutes les briques sont validées sur le G9 (double-run terminé, G4).
|
# 1. Toutes les briques sont validées sur le G9 (double-run terminé, G4).
|
||||||
# 2. Le backup 3-2-1 est fonctionnel + restauration testée (G2).
|
# 2. Le backup 3-2-1 est fonctionnel + restauration testée (G2).
|
||||||
# 3. L'IP du G9 est passée à .25 (G5).
|
# 3. L'IP du G9 est passée à .25 (G5).
|
||||||
# 4. Validation explicite de l'utilisateur (drapeau + saisie du mot de passe).
|
# 4. Validation explicite de l'utilisateur (drapeau + saisie de confirmation).
|
||||||
#
|
#
|
||||||
# Usage :
|
# Usage :
|
||||||
# sudo ./08-decommission.sh --i-understand
|
# sudo ./08-decommission.sh --i-understand
|
||||||
@@ -39,26 +43,28 @@ prereq "Toutes les briques (Vaultwarden, Nextcloud, PhotoPrism, Gitea, n8n, Jell
|
|||||||
prereq "Backup 3-2-1 fonctionnel + restauration testée (données IA incluses)"
|
prereq "Backup 3-2-1 fonctionnel + restauration testée (données IA incluses)"
|
||||||
prereq "IP du G9 passée à .25, tous les services résolus et accessibles"
|
prereq "IP du G9 passée à .25, tous les services résolus et accessibles"
|
||||||
prereq "Sauvegarde FINALE complète du xpenology effectuée et vérifiée"
|
prereq "Sauvegarde FINALE complète du xpenology effectuée et vérifiée"
|
||||||
prereq "Je comprends que le xpenology sera WIPÉ (données détruites)"
|
prereq "Je comprends que le xpenology sera ÉTEINT et DÉBRANCHÉ, disques conservés intacts (0 effacement)"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
read -r -p "Saisir 'WIPE XPENOLOGY' pour confirmer la destruction : " CONF
|
read -r -p "Saisir 'ETEINDRE XPENOLOGY' pour confirmer l'arrêt et le débranchement : " CONF
|
||||||
[[ "$CONF" == "WIPE XPENOLOGY" ]] || die "Confirmation non saisie. Abandon."
|
[[ "$CONF" == "ETEINDRE XPENOLOGY" ]] || die "Confirmation non saisie. Abandon."
|
||||||
|
|
||||||
log "Arrêt des conteneurs + arrêt du système xpenology"
|
log "Arrêt des conteneurs + arrêt du système xpenology"
|
||||||
# L'arrêt propre se fait de préférence via la GUI DSM (arrêt système).
|
# L'arrêt propre se fait de préférence via la GUI DSM (arrêt système).
|
||||||
# En SSH, on arrête les conteneurs puis le système :
|
# En SSH, on arrête les conteneurs puis le système :
|
||||||
ssh -p 222 Master@"$XP_HOST" "sudo /var/packages/ContainerManager/target/usr/bin/docker stop \$(/var/packages/ContainerManager/target/usr/bin/docker ps -q) 2>/dev/null || true"
|
ssh -p 222 Master@"$XP_HOST" "sudo /var/packages/ContainerManager/target/usr/bin/docker stop \$(/var/packages/ContainerManager/target/usr/bin/docker ps -q) 2>/dev/null || true"
|
||||||
journal "Conteneurs xpenology arrêtés" "Arrêt propre avant wipe (G3)" "docker stop sur tous les conteneurs" "redémarrage du xpenology"
|
journal "Conteneurs xpenology arrêtés" "Arrêt propre avant débranchement (G3)" "docker stop sur tous les conteneurs" "redémarrage du xpenology"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "⚠️ ÉTAPE MANUELLE OBLIGATOIRE :"
|
echo "⚠️ ÉTAPE MANUELLE OBLIGATOIRE :"
|
||||||
echo " 1. Dans la GUI DSM : arrêter proprement le système."
|
echo " 1. Dans la GUI DSM : arrêter proprement le système."
|
||||||
echo " 2. Débrancher le xpenology."
|
echo " 2. Débrancher le xpenology."
|
||||||
echo " 3. (Optionnel) Wipe physique des disques si réutilisation/vente."
|
echo " 3. Étiqueter les disques et les CONSERVER INTACTS (0 effacement — décision 12/10)."
|
||||||
|
echo " 4. Récupération ultérieure possible : procédure testée dans RESTORE-NAS-USB.md."
|
||||||
|
echo " (le wipe physique n'est PAS dans le périmètre du kit ; réutilisation/vente = décision séparée)"
|
||||||
echo
|
echo
|
||||||
read -r -p "Une fois le xpenology éteint et débranché, saisir 'DONE' pour journaliser : " FINAL
|
read -r -p "Une fois le xpenology éteint et débranché, saisir 'DONE' pour journaliser : " FINAL
|
||||||
[[ "$FINAL" == "DONE" ]] || die "Non confirmé."
|
[[ "$FINAL" == "DONE" ]] || die "Non confirmé."
|
||||||
|
|
||||||
journal "xpenology décommissionné" "G9 = seul cœur famille (G4/G5)" "éteint + débranché, G9 sur .25" "aucun (irréversible) — backup 3-2-1 testé au préalable"
|
journal "xpenology éteint et débranché (disques intacts)" "G9 = seul cœur famille (G4/G5)" "éteint + débranché, G9 sur .25, 0 effacement" "rebrancher le xpenology (données intactes) ; backup 3-2-1 testé au préalable"
|
||||||
log "Terminé. Le G9 est maintenant le seul cœur famille."
|
log "Terminé. Le G9 est maintenant le seul cœur famille."
|
||||||
|
|||||||
Executable
+140
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# 11-registry.sh — T-A.1 + T-A.5 : moitié technique du registre, GÉNÉRÉE, + détection de dérive.
|
||||||
|
#
|
||||||
|
# Lecture seule : `docker ps` / `docker inspect` distants, lecture des compose du kit.
|
||||||
|
# Aucun conteneur n'est démarré, arrêté, modifié. Aucun secret n'est lu ni affiché.
|
||||||
|
#
|
||||||
|
# Modes :
|
||||||
|
# ./11-registry.sh génère REGISTRE-BRIQUES.md
|
||||||
|
# ./11-registry.sh --check régénère en mémoire, compare, alerte si dérive (rc=1)
|
||||||
|
# ./11-registry.sh --check --alert idem + envoie le payload JSON dans le salon ops
|
||||||
|
#
|
||||||
|
# Sortie : REGISTRE-BRIQUES.md (moitié A, générée — ne pas éditer à la main)
|
||||||
|
# La moitié humaine (comptes, reset, responsables) est dans REGISTRE-IDENTITES.md.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
KIT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
OUT="$KIT/REGISTRE-BRIQUES.md"
|
||||||
|
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
|
||||||
|
HOSTS="${HOSTS:-g9:folzo@192.168.1.24 j5040:folzo@192.168.1.27}"
|
||||||
|
RESTIC_COVERAGE="${RESTIC_COVERAGE:-g9=restic (sources /data /home/folzo /etc, cron 03:30) ; j5040=non (à couvrir Phase 5)}"
|
||||||
|
MODE="generate"
|
||||||
|
ALERT=0
|
||||||
|
for a in "$@"; do
|
||||||
|
if [[ "$a" == "--check" ]]; then MODE="check"; fi
|
||||||
|
if [[ "$a" == "--alert" ]]; then ALERT=1; fi
|
||||||
|
done
|
||||||
|
export SSH_KEY
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
declared_json="$(python3 "$KIT/scripts/registry_inputs.py" declared)"
|
||||||
|
caddy_json="$(python3 "$KIT/scripts/registry_inputs.py" caddy)"
|
||||||
|
echo "$caddy_json" > "$TMP/caddy.json"
|
||||||
|
|
||||||
|
rows_all="$TMP/rows.jsonl"
|
||||||
|
findings_all="$TMP/findings.txt"
|
||||||
|
: > "$rows_all"
|
||||||
|
: > "$findings_all"
|
||||||
|
|
||||||
|
for entry in $HOSTS; do
|
||||||
|
machine="${entry%%:*}"
|
||||||
|
host="${entry#*:}"
|
||||||
|
mip="${host#*@}"
|
||||||
|
ssh -o ConnectTimeout=8 -o BatchMode=yes -i "$SSH_KEY" "$host" \
|
||||||
|
'docker ps -a --format json' > "$TMP/$machine.ps.json" 2>"$TMP/$machine.err" || {
|
||||||
|
echo "[registry] SSH/docker indisponible sur $machine ($host)" >&2
|
||||||
|
echo "$machine: inaccessible (SSH ou docker)" >> "$findings_all"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ssh -o ConnectTimeout=8 -o BatchMode=yes -i "$SSH_KEY" "$host" \
|
||||||
|
'docker ps --format "{{.Names}}\t{{.Ports}}"' > "$TMP/$machine.ports.tsv" 2>/dev/null || : > "$TMP/$machine.ports.tsv"
|
||||||
|
python3 "$KIT/scripts/registry_inputs.py" endpoints "$TMP/$machine.ports.tsv" "$TMP/caddy.json" "$mip" > "$TMP/$machine.ep.json"
|
||||||
|
python3 "$KIT/scripts/registry_parse.py" "$machine" "$(cat "$TMP/$machine.ep.json")" "$declared_json" \
|
||||||
|
< "$TMP/$machine.ps.json" > "$TMP/$machine.rows.json" 2>"$TMP/$machine.findings"
|
||||||
|
python3 -c 'import json,sys
|
||||||
|
for r in json.load(open(sys.argv[1])):
|
||||||
|
print(json.dumps(r, ensure_ascii=False))' "$TMP/$machine.rows.json" >> "$rows_all"
|
||||||
|
sed -n 's/^#findings# //p' "$TMP/$machine.findings" | python3 -c 'import json,sys
|
||||||
|
for line in sys.stdin:
|
||||||
|
line=line.strip()
|
||||||
|
if line:
|
||||||
|
for f in json.loads(line): print(f)' >> "$findings_all"
|
||||||
|
done
|
||||||
|
|
||||||
|
nb_conteneurs="$(wc -l < "$rows_all" | tr -d ' ')"
|
||||||
|
nb_findings="$(grep -c . "$findings_all" || true)"
|
||||||
|
|
||||||
|
generated="$(python3 - "$rows_all" "$nb_conteneurs" <<'PY'
|
||||||
|
import json, sys, datetime
|
||||||
|
rows = [json.loads(l) for l in open(sys.argv[1]) if l.strip()]
|
||||||
|
ts = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
|
||||||
|
print(f"<!-- GÉNÉRÉ PAR scripts/11-registry.sh — ne pas éditer à la main. ts={ts} ; {sys.argv[2]} conteneurs -->")
|
||||||
|
print()
|
||||||
|
print("| Machine | Conteneur | Projet / service (compose) | Image | État | Épinglée | Watchtower | Type | Dans le kit | Endpoint (Caddy) |")
|
||||||
|
print("|---|---|---|---|---|---|---|---|---|---|")
|
||||||
|
for r in rows:
|
||||||
|
print(f"| {r['machine']} | `{r['name']}` | {r['project']} / {r['service']} | `{r['image']}` | {r['state']} | {r['pinned']} | {r['wt']} | {r['stateful']} | {r['kit']} | {r['endpoint'] or '—'} |")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
render="$(cat <<EOF
|
||||||
|
# REGISTRE-BRIQUES — moitié technique (générée)
|
||||||
|
|
||||||
|
> Source : \`scripts/11-registry.sh\` (\`docker ps\` + \`docker inspect\` en **lecture seule** sur .24 et .27,
|
||||||
|
> compose du kit, Caddyfile pour les endpoints). **Ne pas éditer à la main** : la moitié humaine
|
||||||
|
> (comptes, 2FA, procédure de réinitialisation, responsable, break-glass) vit dans
|
||||||
|
> \`REGISTRE-IDENTITES.md\`.
|
||||||
|
>
|
||||||
|
> Périmètre (T-A.10) : **.24 (G9) et .27 (J5040) uniquement**. Le xpenology **.25 est hors périmètre** :
|
||||||
|
> il est décrit dans \`INVENTAIRE-STATEFUL.md\` et ses briques sont **à mourir en Phase 6**.
|
||||||
|
>
|
||||||
|
> Couverture backup : $RESTIC_COVERAGE
|
||||||
|
|
||||||
|
$generated
|
||||||
|
|
||||||
|
## Constats automatiques (détection de dérive)
|
||||||
|
|
||||||
|
$(if [[ -s "$findings_all" ]]; then sed 's/^/- /' "$findings_all"; else echo "- aucun"; fi)
|
||||||
|
|
||||||
|
## Utiliser ce fichier
|
||||||
|
|
||||||
|
- \`./11-registry.sh\` : régénère (à lancer après chaque déploiement / MAJ d'image).
|
||||||
|
- \`./11-registry.sh --check --alert\` : régénère, compare à la version commitée, **alerte en payload JSON**
|
||||||
|
si le réel a dérivé du registre (label perdu, image non épinglée, conteneur hors kit, état ≠ running).
|
||||||
|
- Un \`git diff\` sur ce fichier est, par construction, la détection de dérive.
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "check" ]]; then
|
||||||
|
if [[ ! -f "$OUT" ]]; then
|
||||||
|
echo "[registry] REGISTRE-BRIQUES.md absent — générer d'abord" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
base="$(git -C "$KIT" show HEAD:g9-migration/REGISTRE-BRIQUES.md 2>/dev/null || git -C "$KIT" show HEAD:REGISTRE-BRIQUES.md 2>/dev/null || true)"
|
||||||
|
body_new="$(printf '%s' "$render" | sed '1,4d')"
|
||||||
|
body_old="$(printf '%s' "$base" | sed '1,4d')"
|
||||||
|
if [[ -z "$body_old" ]]; then
|
||||||
|
echo "[registry] pas de version commitée de comparaison" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
if [[ "$(printf '%s' "$body_new" | md5sum)" == "$(printf '%s' "$body_old" | md5sum)" ]]; then
|
||||||
|
echo "[registry] aucune dérive ($nb_conteneurs conteneurs, $nb_findings constats)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "[registry] DÉRIVE détectée :"
|
||||||
|
printf '%s\n' "$nb_findings constats"
|
||||||
|
if [[ "$ALERT" == "1" ]]; then
|
||||||
|
source "$KIT/scripts/lib-notify.sh"
|
||||||
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
payload="$(python3 -c 'import json,sys; print(json.dumps({"ts":sys.argv[1],"source":"10-registry","brique":"registre","host":"g9","check":"registry-drift","http_code":0,"severite":"warn","message":sys.argv[2],"runbook":"RUNBOOKS/registre.md"},ensure_ascii=False))' "$ts" "$nb_conteneurs conteneurs, $nb_findings constats automatiques, diff registre/réel à lire dans REGISTRE-BRIQUES.md")"
|
||||||
|
notify_json "$payload"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$render" > "$OUT"
|
||||||
|
echo "[registry] $OUT généré : $nb_conteneurs conteneurs, $nb_findings constats automatiques"
|
||||||
Executable
+165
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# 12-probes.sh — T-C.1/T-C.2/T-C.5 : sondes APPLICATIVES + alertes JSON + digest.
|
||||||
|
#
|
||||||
|
# Pourquoi : le trou mesuré est `up ≠ utilisable` (Nextcloud 503 en maintenance, Collabora unhealthy,
|
||||||
|
# Jellyfin auth KO, accès n8n perdu, monde Valheim perdu : le conteneur était up dans tous les cas).
|
||||||
|
# Beszel répond « le conteneur tourne ». Ici on répond « la fonction répond ».
|
||||||
|
#
|
||||||
|
# S'exécute SUR LE G9 (root, cron). Lecture seule : HTTP GET + `occ status`. Aucune écriture,
|
||||||
|
# aucun conteneur démarré/arrêté.
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# ./12-probes.sh une passe (imprime les résultats, alerte si KO)
|
||||||
|
# ./12-probes.sh --quiet une passe sans alerte (usage test)
|
||||||
|
# ./12-probes.sh --digest digest quotidien (1 message, pas N)
|
||||||
|
# ./12-probes.sh --install installe /usr/local/bin/g9-probes.sh + cron (*/15 + digest 08:30)
|
||||||
|
# ./12-probes.sh --uninstall retire le script + les 2 lignes de cron
|
||||||
|
#
|
||||||
|
# Alertes : payload JSON {ts, source, brique, host, check, http_code, severite, message, runbook}
|
||||||
|
# Identifiants Matrix : /etc/restic/matrix.env (600 root, jamais affichés).
|
||||||
|
# Anti-bruit : une alerte par brique et par heure max (état dans /var/lib/g9-probes).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STATE_DIR="${STATE_DIR:-/var/lib/g9-probes}"
|
||||||
|
MATRIX_ENV="${MATRIX_ENV:-/etc/restic/matrix.env}"
|
||||||
|
HOST="$(hostname)"
|
||||||
|
QUIET=0
|
||||||
|
MODE="probe"
|
||||||
|
for a in "$@"; do
|
||||||
|
if [[ "$a" == "--quiet" ]]; then QUIET=1; fi
|
||||||
|
if [[ "$a" == "--digest" ]]; then MODE="digest"; fi
|
||||||
|
if [[ "$a" == "--install" ]]; then MODE="install"; fi
|
||||||
|
if [[ "$a" == "--selftest" ]]; then MODE="selftest"; fi
|
||||||
|
if [[ "$a" == "--uninstall" ]]; then MODE="uninstall"; fi
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
install)
|
||||||
|
install -m 755 "$0" /usr/local/bin/g9-probes.sh
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
current="$(crontab -l 2>/dev/null || true)"
|
||||||
|
printf '%s\n' "$current" | grep -v 'g9-probes.sh' > /tmp/cron.new || true
|
||||||
|
cat >> /tmp/cron.new <<'EOF'
|
||||||
|
*/15 * * * * /usr/local/bin/g9-probes.sh >/dev/null 2>&1
|
||||||
|
30 8 * * * /usr/local/bin/g9-probes.sh --digest >/dev/null 2>&1
|
||||||
|
EOF
|
||||||
|
sed -i '/^$/d' /tmp/cron.new
|
||||||
|
crontab /tmp/cron.new
|
||||||
|
rm -f /tmp/cron.new
|
||||||
|
echo "install OK : /usr/local/bin/g9-probes.sh + 2 lignes cron ($(crontab -l | grep -c 'g9-probes.sh') lignes)"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
uninstall)
|
||||||
|
crontab -l 2>/dev/null | grep -v 'g9-probes.sh' | crontab - || true
|
||||||
|
rm -f /usr/local/bin/g9-probes.sh
|
||||||
|
rm -rf "$STATE_DIR"
|
||||||
|
echo "uninstall OK (cron retiré, script et état supprimés)"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
notify() {
|
||||||
|
local payload="$1"
|
||||||
|
if [[ "$QUIET" == "1" ]]; then return 0; fi
|
||||||
|
REGISTRY_B64="$(printf '%s' "$payload" | base64 -w0)" python3 /usr/local/bin/g9-notify.py
|
||||||
|
}
|
||||||
|
|
||||||
|
alert() {
|
||||||
|
local brique="$1" check="$2" code="$3" sev="$4" msg="$5"
|
||||||
|
local key ts payload
|
||||||
|
key="$(echo "$brique" | tr -c 'a-z0-9' '_')"
|
||||||
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
# anti-bruit : 1 alerte / brique / heure (état séparé des OK)
|
||||||
|
if [[ -f "$STATE_DIR/alert-$key" ]] && [[ $(( $(date +%s) - $(stat -c %Y "$STATE_DIR/alert-$key") )) -lt 3600 ]]; then
|
||||||
|
echo "KO(ignoré, alerte <1h) $brique $check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
touch "$STATE_DIR/alert-$key"
|
||||||
|
payload="$(python3 -c 'import json,sys; print(json.dumps({"ts":sys.argv[1],"source":"12-probes","brique":sys.argv[2],"host":sys.argv[3],"check":sys.argv[4],"http_code":int(sys.argv[5]) if sys.argv[5].isdigit() else 0,"severite":sys.argv[6],"message":sys.argv[7],"runbook":f"RUNBOOKS/{sys.argv[2]}.md"},ensure_ascii=False))' "$ts" "$brique" "$HOST" "$check" "$code" "$sev" "$msg")"
|
||||||
|
echo "KO $brique $check ($code) -> $msg"
|
||||||
|
notify "$payload"
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() { echo "OK $1 $2"; touch "$STATE_DIR/$(echo "$1" | tr -c 'a-z0-9' '_')"; }
|
||||||
|
|
||||||
|
http_probe() {
|
||||||
|
local brique="$1" check="$2" url="$3" expect="$4" sev="${5:-high}"
|
||||||
|
local code
|
||||||
|
code="$(curl -skL -o /dev/null -w '%{http_code}' -m 10 "$url" 2>/dev/null || true)"
|
||||||
|
if [[ -z "$code" ]]; then code="000"; fi
|
||||||
|
if [[ "$code" == "$expect" || ( "$expect" == "2xx" && "$code" =~ ^2 ) ]]; then
|
||||||
|
ok "$brique" "$check ($code)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
alert "$brique" "$check" "$code" "$sev" "attendu $expect sur $url"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
tcp_probe() {
|
||||||
|
local brique="$1" check="$2" host="$3" port="$4"
|
||||||
|
if timeout 5 bash -c "</dev/tcp/$host/$port" 2>/dev/null; then
|
||||||
|
ok "$brique" "$check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
alert "$brique" "$check" 0 high "TCP $host:$port refusé/injoignable"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
occ_probe() {
|
||||||
|
local brique="$1" check="$2" container="$3" grep_for="$4"
|
||||||
|
local out
|
||||||
|
out="$(docker exec "$container" php occ "$check" 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$out" | grep -q "$grep_for"; then
|
||||||
|
ok "$brique" "$check"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
alert "$brique" "$check" 0 "high" "occ $check : « $grep_for » absent ($(printf '%s' "$out" | head -c 120))"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
run_all() {
|
||||||
|
http_probe nextcloud "GET /login" "http://127.0.0.1:8080/index.php/login" 2xx
|
||||||
|
occ_probe nextcloud "status" nextcloud "installed: true"
|
||||||
|
http_probe vaultwarden "GET /alive" "http://127.0.0.1:8081/alive" 200
|
||||||
|
http_probe gitea "GET /api/v1/version" "http://127.0.0.1:3000/api/v1/version" 200
|
||||||
|
http_probe jellyfin "GET /System/Info/Public" "http://127.0.0.1:8096/System/Info/Public" 200
|
||||||
|
http_probe photoprism "GET /" "http://127.0.0.1:2342/" 200
|
||||||
|
http_probe collabora "GET /hosting/discovery" "http://127.0.0.1:9980/hosting/discovery" 200
|
||||||
|
http_probe n8n "GET /healthz" "http://127.0.0.1:5678/healthz" 200
|
||||||
|
http_probe synapse "GET /_matrix/client/versions" "http://127.0.0.1:8008/_matrix/client/versions" 200
|
||||||
|
http_probe element "GET /" "http://127.0.0.1:8009/" 200
|
||||||
|
tcp_probe edge "TCP 443 (caddy G9 à l'écoute)" 127.0.0.1 443
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$MODE" == "selftest" ]]; then
|
||||||
|
# Provocation non destructive : sonde volontairement injoignable → prouve la chaîne
|
||||||
|
# détection → payload JSON → salon ops. Aucune brique n'est touchée.
|
||||||
|
QUIET=0
|
||||||
|
rm -f "$STATE_DIR/alert-selftest"
|
||||||
|
alert selftest "TCP 127.0.0.1:9 (sonde volontairement injoignable)" 0 medium "selftest 12-probes : provocation d'un KO pour prouver la chaîne d'alerte"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
results="$(mktemp)"
|
||||||
|
if [[ "$MODE" == "digest" ]]; then QUIET=1; fi
|
||||||
|
{
|
||||||
|
run_all
|
||||||
|
http_probe edge_wan "GET cloud (TLS J5040)" "https://cloud.folzocorp.fr/" 200 "medium"
|
||||||
|
http_probe adguard "GET UI .27" "http://192.168.1.27:8081/" 2xx "medium"
|
||||||
|
} >"$results" 2>&1 || true
|
||||||
|
cat "$results"
|
||||||
|
nb_ok="$(grep -c '^OK' "$results" || true)"
|
||||||
|
nb_ko="$(grep -c '^KO' "$results" || true)"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "digest" ]]; then
|
||||||
|
summary="$(grep '^KO' "$results" | sed 's/^KO //' | head -8 | tr '\n' '; ')"
|
||||||
|
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
payload="$(python3 -c 'import json,sys; print(json.dumps({"ts":sys.argv[1],"source":"12-probes","brique":"digest","host":sys.argv[2],"check":"daily","http_code":0,"severite":"info","message":f"{sys.argv[3]} OK / {sys.argv[4]} KO — {sys.argv[5]}","runbook":"RUNBOOKS/digest.md"},ensure_ascii=False))' "$ts" "$HOST" "$nb_ok" "$nb_ko" "${summary:-tout OK}")"
|
||||||
|
notify "$payload"
|
||||||
|
fi
|
||||||
|
echo "sonde : $nb_ok OK / $nb_ko KO"
|
||||||
|
rm -f "$results"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""g9-notify.py — poste le payload JSON (base64 dans REGISTRY_B64) dans le salon ops.
|
||||||
|
|
||||||
|
Installé sur le G9 (/usr/local/bin/g9-notify.py). Lit /etc/restic/matrix.env (600 root).
|
||||||
|
N'affiche JAMAIS le token. Pattern = restic-notify.sh (Bearer + room URL-encodé + PUT + txnId).
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ENV_PATH = "/etc/restic/matrix.env"
|
||||||
|
|
||||||
|
|
||||||
|
def load_env():
|
||||||
|
env = {}
|
||||||
|
with open(ENV_PATH) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
env[k] = v.strip().strip('"')
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
env = load_env()
|
||||||
|
raw = os.environ.get("REGISTRY_B64", "")
|
||||||
|
if not raw:
|
||||||
|
print("NOTIFY_KO no-payload")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
payload = json.loads(base64.b64decode(raw).decode())
|
||||||
|
except Exception as e:
|
||||||
|
print("NOTIFY_KO bad-payload", type(e).__name__)
|
||||||
|
return
|
||||||
|
body = {"msgtype": "m.text", "body": json.dumps(payload, ensure_ascii=False)}
|
||||||
|
room = urllib.parse.quote(env["MATRIX_ROOM"], safe="")
|
||||||
|
txn = "openfox-%d" % int(time.time() * 1000)
|
||||||
|
url = f"{env['MATRIX_SERVER']}/_matrix/client/v3/rooms/{room}/send/m.room.message/{txn}"
|
||||||
|
req = urllib.request.Request(
|
||||||
|
url, data=json.dumps(body).encode(), method="PUT",
|
||||||
|
headers={"Authorization": "Bearer " + env["MATRIX_TOKEN"],
|
||||||
|
"Content-Type": "application/json"})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
|
print("NOTIFY_OK", r.status)
|
||||||
|
except Exception as e:
|
||||||
|
print("NOTIFY_KO", type(e).__name__)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""g9-verify-alert.py — vérifie que les dernières alertes du salon ops sont des payloads JSON
|
||||||
|
conformes au contrat C-AP2 {ts, source, brique, host, check, http_code, severite, message, runbook}.
|
||||||
|
|
||||||
|
Lit /etc/restic/matrix.env (600 root). Lecture seule (GET /messages). N'affiche jamais le token.
|
||||||
|
Installé sur le G9 : /usr/local/bin/g9-verify-alert.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ENV_PATH = "/etc/restic/matrix.env"
|
||||||
|
REQUIRED = ["ts", "source", "brique", "host", "check", "http_code", "severite", "message", "runbook"]
|
||||||
|
|
||||||
|
|
||||||
|
def load_env():
|
||||||
|
env = {}
|
||||||
|
with open(ENV_PATH) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
env[k] = v.strip().strip('"')
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
limit = int(sys.argv[1]) if len(sys.argv) > 1 else 15
|
||||||
|
env = load_env()
|
||||||
|
room = urllib.parse.quote(env["MATRIX_ROOM"], safe="")
|
||||||
|
url = f"{env['MATRIX_SERVER']}/_matrix/client/v3/rooms/{room}/messages?dir=b&limit={limit}&filter=%7B%22limit%22%3A{limit}%7D"
|
||||||
|
req = urllib.request.Request(url, headers={"Authorization": "Bearer " + env["MATRIX_TOKEN"]})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
|
data = json.loads(r.read().decode())
|
||||||
|
ok = 0
|
||||||
|
bad = 0
|
||||||
|
seen = []
|
||||||
|
for ev in data.get("chunk", []):
|
||||||
|
if ev.get("type") != "m.room.message":
|
||||||
|
continue
|
||||||
|
body = ev.get("content", {}).get("body", "")
|
||||||
|
try:
|
||||||
|
p = json.loads(body)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if not isinstance(p, dict) or "source" not in p:
|
||||||
|
continue
|
||||||
|
missing = [k for k in REQUIRED if k not in p]
|
||||||
|
if missing:
|
||||||
|
bad += 1
|
||||||
|
print(f"NON CONFORME source={p.get('source')} manquant={','.join(missing)}")
|
||||||
|
else:
|
||||||
|
ok += 1
|
||||||
|
seen.append((p["source"], p["severite"], p["brique"], p["ts"]))
|
||||||
|
print(f"payloads JSON conformes: {ok} | non conformes: {bad}")
|
||||||
|
for s in seen[:8]:
|
||||||
|
print(" ", s[0], s[1], s[2], s[3])
|
||||||
|
print("VERIFY_OK" if ok > 0 else "VERIFY_KO")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# lib-notify.sh — notification Matrix en payload JSON structuré (C-AP2).
|
||||||
|
# Identifiants : /etc/restic/matrix.env sur le G9 (600 root, jamais affichés, jamais copiés ici).
|
||||||
|
# Usage : source lib-notify.sh puis notify_json '<json>' (ou notify_simple <source> <brique> <sev> <msg>)
|
||||||
|
# Pattern d'envoi identique à restic-notify.sh : Bearer + room URL-encodé + PUT avec txnId.
|
||||||
|
|
||||||
|
G9_SSH="${G9_SSH:-folzo@192.168.1.24}"
|
||||||
|
G9_SSH_KEY="${G9_SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
|
||||||
|
|
||||||
|
notify_json() {
|
||||||
|
local payload="$1" b64
|
||||||
|
b64="$(printf '%s' "$payload" | base64 -w0)"
|
||||||
|
ssh -o ConnectTimeout=8 -o BatchMode=yes -i "$G9_SSH_KEY" "$G9_SSH" \
|
||||||
|
"REGISTRY_B64='$b64' sudo -E python3 /usr/local/bin/g9-notify.py" 2>&1 | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_simple() {
|
||||||
|
local source="$1" brique="$2" severity="$3" message="$3"
|
||||||
|
:
|
||||||
|
}
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Helpers for 10-registry.sh — lit le kit (lecture seule) et fournit les entrées du registre.
|
||||||
|
|
||||||
|
declared <kit_dir> -> JSON [{project, service, image}] depuis compose/*/docker-compose.yml
|
||||||
|
caddy <kit_dir> -> JSON {port: sous-domaine} depuis compose/*/Caddyfile
|
||||||
|
endpoints <ports_tsv> <caddy_json> -> JSON {conteneur: sous-domaine}
|
||||||
|
Aucun conteneur n'est touché.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def declared(kit):
|
||||||
|
out = []
|
||||||
|
cdir = os.path.join(kit, "compose")
|
||||||
|
if not os.path.isdir(cdir):
|
||||||
|
return out
|
||||||
|
for project in sorted(os.listdir(cdir)):
|
||||||
|
f = os.path.join(cdir, project, "docker-compose.yml")
|
||||||
|
if not os.path.isfile(f):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
data = yaml.safe_load(open(f)) or {}
|
||||||
|
except Exception:
|
||||||
|
data = {}
|
||||||
|
services = data.get("services") or {}
|
||||||
|
for service, spec in services.items():
|
||||||
|
image = "-"
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
image = spec.get("image", "-") or "-"
|
||||||
|
out.append({"project": project, "service": service, "image": image})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def caddy(kit):
|
||||||
|
"""Liste [{site, ip, port, source}] — clé = (ip, port), donc pas de collision entre machines."""
|
||||||
|
entries = []
|
||||||
|
cdir = os.path.join(kit, "compose")
|
||||||
|
if not os.path.isdir(cdir):
|
||||||
|
return entries
|
||||||
|
for project in sorted(os.listdir(cdir)):
|
||||||
|
cf = os.path.join(cdir, project, "Caddyfile")
|
||||||
|
if not os.path.isfile(cf):
|
||||||
|
continue
|
||||||
|
site = None
|
||||||
|
for line in open(cf):
|
||||||
|
s = line.strip()
|
||||||
|
if s.startswith("#"):
|
||||||
|
continue
|
||||||
|
m = re.match(r"^((?:[a-z0-9*-]+\.[a-z0-9.-]+\s+)*[a-z0-9*-]+\.[a-z0-9.-]+)\s*\{$", s)
|
||||||
|
if m:
|
||||||
|
hosts = m.group(1).split()
|
||||||
|
site = next((h for h in hosts if "folzocorp.fr" in h), None)
|
||||||
|
continue
|
||||||
|
m = re.search(r"reverse_proxy\s+(\S+):(\d+)", s)
|
||||||
|
if m and site:
|
||||||
|
ip, port = m.group(1), m.group(2)
|
||||||
|
if re.match(r"^\d+\.\d+\.\d+\.\d+$", ip):
|
||||||
|
entries.append({"site": site, "ip": ip, "port": port, "source": project})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def endpoints(ports_tsv, caddy_json, machine_ip):
|
||||||
|
entries = json.loads(open(caddy_json).read()) if os.path.isfile(caddy_json) else []
|
||||||
|
cmap = {(e["ip"], e["port"]): e["site"] for e in entries}
|
||||||
|
out = {}
|
||||||
|
for line in open(ports_tsv):
|
||||||
|
parts = line.rstrip("\n").split("\t")
|
||||||
|
if len(parts) < 2:
|
||||||
|
continue
|
||||||
|
name, ports = parts[0], parts[1]
|
||||||
|
found = ""
|
||||||
|
for m in re.finditer(r":(\d+)->", ports):
|
||||||
|
site = cmap.get((machine_ip, m.group(1)))
|
||||||
|
if site:
|
||||||
|
found = site
|
||||||
|
break
|
||||||
|
out[name] = found
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cmd = sys.argv[1]
|
||||||
|
kit = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
if cmd == "declared":
|
||||||
|
print(json.dumps(declared(kit)))
|
||||||
|
elif cmd == "caddy":
|
||||||
|
print(json.dumps(caddy(kit)))
|
||||||
|
elif cmd == "endpoints":
|
||||||
|
print(json.dumps(endpoints(sys.argv[2], sys.argv[3], sys.argv[4])))
|
||||||
|
else:
|
||||||
|
sys.exit("usage: registry_inputs.py declared|caddy|endpoints")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Parse `docker ps -a --format json` (stdin) + kit compose declarations, emit markdown rows.
|
||||||
|
|
||||||
|
Read-only helper for 10-registry.sh. No container is touched.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
STATEFUL_HINTS = ("mariadb", "postgres", "redis", "influx", "couchdb", "db", "vault", "data")
|
||||||
|
LABEL_WT = "com.centurylinklabs.watchtower.enable"
|
||||||
|
LABEL_PROJECT = "com.docker.compose.project"
|
||||||
|
LABEL_SERVICE = "com.docker.compose.service"
|
||||||
|
|
||||||
|
|
||||||
|
def is_digest_id(image):
|
||||||
|
return bool(re.fullmatch(r"[0-9a-f]{12,64}", image))
|
||||||
|
|
||||||
|
|
||||||
|
def kit_matches(kit_image, real_image):
|
||||||
|
"""Le kit épingle par digest : `name@sha256:<hex>` == réel `<hex court>` si préfixe."""
|
||||||
|
if not kit_image or kit_image == "-":
|
||||||
|
return True
|
||||||
|
if kit_image == real_image:
|
||||||
|
return True
|
||||||
|
if "@sha256:" in kit_image and is_digest_id(real_image):
|
||||||
|
digest = kit_image.split("@sha256:", 1)[1]
|
||||||
|
return digest.startswith(real_image)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_stateful(name, image, labels):
|
||||||
|
if labels.get(LABEL_WT) == "false":
|
||||||
|
return True
|
||||||
|
low = f"{name} {image}".lower()
|
||||||
|
return any(h in low for h in STATEFUL_HINTS)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
machine = sys.argv[1]
|
||||||
|
endpoints = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
||||||
|
declared = json.loads(sys.argv[3]) if len(sys.argv) > 3 else []
|
||||||
|
declared_keys = {(d["project"], d["service"]) for d in declared}
|
||||||
|
declared_images = {f'{d["project"]}/{d["service"]}': d["image"] for d in declared}
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
findings = []
|
||||||
|
raw = sys.stdin.read().strip()
|
||||||
|
if not raw:
|
||||||
|
print("[]")
|
||||||
|
print("#findings# []", file=sys.stderr)
|
||||||
|
return
|
||||||
|
for line in raw.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
c = json.loads(line)
|
||||||
|
name = c.get("Names", c.get("Name", "?"))
|
||||||
|
image = c.get("Image", "?")
|
||||||
|
state = c.get("State", "?")
|
||||||
|
labels = {}
|
||||||
|
for kv in str(c.get("Labels", "")).split(","):
|
||||||
|
if "=" in kv:
|
||||||
|
k, v = kv.split("=", 1)
|
||||||
|
labels[k] = v
|
||||||
|
project = labels.get(LABEL_PROJECT, "-")
|
||||||
|
service = labels.get(LABEL_SERVICE, "-")
|
||||||
|
wt = labels.get(LABEL_WT)
|
||||||
|
wt_label = "true" if wt == "true" else ("false" if wt == "false" else "ABSENT")
|
||||||
|
pinned = "non (latest)" if image.endswith(":latest") else "oui"
|
||||||
|
if is_digest_id(image):
|
||||||
|
pinned = "oui (digest)"
|
||||||
|
key = f"{project}/{service}"
|
||||||
|
declared_state = "oui" if (project, service) in declared_keys else "NON"
|
||||||
|
drift_img = ""
|
||||||
|
if declared_state == "oui" and not kit_matches(declared_images.get(key), image):
|
||||||
|
drift_img = f"image kit={declared_images.get(key)} != réel={image}"
|
||||||
|
endpoint = endpoints.get(f"{name}", "")
|
||||||
|
rows.append({
|
||||||
|
"machine": machine,
|
||||||
|
"name": name,
|
||||||
|
"project": project,
|
||||||
|
"service": service,
|
||||||
|
"image": image,
|
||||||
|
"state": state,
|
||||||
|
"pinned": pinned,
|
||||||
|
"wt": wt_label,
|
||||||
|
"stateful": "stateful" if is_stateful(name, image, labels) else "stateless",
|
||||||
|
"kit": declared_state,
|
||||||
|
"endpoint": endpoint,
|
||||||
|
})
|
||||||
|
if pinned.startswith("non"):
|
||||||
|
findings.append(f"{machine}/{name}: image non épinglée ({image}) — S11.4 pinning")
|
||||||
|
if wt_label == "ABSENT":
|
||||||
|
findings.append(f"{machine}/{name}: label watchtower absent — S11.4 sélectivité")
|
||||||
|
if declared_state == "NON":
|
||||||
|
findings.append(f"{machine}/{name}: conteneur réel non déclaré dans le kit (compose)")
|
||||||
|
if drift_img:
|
||||||
|
findings.append(f"{machine}/{name}: {drift_img} — dérive kit/réel")
|
||||||
|
if state not in ("running",):
|
||||||
|
findings.append(f"{machine}/{name}: état {state}")
|
||||||
|
|
||||||
|
rows.sort(key=lambda r: (r["machine"], r["name"]))
|
||||||
|
print(json.dumps(rows, ensure_ascii=False))
|
||||||
|
print("#findings# " + json.dumps(findings, ensure_ascii=False), file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user