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:
@@ -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).
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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/*
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
@@ -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) |
|
||||
Executable
+100
@@ -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é."
|
||||
Executable
+42
@@ -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"
|
||||
Executable
+35
@@ -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"
|
||||
Executable
+81
@@ -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."
|
||||
Executable
+76
@@ -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"
|
||||
Executable
+50
@@ -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."
|
||||
Executable
+244
@@ -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"
|
||||
Executable
+83
@@ -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
|
||||
Executable
+209
@@ -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"
|
||||
Executable
+166
@@ -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."
|
||||
Executable
+64
@@ -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."
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user