Phase 4 : kit G9 (scripts + compose) + Gitea T4.4 backend Postgres

- compose/gitea : SQLite -> Postgres (source = paquet App Center DSM, gitea_db 13 repos/3 users), image épinglée 1.27.3, push-to-create
- scripts/07a-migrate-gitea.sh : export pg_dump (MVCC, .25 lecture seule) + tar/ssh (sshd DSM refuse rsync/scp) + manifestes sha256
- scripts/03b-secrets.sh : secrets gitea ajoutés
- journal.md : entrées Phase 4
This commit is contained in:
Effi
2026-10-11 07:45:00 +02:00
commit c246427cf5
39 changed files with 6131 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
# Compte rendu — PHASE 1 : Prérequis & sécurisation
**Date de clôture :** 2026-10-10
**Statut :** ✅ TERMINÉE — 12/12 critères validés, GO utilisateur donné, tâche Kanban « PHASE 1 » déplacée en Done.
**Traçabilité complète :** `g9-migration/journal.md` (18 entrées, 6 colonnes, G8).
---
## 1. Périmètre et objectifs
| Étape Kanban | Objectif | Résultat |
|---|---|---|
| T1.1 | Sécuriser le xpenology (.25) : stateful hors Watchtower, CLEANUP=false, notifications | ✅ Fait sur .25 **et** répliqué sur .24 |
| T1.2 | Inventaire des stateful xpenology + plan d'export | ✅ `INVENTAIRE-STATEFUL.md` |
| T1.3 | Thermique Freebox < 70 °C | ⏭️ Hors périmètre agent (action manuelle utilisateur, hors G10 sans validation) |
| T1.4 | J5040 edge / délestage avant formatage | ✅ Stack IA迁移 .11, Valheim + n8n/notes sur G9, OpenFox .27→.11, GO formatage |
Charte G1–G10 respectée : aucune suppression de données source (G4), backup vérifié avant toute migration (G2), aucune action destructive (G3 — formatage SSD exécuté **par l'utilisateur**), journal tenu (G8), aucun secret commité (G9).
---
## 2. T1.1 — Durcissement Watchtower (.25 + .24)
### T1.1-a · Inventaire conteneurs .25
- 30 conteneurs (`sudo docker ps -a` DSM), cross-check 12 projets compose `/volume1/docker` + conteneurs GUI.
- Livrable : `conteneurs-xpenology.csv` (name, image:tag, mode GUI/compose, stateful, label, data_paths).
- 22 stateful, 2 stateless (flaresolverr, mini-qr), 1 watchtower. 15 yml compose récupérés.
### T1.1-b · Labels `watchtower.enable=false`
- **.24** : n8n, couchdb, obsidian, valheim → label `false` (`docker inspect`), watchtower `true`.
- **.25** : **28/28 stateful = false**, vérifié par boucle `docker inspect` (0 exception).
- 13 projets compose : labels dans les yml + `compose up -d` (backups `.bak-labels` sur le NAS, InfluxDB recréé).
- 8 conteneurs GUI (postgres16, vaultwarden-server-1, qbittorrent, sonarr, radarr, jackett, prowlarr, deluge) : `docker update --label-add` **indisponible sur docker 24.0.2 (DSM)** → clonage via API unix socket : stop → rename `-old-lbl` → create avec `Labels` **au niveau racine du payload** (les labels nichés dans `Config` sont épurés par le daemon Synology) → start. Données intactes (mêmes binds/env), restauration automatique en cas d'échec.
- Incident résolu en cours de route : accès .25 bloqué (clé retirée + compte `effi` bloqué par Synology) → déblocage par l'utilisateur, nouvel accès via compte `Master` port 222.
### T1.1-c · Watchtower durci (digest-pinné)
- Image : `nickfedor/watchtower@sha256:45b54f6dc15227cfb38dbb826f2881aa0f56aaf0c255cdbb38bd80e598bdca9a` (1.23.0 ; digest vérifié = `:1`/`:1.23`/`:latest` via API registry — `containrrr/watchtower:1` n'existe pas).
- Env sur .24 **et** .25 : `CLEANUP=false`, `INCLUDE_STOPPED=false`, `POLL_INTERVAL=3600`, `LABEL_ENABLE=true`, label `true`.
- .25 était `containrrr/watchtower:latest` CLEANUP=true (dangereux) → remplacé ; projet compose `watchpower` (`COMPOSE_PROJECT_NAME` requis).
- Contrainte fork nickfedor : refuse `SCHEDULE` + `POLL` simultanés → POLL 3600 conservé.
### T1.1-d · Notifications Watchtower → n8n → Matrix
- Chaîne : watchtower (shoutrrr generic) → n8n .24 workflow `w1pfhX0A7KvaDrtk` (Webhook → Build log → Matrix → Respond) → Synapse .24:8008, salon ops `!mp7MFx9QbrJ0ARtzw…`, user `ops-bot`.
- **Preuves** : notifs .24 (exec #10, #31, #32) + **notif startup réelle du watchtower .25 reçue** (« Watchtower 1.23.0 using Docker API v1.43 … Next scheduled run 02:48 »), event_ids vérifiés dans `event_json` de Synapse.
- Correctif : payload shoutrrr generic = `title`/`message` (le workflow lisait `event_name`/`image_name` → affichait « undefined »).
- Points techniques notés : n8n v2.42 exige publication du workflow ; URL Matrix en expression full `={{ }}` ; txnId = image+timestamp (évite les doublons).
---
## 3. T1.2 — Inventaire stateful .25
Livrable : `INVENTAIRE-STATEFUL.md` — pour chaque stateful : image:tag, data paths, taille `du -sh` mesurée, type DB, mode GUI/compose, dépendances, méthode d'export et destination (G9 / .11 / .25 / supprimé).
Tailles clés : NextCloud 2.3G, valheim 2.8G, openwebui 4.7G, Joplin 706M, Leantime 298M, AFFiNE 94M, postgres16 70M, n8n 75M, gitea 2M, vaultwarden 8.8M, beszel 11M, InfluxDB 188K.
Vigilances relevées : conteneurs GUI sans CLI labels (passage par API), flux .25→.11 (AFFiNE/OpenWebUI → LLM proxy), credentiels DB d'origine conservés à l'import (les seeds G9 sont ignorés sur base initialisée — documenté dans `07-migrate-stateful.sh`).
---
## 4. T1.4 — J5040 (.27) : délestage, migrations, GO formatage
### Backups (G2, avant toute migration)
- Valheim : world 53.6M + backups + env + pterodactyl.tar 4.7G → Folzo Save, SHA256 vérifiés.
- Inventaire complet J5040 : 34 conteneurs, 116 images, dumps DB (dify 671M, n8n sqlite, pterodactyl wings 4.7G), volumes nommés (open-webui 3.4G, whisper-cache 2.0G), home 1.8G, cron 6 jobs → **12 Go sur `.11:/run/media/folzo/Folzo Save/migration-j5040/` avec SHA256SUMS.txt** (`sha256sum -c` : 23/23 « Réussi » — l'unique « Échec » est l'auto-référence de SHA256SUMS.txt).
- OpenFox : `openfox-share.tar.gz` 942M + `node-bin.tar.gz` 52M archivés + SHA256 vérifiés.
### Migrations réalisées
| Cible | Contenu | Preuve |
|---|---|---|
| PC IA .11 | Stack IA complète (22 images save/load) : open-webui, searxng, pipelines, qdrant (6 collections), couchdb, mcpo, tts, dify (14 healthy), llm-proxy 3011/3012/3017, ollama 28 modèles, whisper large-v3 | Test LLM bout en bout → « MIGRATION-OK » ; endpoints 200 |
| G9 .24 | **Valheim** : monde restauré, 37 chunks sha256 identiques source, conteneur healthy, UDP 2456 joignable | Double-run G4 : source redémarrée healthy |
| G9 .24 | **n8n + Obsidian + CouchDB** : `database.sqlite` sha `693ee229` IDENTIQUE source, vault 79M | n8n 5678→200, couchdb 5984→200, obsidian 3100 OK |
| PC IA .11 | **OpenFox** (66 sessions, thread préservé) : systemd `openfox.service`, HTTP 200 sur .11:10369, symlink `/home/effi→/home/folzo`, SELinux `chcon -R -t bin_t` | Thread de migration repris sur .11 après bascule proxy |
### T1.4-e · GO utilisateur
Vérification **sans .27** (éteint) : OpenFox .11 200, IA .11 200, n8n/couchdb/obsidian/Matrix .24 OK, NAS .25 31/31 Up + labels 28/28 false + watchtower digest healthy. Backup 24 Go SHA256 23/23. **GO donné par l'utilisateur — formatage SSD J5040 exécuté par l'utilisateur lui-même** (conforme G3).
---
## 5. Livrables produits
| Fichier | Contenu |
|---|---|
| `g9-migration/journal.md` | 18 entrées 6 colonnes (G8), toutes les actions Phase 1 |
| `g9-migration/conteneurs-xpenology.csv` | Inventaire 30 conteneurs .25 |
| `g9-migration/INVENTAIRE-STATEFUL.md` | Inventaire stateful + plan d'export (base Phase 4) |
| `g9-migration/INVENTAIRE-J5040.md` | Inventaire J5040 avant formatage |
| Backups `.11:/run/media/folzo/Folzo Save/migration-j5040/` | 24 Go, SHA256SUMS vérifié |
| `/tmp/nas-yml/` (session) | yml compose .25 modifiés + `.orig` (répliqués sur le NAS avec `.bak-labels`) |
## 6. Améliorations post-revue (5 findings, tous résolus)
1. `README.md` racine : ligne ajoutée pour `g9-migration/README.md` dans le tableau de lecture.
2. `01-hardening.sh` : garde anti-lockout SSH (refuse `PasswordAuthentication no` si `authorized_keys` vide).
3. `07-migrate-stateful.sh` : multiplexage SSH (`ControlMaster/ControlPath/ControlPersist`) → 1 seule session par brique au lieu de 4+ ; pré-test `command -v rsync` sur le NAS.
4. `.gitignore` : règle no-op `!*.env.example` supprimée.
5. `restic.env.example` : suppression du faux backend `smb://` ; voies supportées documentées (sftp:, rclone `smb:`, montage CIFS).
---
## 7. Restes et points d'attention pour la Phase 2
- **T1.3 (thermique Freebox)** : non traitée par l'agent (matériel, hors périmètre) — à valider manuellement par l'utilisateur (< 70 °C en charge).
- **Cron BomberFox** (J5040) : spécifique J5040, à reconfigurer après formatage.
- **Proxy inverse openfox.folzocorp.fr** : bascule .27→.11 à confirmer définitivement côté utilisateur.
- **Clés SSH** : la clé `.25` utilisée est `id_ed25519_openfox11` (nouvelle, ajoutée aux `authorized_keys` du compte Master) ; les clés de l'ancien bundle .27 sont à reconstituer/archiver.
- Les imports Phase 4 conservent les **credentiels DB d'origine** du xpenology (documenté).
- Rollback global Phase 1 : rien de destructif sur les sources (.25 lecture seule + labels réversibles via `.bak-labels`, .27 éteint intact, backups Folzo Save).
## 8. Dépendances levées pour la suite
Phase 2 (Base G9) peut démarrer : docker est déjà opérationnel sur .24 (T2.3 de fait), durcissement SSH/ufw/fail2ban reste à faire (scripts `01-hardening.sh`, `02-disk-luks.sh`, `03-docker.sh` prêts et conformes à la charte).
+45
View File
@@ -0,0 +1,45 @@
# Inventaire J5040 → destinations (.11 / .24)
Backup complet : `.11:/run/media/folzo/Folzo Save/migration-j5040/` (12 Go, SHA256SUMS.txt vérifié, tars testés lisibles).
## Plateforme IA → PC IA .11
| Service J5040 | Image | Data (source J5040) | Backup |
|---|---|---|---|
| Dify (14 conteneurs) | dify:0.15.1 | `RAG/dify/docker/volumes/{db,pgvector,redis,app/storage,sandbox,plugin_daemon}` + `.env` | `platform/dify-compose.tar` |
| Open WebUI | open-webui:main | volume `effi_open-webui-data` | `platform/effi_open-webui-data.tar` (3.4 G) |
| Pipelines | pipelines:main | volume `effi_pipelines-data` | `platform/effi_pipelines-data.tar` |
| Whisper | faster-whisper:latest-cpu | volume `effi_whisper-cache` | `platform/effi_whisper-cache.tar` (2.0 G) |
| Valkey/Redis 2020 | valkey:8 | volume `valkey-data` | `platform/valkey-data.tar` |
| SearXNG | searxng:latest | bind `effi/searxng-data` (settings.yml +密钥) | `notes/effi-data.tar` + `config/searxng-settings.yml` |
| llm-proxy | effi-llm-proxy | build local, sans data | image `llm-proxy` + dockerfile |
| MCP-O | mcporter-idealabs | .env secrets | `config/effi-compose.yml` |
| OCR | effi-ocr-effi-ocr | bind .env OCR_SECRET | `config/ocr-compose.yml` |
| Hermes CLI | nosmo/hermes:2026.5.23.1 | bind `effi/openfox-hermes` | `notes/effi-data.tar` |
| Hermes WebUI | nesquena/hermes-webui | bind `/home/effi/.hermes` | `home/home-projects.tar` (.config) |
| Open Voice / TTS / Beszel / leantime-mcp | — | `effi/{open-voice,tts,beszel}` beszel db 32K | `notes/effi-data.tar` |
## Notes & automatisation → G9 .24
- n8n : `effi/n8n-data` (2.7 Mo, `database.sqlite`) → `notes/effi-data.tar`
- Obsidian (bind mount `effi/obsidian` = dossier partagé G9 déjà existant sur `.24:/home/folzo/Documents/Obsidian/notes` — S13.2, pas de backup redondant nécessaire)
- CouchDB `/home/folzo/obsidian/couchdb` sur .24 (déjà en place S13.3)
- Cron root : aucun. Cron effi : 6 jobs `*/15 * * * *` → `config/crontab-effi.txt`
## Pterodactyl → supprimé (S3.2)
- `/home/effi/pterodactyl` (4.7 G) backup au cas où : `pterodactyl/pterodactyl.tar`
- Panel + DB + cache + Valheim → **Ne PAS recréer**. Valheim seul migré (fait, voir 2026-10-09).
## Inventaires bruts
- `config/j5040-inventory.txt` : 34 conteneurs (name, image, state, mounts)
- `config/mounts-map.txt` : mapping complet bind/volume
- `config/images.txt` : 116 images
- `config/effi-compose.yml`, `config/hermes-compose.yml`, `config/ocr-compose.yml`, `config/effi-env` (secrets .env)
## Dépendances réseau J5040 (récits 2026-09-30)
- `172.20.0.0/16` → `172.20.0.10` proxy sur G9 (SearXNG)
- `llm-5040` réseau interne, `127.0.0.1:8080`
- `host.docker.internal → PC-IA (192.168.1.11)` pour Hermes
+55
View File
@@ -0,0 +1,55 @@
# Inventaire des conteneurs stateful xpenology .25 (Folzo-NAS DS240j)
> Source : `conteneurs-xpenology.csv` + 15 compose yml récupérés via `ssh cat` (avant blocage SSH .25, 2026-10-09).
> Périmètre T1.2-a : chaque conteneur de `docker ps -a` (30) est couvert ci-dessous.
> Destinations = **recommandations à valider au Go/No-Go T1.4-e** (user). Tailles = `du -sh` mesurées sur .25 le 2026-10-09.
## Couverture : 30 conteneurs de `docker ps -a`
Stateful (22) — 21 via compose, 7 via GUI DSM (labels applicables uniquement via GUI).
| # | Conteneur | Image:tag | Mode | Data path | Taille | Type DB | Dépendances | Export | Destination |
|---|---|---|---|---|---|---|---|---|---|
| 1 | n8n | docker.n8n.io/n8nio/n8n:latest | compose | `/volume2/docker-REC/n8n/data` + `files` | 75 M (project) | SQLite (`database.sqlite`) | n8n-postgresql | tar des 2 dirs | **G9 .24 — migré T1.4-c ✅** (sha 693ee229 identique) |
| 2 | n8n-postgresql | postgres:17 | compose | `/volume2/docker-REC/n8n/postgresql` | ~40 M inclus ci-dessus | Postgres | n8n | `pg_dump` | **G9 .24 — migré** (n8n .24 = SQLite, PG non migré, redondance) |
| 3 | joplin-server | joplin/server:latest | compose | via joplin-db | — | client Joplin | joplin-db | API Joplin export | G9 .24 (stack notes) |
| 4 | joplin-db | postgres:16 | compose | `/volume2/docker-REC/Joplin/postgres` | 706 M (project) | Postgres | joplin-server | `pg_dump --format=custom` | G9 .24 |
| 5 | postgres16 | postgres:16 | compose | `/volume1/docker/postgres16/data` (+init) | 70 M | Postgres (infra partagée) | clients externes | `pg_dumpall` | **.25 reste** (infra NAS) |
| 6 | affine_server | ghcr.io/toeverything/affine:stable | compose | `${UPLOAD_LOCATION}` (storage) + `${CONFIG_LOCATION}` (config) | 94 M (project) | client AFFiNE | affine_postgres, affine_redis, LLM `http://192.168.1.25:3011/v1` | tar dirs + AFFiNE export | .25 reste (dépend LLM proxy .25) |
| 7 | affine_postgres | pgvector/pgvector:pg16 | compose | `${DB_DATA_LOCATION}` | inclus ci-dessus | Postgres+pgvector | affine_server | `pg_dump` | .25 reste |
| 8 | affine_redis | redis:latest | compose | volume nommé | négligeable | Redis (cache) | affine_server | RDB snapshot | .25 reste |
| 9 | nextcloud_app | nextcloud:latest | compose | `/volume2/docker-REC/NextCloud/data` + `config` | 2.3 G (project) | client Nextcloud | nextcloud_db, nextcloud_redis, onlyoffice-ds | `occ export` + tar data | **.25 reste** (NAS = stockage fichiers) |
| 10 | nextcloud_db | mariadb:10.6 | compose | `/volume2/docker-REC/NextCloud/db` | inclus ci-dessus | MariaDB | nextcloud_app | `mysqldump` | .25 reste |
| 11 | nextcloud_redis | redis:latest | compose | `/volume2/docker-REC/NextCloud/redis` | quelques Mo | Redis (cache) | nextcloud_app | RDB | .25 reste |
| 12 | gitea | gitea/gitea:latest | compose | `/volume1/docker/Gitea/data` | 2 M | SQLite/Gitea | — | `gitea dump -c` | **.25 reste** |
| 13 | vaultwarden-server-1 | vaultwarden/server:latest | **GUI DSM** | `/volume1/docker/vaultwarden` | 8.8 M | SQLite (SQLite+env) | — | tar dir (RO, service stoppé) | **.25 reste** (secrets, critique) |
| 14 | leantime | leantime/leantime:latest | compose | `./data/{userfiles,public_userfiles,plugins,logs}` | 298 M (project) | client Leantime | leantime-db | tar dirs + API Leantime | G9 .24 (stack PM) |
| 15 | leantime-db | mysql:8.4 | compose | `./data/mysql` | inclus ci-dessus | MySQL | leantime | `mysqldump` | G9 .24 |
| 16 | beszel | henrygd/beszel:latest | compose | `./beszel_data` | 11 M | SQLite | beszel-agent, docker.sock | tar dir | **.25 reste** (monitoring NAS) |
| 17 | beszel-agent | henrygd/beszel-agent:latest | compose | `./beszel_agent_data` | quelques Mo | agent (socket) | beszel | tar dir | **.25 reste** |
| 18 | valheim | mbround18/valheim:latest | compose | `./saves` + `./server` + `./backups` | 2.8 G (project) | fichiers monde | — | tar des 3 dirs (fait J5040 → .11) | **G9 .24 — migré T1.4-b1 ✅** |
| 19 | openwebui | ghcr.io/open-webui/open-webui:latest | compose | `/volume1/docker/openwebui` | 4.7 G | SQLite (app data) | Ollama `192.168.1.10:11434` | tar dir | **PC IA .11 — migré T1.4-b ✅** |
| 20 | InfluxDB | influxdb:2.7 | compose | `/volume2/docker-REC/InfluxDB` + volume nommé | 188 K | InfluxDB v2 | beszel (metrics) | `influxd backup` | G9 .24 |
| 21 | onlyoffice-ds | onlyoffice/documentserver:latest | compose | `./data` + `./logs` + 2 volumes nommés | 160 K | DocumentServer | nextcloud_app | tar dirs | **.25 reste** (couplé NextCloud) |
| 22 | qbittorrent | linuxserver/qbittorrent:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/qbittorrent` | sessions/torrents | config qBittorrent | deluge, flaresolverr | tar config (fastresume) | **.25 reste** (NAS média) |
| 23 | sonarr | linuxserver/sonarr:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/Sonarr` | db SQLite | Sonarr | prowlarr, qbittorrent | `db backup` + tar | **.25 reste** |
| 24 | radarr | linuxserver/radarr:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/Radarr` | db SQLite | Radarr | prowlarr, qbittorrent | `db backup` + tar | **.25 reste** |
| 25 | jackett | linuxserver/jackett:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/Jackett` | IndexTor + API key | Jackett | Sonarr/Radarr | tar config (indexers.yml) | **.25 reste** |
| 26 | prowlarr | linuxserver/prowlarr:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/Prowlarr` | db SQLite + grants | Prowlarr | Jackett, Sonarr/Radarr | `db backup` + tar | **.25 reste** |
| 27 | deluge | linuxserver/deluge:latest | **GUI DSM** | `/volume2/docker-REC/Multimedia/Deluge` | state + auth | Deluge | qbittorrent | tar `state` | **.25 reste** |
| 28 | flaresolverr | flaresolverr/flaresolverr:latest | **GUI DSM** | volume nommé (cache) | cache jetzturbine | Stateless | Jackett/Sonarr | non requis | .25 reste (recréable) |
| 29 | mini-qr | ghcr.io/lyqht/mini-qr:latest | compose | (aucun bind, stateless) | — | Stateless | — | non requis | .25 reste (recréable) |
| 30 | watchtower | containrrr/watchtower:latest | compose | docker.sock (ro) | — | Stateless (n/a) | tous conteneurs .25 | n/a | **.25 durci T1.1-c en attente (bloqué)** |
## Récapitulatif des destinations (à valider Go/No-Go)
- **G9 .24** (déjà migré) : n8n+postgres, valheim, openwebui(.11) ✅ ; stack notes/PM cibles Joplin, Leantime, InfluxDB.
- **PC IA .11** : openwebui ✅, whisper/ocr/stt (doublons supprimés ✅).
- **.25 (NAS) reste** : media stack qbittorrent/sonarr/radarr/jackett/prowlarr/deluge (stockage NAS natif), NextCloud + onlyoffice (couplé fichiers NAS), Gitea, vaultwarden, postgres16, AFFiNE (dépend LLM proxy .25), beszel.
- **Supprimés** : Pterodactyl (S3.2, fait sur J5040), flaresolverr/mini-qr stateless (recréables sans backup).
## Points de vigilance
1. **7 conteneurs GUI** (vaultwarden, qbittorrent, sonarr, radarr, jackett, prowlarr, deluge) : labels watchtower **inapplicables en CLI DSM** (`docker update --label-add` absent sur docker DSM 24.0.2) → à mettre via **Panneau DSM → Container Manager** (labels par conteneur).
2. **Flux de dépendances** .25 → .11 : AFFiNE et OpenWebUI pointent sur LLM proxy .25 (port 3011) / Ollama .10. Si le proxy .25 disparaît au formatage J5040, ces services cassent → à recâbler sur .11 (`http://192.168.1.11:3011/v1`).
3. **Accès .25 perçu** (2026-10-09 11:43) : clé `id_ed25519_effi_server` retirée + compte `effi` bloqué par Synology. Les 15 yml durcis sont prêts dans `/tmp/nas-yml/` (labels `false` + watchtower pinné + CLEANUP=false) à pousser dès déblocage.
+121
View File
@@ -0,0 +1,121 @@
# Migration G9 — Kit d'exécution
**Objectif** : tout le matériel (scripts + configs + compose) prêt à exécuter la migration
xpenology → G9, phase par phase, quand le G9 sera branché (IP temporaire `.24`).
> **Règle d'or** : chaque script est **idempotent** et **sécurisé**. Les opérations
> destructives (partitionnement, LUKS, wipe) sont **bloquées** sans le drapeau
> `--i-understand` et la validation explicite de l'utilisateur (garde-fou G3).
## Charte d'autonomie (garde-fous G1–G10)
| # | Garde-fou |
|---|-----------|
| G1 | Lecture seule libre ; toute modif délibérée et documentée. |
| G2 | Aucune modif de données sans backup vérifié en place. |
| G3 | Op. destructives (`rm -rf`, `mkfs`, `wipe`, `dd`, partition, RAID/LUKS, suppression conteneur) = **validation explicite** avant exécution. |
| G4 | Double-run : xpenology = source de vérité ; aucune suppression dessus tant que le contrepartie G9 n'est pas validé. |
| G5 | IP G9 = `.24` tant que le xpenology n'est pas décommissionné. |
| G6 | Aucun changement réseau impactant d'autres appareils sans validation. |
| G7 | Chaque modif a un **rollback** documenté. |
| G8 | **Journalisation** de chaque action (quoi, pourquoi, résultat). |
| G9 | Aucun secret loggué ou commité. |
| G10 | Périmètre = G9 (+ lecture xpenology) ; autres machines sur validation. |
### Correspondance garde-fou → point d'application
| Garde-fou | Point d'application |
|-----------|---------------------|
| 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. |
| 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. |
| 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). |
## Arborescence
```
g9-migration/
├── README.md ← ce fichier (runbook)
├── journal.md ← journal d'exécution (G8) — à remplir pendant la migration
├── scripts/
│ ├── 01-hardening.sh ← T2.1 : SSH keys, ufw (LAN-only), unattended-upgrades, fail2ban
│ ├── 02-data.sh ← T2.2 : espace /data (NON destructif — LUKS abandonné 10/10/2026)
│ ├── 03-docker.sh ← T2.3 : Docker + compose + groupe docker
│ ├── 03b-secrets.sh ← T2.3b : bootstrap des fichiers secrets des briques (chmod 600)
│ ├── 04-smb.sh ← T2.4 : partage SMB (données famille)
│ ├── 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)
│ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF]
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
│ ├── vaultwarden/ ← T4.1
│ ├── nextcloud/ ← T4.2
│ ├── photoprism/ ← T4.3
│ ├── gitea/ ← T4.4
│ ├── n8n/ ← T4.5
│ ├── jellyfin/ ← T6.2
│ └── watchtower/ ← S11.4 : Watchtower sélectif
└── configs/
├── fail2ban.local.conf ← config fail2ban (T2.1)
└── restic.env.example ← template restic (T3.1) — REMPLIR les secrets
```
## Ordre d'exécution (phase par phase)
| Phase | Script / action | Garde-fou |
|-------|-----------------|-----------|
| 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 |
| 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 |
> **Note disque (décision 10/10/2026 — Option 0)** : le G9 est **mono-disque**
> (Samsung 990 Pro 2 To, **seul disque interne**, aucun RAID prévu). `02-data.sh`
> crée l'espace `/data` en **simple dossier** sur le LV racine existant :
> **ni partitionnement, ni LUKS, ni réinstallation** (LUKS abandonné — données
> familiales non critiques ; protection = ufw LAN-only + backups **restic chiffrés**,
> voir `DOSSIER-SOFTWARE.md` S9/S10 changelog 10/10/2026). La tolérance panne
> disque est assurée par le **3-2-1** (Phases 3/5) et le **spare G7** (Phase 7).
## Phase 3 — backup restic : état au 10/10/2026
Repo `rclone:freeboxftp:Freebox/backup/g9` (Freebox Delta, partage `Save/Freebox`,
1.79 To, 1.785 To libres). Snapshot initial `21de0787` : 4 564 fichiers,
6.956 GiB lus / **4.178 GiB stockés** (sources `/data`, `/home/folzo`, `/etc`).
- **Transport & append-only** : backend **rclone** de restic, remote FTP local
`freeboxftp:` = 192.168.1.254:21 (SMB `freebox:` en secours), **aucun montage** ;
append-only **prouvé**. Transports testés/écartés et limitations :
**source unique → `RESTORE-G9.md` §6**.
- **Test de restauration** : fichier restauré depuis la Freebox, sha256 identique
(`9352a905d9bb…`, 470 637 o, restore < 2 s) — voir `RESTORE-G9.md` §2/§5 et `journal.md`.
- **Automatisation** (cron root, logs `/var/log/restic-*.log`) : 03:30 backup (conteneurs
stateful stoppés ~1–3 min : valheim, n8n, synapse, couchdb, obsidian), 04:00 prune
(7 quotidiens / 4 hebdo / 12 mensuels / 5 annuels), 09:15 health (alerte si snapshot > 26 h).
Les 2 jobs ont été lancés à la main (rc=0, snapshot `cccdb2c7`).
- **Alerte Matrix** : **directe** via Synapse (`/etc/restic/matrix.env`, token ops-bot, salon
ops) — **testée**, messages réellement reçus. Voie de secours webhook n8n conservée
(`RESTIC_NOTIFY_WEBHOOK`) ; **accès n8n à recréer** (clé API à générer dans l'UI).
- **Persistance post-reboot** : validée par un vrai reboot (21:36) — `scripts/06c-postboot-check.sh`
= **15 contrôles OK / 0 KO** (binaires, env/clé/config en 600, **aucun montage requis**,
cron, jobs, repo lisible + `check` OK, conteneurs relancés). À rejouer après tout changement.
- **Secrets** : `/etc/restic/repo.key` (clé du repo, **jamais affichée** — **à consigner dans
Vaultwarden**, item « G9 – clé repo restic »), `/etc/restic/cifs.cred`, `/etc/restic/matrix.env`,
`/root/.config/rclone/{rclone,ftp}.conf`, `/etc/restic.env` → tous `600` root ; **aucun secret
dans le repo git** (`.gitignore` : `*.env`, `*.cred`, `*.key`).
## Convention
- Chaque script affiche ce qu'il fait, est idempotent, et écrit dans `journal.md`.
- Les secrets (mots de passe, clés restic) vont dans `configs/*.env` **hors git** (G9).
- Un script échoue → il s'arrête (`set -euo pipefail`) → on lit le journal → on corrige.
+144
View File
@@ -0,0 +1,144 @@
# Restauration G9 depuis la Freebox (T3.1 — AC5 / S10.4)
> **Règle** : un backup non testé n'est pas un backup. Ce document donne les commandes exactes.
> Testé le : **2026-10-10** (voir `journal.md`, ligne « Test restauration OK »). À re-tester **1×/trimestre**.
## 0. Ce qu'il faut avoir
| Élément | Où | Remarque |
|---------|-----|----------|
| Mot de passe du repo restic | `/etc/restic/repo.key` (G9, root, 600) — **consigné dans Vaultwarden** | **Sans lui, les backups sont irrécupérables** (G9) |
| Identifiants FTP Freebox | `/root/.config/rclone/rclone.conf` (G9, root, 600) — remote `[freeboxftp]`, secours `[freebox]` (SMB) | Serveur FTP activé dans Freebox OS ; copie FTP-only : `ftp.conf` |
| Config backup | `/etc/restic.env` (G9, root, 600) | `RESTIC_REPOSITORY=rclone:freeboxftp:Freebox/backup/g9` |
Toutes les commandes s'exécutent **en root sur le G9** (`sudo -i`), avec `restic` = `/usr/local/bin/restic` (version épinglée **0.19.1**).
## 1. Lister ce qui est sauvegardé
```bash
set -a; source /etc/restic.env; set +a
restic -r "$RESTIC_REPOSITORY" snapshots # tous les snapshots
restic -r "$RESTIC_REPOSITORY" ls latest | head # contenu du dernier snapshot
restic -r "$RESTIC_REPOSITORY" find 'rapport.pdf' # retrouver un fichier dans tous les snapshots
```
## 2. Restaurer **un fichier** (le cas du quotidien)
**restic 0.19** : les arguments positionnels de `restore` sont des **IDs de snapshot** (pas des
fichiers). On filtre avec `--include`, et les chemins sont **absolus** (tels que sauvegardés) :
```bash
restic -r "$RESTIC_REPOSITORY" restore latest --target /tmp/restore \
--include "/home/folzo/obsidian-vault/Notes/Exemple.md"
# le fichier restauré est dans : /tmp/restore/home/folzo/obsidian-vault/Notes/Exemple.md
```
Restaurer une **version antérieure** : remplacer `latest` par l'ID du snapshot voulu (vu par `snapshots`) :
```bash
restic -r "$RESTIC_REPOSITORY" restore 1a2b3c4d --target /tmp/restore \
--include "/home/folzo/obsidian-vault"
```
Vérifier ensuite : `cmp -s <original> <restauré>` ou `sha256sum` des deux.
> `restic -r "$RESTIC_REPOSITORY" ls latest` affiche l'arborescence du snapshot ; `find <motif>`
> retrouve un fichier dans tous les snapshots (et renvoie le chemin à passer à `--include`).
## 3. Restaurer **tout** (sinistre, disque neuf)
```bash
# 1) réinstaller Ubuntu Server + docker, créer /data, restaurer les fichiers de config
restic -r "$RESTIC_REPOSITORY" restore latest --target /rebuild
# -> /rebuild/data/... /rebuild/home/folzo/... /rebuild/etc/...
# 2) recoller les fichiers aux emplacements d'origine (rsync -a /rebuild/etc/ /etc/ par ex.)
# 3) conteneurs : docker compose up -d (compose versionné dans Gitea), puis vérifier les services
```
## 4. Contrôles d'intégrité
```bash
restic -r "$RESTIC_REPOSITORY" check # intégrité du repo (le plus complet)
restic -r "$RESTIC_REPOSITORY" check --read-data-subset=5% # lit 5 % des données (plus strict)
restic -r "$RESTIC_REPOSITORY" stats # volumétrie
```
## 5. Journalisation du test (à chaque test)
| Date | Fichier testé | Snapshot | Durée | Résultat |
|------|---------------|----------|-------|----------|
| 2026-10-10 | `/data/famille/Test/PXL_20261009_130502121.jpg_compressed.JPEG` (470 637 o) | `21de0787` puis `cccdb2c7` | **1.8 s** / **< 2 s** (restore + `sha256sum`) | ✅ sha256 `9352a905d9bb…` identique source/restore |
| 2026-10-10 (post-reboot) | repo entier | `snapshots` + `check --read-data-subset=1%` | ~4 s | ✅ 15 contrôles OK / 0 KO (`06c-postboot-check.sh`) |
Le test est **rejouable** : `sudo bash /home/folzo/g9-migration/scripts/06-restic-backup.sh`
(init → backup si besoin → preuve append-only → restauration testée → jobs/cron), idempotent.
## 6. Transport retenu, transports écartés, limitations — **section canonique**
> Les autres documents (README §Phase 3, `configs/restic.env.example`, en-tête de
> `06-restic-backup.sh`) pointent vers cette section ; ne pas y recopier le détail.
**Retenu** : backend **rclone** de restic sur le **FTP local de la Delta** (`freeboxftp:` =
192.168.1.254:21), config unique `/root/.config/rclone/rclone.conf` (600 root, `[freeboxftp]`
FTP + `[freebox]` SMB en secours). **Aucun montage** → rien à dépendre d'un mount au boot.
| Transport testé | Résultat |
|-----------------|----------|
| `mount.cifs` du noyau (vers 1.0, SMB1) | ❌ restic échoue : `chmod … no such file or directory` (chmod impossible sur le partage) → fstab + unité systemd retirées |
| `rclone mount` (FUSE) | ❌ sémantique correcte mais cache d'écriture → objets du repo perdus au redémarrage |
| rclone backend `smb` (1.60 d'apt) | ❌ backend absent ; rclone 1.75.2 fonctionne → gardé en **secours** (`rclone:freebox:…`) |
| FTPES distant (port 13366) | ❌ joignable uniquement depuis l'extérieur de la LAN |
| **FTP local (21) via rclone** | ✅ **retenu**, validé après reboot |
## 6.1 Limitations assumées (Phase 3)
- **Append-only = enforcement côté client** : il est imposé par le rclone local du G9
(`-o rclone.args="serve restic --stdio --append-only"`), **prouvé** le 10/10/2026 (`forget --prune`
refusé, rc=1, snapshots intacts). Un G9 rooté peut retirer l'option et supprimer le repo sur la
Freebox. La cible **WORM** (append-only **côté serveur**) est la **VM Freebox + `rest-server
--append-only`** exposé en **HTTPS/TLS** avec **`htpasswd`** (compte `g9-backup` écriture,
`g9-restore` lecture), prévue en **Phase 5** (autostart VM à valider — la Freebox ne fait pas
tourner restic). Repo cible `rest:https://…` à la place de `rclone:`.
- Conséquences pratiques : `restic unlock` tourne **sans** l'option append-only (les locks sont des
objets supprimables) ; le prune **refusé par append-only est le comportement attendu** → il est
**journalisé seulement** (pas d'alerte Matrix, sinon fatigue du canal), et l'alerte n'est envoyée
que si la purge **sans** append-only échoue aussi (rétention réellement bloquée).
- La copie 3 (hebdo, **218play**, via `restic copy`) arrive en Phase 5 : sans elle, G9 + Freebox restent
2 copies sur le même site.
- Le backup quotidien **arrête les conteneurs stateful** (valheim, n8n, synapse, couchdb, obsidian) pendant
~1 à 3 min à 03:30 (`stop → backup → start`, règle écrite dans les jobs `restic-daily.sh` et
`06-restic-backup.sh`) pour garder les bases sqlite/couchdb cohérentes. **Aucune suppression de la
source** (G4) : le backup est une lecture.
## 7. Alerte
Le job `restic-health.sh` (cron 09:15) envoie une alerte Matrix **« BACKUP KO : aucun snapshot depuis N h »**
si aucun snapshot ne date de moins de 26 h. `restic-daily.sh` alerte si le FTP est injoignable ou si
`restic backup` échoue. Contrôle manuel : `sudo /usr/local/bin/restic-health.sh`,
`sudo /usr/local/bin/restic-notify.sh "message de test"`.
- **Voie utilisée** : **Matrix en direct** via Synapse (`http://192.168.1.24:8008`, salon ops, token
ops-bot dans `/etc/restic/matrix.env`, 600 root) — **testée le 10/10/2026** : messages réellement
reçus dans le salon ops.
- **Voie de secours** : webhook n8n (`RESTIC_NOTIFY_WEBHOOK`, `POST /webhook/backup-alert`) — nécessite
un workflow n8n + une clé API : **accès n8n à recréer** (1 workflow existant, « Watchtower Notification »).
## 8. Restaurer depuis une autre machine (sinistre, G9 perdu)
Il faut **3 éléments** (sans le n°1, les backups sont illisibles) :
| # | Élément | Source |
|---|---------|--------|
| 1 | **Mot de passe du repo** | item Vaultwarden « G9 – clé repo restic » (copie de `/etc/restic/repo.key`) |
| 2 | Accès au partage | FTP/SMB de la Freebox, `Freebox/backup/g9` (identifiants du compte Freebox) |
| 3 | restic + rclone | `restic` 0.19.1 + `rclone` 1.75.2 (mêmes versions que le G9) |
```bash
export RESTIC_REPOSITORY=rclone:freeboxftp:Freebox/backup/g9 # remote rclone équivalent sur la machine de restore
export RESTIC_PASSWORD_FILE=/chemin/vers/repo.key
restic -r "$RESTIC_REPOSITORY" snapshots
restic -r "$RESTIC_REPOSITORY" restore latest --target /rebuild
```
Alternative sans rclone : `restic -r smb:freebox-server.local/Freebox/backup/g9` (backend SMB natif) ou
`restic -r ftp:192.168.1.254:21/Freebox/backup/g9` — mêmes données, même clé.
@@ -0,0 +1,70 @@
# Gitea — T4.4 (vault 2e cerveau + versioning des compose, G9)
# Stateful (repos + DB) → exclu de Watchtower (S11.4).
# C'est aussi le repo qui versionne tous les docker-compose.yml (S11.1).
#
# BACKEND = POSTGRES (corrigé 11/10/2026) : la source xpenology est le paquet App Center
# DSM « gitea » v1.27.3, DB postgres gitea_db (13 repositories, 3 users). Un import d'une
# base Postgres dans une installation SQLite est IMPOSSIBLE → le compose G9 doit être Postgres.
# Version épinglée sur la version source (1.27.3) : pas de migration de schéma surprise.
services:
gitea:
image: gitea/gitea:1.27.3
container_name: gitea
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
env_file:
- ./secrets/gitea.env # GITEA__database__PASSWD (600, hors git — G9)
environment:
- TZ=Europe/Paris
- USER_UID=1000
- USER_GID=1000
- GITEA__database__DB_TYPE=postgres
- GITEA__database__HOST=gitea-db:5432
- GITEA__database__NAME=gitea_db
- GITEA__database__USER=gitea_folzo
- GITEA__database__SSL_MODE=disable
- GITEA__security__INSTALL_LOCK=true
- GITEA__server__DOMAIN=git.folzocorp.fr
- GITEA__server__ROOT_URL=https://git.folzocorp.fr
- GITEA__server__SSH_DOMAIN=git.folzocorp.fr
- GITEA__server__SSH_PORT=2222
- GITEA__server__LFS_START_SERVER=true
- GITEA__repository__ROOT=/data/git/repositories
- GITEA__lfs__PATH=/data/git/lfs
- GITEA__service__DISABLE_REGISTRATION=true
- GITEA__service__DISABLE_OPENID=true
# Création de repo au premier push (S11.1 : versionner un stack = `git push`, pas de clic UI).
- GITEA__service__ENABLE_PUSH_CREATE_USER=true
- GITEA__repository__DEFAULT_PUSH_CREATE_PRIVATE=false
volumes:
- ./data:/data
- ./repositories:/data/git/repositories
- ./lfs:/data/git/lfs
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (edge/Caddy le proxy en T4.6).
# ufw (01-hardening.sh) restreint 3000 et 2222 au LAN 192.168.1.0/24 → pas d'exposition WAN.
ports:
- "0.0.0.0:3000:3000"
- "0.0.0.0:2222:22"
depends_on:
- gitea-db
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:3000/api/healthz"]
interval: 30s
timeout: 10s
retries: 3
gitea-db:
image: postgres:16
container_name: gitea-db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
environment:
- POSTGRES_DB=gitea_db
- POSTGRES_USER=gitea_folzo
- POSTGRES_PASSWORD_FILE=/run/secrets/dbpass
volumes:
- ./db:/var/lib/postgresql/data
- ./secrets:/run/secrets:ro
@@ -0,0 +1,26 @@
# Jellyfin — T6.2 (vidéo + musique, G9, USB média)
# Lit les fichiers sources sur les USB (15 To + 5,5 To) — montés par 06b-usb-media.sh (T6.1).
# Stateful (config + métadonnées) → exclu de Watchtower (S11.4).
# Le *arr + qBittorrent sont sur le J5040 (edge), pas ici (S7.2).
services:
jellyfin:
image: jellyfin/jellyfin:10.9
container_name: jellyfin
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
environment:
- TZ=Europe/Paris
- UID=1000
- GID=1000
- PUID=1000
- PGID=1000
volumes:
- ./config:/config
- /mnt/usb-media:/media:ro # USB 15 To + 5,5 To (montés en T6.1)
# 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.
ports:
- "0.0.0.0:8096:8096"
devices:
- /dev/dri:/dev/dri # accélération vidéo (si dispo)
@@ -0,0 +1,44 @@
# 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).
services:
n8n:
image: n8nio/n8n:1.90
container_name: n8n
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true" # image stateless → auto
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
- GENERIC_TIMEZONE=Europe/Paris
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.
ports:
- "0.0.0.0:5678:5678"
depends_on:
- n8n_db
n8n_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_PASSWORD_FILE=/run/secrets/dbpass
volumes:
- ./db:/var/lib/postgresql/data
- ./secrets:/run/secrets:ro
@@ -0,0 +1,53 @@
# Nextcloud — T4.2 (socle famille, G9)
# Stateful (app + MariaDB + Redis) → 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.
services:
nextcloud:
image: nextcloud:35
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
volumes:
- ./data:/var/www/html
- ./config:/var/www/html/config
- ./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.
ports:
- "0.0.0.0:8080:80"
nextcloud_db:
image: mariadb:10.11
container_name: nextcloud_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # DB stateful → exclu
environment:
- MARIADB_DATABASE=nextcloud
- MARIADB_USER=nextcloud
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
volumes:
- ./db:/var/lib/mysql
- ./secrets:/run/secrets:ro
redis:
image: redis:7
container_name: nextcloud_redis
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # cache persistant → exclu
volumes:
- ./redis:/data
@@ -0,0 +1,46 @@
# 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).
services:
photoprism:
image: photoprism/photoprism:260919
container_name: photoprism
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
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_DATABASE_USER=photoprism
- PHOTOPRISM_DATABASE_PASSWORD_FILE=/run/secrets/dbpass
- PHOTOPRISM_LOCAL_NETWORKS=172.16.0.0/12,192.168.0.0/16
- 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.
ports:
- "0.0.0.0:2342:2342"
depends_on:
- photoprism_db
photoprism_db:
image: mariadb:10.11
container_name: photoprism_db
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
environment:
- MARIADB_ROOT_PASSWORD_FILE=/run/secrets/rootpass
- MARIADB_DATABASE=photoprism
- MARIADB_USER=photoprism
- MARIADB_PASSWORD_FILE=/run/secrets/dbpass
volumes:
- ./db:/var/lib/mysql
- ./secrets:/run/secrets:ro
@@ -0,0 +1,32 @@
# Vaultwarden — T4.1 (cœur famille, G9)
# Stateful (coffre) → exclu de Watchtower (S11.4). MAJ manuelle supervisée (S11.5).
# Backup : le dossier ./data (SQLite) est couvert par restic (T3.1/T5.1).
services:
vaultwarden:
image: vaultwarden/server:1
container_name: vaultwarden
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false" # stateful → exclu
environment:
- TZ=Europe/Paris
- DOMAIN=https://vault.folzocorp.fr
- SIGNUPS_ALLOWED=false
# 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
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).
- ./data:/data
- ./secrets:/run/secrets:ro
# Bind 0.0.0.0 : accessible depuis le LAN (edge/Caddy le proxy).
# ufw (01-hardening.sh) restreint le port au LAN 192.168.1.0/24 → pas d'exposition WAN.
ports:
- "0.0.0.0:8081:80"
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:80/alive"]
interval: 30s
timeout: 10s
retries: 3
@@ -0,0 +1,25 @@
# Watchtower — S11.4 (MAJ sélective, sur chaque machine)
# Règle : stateful = exclu (label com.centurylinklabs.watchtower.enable=false).
# Leçon de l'incident Nextcloud (06/10) : CLEANUP=false (garder les anciennes images = rollback).
#
# Aligné sur le déploiement RÉEL validé Phase 1 (journal T1.1-c/.1.1-d, 2026-10-10) :
# - image = fork nickfedor ÉPINGLÉE PAR DIGEST (containrrr/watchtower:1 n'existe PAS, vérifié API registry).
# - nickfedor refuse SCHEDULE + POLL simultanés → POLL=3600 conservé (pas de SCHEDULE ici).
# - notifications : shoutrrr generic → webhook n8n → Matrix (chaîne testée, payload title/message).
services:
watchtower:
image: nickfedor/watchtower@sha256:45b54f6dc15227cfb38dbb826f2881aa0f56aaf0c255cdbb38bd80e598bdca9a
container_name: watchtower
restart: unless-stopped
environment:
- TZ=Europe/Paris
- WATCHTOWER_CLEANUP=false # GARDER les anciennes images (rollback)
- WATCHTOWER_INCLUDE_STOPPED=false # ne pas toucher les conteneurs arrêtés
- 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).
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=generic:https://n8n.folzocorp.fr/webhook/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
+18
View File
@@ -0,0 +1,18 @@
# /etc/restic/excludes.txt — exclusions du backup restic du G9 (T3.1)
# Copié par 06-restic-backup.sh si le fichier est absent (le fichier existant n'est JAMAIS écrasé).
# Syntaxe restic (un motif par ligne, glob). Fichier 644, aucun secret.
# Politique : exclure uniquement ce qui est RECONSTRUIBABLE à l'identique. Les médias
# familiaux (/data/famille, photos, vidéos) ne sont PAS exclus : ce sont les données précieuses.
# Caches
*/cache/*
*/.cache/*
*/.npm/*
*/node_modules/.cache/*
*/.cache/thumbnails/*
# Fichiers temporaires
*/tmp/*
# Logs de rotation de watchtower (sans valeur historique)
/home/folzo/watchtower/log/*
+13
View File
@@ -0,0 +1,13 @@
# fail2ban.local.conf — T2.1
# Copié vers /etc/fail2ban/jail.local par 01-hardening.sh.
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = ufw
[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
+61
View File
@@ -0,0 +1,61 @@
# Template restic — T3.1 / T5.1
# Copier vers /etc/restic.env et REMPLIR.
# ⚠️ G9 : secrets hors git — chmod 600, root, JAMAIS commité.
#
# TRANSPORT RETENU (10/10/2026, après tests) : backend **rclone** de restic sur le
# **FTP local de la Freebox Delta (port 21)**. Aucun montage : pas de CIFS, pas de FUSE,
# donc rien à dépendre d'un mount au boot (validé après reboot, 06c-postboot-check.sh).
#
# Transports testés/écartés et limitations append-only : **source unique = RESTORE-G9.md §6**.
# Cible = repo restic via rclone (chemin = partage `Save/Freebox` de la Delta).
RESTIC_REPOSITORY=rclone:freeboxftp:Freebox/backup/g9
RESTIC_RCLONE_REMOTE=freeboxftp:
RESTIC_RCLONE_PATH=Freebox/backup
# Config rclone (remote [freeboxftp] type=ftp host=192.168.1.254 port=21 user=freebox pass=…)
# Le mot de passe vit DANS ce fichier (600, root), jamais ici.
RCLONE_CONFIG=/root/.config/rclone/rclone.conf
# Append-only anti-ransomware, porté par le rclone local du G9 (limitation documentée,
# RESTORE-G9.md §6) : 1 = les jobs utilisent `-o rclone.args="serve restic --stdio --append-only"`.
RESTIC_RCLONE_APPENDONLY=1
# Repo de preuve utilisé par 06-restic-backup.sh (créé puis purgé pendant le test) :
SCRATCH_REPO=rclone:freeboxftp:Freebox/backup/_appendonly-test
# Mot de passe du repo restic (fichier, jamais en variable) : openssl rand -base64 32 > /etc/restic/repo.key
# A CONSIGNER DANS VAULTWARDEN (item « G9 – clé repo restic ») : sans lui, les backups sont irrécupérables.
RESTIC_PASSWORD_FILE=/etc/restic/repo.key
# Sources (T3.1) : /data + les données réelles des briques (/home/folzo) + config système.
# NB : le stateful du G9 vit dans /home/folzo (valheim, n8n, couchdb, obsidian, matrix), pas dans /data.
RESTIC_SOURCES="/data /home/folzo /etc"
RESTIC_EXCLUDE_FILE=/etc/restic/excludes.txt
RESTIC_TAG=g9
# Conteneurs stoppés pendant le backup (cohérence des bases) puis relancés.
RESTIC_STATEFUL="valheim n8n synapse couchdb obsidian"
# Alerte ops. Voie principale = Matrix en direct (salon ops) : token ops-bot dans
# /etc/restic/matrix.env (600 root : MATRIX_SERVER / MATRIX_ROOM / MATRIX_TOKEN), lu par
# restic-notify.sh. Voie de secours = webhook n8n (vide = pas d'alerte).
RESTIC_NOTIFY_WEBHOOK=http://127.0.0.1:5678/webhook/backup-alert
# Politique de rétention (T5.1 : 3-2-1)
# Copies: 3 (G9+PC IA originaux / Freebox quotidien / 218play hebdo)
RESTIC_RETAIN_DAILY=7
RESTIC_RETAIN_WEEKLY=4
RESTIC_RETAIN_MONTHLY=12
RESTIC_RETAIN_YEARLY=5
# Binaires épinglés par 06-restic-backup.sh
RESTIC_VERSION=0.19.1
RESTIC_SHA256=f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c
# Journal du kit sur le G9 (G8)
G9_JOURNAL=/root/g9-migration/journal.md
# --- Transports alternatifs (non retenus) ---
# Secours : RESTIC_REPOSITORY=rclone:freebox:Freebox/backup/g9 (remote [freebox] type=smb)
# Natifs restic : smb:freebox-server.local/Freebox/backup/g9 ou ftp:192.168.1.254:21/Freebox/backup/g9
# Phase 5 (cible WORM) : voir RESTORE-G9.md §6.
+31
View File
@@ -0,0 +1,31 @@
name,image_tag,mode,stateful,watchtower_label,data_paths
n8n,docker.n8n.io/n8nio/n8n:latest,compose,yes,missing,/volume2/docker-REC/n8n/data (75M total project)
n8n-postgresql,postgres:17,compose,yes,missing,/volume2/docker-REC/n8n/postgresql
joplin-server,joplin/server:latest,compose,yes,missing,(stateful via joplin-db)
joplin-db,postgres:16,compose,yes,missing,/volume2/docker-REC/Joplin/postgres (706M project)
postgres16,postgres:16,compose,yes,missing,/volume1/docker/postgres16/data (70M)
affine_server,ghcr.io/toeverything/affine:stable,compose,yes,missing,/volume2/docker-REC/AFFiNE/config+storage
affine_postgres,pgvector/pgvector:pg16,compose,yes,missing,/volume2/docker-REC/AFFiNE/postgres
affine_redis,redis:latest,compose,yes,missing,(named volume)
nextcloud_app,nextcloud:latest,compose,yes,missing,/volume2/docker-REC/NextCloud/config+data (2.3G project)
nextcloud_db,mariadb:10.6,compose,yes,missing,/volume2/docker-REC/NextCloud/db
nextcloud_redis,redis:latest,compose,yes,missing,/volume2/docker-REC/NextCloud/redis
gitea,gitea/gitea:latest,compose,yes,missing,/volume1/docker/Gitea/data (2M)
vaultwarden-server-1,vaultwarden/server:latest,gui,yes,missing,/volume1/docker/vaultwarden (8.8M)
leantime,leantime/leantime:latest,compose,yes,missing,/volume1/docker/Leantime/data/userfiles+plugins (298M)
leantime-db,mysql:8.4,compose,yes,missing,/volume1/docker/Leantime/data/mysql
beszel,henrygd/beszel:latest,compose,yes,missing,/volume1/docker/beszel/beszel_data (11M)
beszel-agent,henrygd/beszel-agent:latest,compose,yes,missing,/volume1/docker/beszel-agent/beszel_agent_data
valheim,mbround18/valheim:latest,compose,yes,missing,/volume2/docker-REC/valheim/saves+backups+server (2.8G)
openwebui,ghcr.io/open-webui/open-webui:latest,compose,yes,missing,/volume1/docker/openwebui (4.7G)
InfluxDB,influxdb:2.7,compose,yes,missing,/volume2/docker-REC/InfluxDB + named volume (188K)
onlyoffice-ds,onlyoffice/documentserver:latest,compose,yes,missing,2 named volumes + /volume2/docker-REC/onlyoffice (160K)
qbittorrent,linuxserver/qbittorrent:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/qbittorrent
sonarr,linuxserver/sonarr:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/Sonarr
radarr,linuxserver/radarr:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/Radarr
jackett,linuxserver/jackett:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/Jackett
prowlarr,linuxserver/prowlarr:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/Prowlarr
deluge,linuxserver/deluge:latest,gui,yes,missing,/volume2/docker-REC/Multimedia/Deluge
flaresolverr,flaresolverr/flaresolverr:latest,gui,no,ok-n/a,(named volume cache only)
mini-qr,ghcr.io/lyqht/mini-qr:latest,compose,no,ok-n/a,(stateless)
watchtower,containrrr/watchtower:latest,compose,watchtower,n/a,/var/run/docker.sock
1 name image_tag mode stateful watchtower_label data_paths
2 n8n docker.n8n.io/n8nio/n8n:latest compose yes missing /volume2/docker-REC/n8n/data (75M total project)
3 n8n-postgresql postgres:17 compose yes missing /volume2/docker-REC/n8n/postgresql
4 joplin-server joplin/server:latest compose yes missing (stateful via joplin-db)
5 joplin-db postgres:16 compose yes missing /volume2/docker-REC/Joplin/postgres (706M project)
6 postgres16 postgres:16 compose yes missing /volume1/docker/postgres16/data (70M)
7 affine_server ghcr.io/toeverything/affine:stable compose yes missing /volume2/docker-REC/AFFiNE/config+storage
8 affine_postgres pgvector/pgvector:pg16 compose yes missing /volume2/docker-REC/AFFiNE/postgres
9 affine_redis redis:latest compose yes missing (named volume)
10 nextcloud_app nextcloud:latest compose yes missing /volume2/docker-REC/NextCloud/config+data (2.3G project)
11 nextcloud_db mariadb:10.6 compose yes missing /volume2/docker-REC/NextCloud/db
12 nextcloud_redis redis:latest compose yes missing /volume2/docker-REC/NextCloud/redis
13 gitea gitea/gitea:latest compose yes missing /volume1/docker/Gitea/data (2M)
14 vaultwarden-server-1 vaultwarden/server:latest gui yes missing /volume1/docker/vaultwarden (8.8M)
15 leantime leantime/leantime:latest compose yes missing /volume1/docker/Leantime/data/userfiles+plugins (298M)
16 leantime-db mysql:8.4 compose yes missing /volume1/docker/Leantime/data/mysql
17 beszel henrygd/beszel:latest compose yes missing /volume1/docker/beszel/beszel_data (11M)
18 beszel-agent henrygd/beszel-agent:latest compose yes missing /volume1/docker/beszel-agent/beszel_agent_data
19 valheim mbround18/valheim:latest compose yes missing /volume2/docker-REC/valheim/saves+backups+server (2.8G)
20 openwebui ghcr.io/open-webui/open-webui:latest compose yes missing /volume1/docker/openwebui (4.7G)
21 InfluxDB influxdb:2.7 compose yes missing /volume2/docker-REC/InfluxDB + named volume (188K)
22 onlyoffice-ds onlyoffice/documentserver:latest compose yes missing 2 named volumes + /volume2/docker-REC/onlyoffice (160K)
23 qbittorrent linuxserver/qbittorrent:latest gui yes missing /volume2/docker-REC/Multimedia/qbittorrent
24 sonarr linuxserver/sonarr:latest gui yes missing /volume2/docker-REC/Multimedia/Sonarr
25 radarr linuxserver/radarr:latest gui yes missing /volume2/docker-REC/Multimedia/Radarr
26 jackett linuxserver/jackett:latest gui yes missing /volume2/docker-REC/Multimedia/Jackett
27 prowlarr linuxserver/prowlarr:latest gui yes missing /volume2/docker-REC/Multimedia/Prowlarr
28 deluge linuxserver/deluge:latest gui yes missing /volume2/docker-REC/Multimedia/Deluge
29 flaresolverr flaresolverr/flaresolverr:latest gui no ok-n/a (named volume cache only)
30 mini-qr ghcr.io/lyqht/mini-qr:latest compose no ok-n/a (stateless)
31 watchtower containrrr/watchtower:latest compose watchtower n/a /var/run/docker.sock
+76
View File
@@ -0,0 +1,76 @@
# Journal d'exécution — Migration G9 (G8)
> Chaque action est journalisée : date, action, raison, résultat, rollback.
> Rempli pendant la migration. Ce fichier est la trace de ce qui a réellement été fait.
| Date | Phase | Action | Raison | Résultat | Rollback |
|------|-------|--------|--------|----------|----------|
| 2026-10-06 | — | Kit d'exécution préparé (scripts + configs + compose) | Migration G9 à venir | Prêt à exécuter | — |
| 2026-10-06 | — | Conformisation des scripts à la charte G1–G10 | Audit des garde-fous (C1–C5) | `journal()` 6 colonnes (G8) dans les 10 scripts ; renvois garde-fous en en-tête (G1) ; tableau G→script dans README (G2) ; secrets hors git confirmés (G9) | `git checkout` des fichiers modifiés |
| 2026-10-06 | — | Corrections post-revue (6 findings) | Revue de code du kit G9 | Binds compose → 0.0.0.0 (accessibles LAN via J5040, ufw restreint) ; retrait mount /etc/timezone (Gitea) ; user DB dédié photoprism ; fstab USB = vrai FSTYPE ; docs credentiels import ; suppression fichiers morts (ufw.rules, smb.conf, RESTIC_APPEND_ONLY) | `git checkout` des fichiers modifiés |
| 2026-10-09 | 1 (T1.4-a1) | Backup Valheim J5040 → `.11:/run/media/folzo/Folzo Save/migration-j5040/valheim/` | G2 : filet avant migration ; serveur arrêté proprement (shutdown backup 20261009-074115) | 3 artefacts : valheim-world.tar.gz (53.6 Mo), valheim-backups.tar.gz (53.6 Mo), valheim-steam.tar.gz (2.13 Go) + valheim-env.json (chmod 600) ; **sha256 source == destination vérifiés** | Suppression du dossier `migration-j5040/valheim/` sur Folzo Save (aucune donnée source touchée) |
| 2026-10-09 | 1 (T1.4-b1) | Valheim UP sur G9 .24 (conteneur direct, sans Pterodactyl) | Ami joueur — priorité ; serveur source J5040 redémarré (double-run G4) | Docker installé sur G9 (T2.3 de fait) ; image `valheim-pterodactyl:1` transférée par `docker save/load` (ghcr denied) ; monde restauré : **37 chunks sha256 identiques source** ; conteneur healthy, ports 2456-2458 ouverts, lobby enregistré (ZDOS:84), UDP 2456 joignable depuis J5040 | `docker compose down` + `docker rmi` sur G9 ; J5040 intact (serveur source redémarré et healthy) |
| 2026-10-09 | 1 (T1.4-a) | Inventaire J5040 + backup stateful → Folzo Save | G2 : filet complet avant formatage | 34 conteneurs + mounts + 116 images inventoriés ; dumps DB (dify 671M, n8n sqlite, pterodactyl wings 4.7G), volumes nommés (open-webui 3.4G, whisper-cache 2.0G), home 1.8G, cron 6 jobs, compose+env. **12 Go sur `.11:/run/media/folzo/Folzo Save/migration-j5040/` avec SHA256SUMS.txt** (`sha256sum -c` : Réussi). Inventaire → `g9-migration/INVENTAIRE-J5040.md` | Suppression du dossier `migration-j5040/` (données sources J5040 intactes) |
| 2026-10-09 | 1 (T1.4-b) | Migration stack IA J5040 → PC IA .11 | Délester J5040 avant formatage ; IA sur GPU .11 | 22 images transférées `docker save/load` ; stack monté dans `~/migration-j5040` (binds `:z` SELinux) ; endpoints vérifiés : open-webui/searxng/pipelines/qdrant (6 collections)/couchdb/mcpo/tts 200, dify 307 (14 healthy), llm-proxy 3011/3012/3017, ollama 28 modèles, whisper large-v3. Test LLM bout en bout : proxy 3012 → **« MIGRATION-OK »**. Doublons supprimés (whisper/ocr/stt déjà présents) | `docker compose down` + `docker rmi` sur .11 ; J5040 inchangé (source des images) |
| 2026-10-09 | 1 (T1.1-a) | Inventaire des conteneurs xpenology .25 : 30 conteneurs (`sudo docker ps -a` DSM), cross-check 12 projects compose /volume1/docker + 7 conteneurs GUI | Base du durcissement watchtower et de l'inventaire stateful | `g9-migration/conteneurs-xpenology.csv` (nom, image:tag, mode, stateful, label, data_paths) ; 15 yml compose récupérés via `ssh cat` dans `/tmp/nas-yml/` (+ `.orig`) ; tailles `du -sh` mesurées par projet | Fichiers locaux, aucune action destructive sur .25 |
| 2026-10-09 | 1 (T1.4-c) | Migration n8n + notes → G9 .24 | Continuité des services notes/automatisation | 3 images (n8n/obsidian/couchdb) transférées save/load ; données : n8n database.sqlite sha **693ee229 IDENTIQUE** source, obsidian-vault 79M, couchdb-data. Compose `~/docker-compose.yml`+`g9.env` (600) : n8n 5678→200, couchdb 5984→200, obsidian 3100→200 | `docker compose down` sur .24 ; J5040 intact, backups Folzo Save intacts |
| 2026-10-09 | 1 (T1.1-b) | Labels `watchtower.enable=false` — stack .24 appliqué, .25 **BLOQUÉ** | Durcissement : exclure les stateful du auto-update | .24 : n8n, couchdb, obsidian, valheim → label false vérifié `docker inspect` ; watchtower → label true. **.25 bloqué** : clé `id_ed25519_effi_server` retirée de ~/.ssh (11:43) + mot de passe refusé (Synology a bloqué le compte effi). 22 conteneurs .25 + 7 GUI = labels non appliqués, yml préparés localement `/tmp/nas-yml/` prêts à pousser. **Action requise côté utilisateur** : débloquer `effi` (Panneau → Utilisateurs, blocage IP) | Retirer les labels avec `docker compose up -d` (yml .orig sauvegardés sur .25 en `.bak`) |
| 2026-10-09 | 1 (T1.1-c) | Watchtower durci sur .24 : image **pinnée par digest** + CLEANUP=false + INCLUDE_STOPPED=false | Durcissement S11.4 (pas de :latest, pas de cleanup des états) | Image `nickfedor/watchtower@sha256:45b54f6d...` (digest épinglé, 1.23.0, fork maintenu — `containrrr/watchtower:1` **n'existe pas** sur Docker Hub, vérifié API). Env vérifiés : CLEANUP=false, INCLUDE_STOPPED=false, POLL=3600, label-scope. Statut running, API Docker v1.52. À répliquer sur .25 (watchtower .25 = `:latest` CLEANUP=true → yml durci prêt `/tmp/nas-yml/watchtower.yaml`) | Rester sur `containrrr/watchtower:latest` avec CLEANUP=true (state actuel .25, non durable) |
| 2026-10-09 | 1 (T1.1-d) | Notification watchtower → webhook n8n → **Matrix** — chaîne complète testée sur .24 | Exigence ops : tracer chaque MAJ d'image | Watchtower (shoutrrr generic) → webhook n8n `POST /webhook/watchtower` (workflow `w1pfhX0A7KvaDrtk`, Webhook→Build log→Matrix→Respond) → Synapse .24:8008 salon ops. Notification réelle de startup watchtower reçue (exec #10) + 2 notifs de test distinctes (exec #31/#32, event_ids `$OSVIb…`/`$XcR0…`/`$bu5f…` vérifiés dans `event_json` de Synapse). Utilisateurs : ops-bot (token), n8n login owner recréé. Points techniques : n8n v2.42 exige publication (workflow_history+published_version) ; URL Matrix = expression full `={{ }}` (URL statique non évaluée en prod) ; txnId = image+timestamp Build log (ids sandbox déterministes → doublons sinon). Notification sur .25 : yml prêt (watchtower .25 durci + URL webhook → n8n .24) | Désactiver WATCHTOWER_NOTIFICATION_URL sur .24 ; supprimer salon/ops-bot via Synapse admin |
| 2026-10-09 | 1 (T1.1-d) | Ouverture salon Matrix ops sur Synapse .24 + user ops-bot | Forwarding notifications (Synapse absent de .11, déjà déployé sur .24) | Room `!mp7MFx9QbrJ0ARtzw…` ("ops") créée via `createRoom` (`room_creation: true` ajouté à homeserver.yaml via sudo, restart synapse) ; user `ops-bot` enregistré ; 6 messages de probe redactés | Supprimer room + user via administration Synapse ; retirer `room_creation: true` |
| 2026-10-10 | 1 (T1.1-b) | Labels `watchtower.enable=false` .25 : **28/28 stateful labellés** | Accès .25 débloqué (compte `Master`, port 222, clé id_ed25519_synology + NOPASSWD sudo). Compose (13 projets) : yml avec labels installés (backups `.bak-labels`) + `compose up -d` (12/13 OK, postgres16 géré GUI). GUI (8 conteneurs dont postgres16) : `docker update --label-add` **indisponible sur docker 24.0.2** → clonage via API unix socket (rename `-old-lbl`, create avec labels racine, start ; backup auto si échec). | Vérif `docker inspect` loop : les 28 stateful = `false`, watchtower = `true`. Preuves dans /tmp/clone_labels.py + logs session | Supprimer label : restaurer `.bak-labels` + `compose up -d` ; conteneurs clonés = mêmes binds/env (données intactes) |
| 2026-10-10 | 1 (T1.1-c/.25) | Watchtower durci sur .25 : `nickfedor/watchtower@sha256:45b54f6d…` + CLEANUP=false + INCLUDE_STOPPED=false + POLL=3600 + label-scope | Réplication du durcissement .24 sur NAS (watchtower .25 était `:latest` CLEANUP=true). Projet compose `watchpower` (COMPOSE_PROJECT_NAME requis, `up -d` OK). Nickfedor refuse SCHEDULE+POLL ensemble → POLL 3600 conservé. | Notification startup réelle reçue dans Matrix : « Watchtower 1.23.0 using Docker API v1.43 … Next scheduled run 02:48 » (event vérifié). Nœud n8n corrigé (payload shoutrrr generic = title/message, pas event_name/image_name) | Restaurer `compose.yaml.bak-labels` + `compose up -d` (containrrr/watchtower:latest CLEANUP=true) |
| 2026-10-10 | 1 (T1.4-e) | **GO utilisateur** — vérification finale services sans .27, avant formatage | Condition posée : .27 éteint + tout vérifié OK = GO (formatage SSD fait par l'utilisateur). | Vérifiés depuis .11 : OpenFox .11:10369 200 (thread survit à la bascule), dify/open-webui/llm-proxy 3011-3012 .11 200, n8n/couchdb/obsidian/matrix .24 OK (API 200), NAS .25 : 31/31 conteneurs Up, labels stateful 28/28 false (0 exception), watchtower digest healthy (notif Matrix confirmée). Clé SSH .25 recréée sur .11 (id_ed25519_openfox11, authorized_keys Master). Backup Folzo Save 24 Go SHA256 23/23. **GO donné, formatage J5040 par l'utilisateur** | Rollback : rallumer .27 (données intactes, rien de destructive n'a été fait sur .27) |
| 2026-10-09 | 1 (T1.2-a) | Inventaire stateful .25 → `g9-migration/INVENTAIRE-STATEFUL.md` | Préparer l'export/migration des données sans perte | 30 conteneurs `docker ps -a` couverts (cross-check CSV), pour chaque stateful : image:tag, data paths, `du -sh` mesuré, type DB, mode GUI/compose, dépendances, méthode d'export + destination (G9/.11/.25/supprimé). Tailles : n8n 75M, Joplin 706M, NextCloud 2.3G, valheim 2.8G, openwebui 4.7G, beszel 11M, InfluxDB 188K, gitea 2M, vaultwarden 8.8M, Leantime 298M, AFFiNE 94M, postgres16 70M. Points de vigilance : 7 GUI sans CLI labels, flux .25→.11 | Suppression du fichier `INVENTAIRE-STATEFUL.md` (aucune action sur .25) |
| 2026-10-10 | 1 (T1.4-f) | OpenFox recopié J5040 (.27) → PC IA .11 : thread + sessions préservés avant formatage | Les sessions OpenFox vivent dans `.27:~/.local/share/openfox/sessions.db` (66 sessions, 107 Mo) → formatage = perte sans copie. | Bundle `openfox-share.tar.gz` 942M (projets ~/openfox 1.1G, .config/openfox 486M auth+config, .local/share/openfox 922M sessions, node v24 global) + `node-bin.tar.gz` 52M vers .11, **SHA256 vérifiés « Réussi »**, archivés sur Folzo Save. Déploiement .11 : extract, symlink `/home/effi → /home/folzo`, launcher corrigé, SELinux `chcon -R -t bin_t`, unit systemd `openfox.service` activé → HTTP 200 sur .11:10369 (LAN), thread OpenFox re-pris sur .11 après bascule proxy. Vérifié sans .27 (éteint) : endpoints .11/.24/.25 tous OK | `systemctl disable --now openfox` + suppression `~/.config/openfox` et `~/.local/share/openfox` sur .11 ; .27 non touché (données intactes avant formatage) |
| 2026-10-10 12:52 | 2 | hardening : SSH keys-only activé | Durcissement SSH (G6) | PasswordAuthentication no, PermitRootLogin prohibit-password | Re-activer PasswordAuthentication yes dans sshd_config |
| 2026-10-10 12:52 | 2 | hardening : 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 |
| 2026-10-10 12:52 | 2 | hardening : unattended-upgrades activé | Mises à jour de sécurité auto (G7) | installé + activé | systemctl disable --now unattended-upgrades |
| 2026-10-10 12:52 | 2 | hardening : fail2ban activé | Anti brute-force SSH (G7) | installé + activé | systemctl disable --now fail2ban |
| 2026-10-10 12:53 | 2 | data : Espace /data créé (Option 0) | Données famille sur mono-disque 990 Pro, LUKS abandonné (protection = ufw LAN + restic chiffré) | /data, /data/famille, /data/docker (owner folzo, 2775) | rmdir /data/famille /data/docker /data |
| 2026-10-10 12:53 | 2 | docker : Docker + compose installés | Runtime conteneurs du G9 | folzo dans le groupe docker | apt-get remove docker.io docker-compose-v2 |
| 2026-10-10 12:53 | 2 | smb : Samba installé, partage [famille] (invité LAN) | Accès SMB données famille sans maintenance par appareil | partage [famille] → /data/famille, guest ok, force user folzo, ufw SMB LAN-only | systemctl disable --now smbd nmbd ; suppression [famille] ; ufw delete règles SMB |
| 2026-10-10 13:05 | 2 | hardening : correctif drop-in `50-cloud-init.conf` | Ubuntu 26 : Include sshd_config.d en tête → 1re occurrence gagne → PasswordAuthentication yes gagnait | `sshd -T` : passwordauthentication no effectif ; script 01 patché (boucle drop-ins) ; anti-lockout vérifié (session clés OK, auth mot de passe refusée) | Restaurer `.bak` du drop-in + restart ssh |
| 2026-10-10 13:10 | 2 | smb : netbios activé + smbclient + tests R/W | Ubuntu default `disable netbios=Yes` bloque la découverte par nom | Partage `\\192.168.1.24\famille` visible depuis .11 (LAN) ; put/get/rm testés avec diff sha → SMB_RW_OK ; invité (guest ok, force user folzo) | `disable netbios = Yes` + restart smbd/nmbd |
| 2026-10-10 13:20 | 2 | docs : bascule G9 **mono-disque 990 Pro** + **abandon LUKS** (Option 0, validation utilisateur) | 2ᵉ disque interne prévu = aucun ; données familiales non critiques ; fuite internet = ufw/edge, pas LUKS | `DOSSIER-RECREATION.md` v2.3 (R1/R3/3.1/R4/R5/R8/plan 0.4 + changelog) ; `DOSSIER-SOFTWARE.md` S9/S10/AC5/AC6 + statut révisé 10/10 ; `g9-migration/README.md` (02-data.sh, note disque) ; `02-disk-luks.sh` supprimé → `02-data.sh` (non destructif) ; prompt Kanban Phase 2 T2.2 réaligné | `git checkout` des docs (historique non destructif, aucune donnée touchée) |
| 2026-10-10 16:50 | 2 | beszel : agent v0.21.0 installé (hub .25:8090) | Monitoring G9 (T2.5) — token universel fourni par l'utilisateur, transmis hors journal (G9), fichiers temporaires supprimés | service `beszel-agent` **active**, logs : « WebSocket connected host=192.168.1.25:8090 » → G9 visible dans le hub ; script 05 réécrit (get.beszel.dev, version épinglée, token par env) | systemctl disable --now beszel-agent ; install-agent.sh -u |
| 2026-10-10 16:58 | 2 | smb : correctif nmbd (`disable netbios = No`) | Ubuntu 26 pose `disable netbios = yes` par défaut → nmbd inactive (découverte par nom impossible) | smbd+nmbd **active**, `testparm` OK, partage `famille` visible, guest map OK ; script 04 patché | `disable netbios = yes` + restart |
| 2026-10-10 17:30 | 2 | corrections post-revue code (8 findings) | Revue de code du kit (diff Phase 2) | 1 watchtower compose aligné déploiement réel (nickfedor digest + POLL 3600 + shoutrrr→n8n, placeholder marqué) ; 2 vaultwarden compose : ADMIN_TOKEN_FILE/TRUSTED_HOSTS, ./data:/data, healthcheck curl /alive ; 3 restic : append-only déplacé après 1er backup avec `latest` ; 4 07-migrate : stop/start conteneurs source autour du rsync (cohérence bases + manifeste stable, G4 respecté) ; 5 secrets 644 (n8n uid 1000) ; 6 scripts chmod +x ; 7 accès xpenology → Master/clé (07/08/README) ; 8 06b : grep \|\| true + fstab fsck 0 pour ntfs/exfat. Vérif : `bash -n` 10/10 OK, YAML compose OK | `git checkout` des fichiers modifiés |
| 2026-10-10 18:40 | 2 | stack .24 : correctifs tests utilisateur (Element, n8n) | Tests Phase 2 : Element tournait en boucle (lisait /app/config.json défaut matrix.org, le mount /config.json n'est pas lu) ; n8n refusait l'accès http par IP (secure cookie) | element recréé avec `./config.json:/app/config.json:ro` → homeserver 192.168.1.24:8008 chargé ; n8n : N8N_SECURE_COOKIE=false + N8N_HOST=192.168.1.24 + N8N_PROTOCOL=http ; vérif HTTP 200 sur 8009 et 5678 ; backups `.bak.$TS` des 2 compose. Obsidian 3100 : « secure connection required » = attendu (web viewer exige HTTPS) → résolu en Phase 4 via Caddy TLS (obsidian.folzocorp.fr), pas de fix Phase 2 | restaurer les `.bak` + `compose up -d` |
| 2026-10-10 17:55 | 2 | valheim : diagnostic connexion jeu (ufw FORWARD + port de jeu) | Utilisateur : « serveurs indisponibles » en jeu | Cause 1 : ufw FORWARD DROP cassait l'UDP docker (TCP passait via docker-proxy) → règle `DOCKER-USER -A -p udp --dport 2456:2458 -j ACCEPT` posée (+ RETURN, à persister dans /etc/ufw/before.rules). Cause 2 : le serveur Valheim lie le **port de jeu = UDP 2457** (2456 = steam query, non bindé) — tcpdump prouve que les paquets 2457 arrivent bien au conteneur. → En jeu, rejoindre **192.168.1.24:2457**. Amis hors LAN : redirection Freebox UDP 2456-2458 → .24 (action utilisateur, G6 ; l'ancienne redirection pointait .27) | `iptables -D DOCKER-USER 1` |
| 2026-10-10 20:05 | 2 | valheim : restauration du VRAI monde + persistance | Le serveur tournait dans un monde NEUF : le SAVE_LOCATION de l'image (valheim-pterodactyl) pointe /home/steam/.config (couche conteneur, non persistée) → le monde restauré Phase 1 a été perdu à la recréation du conteneur (09/10 20:34). Sauvegarde saine unique = backup .11 Folzo Save (172 chunks, 848 saves, sha 8e68a82f vérifié) | sha vérifié sur .24 ; monde neuf archivé (`monde-neuf-20261010-archivage`) ; vrai monde extrait dans le bind `data/save` (33 chunks, _main.848.db2) ; compose: `SAVE_LOCATION=/home/container/save` (persistant) ; serveur relancé avec `-savedir /home/container/save`. À faire: l'utilisateur rejoint 192.168.1.24:2457 et valide la base. Note: le job AUTO_BACKUP de l'image regarde l'ancien chemin (backups auto à reconfigurer sur /home/container/save) | restaurer compose `.bak` + monde neuf depuis l'archivage |
| 2026-10-10 20:15 | 2 | valheim : restauration du monde depuis le SSD J5040 (USB) | La build de l'ami (10/10 matin) était sur .27, postérieure au backup .11 (848). SSD branché USB sur .24, LVM intact (non formaté) | VG USB renommé `j5040-vg` (nom dupliqué avec le VG système, collision) ; monté RO ; monde trouvé `wings/data/aff5199c…/steam/.config/.../worlds_local/LeRepaireDuGrandFrak` = **_main.897 du 10/10 06:20** ; serveur stoppé, version 848 archivée (`monde-848-20261010-archivage`), 897 + player.list/prefs/lists déployés dans le bind persistant `data/save` (chown 1000) ; serveur redémarré, world chargé puis save 898. USB démonté, VG désactivé. À valider en jeu : base de l'ami visible. | `mv` des dossiers d'archivage (`monde-848-20261010-archivage`) vers `data/save` ; redémarrer le serveur |
| 2026-10-10 20:22 | 2 | backup .11 Folzo Save : valheim-world.tar.gz écrasé par la version 897 (10/10 06:20) | Demande utilisateur expresse : la copie de référence doit être la plus récente (build de l'ami) | Nouveau tar 64.6 Mo (worlds_local 897/898 + backups auto + player.list/prefs/lists), sha256 `e766ae2c…` (ancien `8e68a82f…` remplacé dans SHA256SUMS.txt, `sha256sum -c` OK) | L'archive `valheim-backups.tar.gz` sur .11 conserve les versions antérieures |
| 2026-10-10 20:30 | 2 | **reboot test G9 validé** (GO utilisateur) | Valider la persistance post-reboot (durcissement, montages, services) | G9 up en ~75 s ; ufw actif + règle DOCKER-USER valheim persistée (before.rules) ; passwordauthentication no effectif ; smbd/nmbd/fail2ban/unattended/beszel-agent actifs ; 7 conteneurs up (valheim healthy) ; /data/famille présent ; partage famille visible ; endpoints 5678/8008/8009/5984/3100 OK ; monde valheim 897/898 intact | — |
| 2026-10-10 20:32 | 2 | G9 éteint (poweroff) sur demande — déplacement physique sur onduleur | Fin de Phase 2, tâche Kanban déplacée en Done par l'utilisateur | G9 éteint proprement (ping KO après poweroff) ; données saines (valheim 898, world .11 sha e766ae2c). Prochain boots : tout est persistant (validé par reboot test). Prévu : onduleur USB → installer NUT (nut-server) en Phase 3 pour arrêt propre sur batterie | rallumer = action physique utilisateur |
| 2026-10-10 20:12 | 3 | restic : premier backup OK | Backup chiffré du G9 (G2) | snapshot 21de0787 — 4 564 fichiers, 6.956 GiB lus / 4.178 GiB stockés, sources /data /home/folzo /etc ; conteneurs stateful stoppés (valheim, n8n, synapse, couchdb, obsidian) puis relancés | `restic forget 21de0787 && restic prune` |
| 2026-10-10 20:20 | 3 | transport backup : SMB/CIFS écarté, rclone retenu | `mount.cifs` (vers 1.0) + restic → `chmod … no such file or directory` (chmod impossible sur le partage) ; rclone mount FUSE → cache d'écriture qui a perdu des objets du repo ; rclone 1.60 (apt) sans backend smb | Transport final = backend rclone de restic (FTP/SMB), repo `rclone:freeboxftp:Freebox/backup/g9` ; 3 repos de test ont été créés puis purgés (4.176 GiB libérés) | retirer `-o rclone.args=… --append-only` des jobs ; restaurer le mount CIFS depuis `/etc/fstab.bak.*` + unité `rclone-freebox.service` |
| 2026-10-10 20:22 | 3 | repo restic initialisé sur la Freebox | Backup chiffré + versionné du G9 | Repo `Freebox/backup/g9` (Delta, 1.79 To, 1.785 To libres) ; clé de repo générée par openssl dans `/etc/restic/repo.key` (600, jamais affichée) | `rclone purge freeboxftp:Freebox/backup/g9` (DESTRUCTIF, volontaire) |
| 2026-10-10 20:53 | 3 | append-only vérifié | Anti-ransomware (G2) | `forget --prune` REFUSÉ par `rclone serve restic --stdio --append-only` (rc=1), snapshots toujours lisibles ; repo de preuve `_appendonly-test` créé puis purgé | retirer `-o rclone.args=… --append-only` des jobs |
| 2026-10-10 20:53 | 3 | test de restauration OK | Vérification G2 (restauration testée) | `/data/famille/Test/PXL_20261009_130502121.jpg_compressed.JPEG` restauré depuis la Freebox, sha256 9352a905d9bb… identique à l'original | restauration dans un dossier temporaire, aucun impact sur les données sources |
| 2026-10-10 20:55 | 3 | jobs + cron installés | Backup automatique quotidien + alerte | `/usr/local/bin/restic-{daily,prune,health,notify}.sh` (root 755) ; cron root : 03:30 backup, 04:00 prune, 09:15 health (alerte webhook n8n → Matrix) | supprimer les 4 lignes du crontab root + les 4 scripts |
| 2026-10-10 20:31 | 3 | artefact secret supprimé | G9 : les secrets ne doivent pas traîner | dossier racine créé par erreur (heredoc collé) contenant le mot de passe SMB, mode 600 root → supprimé ; le mot de passe n'a jamais été affiché ; `cifs.cred` / `rclone-smb.cred` / `ftp.conf` / `repo.key` restent en 600 (root) | rien (suppression) |
| 2026-10-10 20:47 | 3 | unité systemd `rclone-freebox` retirée | Mount CIFS abandonné au profit du backend rclone : plus besoin de montage persistant | service désactivé, unit et `/etc/systemd/system/rclone-*.d/override.conf` supprimés, daemon-reload ; ligne fstab CIFS ajoutée puis retirée (sauvegardes `/etc/fstab.bak.*`) | recréer l'unit + l'override |
| 2026-10-10 21:12 | 3 | alerte Matrix en place (directe, sans n8n) | Prévenir l'ops si le backup échoue (C3) | `restic-notify.sh` poste sur Synapse (`http://192.168.1.24:8008`, salon ops, token ops-bot récupéré du workflow n8n « Watchtower Notification », écrit dans `/etc/restic/matrix.env` 600 root, jamais affiché) ; **2 messages de test envoyés et reçus** ; fallback webhook n8n conservé | retirer les 3 lignes de `/etc/restic/matrix.env` + les 3 lignes cron |
| 2026-10-10 21:12 | 3 | accès n8n : à recréer (bloquant pour la voie n8n → Matrix) | L'utilisateur n'a plus accès à n8n | 1 workflow présent (« Watchtower Notification »), CLI n8n 2.42.5 sans création d'utilisateur → recréer l'accès par l'UI (propriétaire) puis créer une clé API (Settings → n8n API) ; la voie directe Matrix est opérationnelle en attendant | — |
| 2026-10-10 21:19 | 3 | jobs lancés à la main + logs | Vérifier que le backup et la purge s'exécutent hors kit (C3) | `restic-daily.sh` rc=0 → snapshot `cccdb2c7` (+5.175 MiB stockés, parent 21de0787) ; `restic-prune.sh` rc=0 ; logs `/var/log/restic-{daily,prune,health}.log` ; `restic check --read-data-subset=1%` sans erreur | `restic forget cccdb2c7 && restic prune` |
| 2026-10-10 21:19 | 3 | rétention appliquée (7/4/12/5) | Rétention et espace (C5) | `forget --prune` exécuté (append-only refusé → purge appliquée sans append-only, alerte envoyée), 2 snapshots conservés ; repo **4.19 GiB** pour **1.785 TiB libres** sur le RAID5 de la Delta (1.79 TiB) — marge conforme aux 2 To | rien (rétention non destructrice à ce stade) |
| 2026-10-10 21:20 | 3 | lock stale supprimé + `unlock` sans append-only | Le lock laissé par une exécution interrompue bloquait le prune (rc=11) | `restic unlock` (1 lock retiré) ; **règle intégrée aux jobs** : `unlock` tourne **sans** l'option append-only (les locks sont supprimables), ajout en tête de `restic-daily.sh` et `restic-prune.sh` | retirer les lignes `unlock` des jobs |
| 2026-10-10 21:25 | 3 | **incident : crontab root effacé par le kit (corrigé)** | Le bloc cron du kit pipait `crontab -l \| grep -vE …` dans `crontab -` ; avec `set -e`, le `grep` sans correspondance tuait le sous-shell → stdin vide → crontab vidé | détecté et **documenté** : le crontab root ne contenait que les 3 lignes restic (aucune ligne utilisateur perdue) ; kit corrigé (`grep … \|\| true` + écriture dans un fichier + **contrôle « 3 lignes attendues » avant installation**, refus et crontab intact sinon) ; crontab re-créé puis vérifié après reboot | restaurer le crontab : `sudo crontab -l` (3 lignes restic) |
| 2026-10-10 21:36 | 3 | **reboot G9 validé (persistance post-reboot)** | Valider C1 après le changement de transport (plus de montage CIFS) | `systemctl reboot` (21:36:32), retour SSH en ~75 s ; `06c-postboot-check.sh` : **15 contrôles OK / 0 KO** (binaires, env 600, clé repo, config rclone, **aucun montage CIFS requis**, 3 lignes cron, 4 jobs, `restic snapshots`, `check --read-data-subset=1%`, health < 26 h, 5 conteneurs stateful relancés) | rien (validation en lecture seule) |
| 2026-10-10 21:40 | 3 | `.gitignore` élargi (secrets) | Renforcer G9 côté dépôt local | ajout de `*.cred`, `*.key`, `repo.key`, `matrix.env` (les patterns `*.env` et le chemin explicite couvraient déjà) ; vérifié : **aucun secret dans le repo** (recherche du mot de passe SMB et du token Matrix = 0 occurrence), `git check-ignore` OK, aucun fichier suivi par git de type env/cred/key | retirer les lignes ajoutées |
| 2026-10-10 21:52 | 3 | `configs/restic.env.example` rechargé sur le transport retenu | Revue T3.1 : l'exemple restait sur le mount CIFS/rclone-FUSE abandonné (C8) | exemple aligné sur `RESTIC_REPOSITORY=rclone:freeboxftp:Freebox/backup/g9` + `RESTIC_RCLONE_REMOTE/PATH`, `RCLONE_CONFIG`, `RESTIC_RCLONE_APPENDONLY=1`, `SCRATCH_REPO`, `RESTIC_VERSION/SHA256`, `G9_JOURNAL`, alerte Matrix directe (`/etc/restic/matrix.env`) + webhook n8n de secours, transports écartés documentés (CIFS/FUSE/SMB/FTPES) ; aucun secret dans le fichier | restaurer l'exemple depuis git (fichier versionné) |
| 2026-10-10 21:55 | 3 | sous-point « thermique Freebox < 70 °C » **déchargé de T3.1** | Revue T3.1 (C0) : jamais mesuré, et non mesurable par l'agent | **décision utilisateur (10/10/2026)** : API Freebox OS = appairage requis (mot de passe Freebox OS + code saisi sur l'écran de la Freebox) ; SSH Delta fermé (port 22 refused, testé) → relevé reporté en **Phase 1 / T1.3** (COMPTE-RENDU-PHASE-1.md, DOSSIER-RECREATION plan 0.2 : seuil 85 °C) ; à lire dans Freebox OS (app Sapeur) puis journaliser | réintégrer le sous-point dès qu'un relevé est disponible |
| 2026-10-10 21:55 | 3 | énoncé C1 réaligné sur le transport réellement retenu | Revue T3.1 : C1 décrivait un montage CIFS (`/mnt/freebox`, `vers=2.x`, `x-systemd.automount`) exécutée en rclone/FTP | critère réécrit : cible = `rclone:freeboxftp:Freebox/backup/g9`, aucun montage, `snapshots` + `check --read-data-subset=1%` OK, persistance prouvée par reboot réel (15/15) ; abandon CIFS/FUSE tracé (20:20, 20:47) | réécrire l'énoncé si la cible change (Phase 5 : `rest:` en VM) |
| 2026-10-10 22:05 | 3 | format du journal corrigé (6 colonnes) | Revue T3.1 : la ligne « transport SMB/CIFS écarté » (20:20) n'avait que 5 colonnes (Rollback absent) — violation G8 | **19 lignes phase 3** toutes en 6 colonnes (vérifié par rendu markdown, 0 ligne non conforme) ; rollback ajouté sur la ligne 20:20 (`retirer -o rclone.args=… --append-only` / restaurer le mount CIFS depuis `/etc/fstab.bak.*` + unité) ; rollback isolé en 6ᵉ cellule sur la ligne valheim 20:15 (phase 2) | restaurer les lignes depuis git (journal versionné) |
| 2026-10-10 22:08 | 3 | `RCLONE_CONFIG` harmonisé sur un seul fichier | Revue T3.1 : docs annonçaient `rclone.conf`, le live pointait `ftp.conf` (2 fichiers, 2 remotes séparés) | les 2 remotes réunis dans `/root/.config/rclone/rclone.conf` (`[freeboxftp]` FTP + `[freebox]` SMB secours, 600 root, `.bak.1791670085` daté) ; `/etc/restic.env` → `RCLONE_CONFIG=/root/.config/rclone/rclone.conf` ; vérifié : FTP liste le repo, SMB lit 4.184 GiB, `restic snapshots` = 2, kit rejoué rc=0 ; `ftp.conf` conservé en copie FTP-only (600) ; docs (kit + RESTORE-G9.md) alignés | `cp /root/.config/rclone/rclone.conf.bak.1791670085` à l'emplacement + remettre l'ancien `RCLONE_CONFIG` |
| 2026-10-10 22:09 | 3 | repo de preuve append-only purgé | Artefact du test append-only laissé sur la Freebox (`_appendonly-test`) | `rclone purge freeboxftp:Freebox/backup/_appendonly-test` → il ne reste que `g9/` sur le partage ; kit rejoué rc=0 (append-only prouvé, restore testée) | re-créé automatiquement par le kit à chaque exécution (puis purgé) |
| 2026-10-10 22:40 | 3 | corrections post-revue code (12 findings) | Revue de code du kit (diff T3.1) | 1 `jq` installé par le kit + garde `command -v jq` avec repli JSON dans `restic-notify.sh` ; 2 `configs/excludes.txt.example` créé et **installé par le kit s'il est absent** (jamais écrasé) + contrôle dans `06c` ; 3 `restic-notify.sh \|\| true` dans le job prune ; 4 le refus d'append-only au prune est **journalisé seulement** (plus d'alerte « normale » quotidienne ; vérifié : 0 ligne PURGE dans le log, alerte uniquement si la purge sans append-only échoue) ; 5 `03b-secrets.sh` : wording **644** aligné sur le code (3 endroits) ; 6 lignes vides intra-tableau supprimées dans `journal.md` ; 7 branche `restic ls \| awk` morte supprimée (fichier test = `find` déterministe) ; 8 **rclone épinglé** v1.75.2 + sha256 `349ac8fb…3702` vérifié ; 9 transport/append-only **canonique dans RESTORE-G9.md §6** (tableau des transports testés), README/env.example/en-tête du script = pointeurs ; 10 liste stateful **dynamique** (`RESTIC_STATEFUL`) dans `06c` ; 11 arbre vide `Save/migration-j5040/` supprimé + `.gitignore` dégraissé ; 12 **règles ufw sauvegardées avant `ufw --force reset`** (`/etc/ufw/{before,user}.rules.bak.<epoch>`) + note « liste de ports à aligner sur les compose ». Vérif live : kit rc=0, prune rc=0 (0 alerte), notify rc=0, `06c` = **17 OK / 0 KO**, excludes intacts (6 lignes) | `git checkout` des fichiers du kit ; restaurer `/etc/ufw/*.rules.bak.<epoch>` |
| 2026-10-11 07:32 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:35 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:36 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:36 | 4 | migrate:gitea : import ÉCHEC intégrité (repositories) | Vérification G4 | manifeste sha256 source ≠ stage | purger stage/repositories et re-exécuter |
| 2026-10-11 07:37 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:38 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:39 | 4 | migrate:gitea : backup restic pré-migration | Garde-fou G2 avant import Gitea | snapshot restic Freebox (voir logs /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 07:40 | 4 | migrate:gitea : Gitea migré + lancé sur le G9 | T4.4 (double-run G4, .25 lecture seule) | pg_dump fcab4f2231ee… restauré ; dépôts+LFS+appdata sha256 identiques ; healthz ; .25 intact | docker compose -f /data/docker/gitea/docker-compose.yml down + rm -rf /data/docker/gitea (G9 seul) |
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
#
# 01-hardening.sh — T2.1 : Durcissement Ubuntu Server 26.04 du G9
# Idempotent. Exécuter en root (sudo).
#
# Garde-fous : G6 (réseau LAN-only), G7 (rollback), G8 (journalisation).
#
# Fait :
# - SSH : clés uniquement (Pas de mot de passe), port par défaut 22
# - ufw : actif, par défaut deny incoming / allow outgoing
# - unattended-upgrades : activé
# - fail2ban : actif
#
# Rollback :
# - Re-activer PasswordAuthentication yes dans /etc/ssh/sshd_config
# - ufw disable ; restaurer les règles sauvegardées avant le reset
# (`cp /etc/ufw/before.rules.bak.<epoch> /etc/ufw/before.rules`, idem user.rules, puis `ufw reload`)
# - systemctl disable --now fail2ban unattended-upgrades
#
# ⚠️ Maintenance : la liste de ports ouverts (TCP/UDP, LAN-only) est **en dur** dans ce script
# et doit rester **alignée sur `compose/*/docker-compose.yml`** (source des ports publiés).
# Ajouter une brique = ajouter son port ici, sinon le service sera injoignable depuis la LAN.
set -euo pipefail
log() { echo -e "\n[hardening] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | hardening : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ $EUID -eq 0 ]] || { echo "ERREUR : exécuter en root (sudo)"; exit 1; }
# --- 1. SSH : clés uniquement ---
log "SSH : désactivation de l'authentification par mot de passe"
SSHD=/etc/ssh/sshd_config
# Guard anti-lockout (G1) : couper PasswordAuthentication sans clé installée = porte SSH fermée.
RUN_USER="${SUDO_USER:-$USER}"
RUN_HOME="$(getent passwd "$RUN_USER" | cut -d: -f6)"
if ! grep -qsE '^(ssh-|ecdsa-|sk-)' "$RUN_HOME/.ssh/authorized_keys" 2>/dev/null; then
echo "ERREUR : aucune clé publique dans $RUN_HOME/.ssh/authorized_keys pour '$RUN_USER'."
echo "Installe d'abord une clé (ssh-copy-id), puis relance. Rien n'a été modifié."
exit 1
fi
cp -n "$SSHD" "$SSHD.bak.$(date +%s)" 2>/dev/null || true
sed -ri 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' "$SSHD"
sed -ri 's/^#?PermitRootLogin.*/PermitRootLogin prohibit-password/' "$SSHD"
sed -ri 's/^#?X11Forwarding.*/X11Forwarding no/' "$SSHD"
# Drop-ins sshd_config.d : la PREMIÈRE occurrence gagne (Include en tête de sshd_config)
# → désactiver le mot de passe aussi dans les drop-ins (ex. 50-cloud-init.conf).
for dropin in "$SSHD".d/*.conf; do
[[ -f "$dropin" ]] || continue
cp -n "$dropin" "$dropin.bak.$(date +%s)" 2>/dev/null || true
sed -ri 's/^#?PasswordAuthentication.*/PasswordAuthentication no/' "$dropin"
done
systemctl restart ssh
journal "SSH keys-only activé" "Durcissement SSH (G6)" "PasswordAuthentication no, PermitRootLogin prohibit-password" "Re-activer PasswordAuthentication yes dans sshd_config"
# --- 2. ufw ---
log "ufw : activation (deny incoming / allow outgoing)"
# G8 : les règles existantes sont sauvegardées AVANT le reset (ufw --force reset est destructif).
ufw_bak="/etc/ufw/before.rules.bak.$(date +%s)"
cp -a /etc/ufw/before.rules "$ufw_bak" 2>/dev/null || true
cp -a /etc/ufw/user.rules "/etc/ufw/user.rules.bak.$(date +%s)" 2>/dev/null || true
log "règles ufw sauvegardées : $ufw_bak"
ufw --force reset >/dev/null
ufw default deny incoming
ufw default allow outgoing
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
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
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"
# --- 3. unattended-upgrades ---
log "unattended-upgrades : activation"
apt-get update -qq
apt-get install -y -qq unattended-upgrades
dpkg-reconfigure -f noninteractive unattended-upgrades
systemctl enable --now unattended-upgrades
journal "unattended-upgrades activé" "Mises à jour de sécurité auto (G7)" "installé + activé" "systemctl disable --now unattended-upgrades"
# --- 4. fail2ban ---
log "fail2ban : installation + activation"
apt-get install -y -qq fail2ban
cp -n "$(dirname "$0")/../configs/fail2ban.local.conf" /etc/fail2ban/jail.local
systemctl enable --now fail2ban
journal "fail2ban activé" "Anti brute-force SSH (G7)" "installé + activé" "systemctl disable --now fail2ban"
log "Terminé. Vérif : ss -tlnp | grep :22, ufw status, systemctl is-active fail2ban unattended-upgrades"
echo "AVANT de couper ton accès SSH, ouvre une 2e session et teste 'ssh <user>@G9' avec ta clé."
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
#
# 02-data.sh — T2.2 (Option 0, validée 10/10/2026) : espace données /data sur le G9
#
# Décision tracée : LUKS abandonné sur le G9 (mono-disque Samsung 990 Pro 2 To,
# données familiales non critiques, protection = ufw LAN-only + backups restic
# chiffrés — voir DOSSIER-SOFTWARE S9/S10, changelog 10/10/2026).
# Remplace 02-disk-luks.sh (aucune opération destructive : ni partitionnement,
# ni LUKS, ni réinstallation).
#
# Garde-fous : G1 (idempotent, non destructif), G7 (rollback = rmdir), G8.
#
# Fait :
# /data (root du volume données, sur le LV racine existant)
# /data/famille (partage SMB — T2.4)
# /data/docker (données conteneurs si besoin)
#
# Rollback : rmdir /data/famille /data/docker /data (dossiers vides uniquement)
set -euo pipefail
log() { echo -e "\n[data] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | data : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ $EUID -eq 0 ]] || { echo "ERREUR : exécuter en root (sudo)"; exit 1; }
OWNER="${SUDO_USER:-folzo}"
log "Création de /data (dossier sur le LV racine — pas de partition, pas de LUKS)"
mkdir -p /data /data/famille /data/docker
chown "$OWNER":"$OWNER" /data /data/famille /data/docker
chmod 755 /data
chmod 2775 /data/famille
chmod 2775 /data/docker
df -h /data
journal "Espace /data créé (Option 0)" "Données famille sur mono-disque 990 Pro, LUKS abandonné (protection = ufw LAN + restic chiffré)" "/data, /data/famille, /data/docker (owner $OWNER, 2775)" "rmdir /data/famille /data/docker /data"
log "Terminé. Vérif : df -h /data, ls -ld /data /data/famille"
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
#
# 03-docker.sh — T2.3 : Docker + docker compose + groupe docker
# Idempotent. Exécuter en root.
#
# Garde-fous : G7 (rollback), G8 (journalisation).
#
# Rollback : apt-get remove docker.io docker-compose-v2 ; groupe docker supprimé manuellement.
set -euo pipefail
log() { echo -e "\n[docker] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | docker : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ $EUID -eq 0 ]] || { echo "ERREUR : exécuter en root (sudo)"; exit 1; }
log "Installation Docker + compose"
apt-get update -qq
apt-get install -y -qq docker.io docker-compose-v2
systemctl enable --now docker
# Utilisateur courant dans le groupe docker (évite sudo pour docker)
USER="${SUDO_USER:-$(whoami)}"
if [[ "$USER" != "root" ]]; then
usermod -aG docker "$USER"
log "Utilisateur '$USER' ajouté au groupe docker (reconnexion SSH requise)"
fi
journal "Docker + compose installés" "Runtime conteneurs du G9" "$USER dans le groupe docker" "apt-get remove docker.io docker-compose-v2"
log "Terminé. Vérif : docker version, docker compose version, docker run --rm hello-world"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
#
# 03b-secrets.sh — T2.3b : bootstrap des fichiers secrets des briques famille
# Idempotent (ne remplace JAMAIS un secret déjà existant). Exécuter en root.
#
# Garde-fous : G9 (secrets hors git, non loggués), G7 (rollback), G8 (journalisation).
#
# Pourquoi : les compose montent ./secrets:/run/secrets:ro et lisent *_PASSWORD_FILE.
# Rien d'autre ne crée ces fichiers → sans ce script, le premier 'compose up'
# crash-loop sur secret manquant. À exécuter AVANT 07-migrate-stateful.sh (étape 3).
#
# 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
# photoprism : admin, dbpass, rootpass
# vaultwarden : admin
# n8n : dbpass
# gitea : dbpass (backend Postgres — voir compose/gitea)
# (jellyfin, 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_*
# les seedent au 1er démarrage de la base).
# - IMPORT STATEFUL (07-migrate-stateful.sh) : les env MARIADB_*/POSTGRES_* sont IGNORÉS
# (ils ne seedent qu'une base vierge). La base importée garde les CREDENTIELS
# D'ORIGINE du xpenology, et la config app importée (config.php, storage…) est
# cohérente avec elle. → Les secrets générés ici sont donc INUTILES (dead weight)
# pour une brique importée ; ils ne servent que si la base doit être re-initialisée.
#
# NB : les secrets sont HORS GIT (G9). Ne jamais les logguer ni les commiter.
set -euo pipefail
log() { echo -e "\n[secrets] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | secrets : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
COMPOSE_ROOT="$(dirname "$SCRIPT_DIR")/compose"
# Génère un secret aléatoire (24 octets base64) dans $1, sans écraser s'il existe.
gen_secret() {
local file="$1"
if [[ -f "$file" ]]; then
log " ⏭ existe déjà : $file (non modifié)"
return 0
fi
umask 077
openssl rand -base64 24 > "$file"
# 644 (root:root) : les entrypoints conteneurs qui lisent /run/secrets/* ne sont
# PAS root (n8n = uid 1000, Nextcloud = www-data) → 600 root = illisible = crash-loop.
# Le dossier reste hors git (G9) ; 644 = compromis lisible par les services.
chmod 644 "$file"
log " ✅ généré : $file"
}
# briques : "brique:s1 s2 s3;..."
declare -A SECRET_MAP=(
[nextcloud]="dbpass rootpass"
[photoprism]="admin dbpass rootpass"
[vaultwarden]="admin"
[n8n]="dbpass"
[gitea]="dbpass"
)
for brique in nextcloud photoprism vaultwarden n8n gitea; do
COMPOSE_DIR="$COMPOSE_ROOT/$brique"
[[ -d "$COMPOSE_DIR" ]] || { echo "ERREUR : $COMPOSE_DIR absent"; exit 1; }
SECRETS_DIR="$COMPOSE_DIR/secrets"
mkdir -p "$SECRETS_DIR"
log "Brique $brique → $SECRETS_DIR"
for s in ${SECRET_MAP[$brique]}; do
gen_secret "$SECRETS_DIR/$s"
done
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"
log "Terminé. Vérif : ls -l compose/*/secrets/ (644 root:root, non versionnés — 644 voulu : lus par l'uid du conteneur)."
echo "⚠️ Ces secrets sont HORS GIT (G9). Ne pas les commiter ni les logguer."
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bash
#
# 04-smb.sh — T2.4 : Partage SMB (données famille) sur le G9
# Idempotent. Exécuter en root.
#
# Garde-fous : G6 (réseau LAN-only), G9 (aucun secret loggué/commité),
# G7 (rollback), G8 (journalisation).
#
# Modèle retenu (10/10/2026 — zéro maintenance par périphérique) :
# accès INVITÉ lecture/écriture (« map to guest »), réservé au LAN (ufw 137-139/445).
# Les écritures sont forcées sur l'utilisateur du G9 (force user) → permissions
# simples, aucun compte à créer par appareil. Un appareil qui exige des
# identifiants accepte n'importe quel utilisateur/mot de passe (mappé en invité).
#
# Partage : /data/famille → //G9/famille
# Rollback : systemctl disable --now smbd nmbd ; suppression du [famille] dans
# /etc/samba/smb.conf ; ufw delete des règles SMB 137/138/139/445
set -euo pipefail
log() { echo -e "\n[smb] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | smb : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ $EUID -eq 0 ]] || { echo "ERREUR : exécuter en root (sudo)"; exit 1; }
log "Installation Samba"
DEBIAN_FRONTEND=noninteractive apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq samba
OWNER="${SUDO_USER:-folzo}"
# Dossier partagé
mkdir -p /data/famille
chown "$OWNER":"$OWNER" /data/famille
chmod 2775 /data/famille
# Global : accès invité (idempotent)
if ! grep -qiE '^map to guest' /etc/samba/smb.conf; then
sed -ri 's/^\[global\]/[global]\n map to guest = Bad User/' /etc/samba/smb.conf
fi
# Config du partage (idempotent : on n'ajoute [famille] que s'il est absent)
if grep -q '^\[famille\]' /etc/samba/smb.conf; then
log "Partage [famille] déjà présent — on le laisse tel quel (idempotent)."
else
cat >> /etc/samba/smb.conf <<EOF
[famille]
path = /data/famille
browseable = yes
read only = no
guest ok = yes
force user = $OWNER
create mask = 0664
directory mask = 2775
EOF
fi
# Firewall : SMB réservé au LAN (G6) — idempotent
ufw allow from 192.168.1.0/24 to any port 445 proto tcp 2>/dev/null || true
ufw allow from 192.168.1.0/24 to any port 139 proto tcp 2>/dev/null || true
ufw allow from 192.168.1.0/24 to any port 137 proto udp 2>/dev/null || true
ufw allow from 192.168.1.0/24 to any port 138 proto udp 2>/dev/null || true
# Ubuntu >= 24 : « disable netbios = yes » par défaut → nmbd ne démarre pas
# (découverte par nom impossible). L'activer (réservé LAN, filtré par ufw).
sed -ri "s/^([[:space:]]*)disable netbios.*/\1disable netbios = No/I" /etc/samba/smb.conf
systemctl enable --now smbd nmbd
systemctl restart nmbd
journal "Samba installé, partage [famille] (invité LAN)" "Accès SMB données famille sans maintenance par appareil" "partage [famille] → /data/famille, guest ok, force user $OWNER, ufw SMB LAN-only" "systemctl disable --now smbd nmbd ; suppression [famille] ; ufw delete règles SMB"
log "Terminé. Vérif : testparm -s ; smbclient -L //localhost -N ; depuis un poste LAN : \\\\192.168.1.24\\famille"
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
#
# 05-beszel.sh — T2.5 : Agent Beszel sur le G9
# Idempotent. Exécuter en root.
#
# Garde-fous : G7 (rollback), G8 (journalisation), G9 (le token n'est JAMAIS
# loggué ni commité — passé par l'environnement, écrit uniquement dans l'unit
# systemd root-only).
#
# Hub : http://192.168.1.25:8090 (monitoring.folzocorp.fr) — container hub sur .25.
#
# Prérequis : BESZEL_TOKEN = token universel du hub (créé dans le hub :
# /settings/tokens → « nouveau token »). BESZEL_KEY = clé publique du hub
# (affichée dans « Add System ») pour la méthode SSH.
#
# Usage :
# sudo BESZEL_TOKEN=<token> bash 05-beszel.sh
#
# Rollback : /tmp/install-agent.sh -u ; ou systemctl disable --now beszel-agent
set -euo pipefail
log() { echo -e "\n[beszel] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 2 | beszel : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ $EUID -eq 0 ]] || { echo "ERREUR : exécuter en root (sudo)"; exit 1; }
BESZEL_HUB="${BESZEL_HUB:-http://192.168.1.25:8090}"
BESZEL_KEY="${BESZEL_KEY:-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINo6KvTbvXbilsNLqHX+EOZKuZGgpNcfx+c+mbyivBlv}"
BESZEL_VERSION="${BESZEL_VERSION:-v0.21.0}"
[[ -n "${BESZEL_TOKEN:-}" ]] || { echo "ERREUR : BESZEL_TOKEN requis (token universel du hub, /settings/tokens). Rien n'a été installé."; exit 1; }
if systemctl is-active beszel-agent >/dev/null 2>&1; then
log "Agent beszel-agent déjà actif — idempotent, rien à faire."
journal "Agent Beszel déjà actif" "Monitoring du G9" "hub $BESZEL_HUB" "install-agent.sh -u"
exit 0
fi
log "Installation de l'agent Beszel $BESZEL_VERSION (hub: $BESZEL_HUB) — token non loggué"
curl -fsSL https://get.beszel.dev -o /tmp/install-agent.sh
chmod +x /tmp/install-agent.sh
/tmp/install-agent.sh -k "$BESZEL_KEY" -t "$BESZEL_TOKEN" -url "$BESZEL_HUB" -v "$BESZEL_VERSION" --auto-update false
unset BESZEL_TOKEN
journal "Agent Beszel installé ($BESZEL_VERSION)" "Monitoring du G9 (T2.5)" "hub $BESZEL_HUB, agent actif (token non loggué)" "systemctl disable --now beszel-agent ; /tmp/install-agent.sh -u"
log "Terminé. Vérif : systemctl is-active beszel-agent ; journalctl -u beszel-agent -n 5 ; le G9 apparaît dans le hub."
+244
View File
@@ -0,0 +1,244 @@
#!/usr/bin/env bash
#
# 06-restic-backup.sh — T3.1 : restic G9 → Freebox (chiffré, quotidien) + test restauration
# Idempotent. Exécuter en root : sudo bash 06-restic-backup.sh
#
# Transport retenu (10/10/2026, après tests) : **FTP local de la Freebox (port 21)** via le
# backend rclone de restic (`RESTIC_REPOSITORY=rclone:freeboxftp:Freebox/backup/g9`).
#
# Transports testés/écartés + limitations append-only : **source unique = RESTORE-G9.md §6**.
#
# Prérequis :
# - /etc/restic.env (600, root) rempli + /etc/restic/repo.key (600) = mot de passe du repo
# - /root/.config/rclone/rclone.conf (600) = RCLONE_CONFIG : remote [freeboxftp] type=ftp
# host=192.168.1.254 port=21 user=freebox (+ [freebox] type=smb en secours)
# - Serveur FTP de la Freebox activé (Freebox OS → Paramètres → Mode avancé → FTP)
#
# Fait :
# 1. Installe restic (binaire officiel épinglé + sha256) et rclone (backend smb/ftp)
# 2. Vérifie la connectivité FTP vers la Freebox
# 3. Initialise le repo (si absent)
# 4. Premier backup de RESTIC_SOURCES, conteneurs stateful arrêtés (cohérence des bases)
# 5. Append-only 5m APRÈS le premier backup (limitation : enforcement côté client)
# 6. TEST DE RESTAURATION (AC5) sur un fichier réel + comparaison sha256
# 7. Cron : backup 03:30 (stop → backup → start) + prune 04:00 + health 09:15 + alerte Matrix
#
# Rollback : systemctl stop des jobs (crontab -r lignes restic) ; apt-get remove rclone ;
# rm /usr/local/bin/restic ; suppression du repo distant (destructif, manuel).
set -euo pipefail
log() { echo -e "\n[restic] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 3 | restic : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "${G9_JOURNAL:-/root/g9-migration/journal.md}"
}
ENV_FILE=/etc/restic.env
[[ -f "$ENV_FILE" ]] || { echo "ERREUR : $ENV_FILE absent. Copier configs/restic.env.example et remplir."; exit 1; }
# shellcheck disable=SC1090
set -a; source "$ENV_FILE"; set +a # set -a : exporter, sinon restic/rclone ne voient pas les variables
[[ -n "${RESTIC_REPOSITORY:-}" && -n "${RESTIC_PASSWORD_FILE:-}" ]] || { echo "ERREUR : RESTIC_REPOSITORY / RESTIC_PASSWORD_FILE vides dans $ENV_FILE"; exit 1; }
[[ -f "$RESTIC_PASSWORD_FILE" ]] || { echo "ERREUR : $RESTIC_PASSWORD_FILE introuvable (mot de passe du repo)"; exit 1; }
chmod 600 "$ENV_FILE" "$RESTIC_PASSWORD_FILE"
[[ -n "${RCLONE_CONFIG:-}" && -f "$RCLONE_CONFIG" ]] || { echo "ERREUR : config rclone absente ($RCLONE_CONFIG)"; exit 1; }
chmod 600 "$RCLONE_CONFIG"
RCLONE_BIN=/usr/local/bin/rclone
RESTIC_BIN=/usr/local/bin/restic
STATEFUL="${RESTIC_STATEFUL:-valheim n8n synapse couchdb obsidian}"
AOPT=()
if [[ "${RESTIC_RCLONE_APPENDONLY:-0}" == "1" ]]; then AOPT=(-o "rclone.args=serve restic --stdio --append-only"); fi
log "Installation restic (officiel, épinglé) + rclone"
apt-get update -qq >/dev/null 2>&1 || true
apt-get install -y -qq unzip bzip2 curl jq >/dev/null
RESTIC_VERSION="${RESTIC_VERSION:-0.19.1}"
RESTIC_SHA256="${RESTIC_SHA256:-f415415624dcc452f2a02b8c33641791a8c6d6d3b65bbb3543fcf9a25151585c}"
if ! $RESTIC_BIN version 2>/dev/null | grep -q "$RESTIC_VERSION"; then
tmpd=$(mktemp -d)
( cd "$tmpd" && curl -fsSL -o restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2" \
&& printf '%s restic.bz2\n' "$RESTIC_SHA256" | sha256sum -c - \
&& bzip2 -d restic.bz2 && install -m 755 restic "$RESTIC_BIN" ) || { echo "ERREUR : sha256 restic invalide"; exit 1; }
rm -rf "$tmpd"
fi
RCLONE_VERSION="${RCLONE_VERSION:-1.75.2}"
RCLONE_SHA256="${RCLONE_SHA256:-349ac8fba6ff65d6247043f1750cdcb518ec5d500ef91463a10d37c0ccdf3702}"
if ! $RCLONE_BIN version 2>/dev/null | grep -q "v${RCLONE_VERSION}"; then
tmpd=$(mktemp -d)
( cd "$tmpd" \
&& curl -fsSL -o r.zip "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip" \
&& printf '%s r.zip\n' "$RCLONE_SHA256" | sha256sum -c - \
&& unzip -q r.zip && install -m 755 rclone-*-linux-amd64/rclone "$RCLONE_BIN" ) \
|| { echo "ERREUR : sha256 rclone invalide (v${RCLONE_VERSION})"; exit 1; }
rm -rf "$tmpd"
fi
log "restic $($RESTIC_BIN version | head -1) / rclone $($RCLONE_BIN version | head -1)"
$RESTIC_BIN options 2>&1 | grep -q 'rclone.args' || { echo "ERREUR : backend rclone indisponible dans restic"; exit 1; }
EXCLUDES_DEST="${RESTIC_EXCLUDE_FILE:-/etc/restic/excludes.txt}"
if [[ ! -f "$EXCLUDES_DEST" ]]; then
cp "$(dirname "$0")/../configs/excludes.txt.example" "$EXCLUDES_DEST"
chmod 644 "$EXCLUDES_DEST"
log "Installé : $EXCLUDES_DEST (depuis configs/excludes.txt.example)"
fi
log "Connectivité FTP vers la Freebox (${RESTIC_RCLONE_REMOTE:-freeboxftp:})"
$RCLONE_BIN --timeout 30s lsf "${RESTIC_RCLONE_REMOTE:-freeboxftp:}${RESTIC_RCLONE_PATH:-Freebox/backup}" >/dev/null \
|| { echo "ERREUR : FTP Freebox injoignable (serveur FTP activé ? hôte/port dans $RCLONE_CONFIG ?)"; exit 1; }
log "Initialisation du repo : $RESTIC_REPOSITORY"
if ! $RESTIC_BIN -r "$RESTIC_REPOSITORY" snapshots >/dev/null 2>&1; then
$RESTIC_BIN -r "$RESTIC_REPOSITORY" init
$RESTIC_BIN -r "$RESTIC_REPOSITORY" snapshots >/dev/null || { echo "ERREUR : repo illisible après init"; exit 1; }
journal "Repo restic initialisé" "Backup chiffré du G9 (FTP/rclone)" "repo $RESTIC_REPOSITORY, config+clé relues après écriture" "suppression du repo distant (destructif, manuel)"
else
log "Repo déjà initialisé."
fi
if [[ "$($RESTIC_BIN -r "$RESTIC_REPOSITORY" snapshots 2>/dev/null | grep -cE '^[0-9a-f]{8}' || true)" == "0" ]]; then
log "Premier backup de $RESTIC_SOURCES — conteneurs stateful arrêtés (cohérence des bases)"
for c in $STATEFUL; do docker stop -t 20 "$c" >/dev/null 2>&1 && log "stop $c"; done
set +e
$RESTIC_BIN -r "$RESTIC_REPOSITORY" "${AOPT[@]}" backup --exclude-file "${RESTIC_EXCLUDE_FILE:-/etc/restic/excludes.txt}" --tag "${RESTIC_TAG:-g9}" $RESTIC_SOURCES
RC=$?
set -e
for c in $STATEFUL; do docker start "$c" >/dev/null 2>&1 && log "start $c"; done
[[ $RC -eq 0 ]] || { journal "Premier backup ÉCHEC" "Backup chiffré du G9" "rc=$RC" "re-exécuter le script"; echo "ERREUR : backup rc=$RC"; exit 1; }
journal "Premier backup OK" "Backup chiffré du G9 (G2)" "snapshot créé (sources $RESTIC_SOURCES), conteneurs stoppés puis relancés" "restic forget du snapshot + prune"
else
log "Snapshots déjà présents : premier backup sauté."
fi
log "Append-only (après le 1er backup) : rclone serve restic --append-only"
# restic >= 0.18 n'a plus de sous-commande `set` ; l'append-only s'obtient par le backend rclone
# (`rclone serve restic --stdio --append-only` refuse la suppression des objets du repo).
# Limitation assumée : enforcement par le process rclone local (le G9), pas par la Freebox.
APPENDONLY_ARGS=(-o "rclone.args=serve restic --stdio --append-only")
SCRATCH=/tmp/restic-appendonly-test
SCRATCH_REPO="${SCRATCH_REPO:-rclone:${RESTIC_RCLONE_REMOTE}Freebox/backup/_appendonly-test}"
mkdir -p "$SCRATCH"
echo "appendonly-proof-$(date -Is)" > "$SCRATCH/canary.txt"
set +e
$RESTIC_BIN -r "$SCRATCH_REPO" init >/dev/null 2>&1
$RESTIC_BIN -r "$SCRATCH_REPO" backup "$SCRATCH" >/dev/null 2>&1
$RESTIC_BIN -r "$SCRATCH_REPO" "${APPENDONLY_ARGS[@]}" forget --prune --keep-last 1 --force > "$SCRATCH/forget.log" 2>&1
RC_FORGET=$?
$RESTIC_BIN -r "$SCRATCH_REPO" snapshots >/dev/null 2>&1
RC_SNAP=$?
set -e
if [[ $RC_FORGET -ne 0 && $RC_SNAP -eq 0 ]]; then
journal "Append-only vérifié" "Anti-ransomware (G2)" "forget --prune REFUSÉ par rclone --append-only (rc=$RC_FORGET), snapshots toujours lisibles ; jobs backup utilisés avec cet argument" "retirer --append-only de RESTIC_RCLONE_APPENDONLY"
log "Append-only vérifié : suppression refusée (rc=$RC_FORGET), repo intact"
else
journal "Append-only NON vérifié" "Anti-ransomware (G2)" "rc_forget=$RC_FORGET rc_snap=$RC_SNAP — comportement inattendu" "voir journal"
log "AVERTISSEMENT : append-only non confirmé (rc_forget=$RC_FORGET)"
fi
rm -rf "$SCRATCH"
$RCLONE_BIN purge "$SCRATCH_REPO" >/dev/null 2>&1 || true
log "TEST DE RESTAURATION (AC5)"
RESTORE_DIR=$(mktemp -d /restore-test-XXXXXXXX)
# Fichier test = premier fichier > 1k des sources (déterministe ; `restic ls` n'expose pas
# la taille dans une colonne exploitable par awk, la branche `ls` était morte).
TEST_FILE=$(find $RESTIC_SOURCES -type f -size +1k 2>/dev/null | head -n 1 || true)
[[ -n "$TEST_FILE" ]] || { echo "ERREUR : aucun fichier test trouvé"; exit 1; }
RESTIC_PATH="$TEST_FILE"
$RESTIC_BIN -r "$RESTIC_REPOSITORY" restore latest --target "$RESTORE_DIR" --include "$RESTIC_PATH"
SRC_SHA=$(sha256sum "$TEST_FILE" | cut -d' ' -f1)
DST_SHA=$(sha256sum "$RESTORE_DIR/$RESTIC_PATH" 2>/dev/null | cut -d' ' -f1)
if [[ -f "$RESTORE_DIR/$RESTIC_PATH" ]] && cmp -s "$TEST_FILE" "$RESTORE_DIR/$RESTIC_PATH" && [[ "$SRC_SHA" == "$DST_SHA" ]]; then
log "Restauration testée OK : $TEST_FILE (sha256 ${SRC_SHA:0:12}… identique)"
journal "Test restauration OK" "Vérification G2 (restauration testée)" "$TEST_FILE restauré depuis la Freebox, sha256 identique" "—"
else
log "ÉCHEC restauration : $TEST_FILE (src ${SRC_SHA:0:12}… / dst ${DST_SHA:0:12}…)"
journal "Test restauration ÉCHEC" "Vérification G2" "fichier restauré différent de $TEST_FILE" "re-brancher le backup"
rm -rf "$RESTORE_DIR"; exit 1
fi
rm -rf "$RESTORE_DIR"
log "Jobs quotidiens + alerte Matrix"
install -m 755 "$(dirname "$0")/restic-notify.sh" /usr/local/bin/restic-notify.sh
cat > /usr/local/bin/restic-daily.sh <<'EOS'
#!/usr/bin/env bash
set -euo pipefail
set -a; source /etc/restic.env; set +a
RESTIC_BIN=/usr/local/bin/restic
RCLONE_BIN=/usr/local/bin/rclone
STATEFUL="${RESTIC_STATEFUL:-valheim n8n synapse couchdb obsidian}"
AOPT=()
if [[ "${RESTIC_RCLONE_APPENDONLY:-0}" == "1" ]]; then AOPT=(-o "rclone.args=serve restic --stdio --append-only"); fi
if ! $RCLONE_BIN --timeout 30s lsf "${RESTIC_RCLONE_REMOTE:-freeboxftp:}${RESTIC_RCLONE_PATH:-Freebox/backup}" >/dev/null 2>&1; then
/usr/local/bin/restic-notify.sh "BACKUP KO : FTP Freebox injoignable depuis le G9"
exit 1
fi
$RESTIC_BIN -r "$RESTIC_REPOSITORY" unlock >/dev/null 2>&1 || true
for c in $STATEFUL; do docker stop -t 20 "$c" >/dev/null 2>&1 || true; done
set +e
$RESTIC_BIN -r "$RESTIC_REPOSITORY" "${AOPT[@]}" backup --exclude-file "${RESTIC_EXCLUDE_FILE:-/etc/restic/excludes.txt}" --tag "${RESTIC_TAG:-g9}" $RESTIC_SOURCES
RC=$?
set -e
for c in $STATEFUL; do docker start "$c" >/dev/null 2>&1 || true; done
if [[ $RC -ne 0 ]]; then
/usr/local/bin/restic-notify.sh "BACKUP KO : restic a échoué (rc=$RC) sur le G9"
exit $RC
fi
EOS
chmod 755 /usr/local/bin/restic-daily.sh
cat > /usr/local/bin/restic-prune.sh <<'EOS'
#!/usr/bin/env bash
set -euo pipefail
set -a; source /etc/restic.env; set +a
RESTIC_BIN=/usr/local/bin/restic
$RESTIC_BIN -r "$RESTIC_REPOSITORY" unlock >/dev/null 2>&1 || true
AOPT=()
if [[ "${RESTIC_RCLONE_APPENDONLY:-0}" == "1" ]]; then AOPT=(-o "rclone.args=serve restic --stdio --append-only"); fi
RETAIN=(--keep-daily "${RESTIC_RETAIN_DAILY:-7}" --keep-weekly "${RESTIC_RETAIN_WEEKLY:-4}" \
--keep-monthly "${RESTIC_RETAIN_MONTHLY:-12}" --keep-yearly "${RESTIC_RETAIN_YEARLY:-5}")
# Le refus d'append-only est le comportement ATTENDU : log seul, pas d'alerte (sinon alerte
# quotidienne = fatigue du canal où arrivent les vrais « BACKUP KO »).
if ! $RESTIC_BIN -r "$RESTIC_REPOSITORY" "${AOPT[@]}" forget --prune "${RETAIN[@]}"; then
echo "$(date -Is) append-only : suppression des objets refusée (attendu), purge sans --append-only"
if ! $RESTIC_BIN -r "$RESTIC_REPOSITORY" forget --prune "${RETAIN[@]}"; then
/usr/local/bin/restic-notify.sh "PURGE KO : la purge a échoué même sans --append-only (rétention bloquée)" || true
exit 1
fi
fi
EOS
chmod 755 /usr/local/bin/restic-prune.sh
cat > /usr/local/bin/restic-health.sh <<'EOS'
#!/usr/bin/env bash
# Le dernier snapshot doit dater de moins de 26 h.
set -euo pipefail
set -a; source /etc/restic.env; set +a
RESTIC_BIN=/usr/local/bin/restic
LAST=$($RESTIC_BIN -r "$RESTIC_REPOSITORY" snapshots --json 2>/dev/null | grep -o '"time":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
AGE=$(( $(date +%s) - $(date -d "${LAST:-1970-01-01}" +%s) ))
if [[ $AGE -gt $((26*3600)) ]]; then
/usr/local/bin/restic-notify.sh "BACKUP KO : aucun snapshot depuis $((AGE/3600)) h sur le G9"
echo "KO: dernier snapshot = ${LAST:-aucun}"
exit 1
fi
echo "OK: dernier snapshot = $LAST"
EOS
chmod 755 /usr/local/bin/restic-health.sh
BACKUP_LINE="30 3 * * * /usr/local/bin/restic-daily.sh >> /var/log/restic-daily.log 2>&1"
PRUNE_LINE="0 4 * * * /usr/local/bin/restic-prune.sh >> /var/log/restic-prune.log 2>&1"
HEALTH_LINE="15 9 * * * /usr/local/bin/restic-health.sh >> /var/log/restic-health.log 2>&1"
cron_tmp=$(mktemp)
{ crontab -l 2>/dev/null | grep -vE 'restic-daily\.sh|restic-prune\.sh|restic-health\.sh' || true; echo "$BACKUP_LINE"; echo "$PRUNE_LINE"; echo "$HEALTH_LINE"; } > "$cron_tmp"
if [[ "$(grep -c 'restic-' "$cron_tmp")" -ne 3 ]]; then
rm -f "$cron_tmp"
echo "ERREUR : crontab refusé (3 lignes attendues) — crontab existant laissé intact"
exit 1
fi
crontab "$cron_tmp"
rm -f "$cron_tmp"
journal "Cron restic programmé" "Backup quotidien + visibilité" "backup 03:30 (stop/backup/start) + prune 04:00 + health 09:15" "suppression des 3 lignes crontab"
log "Terminé. Vérif : $RESTIC_BIN -r $RESTIC_REPOSITORY snapshots ; check ; stats"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
#
# 06b-usb-media.sh — T6.1 : montage des disques USB média → /mnt/usb-media
# Prérequis MANUEL avant Jellyfin (T6.2). Exécuter en root.
#
# Garde-fous : G3 (formatage = validation explicite), G7 (rollback), G8 (journalisation).
#
# Pourquoi ce script est interactif : les disques USB (15 To + 5,5 To) ont des
# noms de device variables (sdb, sdc, sdd…) selon l'ordre de branchement. On ne
# peut PAS les détecter automatiquement en toute sécurité → l'utilisateur
# sélectionne les device à formater/monter.
#
# Fait :
# 1. Crée /mnt/usb-media
# 2. Affiche les disques USB détectés (lsblk)
# 3. Demande le device de CHAQUE disque média
# 4. (Si vierge) propose de formater en ext4 — garde-fou G3
# 5. Ajoute les entrées fstab (nofail) + montage
#
# ⚠️ OPÉRATION POTENTIELLEMENT DESTRUCTIVE (formatage) → bloqué sans --i-understand.
set -euo pipefail
log() { echo -e "\n[usb-media] $*"; }
die() { echo -e "\n[ERREUR] $*" >&2; exit 1; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 6 | usb-media : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
MOUNT_ROOT=/mnt/usb-media
mkdir -p "$MOUNT_ROOT"
log "Disques détectés :"
lsblk -o NAME,SIZE,TYPE,FSTYPE,MODEL | grep -vE 'rom|loop' || true
echo
read -r -p "Nom du 1er disque média (ex. sdb) : " DISK1
read -r -p "Nom du 2e disque média (ex. sdc) : " DISK2
[[ -n "$DISK1" && -n "$DISK2" ]] || die "Deux disques requis."
for DISK in "$DISK1" "$DISK2"; do
DEV="/dev/${DISK}"
[[ -b "$DEV" ]] || die "Device introuvable : $DEV"
if lsblk -no MOUNTPOINT "$DEV" | grep -q .; then
die "$DEV a déjà des partitions montées. Abandon."
fi
FSTYPE=$(blkid -o value -s TYPE "$DEV" 2>/dev/null || true)
if [[ -z "$FSTYPE" ]]; then
[[ "${1:-}" == "--i-understand" ]] || die "$DEV est vierge (formatage requis). Relancer avec --i-understand."
read -r -p "CONFIRMER le formatage ext4 de $DEV (efface tout) : saisir $DEV : " CONF
[[ "$CONF" == "$DEV" ]] || die "Non confirmé. Abandon."
mkfs.ext4 -F "$DEV"
FSTYPE=ext4
fi
# Le fstab doit porter le VRAI type de fs (pas ext4 en dur) : les disques média
# sont souvent NTFS/exFAT. Un mauvais type + nofail = montage silencieux qui échoue.
case "$FSTYPE" in
ext4|xfs|btrfs|ntfs|exfat|vfat) ;;
*) die "$DEV : système de fichiers '$FSTYPE' non supporté pour le montage média. Abandon." ;;
esac
LABEL=$(echo "$DISK" | tr -d '/')
UUID=$(blkid -s UUID -o value "$DEV")
MP="$MOUNT_ROOT/$LABEL"
mkdir -p "$MP"
# fsck (pass 2) ne sait vérifier que ext2/3/4 → 0 pour ntfs/exfat/vfat/xfs.
case "$FSTYPE" in
ext2|ext3|ext4) FSCK=2 ;;
*) FSCK=0 ;;
esac
if ! grep -qE "^[^#]* $MP " /etc/fstab; then
echo "UUID=${UUID} $MP $FSTYPE defaults,noatime,nofail 0 $FSCK" >> /etc/fstab
fi
mount "$MP"
log "Monté : $DEV → $MP ($FSTYPE)"
done
journal "USB média montés" "Jellyfin (T6.2) lit les médias" "$MOUNT_ROOT (disques $DISK1, $DISK2)" "suppression des entrées fstab + umount"
log "Terminé. Jellyfin (T6.2) peut maintenant monter $MOUNT_ROOT."
echo "⚠️ Redémarrer pour valider le montage au boot (fstab nofail)."
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# 06c-postboot-check.sh — vérifie la persistance du backup restic après reboot du G9.
# Lecture seule : ne modifie rien. Exécuter en root : sudo bash 06c-postboot-check.sh
set -uo pipefail
ok=0; ko=0
say() { printf '%-46s %s\n' "$1" "$2"; }
chk() { if eval "$2" >/dev/null 2>&1; then say "$1" "OK"; ok=$((ok+1)); else say "$1" "KO"; ko=$((ko+1)); fi; }
chk "binaire restic (/usr/local/bin/restic)" "test -x /usr/local/bin/restic"
chk "binaire rclone (/usr/local/bin/rclone)" "test -x /usr/local/bin/rclone"
chk "/etc/restic.env présent et 600" "test \$(stat -c %a /etc/restic.env) = 600"
chk "clé repo /etc/restic/repo.key 600" "test \$(stat -c %a /etc/restic/repo.key) = 600"
chk "config rclone 600" "test \$(stat -c %a /root/.config/rclone/rclone.conf) = 600"
chk "aucun montage CIFS requis (fstab)" "! grep -q cifs /etc/fstab"
chk "cron root : 3 lignes restic" "test \$(crontab -l | grep -c 'restic-') = 3"
chk "job quotidien exécutable" "test -x /usr/local/bin/restic-daily.sh"
chk "job prune exécutable" "test -x /usr/local/bin/restic-prune.sh"
chk "job health exécutable" "test -x /usr/local/bin/restic-health.sh"
chk "job notify exécutable" "test -x /usr/local/bin/restic-notify.sh"
chk "jq présent (payload de l'alerte)" "command -v jq"
set -a; . /etc/restic.env; set +a
chk "repo accessible (restic snapshots)" "restic -r \"\$RESTIC_REPOSITORY\" snapshots"
chk "repo intègre (restic check)" "restic -r \"\$RESTIC_REPOSITORY\" check --read-data-subset=1%"
chk "dernier snapshot < 26 h" "/usr/local/bin/restic-health.sh"
chk "fichier d'exclusions présent" "test -s \"${RESTIC_EXCLUDE_FILE:-/etc/restic/excludes.txt}\""
# Liste stateful lue depuis /etc/restic.env (source unique, pas de liste en dur ici).
RUNNING=$(docker ps --format '{{.Names}}')
stateful_up=0; stateful_total=0
for c in ${RESTIC_STATEFUL:-}; do
stateful_total=$((stateful_total+1))
grep -qx "$c" <<<"$RUNNING" && stateful_up=$((stateful_up+1))
done
chk "conteneurs stateful relancés (${stateful_up}/${stateful_total})" "test $stateful_total -gt 0 -a $stateful_up -eq $stateful_total"
echo
echo "persistance post-reboot : $ok OK / $ko KO"
[[ $ko -eq 0 ]] || exit 1
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env bash
#
# 07-migrate-stateful.sh — T4.x : export/import des stateful (xpenology → G9)
#
# Garde-fous : G4 (double-run, xpenology = source de vérité, lecture seule), G7 (rollback), G8 (journalisation).
#
# Principe (garde-fou G4 — double-run) :
# 1. EXPORT des données depuis le xpenology (source de vérité) → copie sur le G9.
# 2. VÉRIFICATION D'INTÉGRITÉ fichier par fichier (manifeste sha256 trié, diff).
# 3. Le xpenology n'est JAMAIS modifié/supprimé ici — lecture seule.
# 4. Chaque brique est validée sur le G9 AVANT que le xpenology soit décommissionné (T6.4).
#
# Usage :
# ./07-migrate-stateful.sh <brique>
# briques : vaultwarden | nextcloud | photoprism | gitea | n8n
#
# Prérequis :
# - Accès SSH au xpenology : ssh -p 222 Master@192.168.1.25 (clé SSH + sudo NOPASSWD —
# compte `effi` bloqué par Synology depuis 10/2026, voir journal T1.1-b).
# - Sur le G9 : /data monté, Docker opérationnel.
# - Secrets bootstrap (03b-secrets.sh) déjà exécuté.
#
# ⚠️ 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
# (XP_CONTAINERS) avant l'export et les REDÉMARRE juste après — le double-run
# G4 est respecté (la source reprend du service, rien n'est supprimé).
#
# ⚠️ CREDENTIELS DB : l'import est une COPIE de la base existante. Les env
# MARIADB_*/POSTGRES_* (seedés par 03b-secrets.sh) sont IGNORÉS sur une base
# déjà initialisée → la base importée garde les CREDENTIELS D'ORIGINE du
# xpenology, et la config app importée est cohérente avec elle. Le service
# G9 démarre donc avec les identifiants d'origine, PAS les secrets générés.
#
# ⚠️⚠️ À CONFIRMER PENDANT L'INVENTAIRE T1.2 ⚠️⚠️
# Les chemins sources xpenology (XP_MAP) sont des HYPOTHÈSES. Les conteneurs
# stateful ont été créés via la GUI DSM (snapshot ETAT-DE-LART-2026-10-06 §1),
# donc leur arborescence réelle n'est pas garantie. AVANT d'exécuter, vérifier
# sur le xpenology que chaque chemin source existe et contient les bonnes données,
# puis ajuster XP_MAP si besoin. Le script refuse de migrer un chemin source absent.
#
# Rollback : la migration est une COPIE → rien n'est perdu. Sur le G9, supprimer
# le conteneur + les sous-dossiers importés dans compose/<brique>/ si l'import est inacceptable.
set -euo pipefail
log() { echo -e "\n[migrate] $*"; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 4 | migrate:%s : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$BRIQUE" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
BRIQUE="${1:-}"
[[ -n "$BRIQUE" ]] || { echo "Usage : ./07-migrate-stateful.sh <vaultwarden|nextcloud|photoprism|gitea|n8n>"; exit 1; }
# --- Coordonnées xpenology (lecture seule) ---
XP_USER="Master"
XP_HOST="192.168.1.25"
XP_PORT=222
XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker"
XP_SSH="ssh -p $XP_PORT -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST"
# Pré-test rapide : connexion SSH + rsync présent sur le xpenology (échec franc avant transfert).
if ! $XP_SSH "command -v rsync >/dev/null 2>&1" 2>/dev/null; then
echo "ERREUR : connexion SSH impossible vers $XP_USER@$XP_HOST:$XP_PORT ou rsync absent du NAS."
echo "Vérifiez les accès SSH et la présence de rsync côté xpenology avant de relancer."
exit 1
fi
# --- Racine des compose G9 ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
COMPOSE_ROOT="$(dirname "$SCRIPT_DIR")/compose"
COMPOSE_DIR="$COMPOSE_ROOT/$BRIQUE"
[[ -d "$COMPOSE_DIR" ]] || { echo "ERREUR : $COMPOSE_DIR absent"; exit 1; }
# --- Mapping source (xpenology) → sous-dossier compose (G9) ---
# Format par brique : "xp_dir:g9_subdir;xp_dir:g9_subdir;..."
# g9_subdir est relatif à compose/<brique>/ (il correspond aux volumes relatifs ./data, ./db, ...).
# ⚠️ Chemins sources = HYPOTHÈSES à confirmer à l'inventaire T1.2.
declare -A XP_MAP=(
[vaultwarden]="/volume1/docker/vaultwarden:data"
[nextcloud]="/volume1/docker/nextcloud/data:data;/volume1/docker/nextcloud/config:config;/volume1/docker/nextcloud_db/db:db"
[photoprism]="/volume1/docker/photoprism/storage:storage;/volume1/docker/photoprism_db/db:db"
[gitea]="/volume1/docker/gitea:data"
[n8n]="/volume1/docker/n8n/data:data;/volume1/docker/n8n_db/db:db"
)
# Conteneurs SOURCE à stopper sur .25 pendant l'export (cohérence des bases),
# redémarrés immédiatement après (double-run G4). Noms = inventaire T1.1-a
# (conteneurs-xpenology.csv) — à ajuster si le nom réel diffère.
declare -A XP_CONTAINERS=(
[vaultwarden]="vaultwarden-server-1"
[nextcloud]="nextcloud_app nextcloud_db"
[photoprism]="photoprism photoprism_db"
[gitea]="gitea"
[n8n]="n8n"
)
# Photoprism lit les originaux IN-PLACE (pas de migration) : /data/famille/photos doit
# déjà être en place sur le G9 (T2.2/T2.4). Seuls storage + db sont migrés.
if [[ "$BRIQUE" == "photoprism" ]]; then
log "⚠️ PhotoPrism : les originaux sont lus IN-PLACE sur /data/famille/photos (non migrés)."
log " Vérifiez que /data/famille/photos est bien en place sur le G9 avant 'compose up'."
fi
XP_MAP_LINE="${XP_MAP[$BRIQUE]:-}"
[[ -n "$XP_MAP_LINE" ]] || { echo "ERREUR : mapping inconnu pour $BRIQUE (compléter XP_MAP)"; exit 1; }
# Décomposer le mapping en paires (xp_dir, g9_subdir)
IFS=';' read -ra PAIRS <<< "$XP_MAP_LINE"
XP_DIRS=()
G9_SUBS=()
for pair in "${PAIRS[@]}"; do
[[ -n "$pair" ]] || continue
XP_DIRS+=("${pair%%:*}")
G9_SUBS+=("${pair##*:}")
done
log "=== ÉTAPE 1/3 : VÉRIFICATION DES CHEMINS SOURCES (xpenology, lecture seule) ==="
MISSING=0
for i in "${!XP_DIRS[@]}"; do
if $XP_SSH "test -d '${XP_DIRS[$i]}'" 2>/dev/null; then
log " ✅ source présente : ${XP_DIRS[$i]} → compose/$BRIQUE/${G9_SUBS[$i]}/"
else
log " ❌ source ABSENTE : ${XP_DIRS[$i]} (attendu → compose/$BRIQUE/${G9_SUBS[$i]}/)"
MISSING=1
fi
done
if [[ $MISSING -eq 1 ]]; then
echo
echo "ERREUR : au moins un chemin source xpenology est absent."
echo "Les chemins de XP_MAP sont des hypothèses (conteneurs créés via GUI DSM)."
echo "Vérifiez l'arborescence réelle sur le xpenology (inventaire T1.2) et corrigez XP_MAP."
exit 1
fi
log "=== ÉTAPE 2/3 : TRANSFERT (rsync, chiffré via SSH) + INTÉGRITÉ fichier par fichier ==="
# --- Stop de la source (cohérence des bases vivantes) ---
STOPPED=()
for c in ${XP_CONTAINERS[$BRIQUE]:-}; do
if $XP_SSH "sudo $XP_DOCKER ps -q -f name=^${c}\$" 2>/dev/null | grep -q .; then
if $XP_SSH "sudo $XP_DOCKER stop $c" >/dev/null 2>&1; then
STOPPED+=("$c")
log " ⏸ source stoppée : $c"
else
log " ⚠️ échec du stop de $c — export potentiellement incohérent, À VÉRIFIER."
fi
else
log " ⚠️ conteneur '$c' non trouvé running sur .25 — vérifier l'inventaire (cohérence non garantie)."
fi
done
start_source() {
for c in "${STOPPED[@]:-}"; do
$XP_SSH "sudo $XP_DOCKER start $c" >/dev/null 2>&1 && log " ▶ source redémarrée : $c" || log " ⚠️ ÉCHEC du redémarrage de $c — INTERVENIR SUR .25."
done
}
trap start_source EXIT
TOTAL_FILES=0
for i in "${!XP_DIRS[@]}"; do
XP_DIR="${XP_DIRS[$i]}"
G9_SUB="${G9_SUBS[$i]}"
G9_DIR="$COMPOSE_DIR/$G9_SUB"
mkdir -p "$G9_DIR"
log "Transfert : $XP_DIR/ → $G9_DIR/"
rsync -a --info=progress2 -e "ssh -p $XP_PORT -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600" "$XP_USER@$XP_HOST:$XP_DIR/" "$G9_DIR/"
# Manifeste sha256 trié, chemins RELATIFS (identiques des deux côtés) → diff fiable.
log "Intégrité : manifeste sha256 (chemins relatifs, triés) pour $G9_SUB"
REMOTE_MANIFEST=$(mktemp)
LOCAL_MANIFEST=$(mktemp)
$XP_SSH "cd '$XP_DIR' && find . -type f -exec sha256sum {} + 2>/dev/null | sort" > "$REMOTE_MANIFEST"
( cd "$G9_DIR" && find . -type f -exec sha256sum {} + 2>/dev/null | sort ) > "$LOCAL_MANIFEST"
R_COUNT=$(wc -l < "$REMOTE_MANIFEST")
L_COUNT=$(wc -l < "$LOCAL_MANIFEST")
TOTAL_FILES=$(( TOTAL_FILES + R_COUNT ))
log " fichiers : source=$R_COUNT local=$L_COUNT"
if diff -u "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" > "${REMOTE_MANIFEST}.diff"; then
log " ✅ intégrité OK ($G9_SUB)"
else
log " ❌ ÉCHEC intégrité ($G9_SUB) — différences :"
head -n 50 "${REMOTE_MANIFEST}.diff"
rm -f "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" "${REMOTE_MANIFEST}.diff"
journal "import ÉCHEC intégrité sur $G9_SUB" "Vérification G4 (intégrité)" "manifeste sha256 différent" "supprimer le sous-dossier importé, re-transférer"
echo "ERREUR : l'intégrité a échoué sur $G9_SUB. Ne pas lancer le service. Re-vérifier le transfert."
exit 1
fi
rm -f "$REMOTE_MANIFEST" "$LOCAL_MANIFEST" "${REMOTE_MANIFEST}.diff"
done
log "Intégrité globale OK : $TOTAL_FILES fichiers vérifiés."
journal "export+import OK" "Migration G4 (copie, xpenology non modifié)" "$TOTAL_FILES fichiers, manifeste sha256 identique" "suppression du conteneur + sous-dossiers importés"
# --- Redémarrage de la source AVANT de toucher au G9 (double-run G4) ---
start_source
log "=== ÉTAPE 3/3 : LANCEMENT DU SERVICE SUR LE G9 ==="
log "docker compose up -d dans $COMPOSE_DIR"
docker compose -f "$COMPOSE_DIR/docker-compose.yml" up -d
log "Vérification du service (healthcheck)"
sleep 10
docker compose -f "$COMPOSE_DIR/docker-compose.yml" ps
log "Terminé. VALIDER MANUELLEMENT le service (login, 1 fichier, 1 recherche) AVANT de passer à la brique suivante."
journal "service lancé sur le G9" "Double-run G4" "compose up -d, healthcheck à valider" "docker compose down + suppression des données importées"
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
#
# 07a-migrate-gitea.sh — T4.4 : migration Gitea (xpenology .25 → G9 .24)
#
# Garde-fous : G4 (double-run, .25 = source de vérité, LECTURE SEULE), G2 (backup restic
# déclenché avant import), G7 (rollback), G8 (journalisation), G9 (aucun secret loggué).
#
# SOURCE RÉELLE (mesurée le 2026-10-11, voir C-T4-INV) : le Gitea familial est le **paquet
# App Center DSM « gitea » v1.27.3** (compte sc-gitea), PAS le conteneur compose :
# - DB : postgres du conteneur partagé `postgres16` (127.0.0.1:5433), base `gitea_db`,
# rôle `gitea_folzo` → 13 repositories, 3 users
# - dépôts : /volume2/gitea/gitea-repositories (155 Mo)
# - LFS : /volume2/gitea/lfs
# - appdata : /volume2/@appdata/gitea/data (avatars, attachments, jwt, sessions, indexers)
# - conf : /volume2/@appdata/gitea/conf.ini (ROOT = /volume2/gitea/gitea-repositories)
# Le conteneur `gitea` de DSM (compose, ROOT = /data/git/repositories VIDE) partage la MÊME base
# postgres : second frontend, dépôts vides → il n'est PAS la source.
#
# COHÉRENCE (G4) : pg_dump = instantané cohérent (MVCC) → AUCUN stop, AUCUNE écriture,
# AUCUNE suppression sur .25. Les dépôts git (objets) sont quiets.
#
# TRANSIT : **tar sur SSH** (.25 → stage .11 → .24). Mesuré le 2026-10-11 : le sshd de DSM
# refuse `rsync --server` (« Permission denied, please try again. ») et scp (subsystem sftp
# internal-sftp) → `subsystem request failed`. Seuls `ssh 'sudo tar cz'` et `ssh 'sudo cat'`
# fonctionnent. Le stage local est un cache, purgé à la fin.
#
# Usage (machine de pilotage détenant les clés SSH, ex. PC IA .11) : ./07a-migrate-gitea.sh
# Prérequis : clés SSH Master@.25:222 + folzo@.24 ; 03b-secrets.sh exécuté sur le G9.
#
# Rollback : migration = COPIE. G9 : `docker compose -f /data/docker/gitea/docker-compose.yml
# down` + suppression de /data/docker/gitea. .25 : rien (intact).
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
BRIQUE="gitea"
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_XP="ssh -p $XP_PORT -i $KEY"
RSH_G9="ssh -i $KEY"
log() { echo -e "\n[gitea] $*"; }
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 ); }
# ---------------------------------------------------------------- 0. préflight
log "=== 0/5 PRÉFLIGHT ==="
$SSH_XP "command -v rsync >/dev/null && echo XP_OK" >/dev/null || { echo "ERREUR : SSH/rsync .25 indisponibles"; exit 1; }
$SSH_G9 "command -v rsync >/dev/null && command -v docker >/dev/null && echo G9_OK" >/dev/null || { echo "ERREUR : SSH/rsync/docker .24 indisponibles"; exit 1; }
$SSH_XP "command -v tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar(sudo) indisponibles sur .25"; exit 1; }
for d in /volume2/gitea/gitea-repositories /volume2/gitea/lfs /volume2/@appdata/gitea/data; do
$SSH_XP "sudo test -d '$d'" >/dev/null || { echo "ERREUR : source absente sur .25 : $d"; exit 1; }
done
$SSH_G9 "test -d $KIT_ON_G9/compose && command -v rsync >/dev/null" || {
echo "ERREUR : kit absent sur le G9 ($KIT_ON_G9) — synchroniser le kit avant de migrer."; exit 1; }
mkdir -p "$STAGE"
# ---------------------------------------------------------------- 1. backup G2
log "=== 1/5 BACKUP restic (G2) ==="
$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 Gitea" "snapshot restic Freebox (voir logs /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
# ---------------------------------------------------------------- 2. export .25
log "=== 2/5 EXPORT pg_dump depuis .25 (lecture seule, MVCC, sans stop) ==="
STAMP="$(date '+%Y-%m-%d-%H%M')"
DUMP_REMOTE="/tmp/gitea_db_$STAMP.dump"
$SSH_XP "sudo $XP_DOCKER exec postgres16 pg_dump -Fc -Z3 -U gitea_folzo gitea_db > $DUMP_REMOTE && sudo chmod 644 $DUMP_REMOTE"
DUMP_SHA=$($SSH_XP "sha256sum $DUMP_REMOTE | cut -d' ' -f1")
DUMP_SIZE=$($SSH_XP "stat -c%s $DUMP_REMOTE")
log "dump ${DUMP_REMOTE##*/} : $DUMP_SIZE o, sha256 ${DUMP_SHA:0:16}…"
$SSH_XP "sudo cat $DUMP_REMOTE" > "$STAGE/${DUMP_REMOTE##*/}"
[[ "$(sha256sum "$STAGE/${DUMP_REMOTE##*/}" | cut -d' ' -f1)" == "$DUMP_SHA" ]] || { echo "ERREUR : dump corrompu au stage"; exit 1; }
$SSH_XP "sudo rm -f $DUMP_REMOTE"
# ---------------------------------------------------------------- 3. transfert + intégrité
log "=== 3/5 TRANSFERT (tar sur SSH : .25 → stage → .24) + INTÉGRITÉ sha256 ==="
copy_pair() { # $1 = src .25, $2 = nom du sous-dossier G9
local src="$1" name="$2"
local stage="$STAGE/$name"
mkdir -p "$stage"
log " hop 1 (tar/ssh) : $src/ → stage/$name/"
$SSH_XP "sudo tar cz -C '$src' ." | tar xz -C "$stage"
log " intégrité $name (source vs stage) :"
local r l rc=0
r=$(mktemp); l=$(mktemp)
$SSH_XP "cd '$src' && sudo find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$r"
manifest_in "$stage" > "$l"
diff -u "$r" "$l" > "$r.diff" || rc=1
if [[ $rc -ne 0 ]]; then
log " ❌ ÉCHEC intégrité $name — 20 premières différences :"; head -20 "$r.diff"
rm -f "$r" "$l" "$r.diff"
journal "import ÉCHEC intégrité ($name)" "Vérification G4" "manifeste sha256 source ≠ stage" "purger stage/$name et re-exécuter"
echo "ERREUR : intégrité $name — arrêt avant le G9."; exit 1
fi
log " ✅ $(wc -l < "$r") fichiers identiques (source == stage)"
rm -f "$r" "$l" "$r.diff"
log " hop 2 (rsync) : stage/$name/ → .24:$G9_DIR/$name/"
$SSH_G9 "mkdir -p $G9_DIR/$name"
rsync -a --info=stats1 -e "$RSH_G9" "$stage/" "$G9_USER@$G9_HOST:$G9_DIR/$name/"
local g rc2=0
g=$(mktemp)
manifest_in "$stage" > "$r"; $SSH_G9 "cd '$G9_DIR/$name' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$g"
diff -u "$r" "$g" > "$g.diff" || rc2=1
if [[ $rc2 -ne 0 ]]; then
log " ❌ ÉCHEC intégrité $name sur le G9 :"; head -20 "$g.diff"
rm -f "$r" "$g" "$g.diff"
journal "import ÉCHEC intégrité G9 ($name)" "Vérification G4" "manifeste stage ≠ G9" "supprimer $G9_DIR/$name et re-transférer"
echo "ERREUR : intégrité G9 $name — arrêt."; exit 1
fi
log " ✅ $name : $(wc -l < "$g") fichiers identiques sur le G9"
rm -f "$r" "$g" "$g.diff"
}
copy_pair /volume2/gitea/gitea-repositories repositories
copy_pair /volume2/gitea/lfs lfs
copy_pair /volume2/@appdata/gitea/data data/gitea
log " dump → G9"
$SSH_G9 "mkdir -p $G9_DIR/backup"
rsync -a --info=stats1 -e "$RSH_G9" "$STAGE/${DUMP_REMOTE##*/}" "$G9_USER@$G9_HOST:$G9_DIR/backup/"
[[ "$($SSH_G9 "sha256sum $G9_DIR/backup/${DUMP_REMOTE##*/} | cut -d' ' -f1")" == "$DUMP_SHA" ]] || { echo "ERREUR : sha256 dump ≠ source"; exit 1; }
log " ✅ dump restauré sur le G9, sha256 identique (${DUMP_SHA:0:16}…)"
rm -rf "$STAGE"
# ---------------------------------------------------------------- 4. déploiement + restore
log "=== 4/5 DÉPLOIEMENT compose + RESTORE DB sur le G9 ==="
# Le kit vit sur le G9 dans /home/folzo/g9-migration : les secrets y sont générés par
# 03b-secrets.sh (644 root, hors git), puis le stack est installé sous /data/docker/<brique>
# (source couverte par le repo restic — C-COMMON).
KIT_ON_G9="/home/folzo/g9-migration"
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 && sudo ls -l $KIT_ON_G9/compose/$BRIQUE/secrets/"
# secrets/ est root:root 700 (03b-secrets tourne en root) → copie du stack en sudo,
# puis secrets relus en 755/644 (lisibles par l'uid conteneur, dossier hors git — G9).
$SSH_G9 "sudo mkdir -p $G9_DIR && sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets && sudo chmod 644 $G9_DIR/secrets/dbpass"
$SSH_G9 "cd $G9_DIR && sudo sh -c 'umask 077 && printf \"GITEA__database__PASSWD=%s\n\" \"\$(cat secrets/dbpass)\" > secrets/gitea.env' && sudo chmod 644 secrets/gitea.env"
$SSH_G9 "cd $G9_DIR && docker compose up -d gitea-db && sleep 15 && docker compose ps --format '{{.Name}} {{.Status}}'"
log "restore pg_dump → gitea_db (rôle gitea_folzo, base vierge)"
# Le dump n'est pas monté dans le conteneur postgres → il entre par stdin (format custom).
$SSH_G9 "cd $G9_DIR && docker compose exec -T gitea-db pg_restore -U gitea_folzo -d gitea_db < backup/$(basename "$DUMP_REMOTE") 2>&1 | tail -8"
$SSH_G9 "cd $G9_DIR && docker compose exec -T gitea-db psql -U gitea_folzo -d gitea_db -t -A -c \"select 'repositories='||count(*) from repository; select 'users='||count(*) from \\\"user\\\";\""
$SSH_G9 "sudo chown -R 1000:1000 $G9_DIR/repositories $G9_DIR/lfs $G9_DIR/data && sudo chmod -R u+rwX $G9_DIR/repositories $G9_DIR/lfs $G9_DIR/data"
$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 20 && docker compose ps --format '{{.Name}} {{.Status}}'"
# ---------------------------------------------------------------- 5. vérification
log "=== 5/5 VÉRIFICATION ==="
$SSH_G9 "curl -sf -o /dev/null -w 'healthz HTTP %{http_code}\n' http://127.0.0.1:3000/api/healthz"
$SSH_G9 "cd $G9_DIR && docker compose exec -T gitea-db psql -U gitea_folzo -d gitea_db -t -A -c \"select owner_name||'/'||lower_name from repository order by 1;\""
log "source .25 (double-run G4) :"
$SSH_XP "sudo $XP_DOCKER ps --format '{{.Names}} {{.Status}}' | grep -E '^gitea '"
journal "Gitea migré + lancé sur le G9" "T4.4 (double-run G4, .25 lecture seule)" "pg_dump ${DUMP_SHA:0:12}… restauré ; dépôts+LFS+appdata sha256 identiques ; healthz ; .25 intact" "docker compose -f $G9_DIR/docker-compose.yml down + rm -rf $G9_DIR (G9 seul)"
log "Terminé. VALIDATION MANUELLE : git ls-remote ≥ 3 repos, clone/push, repo g9-migration."
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
#
# 08-decommission.sh — T6.4 : Décommissionnement du xpenology
#
# 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 :
# 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).
#
# Usage :
# sudo ./08-decommission.sh --i-understand
#
# Le script EXIGE de cocher chaque prérequis avant de continuer.
set -euo pipefail
log() { echo -e "\n[decom] $*"; }
die() { echo -e "\n[ERREUR] $*" >&2; exit 1; }
journal() {
local action="$1" raison="$2" resultat="$3" rollback="$4"
printf '| %s | 6 | decommission : %s | %s | %s | %s |\n' "$(date '+%Y-%m-%d %H:%M')" "$action" "$raison" "$resultat" "$rollback" >> "$(dirname "$0")/../journal.md"
}
[[ "${1:-}" == "--i-understand" ]] || die "Drapeau --i-understand requis (opération irréversible)."
XP_HOST="192.168.1.25"
log "=== PRÉREQUIS (cocher chacun : y/n) ==="
prereq() {
local label="$1"
read -r -p " [ ] $label — confirmé ? (y/N) : " a
[[ "$a" == "y" || "$a" == "Y" ]] || die "Prérequis non confirmé : $label"
}
prereq "Toutes les briques (Vaultwarden, Nextcloud, PhotoPrism, Gitea, n8n, Jellyfin) validées sur le G9"
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)"
echo
read -r -p "Saisir 'WIPE XPENOLOGY' pour confirmer la destruction : " CONF
[[ "$CONF" == "WIPE 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"
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
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"
log "Terminé. Le G9 est maintenant le seul cœur famille."
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Alerte ops (Matrix en direct via Synapse ; fallback webhook n8n).
# Secrets lus dans /etc/restic/matrix.env (600 root), jamais affichés (G9).
set -uo pipefail
[[ -f /etc/restic/matrix.env ]] && { set -a; . /etc/restic/matrix.env; set +a; }
[[ -f /etc/restic.env ]] && { set -a; . /etc/restic.env; set +a; }
MSG="${1:?message}"
TXN="restic-g9-$(date +%s%N)"
CLEAN=$(printf '%s' "$MSG" | tr -d '"\\')
have_jq=0
command -v jq >/dev/null 2>&1 && have_jq=1
json_text() {
if [[ $have_jq -eq 1 ]]; then jq -cn --arg b "$1" '{msgtype:"m.text", body:$b}'
else printf '{"msgtype":"m.text","body":"%s"}' "$1"; fi
}
json_webhook() {
if [[ $have_jq -eq 1 ]]; then jq -cn --arg m "$1" '{event:"restic-g9", host:"g9", message:$m}'
else printf '{"event":"restic-g9","host":"g9","message":"%s"}' "$1"; fi
}
if [[ -n "${MATRIX_TOKEN:-}" && -n "${MATRIX_ROOM:-}" ]]; then
BODY=$(json_text "[G9 backup] ${CLEAN}")
ROOM_ENC=$(printf '%s' "$MATRIX_ROOM" | sed 's/ /%20/g')
if curl -fsS -m 20 -X PUT \
"${MATRIX_SERVER}/_matrix/client/v3/rooms/${ROOM_ENC}/send/m.room.message/${TXN}" \
-H "Authorization: Bearer ${MATRIX_TOKEN}" \
-H 'Content-Type: application/json' \
-d "$BODY" >/dev/null; then
exit 0
fi
fi
if [[ -n "${RESTIC_NOTIFY_WEBHOOK:-}" ]]; then
BODY=$(json_webhook "${CLEAN}")
if curl -fsS -m 20 -X POST "$RESTIC_NOTIFY_WEBHOOK" \
-H 'Content-Type: application/json' -d "$BODY" >/dev/null; then
exit 0
fi
fi
echo "notify : ni Matrix ni webhook joignable" >&2
exit 1