phase 4.5: registre identites + runbooks + sondes applicatives (cron G9) + G11 + etudes exposition/whatsapp + alignement 08-decommission et S8/S9.2/S11/S14
This commit is contained in:
+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)
|
||||
|
||||
> **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
|
||||
|
||||
| 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 |
|
||||
| **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 |
|
||||
|
||||
### 8.2 L'usage famille (résilience = usage)
|
||||
|
||||
| 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) |
|
||||
| **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 |
|
||||
|
||||
### 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 |
|
||||
|---------|--------|
|
||||
| **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). |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **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 |
|
||||
|
||||
### 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é | ✅ | ❌ | 🟡 |
|
||||
| **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
|
||||
|
||||
@@ -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**.
|
||||
|
||||
### 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)
|
||||
@@ -867,6 +903,7 @@ Phase A (Fondations logicielles) ──► Phase B (Cœur famille) ──► Pha
|
||||
|
||||
| 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. |
|
||||
| 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). |
|
||||
|
||||
@@ -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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
@@ -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). |
|
||||
| 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). |
|
||||
| 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
|
||||
|
||||
@@ -63,7 +64,14 @@ g9-migration/
|
||||
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
|
||||
│ ├── 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)
|
||||
│ └── 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)
|
||||
│ ├── vaultwarden/ ← T4.1
|
||||
│ ├── nextcloud/ ← T4.2
|
||||
@@ -80,6 +88,32 @@ g9-migration/
|
||||
└── 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)
|
||||
|
||||
| Phase | Script / action | Garde-fou |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# REGISTRE-BRIQUES — moitié technique (générée)
|
||||
|
||||
> Source : `scripts/10-registry.sh` (`docker ps` + `docker inspect` en **lecture seule** sur .24 et .27,
|
||||
> 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`.
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
> Couverture backup : g9=restic (sources /data /home/folzo /etc, cron 03:30) ; j5040=non (à couvrir Phase 5)
|
||||
|
||||
<!-- GÉNÉRÉ PAR scripts/10-registry.sh — ne pas éditer à la main. ts=2026-10-12 14:17 UTC ; 21 conteneurs -->
|
||||
<!-- 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) |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
## Utiliser ce fichier
|
||||
|
||||
- `./10-registry.sh` : régénère (à lancer après chaque déploiement / MAJ d'image).
|
||||
- `./10-registry.sh --check --alert` : régénère, compare à la version commitée, **alerte en payload JSON**
|
||||
- `./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: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 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).
|
||||
#
|
||||
# ⚠️ OPÉRATION DESTRUCTIVE ET IRRÉVERSIBLE (garde-fous G3 + G4) :
|
||||
# Ce script ARRÊTE et WIPE le xpenology. Il ne s'exécute QUE si :
|
||||
# ⚠️ OPÉRATION IRRÉVERSIBLE CÔTÉ RÉSEAU (garde-fous G3 + G4) :
|
||||
# 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).
|
||||
# 2. Le backup 3-2-1 est fonctionnel + restauration testée (G2).
|
||||
# 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 :
|
||||
# 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 "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 "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
|
||||
read -r -p "Saisir 'WIPE XPENOLOGY' pour confirmer la destruction : " CONF
|
||||
[[ "$CONF" == "WIPE XPENOLOGY" ]] || die "Confirmation non saisie. Abandon."
|
||||
read -r -p "Saisir 'ETEINDRE XPENOLOGY' pour confirmer l'arrêt et le débranchement : " CONF
|
||||
[[ "$CONF" == "ETEINDRE XPENOLOGY" ]] || die "Confirmation non saisie. Abandon."
|
||||
|
||||
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).
|
||||
# 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"
|
||||
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 "⚠️ ÉTAPE MANUELLE OBLIGATOIRE :"
|
||||
echo " 1. Dans la GUI DSM : arrêter proprement le système."
|
||||
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
|
||||
read -r -p "Une fois le xpenology éteint et débranché, saisir 'DONE' pour journaliser : " FINAL
|
||||
[[ "$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."
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 10-registry.sh — T-A.1 + T-A.5 : moitié technique du registre, GÉNÉRÉE, + détection de dérive.
|
||||
# 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 :
|
||||
# ./10-registry.sh génère REGISTRE-BRIQUES.md
|
||||
# ./10-registry.sh --check régénère en mémoire, compare, alerte si dérive (rc=1)
|
||||
# ./10-registry.sh --check --alert idem + envoie le payload JSON dans le salon ops
|
||||
# ./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.
|
||||
@@ -23,8 +23,8 @@ RESTIC_COVERAGE="${RESTIC_COVERAGE:-g9=restic (sources /data /home/folzo /etc, c
|
||||
MODE="generate"
|
||||
ALERT=0
|
||||
for a in "$@"; do
|
||||
[[ "$a" == "--check" ]] && MODE="check"
|
||||
[[ "$a" == "--alert" ]] && ALERT=1
|
||||
if [[ "$a" == "--check" ]]; then MODE="check"; fi
|
||||
if [[ "$a" == "--alert" ]]; then ALERT=1; fi
|
||||
done
|
||||
export SSH_KEY
|
||||
|
||||
@@ -72,7 +72,7 @@ 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/10-registry.sh — ne pas éditer à la main. ts={ts} ; {sys.argv[2]} conteneurs -->")
|
||||
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("|---|---|---|---|---|---|---|---|---|---|")
|
||||
@@ -84,7 +84,7 @@ PY
|
||||
render="$(cat <<EOF
|
||||
# REGISTRE-BRIQUES — moitié technique (générée)
|
||||
|
||||
> Source : \`scripts/10-registry.sh\` (\`docker ps\` + \`docker inspect\` en **lecture seule** sur .24 et .27,
|
||||
> 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\`.
|
||||
@@ -102,8 +102,8 @@ $(if [[ -s "$findings_all" ]]; then sed 's/^/- /' "$findings_all"; else echo "-
|
||||
|
||||
## Utiliser ce fichier
|
||||
|
||||
- \`./10-registry.sh\` : régénère (à lancer après chaque déploiement / MAJ d'image).
|
||||
- \`./10-registry.sh --check --alert\` : régénère, compare à la version commitée, **alerte en payload JSON**
|
||||
- \`./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
|
||||
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()
|
||||
@@ -1,45 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib-notify.sh — notification Matrix en payload JSON structuré (C-AP2).
|
||||
# Source de vérité des identifiants : /etc/restic/matrix.env sur le G9 (600 root, jamais affichée).
|
||||
# Usage : source lib-notify.sh puis notify_json '<json>'
|
||||
# Aucun secret n'est affiché : le token est lu côté G9, jamais exporté ici.
|
||||
# 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"
|
||||
local b64
|
||||
local payload="$1" b64
|
||||
b64="$(printf '%s' "$payload" | base64 -w0)"
|
||||
ssh -o ConnectTimeout=8 -o BatchMode=yes -i "$G9_SSH_KEY" "$G9_SSH" \
|
||||
"MATTRIX_B64='$b64' sudo -E python3 - <<'PY'
|
||||
import base64, json, os, urllib.request
|
||||
env = {}
|
||||
for line in open('/etc/restic/matrix.env'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
k, v = line.split('=', 1)
|
||||
env[k] = v.strip().strip('\"')
|
||||
server = env['MATRIX_SERVER']; room = env['MATRIX_ROOM']; token = env['MATRIX_TOKEN']
|
||||
payload = json.loads(base64.b64decode(os.environ['MATTRIX_B64']).decode())
|
||||
body = {'msgtype': 'm.text', 'body': json.dumps(payload, ensure_ascii=False)}
|
||||
req = urllib.request.Request(
|
||||
f'{server}/_matrix/client/v3/rooms/{room}/send/m.room.message/openfox-{int(__import__(\"time\").time()*1000)}',
|
||||
data=json.dumps(body).encode(), method='PUT',
|
||||
headers={'Authorization': '***' + 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__)
|
||||
PY" 2>&1 | tail -1
|
||||
"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="$4"
|
||||
local ts payload
|
||||
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
payload="$(python3 -c 'import json,sys; print(json.dumps({"ts":sys.argv[1],"source":sys.argv[2],"brique":sys.argv[3],"host":sys.argv[4],"check":"notify","http_code":0,"severite":sys.argv[5],"message":sys.argv[6],"runbook":f"RUNBOOKS/{sys.argv[3]}.md"},ensure_ascii=False))' "$ts" "$source" "$brique" "${G9_SSH##*@}" "$severity" "$message")"
|
||||
notify_json "$payload"
|
||||
local source="$1" brique="$2" severity="$3" message="$3"
|
||||
:
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user