Compare commits

..
7 Commits
20 changed files with 1111 additions and 61 deletions
+3
View File
@@ -9,3 +9,6 @@ g9-migration/configs/restic.env
# Sauvegardes générées par les scripts (ex. 01-hardening.sh)
*.bak.*
# Artefacts playwright-cli (snapshots navigateur)
.playwright-cli/
+215
View File
@@ -0,0 +1,215 @@
# Guide Matrix — utilisation et connexion
> **Objet** : rejoindre et administrer le serveur Matrix (Synapse + Element) auto-hébergé.
> **Source** : déploiement documenté dans `g9-migration/journal.md` (Phase 2, 10/10/2026) et `DOSSIER-SOFTWARE.md` S8.
> **Aucun secret dans ce guide** — les identifiants vivent dans **Vaultwarden**, jamais dans le dépôt (`.gitignore` : `matrix.env`, `*.key`, `*.cred`).
---
## 1. État du serveur
| Composant | Machine | URL | Rôle |
|-----------|---------|-----|------|
| **Synapse** (homeserver) | G9 (`.24`) | `http://192.168.1.24:8008` | Serveur Matrix (API + fédération) |
| **Element Web** (client) | G9 (`.24`) | `http://192.168.1.24:8009` | Client web, préconfiguré sur le homeserver local |
| **Salon ops** | — | `!mp7MFx9QbrJ0ARtzw…` | Canal d'alertes (watchtower, restic, n8n) — user `ops-bot` |
| **Cible TLS (Phase 4)** | — | `https://matrix.folzocorp.fr` | Accès sécurisé via Caddy (planifié, non en place) |
- Version Synapse : **1.162.0** ; version Element Web : **1.12.30**.
- `server_name` du homeserver : `192.168.1.24` → les identifiants ont la forme `@pseudo:192.168.1.24`.
- Cible finale documentée : migration Matrix + Jitsi vers le **J5040 (`.1`)** (16 Go) — voir `DOSSIER-SOFTWARE.md` S2.4/S8.
---
## 2. Connexion
### 2.1 Element Web (PC, LAN)
1. Ouvrir **`http://192.168.1.24:8009`**.
2. Le homeserver est **déjà préconfiguré** (`config.json` monté en lecture seule dans le conteneur → `base_url: http://192.168.1.24:8008`). Rien à saisir.
3. Se connecter avec son compte (§3).
> ⚠️ **En `http://IP`, Element Web affiche un rond infini** (contexte non sécurisé) — lire la section **5. Dépannage** avant de diagnostiquer le serveur. Fix recommandé : tunnel `localhost` (§5.2) ou TLS Phase 4.
### 2.2 Element Android (téléphone)
1. Installer **Element** (Play Store / F-Droid).
2. À l'écran de bienvenue : **Modifier le serveur d'accueil** (Changer de serveur).
3. Saisir `http://192.168.1.24:8008` → **Suivant** → **Se connecter**.
4. Nom d'utilisateur = le pseudo **sans le domaine** (ex. `effi`, pas `@effi:192.168.1.24`) + mot de passe.
### 2.3 Element Desktop (PC)
1. Installer Element Desktop (le navigateur n'est pas concerné par la restriction de contexte sécurisé).
2. Écran de connexion → **Modifier le serveur d'accueil** → `http://192.168.1.24:8008`.
### 2.4 Accès depuis l'extérieur (hors LAN)
Matrix n'est **pas exposé** sur Internet (volontairement). Deux voies :
| Voie | Comment |
|------|---------|
| **WireGuard** (J5040 `.1`) | Se connecter au VPN depuis le téléphone/PC portable → le LAN `.1/24` est atteignable → ouvrir `http://192.168.1.24:8009` (Android : saisir directement le homeserver dans l'app) |
| **TLS Phase 4** (à faire) | Caddy + `matrix.folzocorp.fr` → l'app Element utilise `https://matrix.folzocorp.fr`, accessible de façon sécurisée |
---
## 3. Création de compte
> L'**inscription publique est désactivée** (pas de self-registration sur un serveur privé). Les comptes se créent **côté serveur**, en SSH sur le G9 (`.24`).
### 3.1 Prérequis : un compte admin Synapse
Si aucun admin n'existe, en créer un via le conteneur Synapse :
```bash
# sur .24 — trouver le nom du conteneur
docker ps --format '{{.Names}}' | grep -i synapse
# créer un utilisateur admin (le script lit le shared secret du homeserver.yaml)
docker exec -it <conteneur_synapse> register_new_matrix_user \
-c /data/homeserver.yaml -a -u <ADMIN> -p '<MOT_DE_PASSE_ADMIN>'
```
> Si `register_new_matrix_user` refuse (pas de `registration_shared_secret` dans `homeserver.yaml`) : l'ajouter via `sudo` dans `/data/homeserver.yaml` (`registration_shared_secret: <SECRET_ALEATOIRE>`, généré par `openssl rand -hex 32`), **restart du conteneur**, relancer la commande, puis retirer le secret et restart à nouveau. Même pattern que `room_creation: true` (journal 2026-10-09). Stocker `<MOT_DE_PASSE_ADMIN>` dans **Vaultwarden**.
### 3.2 Créer les comptes (famille, bots) via l'API admin
**Étape 1 — obtenir un token admin** (login avec le compte admin) :
```bash
curl -s -X POST 'http://192.168.1.24:8008/_matrix/client/v3/login' \
-H 'Content-Type: application/json' \
-d '{
"auth": {
"type": "m.login.password",
"identifier": { "type": "m.id.user", "user": "<ADMIN>" },
"passphrase": "<MOT_DE_PASSE_ADMIN>"
}
}'
# → réponse JSON : récupérer "access_token" (= <TOKEN_ADMIN>)
```
**Étape 2 — créer l'utilisateur** (API admin v2, `PUT` = enregistrement) :
```bash
curl -s -X PUT 'http://192.168.1.24:8008/_synapse/admin/v2/users/@<PSEUDO>:192.168.1.24' \
-H "Authorization: Bearer <TOKEN_ADMIN>" \
-H 'Content-Type: application/json' \
-d '{
"password": "<MOT_DE_PASSE_UTILISATEUR>",
"display_name": "<Nom affiché>",
"admin": false
}'
# → 201 Created = compte créé
```
- `admin: true` pour un second administrateur, `false` pour la famille/bots.
- Le pseudo est en minuscules, sans espaces ni accents (règle Matrix).
### 3.3 Première connexion + chiffrement (E2E)
1. Ouvrir Element (§2), se connecter avec `@<PSEUDO>:192.168.1.24` (Web/Desktop : pseudo seul suffit).
2. Element demande une **phrase de passe de récupération** (secure backup) → la **noter hors-ligne** (papier), c'est la porte de sortie du chiffrement.
3. Vérifier ses sessions : **Paramètres → Sécurité → Vérification** (QR code entre téléphone et PC) pour valider le chiffrement de bout en bout.
### 3.4 Désinscription / suppression
- **Désactiver** un compte (propre, conserve la trace) :
```bash
curl -s -X POST 'http://192.168.1.24:8008/_synapse/admin/v1/deactivate/<PSEUDO>' \
-H "Authorization: Bearer <TOKEN_ADMIN>" \
-H 'Content-Type: application/json' \
-d '{ "erase": true }'
```
- Vérifier que l'inscription publique reste fermée dans `homeserver.yaml` : `disable_registration: true` (ne pas activer `enable_registration` sans point d'accès protégé).
---
## 4. Usage type (rappel du plan, S8)
| Salon | Contenu |
|-------|---------|
| **Famille** | Messagerie quotidienne (remplace WhatsApp), visio Jitsi intégrée à Element |
| **Ops** | Alertes machine : watchtower (MAJ images), restic (backup G9), n8n — via `ops-bot` |
| **hermes** (prévu) | Salon où la famille parle à Effi (assistant, Phase 3 du second cerveau) |
---
## 5. Dépannage
### 5.1 Le rond infini dans Element Web (cause racine documentée)
**Symptôme** : page noire, spinner infini sur `http://192.168.1.24:8009`. Console (F12) :
```
Browser missing feature: 'crypto'
Browser missing feature: 'securecontext'
Error initialising Matrix session TypeError: window.crypto.randomUUID is not a function
```
**Cause** : Element ≥ 1.12 exige un **contexte sécurisé** (HTTPS, `localhost` ou `file:`). En `http://<IP>`, le navigateur coupe `crypto.randomUUID` → Element n'arrive pas à initialiser la session. **Le serveur n'y est pas pour rien.**
Anodins (à ne pas poursuivre) : `404 config.<host>.json` (config optionnelle par hôte) et `404 /_matrix/client/v1/auth_metadata` (endpoint optionnel).
**Cause secondaire** : un vieux **service worker** peut garder en cache une config obsolète (la config cassée « matrix.org » d'avant le fix du 10/10). Nettoyage : F12 → Application → Service Workers → *Unregister*, puis Storage → *Clear site data*, puis Ctrl+Maj+R.
### 5.2 Workaround sans TLS : tunnel localhost
`localhost` est un contexte sécurisé → Element fonctionne. Depuis le PC :
```bash
ssh -L 8009:192.168.1.24:8009 -L 8008:192.168.1.24:8008 <user>@192.168.1.24
# puis ouvrir http://localhost:8009
```
### 5.3 Fix définitif : TLS (Phase 4)
Caddy + `matrix.folzocorp.fr` (même pattern que `obsidian.folzocorp.fr`, journal Phase 4) → Element Web servi en HTTPS, spinner résolu, accès distant propre.
### 5.4 Commandes de diagnostic (côté serveur, tout doit répondre)
```bash
# 1) API vivante → JSON avec "versions":[...] (200)
curl -s http://192.168.1.24:8008/_matrix/client/versions
# 2) Element servi + config correcte → 200 + base_url = http://192.168.1.24:8008
curl -s http://192.168.1.24:8009/ -o /dev/null -w '%{http_code}\n'
curl -s http://192.168.1.24:8009/config.json
# 3) CORS (sans lui, Element web est muet) → Access-Control-Allow-Origin: *
curl -s -D - -o /dev/null -H "Origin: http://192.168.1.24:8009" \
http://192.168.1.24:8008/_matrix/client/versions | grep -i access-control
# 4) Conteneurs
docker ps --format '{{.Names}}\t{{.Status}}' | grep -Ei 'synapse|element'
```
**Résultats vérifiés le 11/10/2026** : 1) `200` + versions r0.0.1→v1.12 ; 2) `200` + `base_url: http://192.168.1.24:8008` ; 3) `Access-Control-Allow-Origin: *` ; 4) synapse `Synapse/1.162.0` UP. Si ces 4 tests passent et qu'Element tourne en rond → problème 100 % navigateur (§5.1/5.2).
### 5.5 Alertes ops absentes du salon ops
- Vérifier le token `ops-bot` : côté G9, `/etc/restic/matrix.env` (root, `600`) pour restic ; workflow n8n « Watchtower Notification » pour les MAJ d'images.
- Test d'envoi direct :
```bash
curl -s -X POST 'http://192.168.1.24:8008/_matrix/client/v3/rooms/!mp7MFx9QbrJ0ARtzw…/send/m.room.message/test1' \
-H "Authorization: Bearer <TOKEN_OPS_BOT>" \
-H 'Content-Type: application/json' \
-d '{"msgtype":"m.text","body":"test alerte"}'
# → 200 + {"event_id":"$…"}
```
---
## 6. Sécurité (règles du projet)
| Règle | Application Matrix |
|-------|--------------------|
| Secrets hors dépôt | Token admin, mot de passe admin, `matrix.env` → **Vaultwarden** / root only, jamais git |
| Pas d'exposition Internet | Accès LAN ou WireGuard ; TLS Phase 4 pour l'exposition contrôlée |
| 2FA | À activer sur les comptes admin (S9 : 2FA sur tous les services exposés) |
| Comptes famille | `admin: false`, mot de passe unique par personne, phrase de récupération E2E notée hors-ligne |
---
*Guide créé le 11/10/2026 — état vérifié ce jour sur `.24` (Synapse 1.162.0, Element 1.12.30). À réaligner quand Matrix migrera vers le J5040 (`.1`) et que `matrix.folzocorp.fr` (Caddy TLS) sera en place.*
+120
View File
@@ -0,0 +1,120 @@
# Phase 4 — récupération du thread supprimé
> **Ce fichier a deux objets** : §0 = état consolidé de la Phase 4 (briques, versions,
> incidents, .25) ; §1-5 = rapport de récupération du thread de session supprimé (07:20 UTC).
## 0. État consolidé de la Phase 4 (2026-10-11)
### 0.1 Briques déployées sur G9 (.24) et versions épinglées
| Brique | Image épinglée | État | Watchtower |
|---|---|---|---|
| Vaultwarden (T4.1) | `vaultwarden/server:1.37.4` | healthy, coffre importé (953 ciphers, 2FA), ADMIN_TOKEN argon2 | false |
| Nextcloud (T4.2) | `nextcloud:35.0.1` (35.0.1.1 = version source) | healthy, `occ status` installed, 8 users, 1522 fichiers (scan 0 erreur), 67 apps activées = liste source | false |
| — MariaDB | `mariadb:10.11` (utf8mb4_bin, import en une transaction) | up | false |
| — Redis | `redis:7` | up | false |
| — Collabora (remplace OnlyOffice .25) | `collabora/code:26.04.5.1.1` | healthy, discovery 200, wopi_url posé | false |
| Gitea (T4.4) | `gitea/gitea:1.27.3` + `postgres:16` | healthy, 15 repos / 4 users importés, push-create actif | false |
| Jellyfin (T4.5) | `jellyfin/jellyfin:10.9` (10.9.11) | healthy, média par RÉFÉRENCE (`/mnt/usb-media` → `/media:ro`, aucune copie), 1 médiathèque = `/media/films`, Items Movie = 1 | false |
| edge (T4.6) | `caddy:2` (digest `f2a1290d…`) | healthy, HTTPS interne (CA interne), 443 seul port exposé WAN | **true** (stateless) |
| Watchtower (T4.7) | `nickfedor/watchtower@sha256:45b54f6d…` | healthy, scope label-enable : dry-run `scanned=2` (edge + watchtower), 14 stateful ignorés | true |
| PhotoPrism (T4.3) | — | **non déployée** : `07d-migrate-photoprism.sh` écrit/testé, exécution reportée (coût = copie 530 Go ; média servi par Jellyfin) | — |
### 0.2 Incidents rencontrés et correctifs (détail = journal.md)
1. **Collabora TLS** : l'image 26.04 sert **TLS par défaut** (discovery http = 000) ; env `extra_features`/`O:ssl.enable` non appliqués → fix `command: ["--o:ssl.enable=false"]` (CMD appendu à l'entrypoint).
2. **Collabora healthcheck** : image **minimale** (ni `sh` ni `curl`) → `CMD curl` = unhealthy permanent → healthcheck retiré du compose (sonde externe `curl :9980/hosting/discovery`).
3. **Jellyfin auth** : le wizard laisse `Users.EnableLocalPassword=0` → « Invalid username or password » ; fix = UPDATE sqlite + restart (hash PBKDF2-SHA512 vérifié == secret) ; API = en-tête combiné **X-Emby-Authorization** requis (sinon 400 « Value cannot be null »).
4. **Caddy** : snippet `(name)` inutilisable comme adresse (« ambiguous site definition ») → `import` ; `request_id` n'est pas un directive site (validate KO) ; Caddy logue les en-têtes **reçus du client**, pas ceux envoyés en amont.
5. **Nextcloud / edge** : le dépôt d'un second `*.config.php` monté n'a pas été retenu (merge `array_replace_recursive` + config.php déjà porteur des clés) → résolu en écrivant **`g9-double-run.config.php` = source de vérité du kit**, monté `:ro` : `overwritehost=cloud.folzocorp.fr`, `overwriteprotocol=https`, `trusted_proxies=[192.168.1.0/24, 172.16.0.0/12]`, `trusted_domains += cloud.folzocorp.fr`.
6. **Watchtower** : fork nickfedor = `SCHEDULE` XOR `POLL`, pas de `--once` (⇒ `-R`) ; notification alignée `generic+http://n8n:5678/webhook/watchtower` (webhook testé **200** depuis le réseau Docker ; l'edge n'écoute que 443).
### 0.3 Alignement proxy (TRUSTED_PROXIES)
- Nextcloud : `trusted_proxies = [192.168.1.0/24, 172.16.0.0/12]` (bridge Docker de l'edge) + `overwrite*` https/host — **preuve côté app** : redirect `https://cloud.folzocorp.fr/` → `Location: https://cloud.folzocorp.fr/index.php/login`.
- Vaultwarden : `TRUSTED_HOSTS=192.168.1.0/24,172.16.0.0/12` (même réseau de confiance).
- Caddy : `X-Forwarded-Proto/Host/Port/For` posés par défaut sur tous les `reverse_proxy`.
- ufw : **443/tcp = seul port exposé WAN** (+ 80 redirect ACME) ; ports briques (3000/2222/3100/5678/5984/8008/8009/8080/8081/8096/9980) = `ALLOW IN 192.168.1.0/24` + default deny incoming.
### 0.4 État du xpenology .25 (double-run G4, rien de destructif)
- `nextcloud_app`, `nextcloud_db`, `nextcloud_redis`, `vaultwarden-server-1`, `gitea` **relancés et up** après les exports ; dumps/transitaires supprimés sur .25, pilot et G9.
- Aucun partage, aucune donnée, aucun conteneur .25 supprimé ; transports .25→pilot = `ssh 'sudo tar cz'`/`mysqldump` en lecture seule (sshd DSM refuse `rsync --server` et scp).
---
# Rapport de récupération (thread supprimé)
**Session** : `e67f1166-d70c-42cd-a295-79513757233e` (tâche Kanban `8e3e7f073a6806cd` — PHASE 4)
**Suppression** : 2026-10-11 07:20:04 UTC (trace systemd : `Runner error FOREIGN KEY constraint failed, sessionId=e67f1166…`)
**Méthode** : suppression = `DELETE` sur `sessions` + cascade sur `events`. Les pages SQLite libérées gardent leur contenu tant qu'elles ne sont pas réallouées → caravage des pages libres de `sessions.db` (snapshot), reconstruction des enregistrements.
## 1. Ce qui a été récupéré
| Élément | Valeur |
|---|---|
| Snapshot de travail | `snap1` pris à 08:20:34 UTC (1 h après la suppression) |
| Pages contenant l'id de session | 21 232 |
| Texte imprimable ≥ 400 caractères | 34 227 blocs = **47,9 Mo** |
| Objets JSON payload reconstitués | **51 420 = 34,68 Mo** |
| Entrées d'index `events` (session_id, seq / event_type) | 79 830 → 37 821 seq, 42 009 event_type, plage seq 5 → 158 799 |
| Tours de conversation reconstitués | **130** (129 assistant + 3 user côté rôles) |
| Événements réinsérés dans `sessions.db` | **64 657** (33,85 Mo de payload) |
| Appels d'outils avec arguments intacts | 31 998 |
| Résultats / sorties d'outils | 124 / 275 |
## 2. Ce qui est perdu (non inventé)
- **~75 % des événements** : les pages leaf/overflow ont été réallouées et **zérées** par SQLite après la suppression (15 951 pages contenaient encore l'id de session mais avec leur tableau de cellules remis à zéro).
- **La prose du fil** (messages de l'assistant) est récupérée **en fragments dans le désordre** : les deltas de streaming sont stockés dans des pages distinctes, l'ordre d'origine n'a pas pu être reconstitué. Les 130 messages insérés contiennent donc des fragments réels, recollés dans l'ordre des pages (ordre d'allocation ≈ ordre chronologique), pas un texte propre.
- **Presque tous les messages utilisateur** sont perdus (payloads courts, pages détruites) : 3 rôles `user` identifiés, sans contenu.
- Volume attendu ≈ 158 800 événements / 80–110 Mo de payload → **≈ 35 % récupéré**.
## 3. Où sont les artefacts
| Fichier | Contenu |
|---|---|
| `/home/folzo/openfox/_recover/snap1/` | copie octet-pour-octet d'origine (`sessions.db`, `-wal`, `-shm`) |
| `/home/folzo/openfox/_recover/orig.sha256` | sha256 des fichiers d'origine |
| `/home/folzo/openfox/_recover/snap2/` + `snap2.sha256` | état **immédiatement avant** la réinsertion (point de rollback) |
| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON bruts (+ page, offset) |
| `/home/folzo/openfox/_recover/phase4_index.jsonl` | 79 830 entrées d'index (rowid, seq, event_type) |
| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements synthétisé |
| `g9-migration/PHASE4-RECOLLEMENT.md` §5 | journal des actions récupérées |
## 4. Rollback
**Chirurgical** (recommandé, ne touche à rien d'autre) :
```sql
DELETE FROM events WHERE session_id='e67f1166-d70c-42cd-a295-79513757233e';
DELETE FROM sessions WHERE id='e67f1166-d70c-42cd-a295-79513757233e';
```
**Restauration à l'état pré-réinsertion** :
```
systemctl stop openfox
cp /home/folzo/openfox/_recover/snap2/sessions.db{,-wal,-shm} /home/folzo/.local/share/openfox/
sha256sum -c /home/folzo/openfox/_recover/snap2.sha256
systemctl start openfox
```
`PRAGMA integrity_check` = `ok` avant et après réinsertion. Les fichiers d'origine n'ont jamais été modifiés en écriture directe : la réinsertion est passée par SQLite (WAL), et `snap1` reste la copie de l'état post-suppression.
## 5. Journal des actions récupérées
Le journal des 32 409 actions récupérées (appels d'outils + arguments intacts, résultats,
sorties) n'est **pas versionné ici** — il duplique les artefacts et concerne la base de
sessions OpenFox, pas le kit G9. Consultable / rejouable :
| Artefact | Contenu |
|---|---|
| `/home/folzo/openfox/_recover/phase4_actions.md` | journal brut complet (32 409 lignes, page-ordre) |
| `/home/folzo/openfox/_recover/phase4_actions_curated.md` | version filtrée/dédupliquée (758 lignes) |
| `/home/folzo/openfox/_recover/phase4_json.jsonl` | 51 420 payloads JSON (+ page, offset) |
| `/home/folzo/openfox/_recover/phase4_events.jsonl` | flux d'événements réinsérés dans la base |
Reprise du travail Phase 4 : la tâche Kanban `8e3e7f073a6806cd` est toujours bindée à la
session `e67f1166…` et le runner n'a **jamais cessé d'écrire** (journal 09:18 → 10:13,
commits `a1864c6`, scripts 07c/07d) — c'est la source operative, pas le fil recoveré.
+13 -4
View File
@@ -29,7 +29,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`. |
| G4 | `07-migrate-stateful.sh` : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
| G4 | `07a`–`07d` (+ `07-migrate-stateful.sh` générique) : lecture seule du xpenology + intégrité sha256 ; `08-decommission.sh` exige la validation de toutes les briques avant wipe. |
| G5 | README (ordre d'exécution) + `08-decommission.sh` : IP `.24`→`.25` uniquement au décommissionnement. |
| G6 | `01-hardening.sh` : ufw LAN-only (`192.168.1.0/24`), pas d'exposition WAN. |
| G7 | Chaque script : section **Rollback** en en-tête + colonne Rollback du journal. |
@@ -52,7 +52,15 @@ g9-migration/
│ ├── 05-beszel.sh ← T2.5 : agent Beszel
│ ├── 06-restic-backup.sh ← T3.1 : restic G9 → Freebox + test restauration
│ ├── 06b-usb-media.sh ← T6.1 : montage des disques USB média → /mnt/usb-media
│ ├── 07-migrate-stateful.sh← T4.x : export/import des stateful (xpenology → G9)
│ ├── 06c-postboot-check.sh ← contrôles post-reboot (lecture seule, 15+ checks)
│ ├── 07-migrate-stateful.sh← T4.x : générique (rsync) — INOPÉRANT sur le DSM : sshd
│ │ refuse `rsync --server` et scp → utiliser 07a–07d
│ ├── 07a-migrate-gitea.sh ← T4.4 : Gitea + backend Postgres (transport tar sur SSH)
│ ├── 07b-migrate-vaultwarden.sh ← T4.1 : Vaultwarden (coffre + 2FA + ADMIN_TOKEN)
│ ├── 07c-migrate-nextcloud.sh ← T4.2 : Nextcloud + MariaDB + Collabora
│ ├── 07d-migrate-photoprism.sh ← T4.3 : PhotoPrism (copie 530 Go + indexation)
│ ├── 07e-deploy-edge.sh ← T4.6 : edge Caddy (validate → up → ufw 443 → vérifs HTTPS/SNI)
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
│ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF]
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
│ ├── vaultwarden/ ← T4.1
@@ -60,7 +68,8 @@ g9-migration/
│ ├── photoprism/ ← T4.3
│ ├── gitea/ ← T4.4
│ ├── n8n/ ← T4.5
│ ├── jellyfin/ ← T6.2
│ ├── jellyfin/ ← T4.5 (média par référence, /media:ro)
│ ├── edge/ ← T4.6 : Caddy (Caddyfile versionné = config)
│ └── watchtower/ ← S11.4 : Watchtower sélectif
└── configs/
├── fail2ban.local.conf ← config fail2ban (T2.1)
@@ -74,7 +83,7 @@ g9-migration/
| 1 | (xpenology) exclure stateful de Watchtower + inventaire | G4 |
| 2 | `01-hardening.sh` → `02-data.sh` → `03-docker.sh` → `03b-secrets.sh` → `04-smb.sh` → `05-beszel.sh` | — |
| 3 | `06-restic-backup.sh` (backup + test restauration) | G2 |
| 4 | `07-migrate-stateful.sh` (par brique, double-run) — **après** `03b-secrets.sh` | G4 |
| 4 | **par brique, dans l'ordre** — **après** `03b-secrets.sh` : `07b-migrate-vaultwarden.sh` (T4.1) → `07c-migrate-nextcloud.sh` (T4.2) → `07d-migrate-photoprism.sh` (T4.3, **reporté**) → `07a-migrate-gitea.sh` (T4.4) → Jellyfin (T4.5, frais) → `07e-deploy-edge.sh` (T4.6, Caddy TLS interne) → Watchtower (T4.7, scope stateless). `07-migrate-stateful.sh` = script **générique rsync**, inopérant sur le DSM (sshd refuse `rsync --server`/scp) ; transport réel des 07a–07d = **tar sur SSH** | G4 |
| 5 | restic 3-2-1 complet (G9 + PC IA → Freebox → 218play) | G2 |
| 6 | `06b-usb-media.sh` (T6.1) → Jellyfin (T6.2) → IP .24→.25 → `08-decommission.sh` | G3, G4, G5 |
| 7 | (G7) wipe DSM + Ubuntu neuf + boot test | G3, G10 |
+68
View File
@@ -0,0 +1,68 @@
# Caddyfile — T4.6 (edge G9). Reverse proxy LAN + HTTPS interne (CA interne Caddy).
# Sous-domaines = ceux déjà configurés côté apps (vaultwarden DOMAIN, gitea ROOT_URL,
# n8n N8N_HOST, Nextcloud overwritehost) → la bascule T4.6 ne touche aucune brique.
# Résolution LAN : AdGuard Home (J5040, S9.1) ou hosts statiques ; test = `curl -k
# --resolve <host>:443:192.168.1.24 https://<host>/`.
(folzocorp) {
tls internal
encode gzip
log {
output file /data/caddy-access.log {
roll_size 10mb
roll_keep 5
}
format json
}
}
pass.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:8081
}
cloud.folzocorp.fr nextcloud.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
# WOPI : Nextcloud (1.24:8080) ↔ Collabora (1.24:9980) en http LAN (wopi_url en DB).
reverse_proxy 192.168.1.24:8080
}
git.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:3000
}
media.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:8096
}
n8n.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:5678
}
matrix.folzocorp.fr {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:8008
}
# Accès par IP (SNI) : `curl -k https://192.168.1.24` → Nextcloud (défaut LAN).
192.168.1.24 {
import folzocorp
# Vérif T4.6 : `header` echo en réponse les X-Forwarded-* envoyés à la brique
# (mesurable : `curl -kI https://<host>/…`) — Caddy les pose par défaut sur tous les reverse_proxy.
reverse_proxy 192.168.1.24:8080
}
@@ -0,0 +1,33 @@
# edge — T4.6 : reverse proxy LAN + HTTPS interne (Caddy 2)
# Stateless (le Caddyfile est dans git, les certificats dans un volume) → Watchtower ON
# (convention S11.4 : image épinglée = exclue ; ici le pinning est tag + digest consigné).
#
# TLS : `tls internal` = ACME local (CA interne Caddy) — HTTPS interne, sans exposition
# de port 80 ni DNS challenge. Les clients LAN doivent faire confiance au CA interne
# (`caddy trust` ou export du CA) ; curl se teste avec `-k`.
#
# Let's Encrypt (plan S9.1, edge J5040) : remplacer `tls internal` par le DNS challenge
# quand AdGuard Home (J5040) résoudra *.folzocorp.fr en LAN → bascule sans toucher aux briques.
#
# ufw (01-hardening.sh) : 443 = SEUL port exposé WAN ; les ports briques (3000/8080/8081/
# 8096/9980/5678/8008) restent LAN-only (default deny incoming).
services:
edge:
image: caddy:2
# Digest consigné (S11.4) : sha256:f2a1290d0463aad60660d4ec134943f183ee2a5f6c3eb7bf32dd984f2f020772
container_name: edge
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data
- ./config:/config
ports:
- "0.0.0.0:443:443"
- "0.0.0.0:80:80"
healthcheck:
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
interval: 60s
timeout: 10s
retries: 3
@@ -24,3 +24,9 @@ services:
- "0.0.0.0:8096:8096"
devices:
- /dev/dri:/dev/dri # accélération vidéo (si dispo)
# Wizard 10.9 accompli via navigateur (T4.5) : user admin « g9ops » (mot de passe dans
# ./secrets/jellyfin-admin, 644 root:root, hors git G9). PIÈGE mesuré : le wizard laisse
# Users.EnableLocalPassword=0 → AuthenticateByName = « Invalid username or password ».
# Fix : UPDATE Users SET EnableLocalPassword=1 (sqlite /config/data/jellyfin.db) + restart.
# API : POST /Users/AuthenticateByName exige l'en-tête combiné X-Emby-Authorization
# (Emby Username=…, DeviceId=…, Device=…, Client=…, Version=…) + body {"Username","Pw"}.
+34 -16
View File
@@ -1,44 +1,62 @@
# n8n — T4.5 (automatisation + alertes → Matrix, G9)
# Le conteneur n8n est stateless (image) mais sa DB (Postgres) est stateful → exclu.
# Workflows exportables (JSON). Licence fair-code (notée, S3.1).
# n8n — T4.5 (automatisation, G9). Source .25 = n8n **2.42.5** sur Postgres 17
# (db `2025_n8n`, user `2025_n8n_user`, 4 workflows, 4 credentials, 1 user) → vérité terrain.
# Image **épinglée à 2.42.5** : pas de migration de schéma pendant le double-run (S11.4).
# Clé de chiffrement : .n8n/config importé depuis la source (copie du volume `data`) → les
# credentials existants restent déchiffrables. n8n ne connaît pas les *_FILE → env_file.
services:
n8n:
image: n8nio/n8n:1.90
image: docker.n8n.io/n8nio/n8n:2.42.5
container_name: n8n
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true" # image stateless → auto
- "com.centurylinklabs.watchtower.enable=false" # version épinglée (schéma DB) → exclu
env_file:
- ./secrets/n8n.env
environment:
- TZ=Europe/Paris
- N8N_HOST=n8n.folzocorp.fr
- N8N_PROTOCOL=https
- N8N_PORT=5678
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=n8n_db
- DB_POSTGRESDB_DATABASE=n8n
- DB_POSTGRESDB_USER=n8n
- DB_POSTGRESDB_PASSWORD_FILE=/run/secrets/dbpass
- WEBHOOK_URL=https://n8n.folzocorp.fr
- N8N_RELEASE_TYPE=stable
- N8N_RUNNERS_ENABLED=true
- NODE_ENV=production
- GENERIC_TIMEZONE=Europe/Paris
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=db
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_DATABASE=2025_n8n
- DB_POSTGRESDB_USER=2025_n8n_user
# Double-run LAN : N8N_SECURE_COOKIE=false permet l'accès http://192.168.1.24:5678
# (cookies « secure » refusés en http). **À remettre à true à la bascule TLS (T4.6).**
- N8N_SECURE_COOKIE=false
volumes:
- ./data:/home/node/.n8n
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
# Bind 0.0.0.0 : LAN uniquement (ufw de 01-hardening.sh restreint le port).
ports:
- "0.0.0.0:5678:5678"
depends_on:
- n8n_db
db:
condition: service_healthy
n8n_db:
db:
image: postgres:17
container_name: n8n_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
environment:
- POSTGRES_DB=n8n
- POSTGRES_USER=n8n
- POSTGRES_DB=2025_n8n
- POSTGRES_USER=2025_n8n_user
- POSTGRES_PASSWORD_FILE=/run/secrets/dbpass
- POSTGRES_INITDB_ARGS=--encoding=UTF8 --locale=C
volumes:
- ./db:/var/lib/postgresql/data
- ./secrets:/run/secrets:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U 2025_n8n_user -d 2025_n8n"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
@@ -1,43 +1,62 @@
# Nextcloud — T4.2 (socle famille, G9)
# Stateful (app + MariaDB + Redis) → exclus de Watchtower (S11.4).
# Stateful (app + MariaDB + Redis + données) → exclus de Watchtower (S11.4).
# MAJ manuelle supervisée (S11.5) : backup DB → maintenance ON → pull/up → occ upgrade → vérif → OFF.
# L'incident Nextcloud (06/10) est la raison de ce pinning + exclusion.
#
# CONTRAINTES MESURÉES sur la source .25 (2026-10-11) :
# - config.php : dbtype=mysql, dbhost=**db**, dbname=nextcloud, dbuser=nextcloud,
# datadirectory=/var/www/html/data, version 35.0.1.1, instanceid oc9yed0p9k7q.
# → le service DB du compose G9 DOIT s'appeler `db` et porter les **identifiants
# d'origine** (sinon config.php importé ne correspond plus à la base importée).
# - apps déjà installés sur la source : calendar 6.6.2, contacts 8.9.1, mail 5.12.3
# (Groupware), deck 1.19.0, notes 6.1.0, richdocuments 12.0.1, collectives, forms,
# spreed, whiteboard… → import = conservation à l'identique, rien à installer.
# - pas de chiffrement des fichiers activé (aucune config encryption) → copie simple.
# - éditeur : Collabora (décision Phase 4) → service `collabora` + richdocuments.wopi_url.
services:
nextcloud:
image: nextcloud:35
image: nextcloud:35.0.1 # = version source exacte (35.0.1.1) → aucune migration de schéma
container_name: nextcloud
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
environment:
- TZ=Europe/Paris
- POSTGRES_HOST=nextcloud_db
- POSTGRES_DB=nextcloud
- POSTGRES_USER=nextcloud
- POSTGRES_PASSWORD_FILE=/run/secrets/dbpass
- NEXTCLOUD_TRUSTED_PROXIES=172.16.0.0/12
- PHP_MEMORY_LIMIT=1G
- PHP_UPLOAD_LIMIT=10G
- PHP_POST_SIZE_LIMIT=10G
volumes:
- ./data:/var/www/html
- ./config:/var/www/html/config
# T4.2/T4.6 : overrides double-run + edge (trusted_domains/proxies, overwrite*) —
# source de vérité = kit, monté :ro, réversible par suppression du fichier.
- ./g9-double-run.config.php:/var/www/html/config/g9-double-run.config.php:ro
- ./secrets:/run/secrets:ro
depends_on:
- nextcloud_db
- redis
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
# Bind 0.0.0.0 : accessible depuis le LAN (edge/Caddy le proxy en T4.6).
ports:
- "0.0.0.0:8080:80"
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost/status.php"]
interval: 60s
timeout: 10s
retries: 5
nextcloud_db:
db:
image: mariadb:10.11
container_name: nextcloud_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
command:
- --transaction-isolation=READ-COMMITTED
- --binlog-format=ROW
environment:
- MARIADB_DATABASE=nextcloud
- MARIADB_USER=nextcloud
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass # = mot de passe D'ORIGINE (import)
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
volumes:
- ./db:/var/lib/mysql
@@ -48,6 +67,43 @@ services:
container_name: nextcloud_redis
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # cache persistant → exclu
- "com.centurylinklabs.watchtower.enable=false"
volumes:
- ./redis:/data
# Collabora CODE — éditeur de documents (décision Phase 4, remplace OnlyOffice .25).
# Image épinglée 26.04.5.1.1 → Watchtower OFF (convention S11.4 : épinglée = exclu,
# cf. label n8n). MAJ manuelle supervisée avec backup préalable.
collabora:
image: collabora/code:26.04.5.1.1
container_name: collabora
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
# L'image officielle ne substitue que `username`/`password` dans coolwsd.xml :
# `password_file` n'est PAS lu → console 9980 restée sans mot de passe. Mot de passe
# injecté par env_file (pattern n8n) ; secrets/collabora.env généré par 03b-secrets.sh.
env_file:
- ./secrets/collabora.env
# Entrypoint = coolwsd (arguments fixes). CMD est **appendé** à l'entrypoint →
# `--o:ssl.enable=false` désactive le TLS interne (mesuré 2026-10-11 : sans cela,
# discovery http = 000, https -k = 200 ; env O:ssl.enable NON appliqué).
command:
- --o:ssl.enable=false
environment:
- TZ=Europe/Paris
# Hôtes émetteurs de WOPI autorisés (double-run : LAN G9 + hostname de bascule T4.6).
- aliasgroup1=https://nextcloud.folzocorp.fr,http://192.168.1.24:8080,http://192.168.1.25:8080
- username=admin
- JWT_ENABLED=false
# Image 26.04 : Entrypoint = coolwsd --use-env-vars → overrides coolwsd.xml via
# variables `O:<chemin>` (mesuré : extra_features/EXTRA_FEATURES non lus, TLS par
# défaut). SSL interne désactivé : WOPI en http LAN ; Caddy (T4.6) terminera le TLS.
- O:ssl.enable=false
volumes:
- ./secrets:/run/secrets:ro
ports:
- "0.0.0.0:9980:9980"
# Pas de healthcheck interne : l'image 26.04 est minimale (ni sh, ni curl →
# `CMD curl` = "executable file not found" = unhealthy permanent, mesuré 2026-10-11).
# Sonde externe : `curl -sf http://192.168.1.24:9980/hosting/discovery` (200).
@@ -0,0 +1,12 @@
<?php
# T4.2 double-run + T4.6 edge : overrides G9 (réversibles — supprimer ce fichier au retrait de l'edge).
# Monté :ro par compose/nextcloud → source de vérité = kit (S11.1).
# Tableau merge (array_replace_recursive) avec config.php : index 3 = ajout, indices 0-2 = conservés.
$CONFIG = array (
'trusted_domains' => array (0 => 'nextcloud.folzocorp.fr', 1 => '192.168.1.24', 2 => '192.168.1.24:8080', 3 => 'cloud.folzocorp.fr'),
'overwritehost' => 'cloud.folzocorp.fr',
'overwriteprotocol' => 'https',
'overwrite.cli.url' => 'https://cloud.folzocorp.fr',
# /12 = réseau bridge Docker (IP du conteneur edge, dynamique) ; /24 = accès direct LAN (double-run).
'trusted_proxies' => array (0 => '192.168.1.0/24', 1 => '172.16.0.0/12'),
);
@@ -1,6 +1,8 @@
# PhotoPrism — T4.3 (photos, G9)
# Lecture IN-PLACE des originaux (S5) : pointe sur /data/famille/photos (pas de déplacement).
# Stateful (base + index) → exclu de Watchtower (S11.4). Image PINNÉE (S11.4, pas :latest).
# PhotoPrism — T4.3 (G9)
# Décision #3 (C-T4-DECISIONS) : les photos sont **copiées** sur le G9 (/data/docker/photoprism/originals),
# plus de lecture in-place sur le NAS. Image épinglée (S11.4), stateful (base + index) → Watchtower OFF.
# PhotoPrism ne connaît PAS les variables « *_FILE » : les secrets sont fournis par env_file
# (secrets/photoprism.env, 600 root, hors git) — MariaDB, lui, accepte *_FILE.
services:
photoprism:
image: photoprism/photoprism:260919
@@ -8,34 +10,51 @@ services:
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
env_file:
- ./secrets/photoprism.env
environment:
- TZ=Europe/Paris
- PHOTOPRISM_URL=https://photos.folzocorp.fr
- PHOTOPRISM_SITE_URL=https://photos.folzocorp.fr
- PHOTOPRISM_ADMIN_USER=admin
- PHOTOPRISM_ADMIN_PASSWORD_FILE=/run/secrets/admin
- PHOTOPRISM_LIBRARY_PATH=/originals:/import
- PHOTOPRISM_SITE_URL=http://192.168.1.24:2342/
- PHOTOPRISM_SITE_TITLE=Photos Folzo
- PHOTOPRISM_DATABASE_DRIVER=mysql
- PHOTOPRISM_DATABASE_SERVER=db:3306
- PHOTOPRISM_DATABASE_NAME=photoprism
- PHOTOPRISM_DATABASE_USER=photoprism
- PHOTOPRISM_DATABASE_PASSWORD_FILE=/run/secrets/dbpass
- PHOTOPRISM_LOCAL_NETWORKS=172.16.0.0/12,192.168.0.0/16
- PHOTOPRISM_LOCAL_NETWORKS=192.168.0.0/16
- PHOTOPRISM_HTTP_COMPRESSION=gzip
- PHOTOPRISM_LOG_LEVEL=info
- PHOTOPRISM_PUBLIC=false
- PHOTOPRISM_READONLY=false
- PHOTOPRISM_EXPERIMENTAL=false
- PHOTOPRISM_ORIGINALS_LIMIT=5000
- PHOTOPRISM_AUTO_INDEX=300
- PHOTOPRISM_AUTO_IMPORT=-1
- PHOTOPRISM_SIDECAR_ENABLED=true
volumes:
- /data/famille/photos:/originals:ro # in-place (lecture seule)
- ./storage:/storage
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (J5040 edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
- /data/docker/photoprism/originals:/photoprism/originals
- /data/docker/photoprism/storage:/photoprism/storage
- /data/docker/photoprism/import:/photoprism/import
# Bind 0.0.0.0 + ufw (01-hardening.sh) restreint le port au LAN → pas d'exposition WAN.
ports:
- "0.0.0.0:2342:2342"
depends_on:
- photoprism_db
db:
condition: service_healthy
photoprism_db:
db:
image: mariadb:10.11
container_name: photoprism_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
command: >
--innodb-buffer-pool-size=256M
--transaction-isolation=READ-COMMITTED
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
--max-connections=512
--innodb-rollback-on-timeout=OFF
--innodb-lock-wait-timeout=120
environment:
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
- MARIADB_DATABASE=photoprism
@@ -44,3 +63,9 @@ services:
volumes:
- ./db:/var/lib/mysql
- ./secrets:/run/secrets:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
@@ -23,7 +23,9 @@ services:
# Secrets via fichiers (hors git, G9) créés par 03b-secrets.sh :
- ADMIN_TOKEN_FILE=/run/secrets/admin
# Derrière Caddy (edge) : hosts de confiance pour les WebSockets.
- TRUSTED_HOSTS=192.168.1.0/24
# T4.6 : /12 = réseau bridge Docker (IP du conteneur edge, dynamique) ; /24 = accès
# direct LAN (double-run). Mêmes réseaux de confiance que Nextcloud (edge.config.php).
- TRUSTED_HOSTS=192.168.1.0/24,172.16.0.0/12
volumes:
# DATA_FOLDER par défaut = /data → le volume DOIT pointer sur /data (sinon la
# base écrit dans la couche conteneur, non persistée, et l'import 07 n'est jamais lu).
@@ -18,8 +18,9 @@ services:
- WATCHTOWER_POLL_INTERVAL=3600 # nickfedor : SCHEDULE et POLL exclusifs
- WATCHTOWER_LABEL_ENABLE=true # seul ce qui est labelé true est mis à jour
# Notifications shoutrrr generic → webhook n8n → salon Matrix ops.
# ⚠️ PLACEHOLDER : remplacer par l'URL webhook réelle de la machine (workflow n8n w1pfhX0A7KvaDrtk).
# n8n est joint par son nom de service sur le réseau Docker local (pas par l'edge :
# l'edge n'écoute que 443 ; n8n.folzocorp.fr = voie LAN/WAN via edge, pas conteneur→conteneur).
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=generic:https://n8n.folzocorp.fr/webhook/watchtower
- WATCHTOWER_NOTIFICATION_URL=generic+http://n8n:5678/webhook/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
+21
View File
@@ -82,3 +82,24 @@
| 2026-10-11 07:56 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:57 | 4 | migrate:vaultwarden : coffre importé + service lancé | T4.1 (double-run G4) | Source stoppée → tar/ssh → **source relancée healthy** ; 621 fichiers sha256 identiques (source == stage == G9) ; G9 = 953 ciphers / 1 user (identiques à .25, revérifié après coup : .25 healthy 953) ; migrations diesel OK (v.20260902120000) ; alive 200, WebVault 200 ; DOMAIN conservé = `pass.folzocorp.fr` (clients existants intacts) ; image épinglée 1.37.4 (génération source, image .25 pullée 2026-10-05) | docker compose -f /data/docker/vaultwarden/docker-compose.yml down + rm -rf /data/docker/vaultwarden ; .25 intact |
| 2026-10-11 08:01 | 4 | migrate:vaultwarden : 2FA + ADMIN_TOKEN + emergency access | T4.1 (critères 2FA + emergency access) | **2FA TOTP déjà actif côté source et importé avec le coffre** : twofactor(atype=0, enabled=1, last_used=59703916) identique sur le G9 → l'appli d'authentification de l'utilisateur continue sans re-scannage ; ADMIN_TOKEN **haché argon2id** (m=64MiB,t=3,p=4 — preset bitwarden, python3-argon2) : avis « plain text ADMIN_TOKEN » disparu, login admin 200 (dashboard Users/Invitations/Organizations/**Emergency** accessibles) ; token clair dans `/data/docker/vaultwarden/secrets/admin-plain` (600 root, à consigner dans le coffre puis supprimer) ; `EMERGENCY_ACCESS_ALLOWED=true` + `INVITATIONS_ALLOWED=true` posés | restaurer secrets/admin (token clair) + restart ; retirer les 2 env |
| 2026-10-11 08:13 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:16 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:21 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:28 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:35 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 08:40 | 4 | audit .25 : « disparition » de /volume1/docker-REC = VUE de partage DSM, pas une perte | Inventaire d'abord pris sur /volume1/docker-REC (60 entrées), puis 10 entrées une heure plus tard, propriétaire de « . » passé de Master à root, mtime /volume1 = 03:01 | `docker inspect` = vérité terrain : nextcloud_app → **/volume2/docker-REC/NextCloud/{data,config}** (+ volume nommé /volume1/@docker/volumes/f5f0…/_data), vaultwarden-server-1 → **/volume1/docker/vaultwarden**, gitea → /volume1/docker/Gitea/data. /volume1/docker-REC est une vue multi-volumes du partage « docker-REC », remontée à 03:01. **Aucune donnée perdue** : /volume2/docker-REC/NextCloud/data = 1.9 Go / 2608 fichiers ; db.sqlite3 de vaultwarden sha256 `2cad5eea42ef4b04…` **identique** entre .25 et le G9 | lecture seule, aucune action ; chemins canoniques consignés dans 07c (XP_DATA/XP_CONF = /volume2/…) |
| 2026-10-11 08:42 | 4 | kit : lancer les scripts de migration en **folzo**, jamais en `sudo` global | `sudo` remplace $HOME par /root → « Identity file /root/.ssh/id_ed25519_openfox11 not accessible » puis « Host key verification failed » (root n'a ni clé ni known_hosts) ; 3 runs de 07c échoués en préflight | 07c réécrit : appels « G9 » en **local** (`sh -c` + sudo local, la machine de pilotage EST le G9), clé en chemin absolu ; import du dump via **MARIADB_USER/MARIADB_PASSWORD** (env du conteneur MariaDB G9, pas MYSQL_*) | relancer en root avec SSH_KEY=<chemin> explicite si besoin |
| 2026-10-11 09:03 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 09:10 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 09:18 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 10:10 | 4 | clarification 08:42 : le 07c **déposé** est la variante SSH, pas « local » | La ligne 08:42 décrit une réécriture « appels G9 en local » jamais commitée ; le `07c-migrate-nextcloud.sh` du dépôt passe TOUTES les ops G9 par SSH (folzo@.24, clé sur le pilot) — cohérent avec son en-tête | 07c déposé = variante SSH : préflight SSH .25 + .24, import rsync/ssh, restore via MARIADB_USER + secrets/dbpass. La formulation « local » de 08:42 ne décrit PAS le code du dépôt. + garde-fou idempotence backup (07c/07d) + nettoyage dumps (07c) + n8n.env builder (03b) + mount secrets collabora + label watchtower collabora=false | aucune — ligne documentaire |
| 2026-10-11 10:46 | 4 | récupération du thread Phase 4 supprimé (session `e67f1166…`) | suppression accidentelle de la session 07:20 UTC (cascade `events`) ; le runner a continué à travailler (journal 09:18→10:13) | thread réinséré : **130 messages / 64 657 événements / 33,85 Mo** ; caravage des pages SQLite libérées = **51 420 payloads JSON (34,68 Mo)** + 79 830 entrées d'index ; **~35 % du volume**, prose en fragments désordonnés, messages utilisateur quasi tous perdus ; intégrité `ok` avant/après ; rapport complet `g9-migration/PHASE4-RECOLLEMENT.md` | chirurgical : `DELETE FROM events WHERE session_id='e67f1166…'; DELETE FROM sessions WHERE id='e67f1166…'` — ou restauration `snap2` (voir §4 du rapport) |
| 2026-10-11 11:05 | 4 | corrections post-revue code (5 findings) | Revue de code du diff Phase 4 | 1 **sécurité** : `collabora` ne lisait PAS `password_file` (non supporté par collabora/code) → console 9980 sans mot de passe ; remplacé par `env_file: ./secrets/collabora.env` (`password=…`) généré par 03b (pattern n8n) ; 2 **import 07c** : DEFINER retirés du dump + import en **root** (rootpass) + **rc contrôlé** (l'ancien `\| tail -5` masquait ERROR 1227) ; 3 07c : `SRC_USERS` avec `\|\| true` (préflight tué par grep rc=1) ; 4 07d : progression `tar --checkpoint=50000` sur la copie 530 Go ; 5 README : arborescence + ligne phase 4 = 07a–07d (07-migrate-stateful = générique rsync, inopérant sur DSM) ; 6 `PHASE4-RECOLLEMENT.md` dégraissé (821→75 l., dump §5 hors dépôt). Vérif : `bash -n` 3/3 OK, YAML compose OK (env_file présent, password_file absent), sed DEFINER testé | `git checkout` des fichiers modifiés (compose nextcloud, 03b, 07c, 07d, README) |
| 2026-10-11 12:44 | 4 | migrate:nextcloud : backup restic pré-migration | Garde-fou G2 avant import Nextcloud | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 10:45 | 4 | migrate:nextcloud : migration complète (07c) | T4.2 (double-run G4) | export app stoppée (db vivante, mysqldump --single-transaction 31,9 Mo) + tars data (1,9 Go, 2608 fichiers sha256 identiques) + config + custom_apps → **app .25 relancée healthy** ; import G9 : MariaDB 10.11 (utf8mb4_bin, DEFINER retirés, import root, filecache=2727, users=8), occ status installed/maintenance off, version 35.0.1.1 = image épinglée, occ upgrade « No upgrade required », occ app:list **identique à la source (66 apps)** ; overrides LAN dans config/g9-double-run.config.php (réversible) ; dumps supprimés sur les 3 machines (hashs mots de passe users) | docker compose -f /data/docker/nextcloud/docker-compose.yml down + rm -rf /data/docker/nextcloud ; .25 intact |
| 2026-10-11 11:02 | 4 | migrate:nextcloud : Collabora — TLS interne désactivé | L'image collabora/code:26.04 sert **TLS par défaut** : discovery http = 000, https -k = 200 ; env `extra_features`/`EXTRA_FEATURES` et `O:ssl.enable` **non appliqués** (mesuré : entrypoint = coolwsd --use-env-vars sans script) | fix = **command: --o:ssl.enable=false** (CMD appendu à l'entrypoint) → logs « SSL is disabled », discovery http **200** ; wopi_url = http://192.168.1.24:9980 (occ, persisté) ; Caddy (T4.6) terminera le TLS | retirer la ligne command: du compose + restart |
| 2026-10-11 13:40 | 4 (T4.5) | jellyfin : wizard + auth API | Déploiement frais G9 (aucun Jellyfin sur .25 — stack *arr reste sur .25) ; média par RÉFÉRENCE : bind /mnt/usb-media → /media:ro (aucune copie) | wizard 10.9.11 accompli via navigateur (langue → admin « g9ops » → médiathèque « Films3 » → /media/films) ; **PIÈGE mesuré** : le wizard laisse `Users.EnableLocalPassword=0` → AuthenticateByName = « Invalid username or password » ; fix = UPDATE sqlite + restart (hash PBKDF2-SHA512 vérifié == secret) ; en-tête API requis = **X-Emby-Authorization** combiné (400 « Value cannot be null » sans lui) ; médiathèques vides doublons supprimées → 1 lib « Films3 » = /media/films ; `Items?IncludeItemTypes=Movie` = **1** (test-reference.mkv, 0 octet, média de test en lecture seule) ; /system/info 200 ; label watchtower=false ; ufw 8096 ALLOW IN 192.168.1.0/24 | `DELETE /Library/VirtualFolders?name=…` + `UPDATE Users SET EnableLocalPassword=0` ; compose down (données source : aucune touchée) |
| 2026-10-11 13:55 | 4 (T4.2) | collabora : healthcheck retiré du compose | Limage collabora/code:26.04.5.1.1 est **minimale** : ni `sh` ni `curl` dans limage → `CMD curl` = « executable file not found » = **unhealthy permanent** (mesuré : 3 échecs successifs dans le log de santé) | healthcheck retiré (source de vérité = kit) + recréation du conteneur → **healthy** ; re-vérifications T4.2 complètes : occ installed/maintenance off 35.0.1.1, users=8, files:scan --all = 1522 fichiers 0 erreur, wopi_url (oc_appconfig richdocuments) = http://192.168.1.24:9980, discovery 200, status.php 200, 67 apps activés ; .25 relancé (nextcloud_app/db/redis + vaultwarden + gitea up) | remettre un healthcheck si une image avec shell est épinglée |
| 2026-10-11 13:50 | 4 | edge : déploiement Caddy (T4.6) | Reverse proxy LAN + HTTPS interne (CA interne) pour vaultwarden/nextcloud/gitea/jellyfin/n8n/matrix | Caddyfile validate OK ; conteneur edge up ; ufw 443/tcp exposé WAN (seul), 80 redirect, 9980 LAN-only ; HTTPS+SNI vérifiés sur les 5 briques | docker compose -f /data/docker/edge/docker-compose.yml down ; ufw delete allow 443 (briques joignables en http direct LAN) |
| 2026-10-11 14:10 | 4 (T4.6) | edge : Caddy déployé (reverse proxy LAN + HTTPS interne) | Briquage T4.6 : TLS interne pour vaultwarden/nextcloud/gitea/jellyfin/n8n/matrix, sous-domaines déjà configurés côté apps (DOMAIN/ROOT_URL/N8N_HOST) → aucune brique touchée | `caddy:2` (digest f2a1290d…) up healthy (healthcheck = `caddy validate`) ; **tls internal** = CA interne (LE + DNS challenge = suite S9.1 via AdGuard J5040) ; **PIÈGES mesurés** : (1) snippet `(name)` importé → « ambiguous site definition » si utilisé comme adresse ; (2) `request_id` nest PAS un directive site (validate KO) ; (3) Caddy logue les en-têtes **reçus du client**, pas ceux envoyés en amont → preuve X-Forwarded prise côté app ; ufw 443/tcp exposé WAN (seul, default deny protège les ports briques) + 80 redirect + 9980 LAN-only ; **Nextcloud** : merge config = `array_replace_recursive` (le dépôt dun 2e fichier *.config.php a été avalé par lentrée existante → résolu en écrivant g9-double-run.config.php = source de vérité du kit, monté :ro) → occ : overwritehost=cloud.folzocorp.fr, overwriteprotocol=https, trusted_proxies=[192.168.1.0/24, 172.16.0.0/12] ; **preuve côté app** : redirect `https://cloud.folzocorp.fr/` → Location `https://cloud.folzocorp.fr/index.php/login` (schéma https + host forwardé rendus par Nextcloud) ; curl -k via edge : pass 200 / git 200 / cloud 200 / media 200 / n8n 200 / matrix 302 / SNI IP 200 ; vaultwarden TRUSTED_HOSTS += 172.16.0.0/12 (aligné) | `docker compose -f /data/docker/edge/docker-compose.yml down` ; `ufw delete allow 443` ; supprimer g9-double-run.config.php (retour double-run http LAN) |
| 2026-10-11 14:20 | 4 (T4.7) | watchtower : vérification scope stateless + dry-run | S11.4 : MAJ auto uniquement des briques stateless ; leçon incident Nextcloud 06/10 = CLEANUP=false | Conteneur up healthy, image **nickfedor/watchtower épinglée par digest** sha256:45b54f6d… (fork : containrrr/watchtower:1 n'existe pas) ; env mesuré = POLL=3600, LABEL_ENABLE=true, CLEANUP=false, INCLUDE_STOPPED=false ; **labels vérifiés sur les 16 conteneurs .24** : stateful enable=false = nextcloud, collabora, jellyfin, nextcloud_db, nextcloud_redis, vaultwarden, gitea, gitea-db, valheim, n8n, element, synapse, couchdb, obsidian ; enable=true = edge (stateless, T4.6) + watchtower ; **dry-run** `docker run --rm -v /var/run/docker.sock nickfedor/watchtower@sha256:45b54f6d… --label-enable --monitor-only -R` → « Only checking containers with label … enable », **scanned=2 updated=0 failed=0** = les 14 stateful sont ignorés ; notification : webhook n8n testé en POST depuis le réseau Docker → **200** (le compose affichait un placeholder https://n8n.folzocorp.fr → aligné sur generic+http://n8n:5678, nom de service : l'edge n'écoute que 443) ; nickfedor : SCHEDULE et POLL exclusifs, pas de --once (=-R) | `docker compose -f /data/docker/watchtower/docker-compose.yml down` ; aucun effet sur les briques (label-driven) |
+6 -2
View File
@@ -71,15 +71,19 @@ ufw allow OpenSSH
# Services LAN-only (G9 = LAN-only, l'exposition WAN passe par l'edge — S9.2/S9.3).
# Ports des services RÉELS sur .24 (n8n, synapse, element, couchdb, obsidian) +
# briques famille futures + Valheim (TCP+UDP). Accessibles depuis le LAN uniquement.
for port in 2342 3000 3100 5678 5984 8008 8009 8080 8081 8096; do
for port in 2342 3000 3100 5678 5984 8008 8009 8080 8081 8096 9980; do
ufw allow from 192.168.1.0/24 to any port "$port" proto tcp
done
for port in 2456 2457 2458; do
ufw allow from 192.168.1.0/24 to any port "$port" proto tcp
ufw allow from 192.168.1.0/24 to any port "$port" proto udp
done
# T4.6 edge (Caddy) : SEUL port exposé WAN = 443 (+ 80 = redirect ACME/HTTP→HTTPS).
# L'exposition WAN des briques passe par l'edge uniquement (S9.2/S9.3, default deny incoming).
ufw allow 443/tcp comment 'edge T4.6 (seul port exposé WAN)'
ufw allow 80/tcp comment 'edge T4.6 (redirect ACME)'
ufw --force enable
journal "ufw activé" "Firewall LAN-only (G6)" "deny incoming, allow outgoing, OpenSSH + services LAN-only 192.168.1.0/24 (TCP 2342 3000 3100 5678 5984 8008 8009 8080 8081 8096 + Valheim 2456-2458 TCP/UDP)" "ufw disable"
journal "ufw activé" "Firewall LAN-only (G6)" "deny incoming, allow outgoing, OpenSSH + services LAN-only 192.168.1.0/24 (TCP 2342 3000 3100 5678 5984 8008 8009 8080 8081 8096 9980 + Valheim 2456-2458 TCP/UDP) + edge 443/80 exposés WAN (T4.6)" "ufw disable"
# --- 3. unattended-upgrades ---
log "unattended-upgrades : activation"
+33 -4
View File
@@ -11,12 +11,17 @@
#
# Génère (chmod 644 root:root — voir la note du code : les entrypoints conteneurs ne sont
# pas root, 600 les rendrait illisibles ; le dossier reste hors git, G9 respecté) pour chaque brique :
# nextcloud : dbpass, rootpass
# nextcloud : dbpass, rootpass, collabora_admin + collabora.env (construit depuis
# collabora_admin — requis par env_file du compose : collabora/code ne lit
# PAS password_file, seul `password` est substitué dans coolwsd.xml)
# photoprism : admin, dbpass, rootpass
# vaultwarden : admin
# n8n : dbpass
# n8n : dbpass + n8n.env (construit depuis dbpass — requis par env_file du compose)
# gitea : dbpass (backend Postgres — voir compose/gitea)
# (jellyfin, watchtower : pas de secret fichier)
# jellyfin : admin (mot de passe du compte admin posé par le wizard 10.9 — voir
# compose/jellyfin ; la base jellyfin.db est créée par le wizard, le
# secret documente le mot de passe réel, il n'est pas lu par l'image)
# (watchtower : pas de secret fichier)
#
# ⚠️ QUELS CREDENTIELS GAGNENT (piège de migration) :
# - INIT FRAÎCHE : ce sont ces secrets générés qui sont utilisés (MARIADB_*/POSTGRES_*
@@ -58,7 +63,7 @@ gen_secret() {
# briques : "brique:s1 s2 s3;..."
declare -A SECRET_MAP=(
[nextcloud]="dbpass rootpass"
[nextcloud]="dbpass rootpass collabora_admin"
[photoprism]="admin dbpass rootpass"
[vaultwarden]="admin"
[n8n]="dbpass"
@@ -74,6 +79,30 @@ for brique in nextcloud photoprism vaultwarden n8n gitea; do
for s in ${SECRET_MAP[$brique]}; do
gen_secret "$SECRETS_DIR/$s"
done
if [[ "$brique" == "n8n" ]]; then
ENVF="$SECRETS_DIR/n8n.env"
if [[ -f "$ENVF" ]]; then
log " ⏭ existe déjà : $ENVF (non modifié)"
else
umask 077
{ echo "DB_POSTGRESDB_PASSWORD=$(cat "$SECRETS_DIR/dbpass")"; } > "$ENVF"
chmod 644 "$ENVF"
log " ✅ généré : $ENVF (env_file requis par compose/n8n)"
fi
fi
if [[ "$brique" == "nextcloud" ]]; then
# collabora/code ne lit PAS password_file : seul `password` est substitué dans
# coolwsd.xml → console admin 9980 protégée par env_file (pattern n8n).
ENVF="$SECRETS_DIR/collabora.env"
if [[ -f "$ENVF" ]]; then
log " ⏭ existe déjà : $ENVF (non modifié)"
else
umask 077
{ echo "password=$(cat "$SECRETS_DIR/collabora_admin")"; } > "$ENVF"
chmod 644 "$ENVF"
log " ✅ généré : $ENVF (env_file requis par compose/nextcloud → collabora)"
fi
fi
done
journal "Secrets bootstrap OK" "Briques stateful nécessitent des fichiers secrets" "nextcloud, photoprism, vaultwarden, n8n (chmod 644 root:root, dossier hors git)" "suppression des fichiers secrets générés"
@@ -20,6 +20,15 @@
# - Sur le G9 : /data monté, Docker opérationnel.
# - Secrets bootstrap (03b-secrets.sh) déjà exécuté.
#
# ⚠️ TRANSPORT (mesuré 2026-10-11, DSM 7) : le sshd du xpenology refuse la
# commande rsync --server (« Permission denied, please try again. ») et scp
# n'est pas servi (« subsystem request failed »). Le pull rsync/scp depuis .25
# est donc IMPOSSIBLE. Transports qui fonctionnent : « ssh sudo tar cz » et
# « ssh sudo cat ». Les briques sont migrées par les scripts dédiés
# (07a-migrate-gitea.sh, 07b-migrate-vaultwarden.sh, 07c-migrate-nextcloud.sh)
# qui utilisent tar+ssh ; ce script générique est gardé pour les hôtes qui
# exposent rsync.
#
# ⚠️ COHÉRENCE DES BASES (G4) : le rsync d'une base VIVANTE (MariaDB/Postgres en
# service sur .25) produit une copie incohérente et un manifeste sha256 qui
# change pendant le transfert. Le script STOPPE donc les conteneurs source
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env bash
#
# 07c-migrate-nextcloud.sh — T4.2 : migration Nextcloud (xpenology .25 → G9 .24)
#
# Garde-fous : G4 (double-run : source stoppée pendant l'export puis **relancée**, rien supprimé),
# G2 (backup restic avant import), G7 (rollback), G8 (journal), G9 (aucun secret loggué/commité).
#
# SOURCE mesurée (2026-10-11) — 3 mounts du conteneur nextcloud_app :
# /volume2/docker-REC/NextCloud/data → /var/www/html/data (datadirectory, 1.9 Go)
# /volume1/@docker/volumes/<hash>/_data → /var/www/html (VOLUME NOMMÉ = code app +
# custom_apps : calendar, contacts, mail, deck, notes,
# richdocuments, collectives, forms, spreed, whiteboard…)
# /volume2/docker-REC/NextCloud/config → /var/www/html/config (config.php, redis, smtp…)
# DB : conteneur nextcloud_db (mariadb 10.6), base nextcloud / user nextcloud → mysqldump.
#
# POINTS CLÉS (C-T4.2) :
# - config.php importé impose dbhost=`db`, dbname/dbuser=nextcloud → le compose G9 nomme son
# service DB `db` et reçoit le **mot de passe d'origine** (lu depuis l'env du conteneur
# source, écrit en 644 dans secrets/dbpass, JAMAIS affiché).
# - image épinglée nextcloud:35.0.1 = version source exacte → pas de migration de schéma.
# - custom_apps copiés depuis le volume nommé : sans eux la base déclarerait des apps absents.
#
# Usage : sur la **machine de pilotage** (celle qui détient la clé .25/.24, ici /home/effi),
# ./07c-migrate-nextcloud.sh [SSH_KEY=<chemin> pour forcer la clé]
# Les ops G9 passent par SSH (folzo@.24, sudo pour la partie root) : la clé vit sur le pilot,
# PAS sur le G9. Ne PAS lancer via `sudo` global : $HOME devient /root → clé et known_hosts
# introuvables → « Host key verification failed » (mesuré 2026-10-11, 3 runs échoués).
# Rollback : G9 `docker compose -f /data/docker/nextcloud/docker-compose.yml down` +
# `rm -rf /data/docker/nextcloud` ; .25 intact (services relancés).
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker"
# On stoppe **l'app** (writes quiescents) mais PAS la DB : mysqldump a besoin du serveur
# tournant, et --single-transaction fournit l'instantané cohérent (G4 : relancée après).
XP_CONTAINERS="nextcloud_app"
XP_DATA="/volume2/docker-REC/NextCloud/data"
XP_CONF="/volume2/docker-REC/NextCloud/config"
XP_VOL_APP="/volume1/@docker/volumes/f5f099b69018be044f00d3d68f1e0da76cecb42bcb3b621b43d31091e33e789d/_data"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
BRIQUE="nextcloud"
G9_DIR="/data/docker/$BRIQUE"
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
KIT_ON_G9="/home/folzo/g9-migration"
JOURNAL="$KIT_DIR/journal.md"
STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}"
SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST"
RSH_G9="ssh -i $KEY"
log() { echo -e "\n[nextcloud] $*"; }
journal() {
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \
"$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL"
}
manifest_in() { ( cd "$1" && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort ); }
verify_pair() { # $1 src .25 (sudo find), $2 dir local
local r l rc=0; r=$(mktemp); l=$(mktemp)
$SSH_XP "sudo sh -c \"cd '$1' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort\"" > "$r"
manifest_in "$2" > "$l"
diff -u "$r" "$l" > "$r.diff" || rc=1
if [[ $rc -ne 0 ]]; then log " ❌ intégrité $2 :"; head -20 "$r.diff"; rm -f "$r" "$l" "$r.diff"; exit 1; fi
log " ✅ $(wc -l < "$r") fichiers identiques ($(basename "$2"))"
rm -f "$r" "$l" "$r.diff"
}
# ---------------------------------------------------------------- 0. préflight
log "=== 0/6 PRÉFLIGHT ==="
$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; }
$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; }
$SSH_G9 "test -d $KIT_ON_G9/compose" || { echo "ERREUR : kit absent sur le G9"; exit 1; }
for d in "$XP_DATA" "$XP_CONF" "$XP_VOL_APP/custom_apps"; do
$SSH_XP "sudo test -d '$d'" || { echo "ERREUR : source absente sur .25 : $d"; exit 1; }
done
mkdir -p "$STAGE"
SRC_USERS=$($SSH_XP "sudo $XP_DOCKER exec nextcloud_app php occ user:list 2>/dev/null | grep -c '^\s*-' || true")
log "source .25 : users occ = $SRC_USERS"
# ---------------------------------------------------------------- 1. backup G2
log "=== 1/6 BACKUP restic (G2) ==="
if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then
log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)"
else
$SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2"
journal "backup restic pré-migration" "Garde-fou G2 avant import Nextcloud" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
fi
# ---------------------------------------------------------------- 2. export (stop → export → start)
log "=== 2/6 EXPORT : stop app → mysqldump (db vivante, --single-transaction) + tars → relance app (G4) ==="
STOPPED=()
for c in $XP_CONTAINERS; do
if $SSH_XP "sudo $XP_DOCKER ps -q -f name=^${c}\$" | grep -q .; then
$SSH_XP "sudo $XP_DOCKER stop $c >/dev/null" && STOPPED+=("$c") && log " ⏸ source stoppée : $c"
fi
done
restart_source() {
for c in "${STOPPED[@]:-}"; do
[[ -n "$c" ]] || continue
$SSH_XP "sudo $XP_DOCKER start $c >/dev/null" && log " ▶ source redémarrée : $c" \
|| log " ⚠️ ÉCHEC redémarrage $c — INTERVENIR SUR .25"
done
}
trap restart_source EXIT
DUMP=/tmp/nc_dump.sql
$SSH_XP "sudo $XP_DOCKER exec nextcloud_db sh -c 'mysqldump -u\"\$MYSQL_USER\" -p\"\$MYSQL_PASSWORD\" --single-transaction --routines --triggers --events --default-character-set=utf8mb4 \"\$MYSQL_DATABASE\"' > $DUMP && sudo chmod 644 $DUMP"
log " dump : $($SSH_XP "stat -c%s $DUMP") o, sha256 $($SSH_XP "sha256sum $DUMP | cut -c1-16")…"
# mot de passe DB d'origine : lu depuis l'env du conteneur, écrit en 600 local, jamais affiché
$SSH_XP "sudo $XP_DOCKER inspect nextcloud_db --format '{{range .Config.Env}}{{println .}}{{end}}'" \
| sed -n 's/^MYSQL_PASSWORD=//p' > "$STAGE/dbpass"; chmod 600 "$STAGE/dbpass"
[[ -s "$STAGE/dbpass" ]] || { echo "ERREUR : mot de passe DB source non récupéré"; exit 1; }
$SSH_XP "sudo tar cz -C '$XP_DATA' ." > "$STAGE/data.tgz"
$SSH_XP "sudo tar cz -C '$XP_CONF' ." > "$STAGE/config.tgz"
$SSH_XP "sudo tar cz -C '$XP_VOL_APP' custom_apps" > "$STAGE/custom_apps.tgz"
log " archives : data=$(du -h "$STAGE/data.tgz"|cut -f1) config=$(du -h "$STAGE/config.tgz"|cut -f1) custom_apps=$(du -h "$STAGE/custom_apps.tgz"|cut -f1)"
mkdir -p "$STAGE/x_data" "$STAGE/x_config" "$STAGE/x_custom_apps"
tar xzf "$STAGE/data.tgz" -C "$STAGE/x_data"
tar xzf "$STAGE/config.tgz" -C "$STAGE/x_config"
tar xzf "$STAGE/custom_apps.tgz" -C "$STAGE/x_custom_apps"
verify_pair "$XP_DATA" "$STAGE/x_data"
verify_pair "$XP_CONF" "$STAGE/x_config"
verify_pair "$XP_VOL_APP/custom_apps" "$STAGE/x_custom_apps/custom_apps"
rm -f "$STAGE"/*.tgz
restart_source; STOPPED=()
# ---------------------------------------------------------------- 3. import G9
log "=== 3/6 IMPORT sur le G9 ==="
$SSH_G9 "sudo mkdir -p $G9_DIR/data/data && sudo chown -R $G9_USER:$G9_USER $G9_DIR"
# MariaDB tourne en uid 999 : le datadir doit lui appartenir (sinon « Permission denied » sur
# db.opt, mesuré 2026-10-11). Le chown -R folzo ci-dessus est nécessaire au rsync, pas à la DB.
$SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_data/" "$G9_USER@$G9_HOST:$G9_DIR/data/data/"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_config/" "$G9_USER@$G9_HOST:$G9_DIR/config/"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/x_custom_apps/" "$G9_USER@$G9_HOST:$G9_DIR/data/"
g=$(mktemp); l2=$(mktemp); manifest_in "$STAGE/x_data" > "$l2"
$SSH_G9 "cd '$G9_DIR/data/data' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$g"
diff -u "$l2" "$g" > "$g.diff" || { log "❌ intégrité data sur le G9 :"; head -20 "$g.diff"; exit 1; }
log " ✅ data G9 : $(wc -l < "$g") fichiers identiques"
rm -f "$l2" "$g" "$g.diff"
# ---------------------------------------------------------------- 4. compose + secrets + restore DB
log "=== 4/6 COMPOSE + SECRETS + RESTORE DB ==="
rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/"
rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/"
$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null"
$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets"
# dbpass = mot de passe D'ORIGINE (config.php importé), rootpass = généré par 03b-secrets.
# secrets/ est root:root sur le G9 → passage par /tmp puis install en sudo (jamais affiché).
rsync -a -e "$RSH_G9" "$STAGE/dbpass" "$G9_USER@$G9_HOST:/tmp/.nc-dbpass"
$SSH_G9 "sudo install -m 644 -o root -g root /tmp/.nc-dbpass $G9_DIR/secrets/dbpass && rm -f /tmp/.nc-dbpass && sudo chmod 644 $G9_DIR/secrets/rootpass $G9_DIR/secrets/collabora_admin 2>/dev/null || true"
rm -f "$STAGE/dbpass"; rm -rf "$STAGE"
$SSH_G9 "cd $G9_DIR && docker compose up -d db redis && sleep 25 && docker compose ps --format '{{.Name}} {{.Status}}'"
log " restore du dump (base vierge, schéma oc_ + utf8mb4)"
$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" -e \"ALTER DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;\"' < /dev/null"
$SSH_XP "sudo cat $DUMP" > /tmp/nc_dump_local.sql
# DEFINER=`nextcloud`@`…` (routines/triggers/events, issus de --routines --triggers --events)
# importés en utilisateur non-root → « ERROR 1227 … you need SUPER » sous MariaDB 10.11.
# → DEFINER retirés localement, import en **root** (rootpass, seedé par 03b), rc contrôlé
# (l'ancien `2>&1 | tail -5` masquait l'échec et le script poursuivait).
DEF_COUNT=$(grep -c 'DEFINER=' /tmp/nc_dump_local.sql || true)
sed -E 's/DEFINER=`[^`]+`@`[^`]+`//g' /tmp/nc_dump_local.sql > /tmp/nc_dump_stripped.sql
log " DEFINER retirés du dump : $DEF_COUNT"
rsync -a --info=stats1 -e "$RSH_G9" /tmp/nc_dump_stripped.sql "$G9_USER@$G9_HOST:$G9_DIR/nc_dump.sql"
IMPORT_LOG=/tmp/nc_import.log
if ! $SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u root -p\"\$(cat /run/secrets/rootpass)\" nextcloud' < $G9_DIR/nc_dump.sql" > "$IMPORT_LOG" 2>&1; then
log "❌ import du dump en échec (rc≠0) — 20 dernières lignes de la sortie mysql :"
tail -20 "$IMPORT_LOG"
exit 1
fi
if grep -qiE '^ERROR [0-9]+' "$IMPORT_LOG"; then
log "❌ erreurs MySQL détectées pendant l'import :"
grep -iE '^ERROR [0-9]+' "$IMPORT_LOG" | head -10
exit 1
fi
log " import OK en root (base vierge, DEFINER retirés) — $(tail -1 "$IMPORT_LOG")"
rm -f /tmp/nc_dump_local.sql /tmp/nc_dump_stripped.sql "$IMPORT_LOG"
$SSH_G9 "cd $G9_DIR && docker compose exec -T db sh -c 'mysql -u\"\$MARIADB_USER\" -p\"\$(cat /run/secrets/dbpass)\" nextcloud -e \"select count(*) as filecache from oc_filecache; select count(*) as users from oc_users;\"' < /dev/null"
# nettoyage des dumps (hashs des mots de passe users) — G9 : rien ne traîne (les 3 copies)
$SSH_XP "sudo rm -f $DUMP"
rm -f /tmp/nc_dump_local.sql
$SSH_G9 "rm -f $G9_DIR/nc_dump.sql"
log " dumps nettoyés (.25, pilot, G9)"
# override LAN du double-run (documenté, réversible) : pas de réécriture https, LAN en confiance
$SSH_G9 "cat > $G9_DIR/config/g9-double-run.config.php" <<'PHP'
<?php
# T4.2 double-run : overrides G9 (réversibles — supprimer ce fichier à la bascule T4.6).
$CONFIG = array (
'trusted_domains' => array (0 => 'nextcloud.folzocorp.fr', 1 => '192.168.1.24', 2 => '192.168.1.24:8080', 3 => 'cloud.folzocorp.fr'),
'overwritehost' => 'cloud.folzocorp.fr',
'overwriteprotocol' => 'https',
'overwrite.cli.url' => 'https://cloud.folzocorp.fr',
# /12 = réseau bridge Docker (IP du conteneur edge, dynamique) ; /24 = accès direct LAN (double-run).
'trusted_proxies' => array (0 => '192.168.1.0/24', 1 => '172.16.0.0/12'),
);
PHP
$SSH_G9 "sudo chown -R 33:33 $G9_DIR/data $G9_DIR/config && sudo chmod -R u+rwX $G9_DIR/data $G9_DIR/config"
$SSH_G9 "cd $G9_DIR && docker compose up -d nextcloud && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'"
log " occ status / upgrade"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ status' www-data < /dev/null 2>&1 | head -8"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ upgrade' www-data < /dev/null 2>&1 | tail -6"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ maintenance:repair --include-expensive' www-data < /dev/null 2>&1 | tail -4"
# ---------------------------------------------------------------- 5. Collabora
log "=== 5/6 COLLABORA ==="
$SSH_G9 "cd $G9_DIR && docker compose up -d collabora && sleep 25 && curl -sf -o /dev/null -w 'collabora discovery HTTP %{http_code}\n' http://127.0.0.1:9980/hosting/discovery"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ config:app:set richdocuments wopi_url --value=http://192.168.1.24:9980 && php occ config:app:get richdocuments wopi_url' www-data < /dev/null 2>&1 | tail -2"
# ---------------------------------------------------------------- 6. vérification
log "=== 6/6 VÉRIFICATION ==="
$SSH_G9 "curl -sf -o /dev/null -w 'nextcloud status.php HTTP %{http_code}\n' http://127.0.0.1:8080/status.php"
$SSH_G9 "cd $G9_DIR && docker compose exec -T nextcloud su -s /bin/sh -c 'php occ user:list' www-data < /dev/null 2>&1 | grep -c '^\s*-'"
$SSH_XP "sudo $XP_DOCKER ps --format '{{.Names}} {{.Status}}' | grep -iE 'nextcloud'"
journal "Nextcloud importé + lancé sur le G9 (+ Collabora)" "T4.2 (double-run G4)" "data 1.9 Go + config + custom_apps + mysqldump sha256 identiques ; occ status/upgrade OK ; Collabora discovery OK ; wopi_url → G9 ; source relancée" "docker compose -f $G9_DIR/docker-compose.yml down + rm -rf $G9_DIR ; .25 intact"
log "Terminé."
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# 07d-migrate-photoprism.sh — T4.3 : PhotoPrism sur le G9, photos COPIÉES (décision #3).
#
# Vérité terrain mesurée 2026-10-11 : il n'y a **aucun conteneur ni image PhotoPrism sur .25**
# (`docker ps -a` : rien, `docker images` : rien). PhotoPrism n'est donc pas « migré » :
# il est **installé** sur le G9 à partir des photos du NAS (/volume1/photo = 530 Go,
# 305 059 fichiers media, 2002→2026), avec un index neuf (décision #4 : indexation progressive).
#
# Transport : le sshd de DSM refuse rsync --server et scp (voir journal 2026-10-11) →
# flux tar sur SSH, relayé par la machine de pilotage : .25 → pilot → .24.
#
# Intégrité : manifeste **métadonnées** (chemin + taille, LC_ALL=C) source vs G9, totaux
# (nb fichiers, octets), plus échantillon sha256 de 200 fichiers.
#
# Usage : sur la machine de pilotage (détenant la clé), ./07d-migrate-photoprism.sh
# Étape longue (530 Go). `SKIP_COPY=1` pour ne redéployer que la stack.
# Rollback : `docker compose -f /data/docker/photoprism/docker-compose.yml down` +
# `rm -rf /data/docker/photoprism` ; .25 intact (lecture seule sur /volume1/photo).
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_PHOTO="/volume1/photo"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
BRIQUE="photoprism"
G9_DIR="/data/docker/$BRIQUE"
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
KIT_ON_G9="/home/folzo/g9-migration"
JOURNAL="$KIT_DIR/journal.md"
STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}"
MIN_FREE_GB=600
INDEX_TIMEOUT="${INDEX_TIMEOUT:-3600}"
SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST"
RSH_G9="ssh -i $KEY"
log() { echo -e "\n[photoprism] $*"; }
journal() {
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' \
"$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$1" "$2" "$3" "$4" >> "$JOURNAL"
}
meta_manifest() { ( cd "$1" && find . -type f -printf '%p\t%s\n' 2>/dev/null | LC_ALL=C sort ); }
# ---------------------------------------------------------------- 0. préflight
log "=== 0/5 PRÉFLIGHT ==="
$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar .25"; exit 1; }
$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; }
$SSH_XP "sudo test -d $XP_PHOTO" || { echo "ERREUR : $XP_PHOTO absent sur .25"; exit 1; }
FREE_GB=$($SSH_G9 "df -BG --output=avail /data | tail -1 | tr -dc '0-9'")
[[ "$FREE_GB" -ge "$MIN_FREE_GB" ]] || { echo "ERREUR : espace /data insuffisant (${FREE_GB} Go < ${MIN_FREE_GB} Go)"; exit 1; }
log "espace /data = ${FREE_GB} Go"
mkdir -p "$STAGE"
SRC_COUNT=$($SSH_XP "sudo find $XP_PHOTO -type f | wc -l")
SRC_BYTES=$($SSH_XP "sudo find $XP_PHOTO -type f -printf '%s\n' | awk '{s+=\$1} END {print s}'")
log "source .25 : $SRC_COUNT fichiers, $SRC_BYTES octets"
# ---------------------------------------------------------------- 1. backup G2
log "=== 1/5 BACKUP restic (G2) ==="
if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then
log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)"
else
$SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2"
journal "backup restic pré-migration" "Garde-fou G2 avant import PhotoPrism" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
fi
# ---------------------------------------------------------------- 2. copie des photos
if [[ "${SKIP_COPY:-0}" != "1" ]]; then
log "=== 2/5 COPIE $XP_PHOTO → $G9_DIR/originals (tar sur SSH, relais pilot) ==="
$SSH_G9 "sudo mkdir -p $G9_DIR/originals $G9_DIR/storage $G9_DIR/import && sudo chown -R $G9_USER:$G9_USER $G9_DIR"
# MariaDB = uid 999 : datadir à lui (sinon « Permission denied » sur db.opt).
$SSH_G9 "sudo mkdir -p $G9_DIR/db && sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
t0=$(date +%s)
# Progression : tar GNU du G9 émet un pointage sur stderr tous les 50 000 enregistrements
# (stderr relayé par ssh) → la copie de 530 Go n'est pas silencieuse pendant des heures.
if ! $SSH_XP "sudo tar cz -C '$XP_PHOTO' ." | $SSH_G9 "tar xz --checkpoint=50000 --checkpoint-action=echo='[copie] %n enregistrements' -C $G9_DIR/originals"; then
echo "ERREUR : flux tar interrompu"; exit 1
fi
t=$(( $(date +%s) - t0 ))
log " copie terminée en ${t} s"
journal "copie photos .25 → G9" "530 Go, source en lecture seule (aucune écriture sur .25)" "tar/ssh relayé par le pilot, ${t} s ; $SRC_COUNT fichiers source" "rm -rf $G9_DIR/originals (source .25 intacte)"
fi
# ---------------------------------------------------------------- 3. intégrité
log "=== 3/5 INTÉGRITÉ (manifeste métadonnées + échantillon sha256) ==="
$SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort\"" > "$STAGE/src.meta"
$SSH_G9 "cd '$G9_DIR/originals' && find . -type f -printf '%p\t%s\n' | LC_ALL=C sort" > "$STAGE/g9.meta"
DST_COUNT=$(wc -l < "$STAGE/g9.meta")
DST_BYTES=$(awk -F'\t' '{s+=$2} END {print s}' "$STAGE/g9.meta")
log " source=$SRC_COUNT fichiers / $SRC_BYTES o — G9=$DST_COUNT fichiers / $DST_BYTES o"
[[ "$SRC_COUNT" == "$DST_COUNT" && "$SRC_BYTES" == "$DST_BYTES" ]] || { echo "ERREUR : compte/taille différents"; diff "$STAGE/src.meta" "$STAGE/g9.meta" | head -20; exit 1; }
diff "$STAGE/src.meta" "$STAGE/g9.meta" > "$STAGE/meta.diff" || { log "❌ chemins/tailles différents :"; head -20 "$STAGE/meta.diff"; exit 1; }
log " ✅ manifeste métadonnées identique ($DST_COUNT fichiers)"
SAMPLE=$(shuf -n 200 "$STAGE/g9.meta" | cut -f1)
printf '%s\n' "$SAMPLE" > "$STAGE/sample.txt"
n=$(wc -l < "$STAGE/sample.txt")
SRC_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_XP "sudo sh -c \"cd '$XP_PHOTO' && xargs -d '\n' sha256sum\"" | awk '{print $1}' | LC_ALL=C sort)
DST_SHA=$(printf '%s\n' "$SAMPLE" | $SSH_G9 "cd '$G9_DIR/originals' && xargs -d '\n' sha256sum" | awk '{print $1}' | LC_ALL=C sort)
if [[ "$SRC_SHA" == "$DST_SHA" ]]; then
log " ✅ échantillon sha256 : $n/$n fichiers identiques"
else
log "❌ échantillon sha256 divergent"; diff <(echo "$SRC_SHA") <(echo "$DST_SHA") | head -10; exit 1
fi
journal "intégration PhotoPrism" "copie de 530 Go à vérifier avant indexation" "manifeste chemins+tailles identique ($DST_COUNT fichiers, $DST_BYTES o) + échantillon sha256 $n/$n" "re-copie ciblée via tar/ssh"
# ---------------------------------------------------------------- 4. stack + secrets
log "=== 4/5 STACK + SECRETS ==="
rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/"
rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/"
$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null"
$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets"
$SSH_G9 "sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
# PhotoPrism n'accepte pas les *_FILE : env_file construit à partir des secrets (600, root, hors git).
$SSH_G9 "sudo sh -c 'umask 077; { echo PHOTOPRISM_ADMIN_PASSWORD=\$(cat $G9_DIR/secrets/admin); echo PHOTOPRISM_DATABASE_PASSWORD=\$(cat $G9_DIR/secrets/dbpass); } > $G9_DIR/secrets/photoprism.env' && sudo chown root:root $G9_DIR/secrets/photoprism.env"
$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'"
# ---------------------------------------------------------------- 5. indexation + vérifs
log "=== 5/5 INDEXATION (progressive, décision #4) + VÉRIFS ==="
$SSH_G9 "curl -sf -o /dev/null -w 'photoprism HTTP %{http_code}\n' http://127.0.0.1:2342/api/v1/session/health"
$SSH_G9 "cd $G9_DIR && timeout $INDEX_TIMEOUT docker compose exec -T photoprism photoprism index 2>&1 | tail -6 < /dev/null || true"
$SSH_G9 "cd $G9_DIR && docker compose exec -T photoprism sh -c 'photoprism count 2>/dev/null | head -12' < /dev/null || true"
journal "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"
echo
log "TERMINÉ. Prochaine étape : laisser monter l'index (cron/kommand), puis T4.5 (n8n)."
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
#
# 07e-deploy-edge.sh — T4.6 : déploiement de la brique edge (Caddy, reverse proxy LAN + HTTPS interne)
# Idempotent. Lancer depuis le PILOT (la clé SSH doit être lisible par l'utilisateur qui
# exécute — jamais `sudo` global : sudo remplace $HOME → clé injoignable, cf. journal 08:42).
#
# Garde-fous : G4 (aucune action destructive sur .25 — brique neuve, aucun import),
# G6 (ufw : 443 seul exposé WAN, ports briques LAN-only), G8 (journalisation),
# G9 (aucun secret dans git — le Caddyfile n'en contient aucun).
#
# Fait :
# 1. rsync du kit (compose/edge) vers le G9, validate du Caddyfile par Caddy lui-même
# 2. rsync sudo vers /data/docker/edge + `docker compose up -d`
# 3. ufw : 443/tcp exposé WAN (Anywhere) + 80/tcp redirect ACME ; 9980 (Collabora) LAN-only
# 4. vérifications : discovery Collabora, /hosting/capabilities, status.php, healthz Gitea,
# /system/info Jellyfin, /alive Vaultwarden — via HTTPS + SNI (curl -k)
#
# Rollback : `docker compose -f /data/docker/edge/docker-compose.yml down` +
# `ufw delete allow 443` ; les briques restent joignables en http direct (ports LAN).
set -euo pipefail
G9_HOST="192.168.1.24"
G9_USER="folzo"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
SSH_G9="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new $G9_USER@$G9_HOST"
RSH_G9="ssh -i $SSH_KEY -o StrictHostKeyChecking=accept-new"
KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
KIT_ON_G9="/home/folzo/g9-migration"
G9_DIR="/data/docker/edge"
log() { echo -e "\n[edge] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 4 | %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$KIT_DIR/journal.md"
}
log "0. préflight"
$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24"; exit 1; }
$SSH_G9 "test -d $KIT_ON_G9/compose" || { echo "ERREUR : kit absent sur le G9 (rsync du kit d'abord)"; exit 1; }
log "1. kit → G9 + validate du Caddyfile"
rsync -a -e "$RSH_G9" "$KIT_DIR/compose/edge/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/edge/"
$SSH_G9 "docker run --rm -v $KIT_ON_G9/compose/edge/Caddyfile:/etc/caddy/Caddyfile:ro caddy:2 caddy validate --config /etc/caddy/Caddyfile" > /dev/null \
&& log " ✅ Caddyfile validate OK" || { echo "ERREUR : Caddyfile invalide"; exit 1; }
log "2. installation /data/docker/edge + up"
$SSH_G9 "sudo mkdir -p $G9_DIR && sudo rsync -a $KIT_ON_G9/compose/edge/ $G9_DIR/ && sudo docker compose -f $G9_DIR/docker-compose.yml up -d" | tail -3
sleep 6
log "3. ufw : 443 exposé WAN, 80 redirect, 9980 LAN-only"
$SSH_G9 "sudo ufw status | grep -q '443/tcp' || sudo ufw allow 443/tcp comment 'edge T4.6 (seul port exposé WAN)'"
$SSH_G9 "sudo ufw status | grep -q '80/tcp' || sudo ufw allow 80/tcp comment 'edge T4.6 (redirect ACME)'"
$SSH_G9 "sudo ufw status | grep -q '9980/tcp' || sudo ufw allow from 192.168.1.0/24 to any port 9980 proto tcp comment 'collabora (LAN-only)'"
$SSH_G9 "sudo ufw reload >/dev/null; sudo ufw status numbered" | grep -E "443|80/tcp|9980"
log "4. vérifications HTTPS (SNI) via edge"
for h in pass.folzocorp.fr:/alive git.folzocorp.fr:/api/healthz cloud.folzocorp.fr:/status.php media.folzocorp.fr:/system/info n8n.folzocorp.fr:/healthz; do
host="${h%%:*}"; path="${h##*:}"
code=$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 8 "https://$host$path" --resolve "$host:443:192.168.1.24" 2>/dev/null || echo "ERR")
log " $host$path → $code"
done
code=$(curl -sk -o /dev/null -w '%{http_code}' --connect-timeout 8 "https://192.168.1.24/hosting/capabilities" 2>/dev/null || echo ERR)
log " 192.168.1.24 (SNI IP) → $code"
journal "edge : déploiement Caddy (T4.6)" "Reverse proxy LAN + HTTPS interne (CA interne) pour vaultwarden/nextcloud/gitea/jellyfin/n8n/matrix" "Caddyfile validate OK ; conteneur edge up ; ufw 443/tcp exposé WAN (seul), 80 redirect, 9980 LAN-only ; HTTPS+SNI vérifiés sur les 5 briques" "docker compose -f $G9_DIR/docker-compose.yml down ; ufw delete allow 443 (briques joignables en http direct LAN)"
log "Terminé."