Compare commits

..
10 Commits
Author SHA1 Message Date
openfox-pilot 2f98c32da1 docs clôture : PHASE4 §0.1 PhotoPrism ✅ + §0.5 J5040 E1-E4 ; README 07d ✅ + E aligné ; 10-verify fixes (include restore, chemins) 2026-10-12 15:14:11 +02:00
openfox-pilot a5b2909722 07d: fixes idempotence snapshots (env restic), compose via sudo (env_file 600 root), health /api/v1/health ; 10-verify: fichier test depuis g9.meta (garanti dans snapshot) 2026-10-12 14:17:39 +02:00
openfox-pilot 6ee136b35d critère 3 : script test restore photo (10-verify-restore-photo.sh) + décision chiffrement journalisée 2026-10-12 12:44:51 +02:00
openfox-pilot d4f68cb8f7 wireguard: clés serveur+portable générées (600 sur J5040, gitignored) 2026-10-11 18:59:32 +02:00
openfox-pilot 71f1ec33fd E4 : Caddy OVH sur J5040, 7/7 certs LE via DNS-01, clé OVH renouvelée ; copie parallèle 4 flux 2026-10-11 18:27:42 +02:00
openfox-pilot a3a198027d E4: clé OVH NAS expirée (diagnostic API), secrets 600 sur J5040, journal 2026-10-11 17:39:42 +02:00
openfox-pilot 6516d67d0b compose wireguard (E4, linuxserver épinglé digest) 2026-10-11 17:08:51 +02:00
openfox-pilot 0b95d24046 E2 bascule DNS .27 active (bail G9 renouvelé, filtrage vérifié) 2026-10-11 17:07:31 +02:00
openfox-pilot e2dc0176ff E3 préparé : baux statiques Freebox (OCR) -> /srv/dnsmasq/leases, dnsmasq --test OK, 09 corrigé (conf-file retiré) 2026-10-11 16:55:20 +02:00
openfox-pilot ffe3f2ac91 J5040 E1-E2 + préparation E3/E4 : durcissement .27, AdGuard déployé (config VM migrée), compose adguard/edge-j5040 (Caddy OVH), 09-j5040-dhcp.sh, 07d sans gzip, README/journal alignés 2026-10-11 16:38:05 +02:00
12 changed files with 324 additions and 20 deletions
+3
View File
@@ -12,3 +12,6 @@ g9-migration/configs/restic.env
# Artefacts playwright-cli (snapshots navigateur) # Artefacts playwright-cli (snapshots navigateur)
.playwright-cli/ .playwright-cli/
# Config WireGuard générée (clés privées — G9)
**/wireguard/wg/
+10 -1
View File
@@ -18,7 +18,7 @@
| Jellyfin (T4.5) | `jellyfin/jellyfin:10.9` (10.9.11) | healthy, média par RÉFÉRENCE (`/mnt/usb-media` → `/media:ro`, aucune copie), 1 médiathèque = `/media/films`, Items Movie = 1 | false | | Jellyfin (T4.5) | `jellyfin/jellyfin:10.9` (10.9.11) | healthy, média par RÉFÉRENCE (`/mnt/usb-media` → `/media:ro`, aucune copie), 1 médiathèque = `/media/films`, Items Movie = 1 | false |
| edge (T4.6) | `caddy:2` (digest `f2a1290d…`) | healthy, HTTPS interne (CA interne), 443 seul port exposé WAN | **true** (stateless) | | edge (T4.6) | `caddy:2` (digest `f2a1290d…`) | healthy, HTTPS interne (CA interne), 443 seul port exposé WAN | **true** (stateless) |
| Watchtower (T4.7) | `nickfedor/watchtower@sha256:45b54f6d…` | healthy, scope label-enable : dry-run `scanned=2` (edge + watchtower), 14 stateful ignorés | true | | Watchtower (T4.7) | `nickfedor/watchtower@sha256:45b54f6d…` | healthy, scope label-enable : dry-run `scanned=2` (edge + watchtower), 14 stateful ignorés | true |
| PhotoPrism (T4.3) | — | **non déployée** : `07d-migrate-photoprism.sh` écrit/testé, exécution reportée (coût = copie 530 Go ; média servi par Jellyfin) | — | | PhotoPrism (T4.3) | `photoprism/photoprism:260919` + `mariadb:10.11` | **✅ 12/10** : 433 941 fichiers / 567 Go copiés depuis `/volume1/photo` (manifeste chemins+tailles 0 écart, sha256 200/200), stack healthy port 2342 (LAN) + `photos.folzocorp.fr` via edge J5040 ; index progressif (AUTO_INDEX 300 s, chaîne index→faces→analyze) ; snapshot restic + restore photo prouvée (`10-verify-restore-photo.sh`) | false |
### 0.2 Incidents rencontrés et correctifs (détail = journal.md) ### 0.2 Incidents rencontrés et correctifs (détail = journal.md)
@@ -41,6 +41,15 @@
- `nextcloud_app`, `nextcloud_db`, `nextcloud_redis`, `vaultwarden-server-1`, `gitea` **relancés et up** après les exports ; dumps/transitaires supprimés sur .25, pilot et G9. - `nextcloud_app`, `nextcloud_db`, `nextcloud_redis`, `vaultwarden-server-1`, `gitea` **relancés et up** après les exports ; dumps/transitaires supprimés sur .25, pilot et G9.
- Aucun partage, aucune donnée, aucun conteneur .25 supprimé ; transports .25→pilot = `ssh 'sudo tar cz'`/`mysqldump` en lecture seule (sshd DSM refuse `rsync --server` et scp). - Aucun partage, aucune donnée, aucun conteneur .25 supprimé ; transports .25→pilot = `ssh 'sudo tar cz'`/`mysqldump` en lecture seule (sshd DSM refuse `rsync --server` et scp).
### 0.5 J5040 (.27) — E1→E4 (11–12/10)
| Étape | État | Preuve |
|---|---|---|
| E1 durcissement | ✅ | SSH clés-only, ufw (LAN 53/3000/8081 + WAN 80/443/51820), fail2ban, unattended-upgrades, Docker 29.1.3 ; RAM 8 Go sous test (upgrade 16 Go si > 70 %) |
| E2 AdGuard | ✅ bascule faite | `adguard/adguardhome` (digest `aba9e3bf…`) sur .27, UI 8081, DNS 53 ; config VM .1 migrée (2 filtres 182 128 + 6 540 règles, safe_search, profils parents, users bcrypt) ; resolvants clients → .27 ; filtrage testé ; VM .1 conservée secours |
| E3 DHCP | préparé, **GO utilisateur (G6)** | baux statiques Freebox repris (35 appareils, OCR 38) dans `/srv/dnsmasq/leases`, `dnsmasq --test` OK, script `09-j5040-dhcp.sh` prêt ; bascule = Freebox Pop + dnsmasq |
| E4 edge + WireGuard | certs ✅, **GO utilisateur pour WAN** | Caddy OVH DNS-01 (plugin `maxcryger/caddy-ovh` digest `502996b5…`) : **7/7 certs LE** + proxy 6 briques (photos 307 depuis 12/10) ; WireGuard `wg0.conf` + client prêts (600, gitignorés) ; forward WAN Freebox→.27 et démontage edge G9 = après GO |
--- ---
# Rapport de récupération (thread supprimé) # Rapport de récupération (thread supprimé)
+7 -2
View File
@@ -61,6 +61,8 @@ g9-migration/
│ ├── 07d-migrate-photoprism.sh ← T4.3 : PhotoPrism (copie 530 Go + indexation) │ ├── 07d-migrate-photoprism.sh ← T4.3 : PhotoPrism (copie 530 Go + indexation)
│ ├── 07e-deploy-edge.sh ← T4.6 : edge Caddy (validate → up → ufw 443 → vérifs HTTPS/SNI) │ ├── 07e-deploy-edge.sh ← T4.6 : edge Caddy (validate → up → ufw 443 → vérifs HTTPS/SNI)
│ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06) │ ├── restic-notify.sh ← alerte Matrix des jobs restic (posée par 06)
│ ├── 09-j5040-dhcp.sh ← E3 : DHCP dnsmasq sur J5040 (pré-bascule bridge)
│ ├── 10-verify-restore-photo.sh ← critère 3 : preuve restore photo (sha256 vs .25)
│ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF] │ └── 08-decommission.sh ← T6.4 : décommissionnement xpenology [DESTRUCTIF]
├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea) ├── compose/ ← docker-compose.yml versionnés (stockés aussi dans Gitea)
│ ├── vaultwarden/ ← T4.1 │ ├── vaultwarden/ ← T4.1
@@ -69,7 +71,9 @@ g9-migration/
│ ├── gitea/ ← T4.4 │ ├── gitea/ ← T4.4
│ ├── n8n/ ← T4.5 │ ├── n8n/ ← T4.5
│ ├── jellyfin/ ← T4.5 (média par référence, /media:ro) │ ├── jellyfin/ ← T4.5 (média par référence, /media:ro)
│ ├── edge/ ← T4.6 : Caddy (Caddyfile versionné = config) │ ├── edge/ ← T4.6 : Caddy G9 (CA interne, edge transitoire)
│ ├── edge-j5040/ ← E4 : Caddy J5040 + plugin OVH (LE DNS-01, Caddyfile versionné)
│ ├── adguard/ ← E2 : AdGuard Home (DNS filtré, image épinglée digest)
│ └── watchtower/ ← S11.4 : Watchtower sélectif │ └── watchtower/ ← S11.4 : Watchtower sélectif
└── configs/ └── configs/
├── fail2ban.local.conf ← config fail2ban (T2.1) ├── fail2ban.local.conf ← config fail2ban (T2.1)
@@ -83,8 +87,9 @@ g9-migration/
| 1 | (xpenology) exclure stateful de Watchtower + inventaire | G4 | | 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` | — | | 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 | | 3 | `06-restic-backup.sh` (backup + test restauration) | G2 |
| 4 | **par brique, dans l'ordre** — **après** `03b-secrets.sh` : `07b-migrate-vaultwarden.sh` (T4.1) → `07c-migrate-nextcloud.sh` (T4.2) → `07d-migrate-photoprism.sh` (T4.3, **reporté**) → `07a-migrate-gitea.sh` (T4.4) → Jellyfin (T4.5, frais) → `07e-deploy-edge.sh` (T4.6, Caddy TLS interne) → Watchtower (T4.7, scope stateless). `07-migrate-stateful.sh` = script **générique rsync**, inopérant sur le DSM (sshd refuse `rsync --server`/scp) ; transport réel des 07a–07d = **tar sur SSH** | G4 | | 4 | **par brique, dans l'ordre** — **après** `03b-secrets.sh` : `07b-migrate-vaultwarden.sh` (T4.1) → `07c-migrate-nextcloud.sh` (T4.2) → `07d-migrate-photoprism.sh` (T4.3) **✅ 12/10** : 433 941 fichiers / 567 Go copiés, manifeste 0 écart + sha256 200/200, stack up (port 2342), index progressif, snapshot restic + restore prouvée (`10-verify-restore-photo.sh`) → `07a-migrate-gitea.sh` (T4.4) → Jellyfin (T4.5, frais) → `07e-deploy-edge.sh` (T4.6, Caddy TLS interne) → Watchtower (T4.7, scope stateless). `07-migrate-stateful.sh` = script **générique rsync**, inopérant sur le DSM (sshd refuse `rsync --server`/scp) ; transport réel des 07a–07d = **tar sur SSH** | G4 |
| 5 | restic 3-2-1 complet (G9 + PC IA → Freebox → 218play) | G2 | | 5 | restic 3-2-1 complet (G9 + PC IA → Freebox → 218play) | G2 |
| E (J5040, parallèle) | **E1** durcissement + Docker (`01`/`03` paramétrés, .27) ✅ → **E2** AdGuard (VM Freebox .1 → .27) ✅ bascule DNS faite (résolvants clients → .27, filtres testés) → **E3** DHCP (`09-j5040-dhcp.sh`, baux 35 appareils prêts) + Freebox **mode bridge** = **GO utilisateur (G6)** → **E4** Caddy OVH (`compose/edge-j5040`) ✅ 7/7 certs LE (DNS-01) + WireGuard prêt ; forward WAN Freebox→.27 + démontage edge G9 = **GO utilisateur** | G6, G7 |
| 6 | `06b-usb-media.sh` (T6.1) → Jellyfin (T6.2) → IP .24→.25 → `08-decommission.sh` | G3, G4, G5 | | 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 | | 7 | (G7) wipe DSM + Ubuntu neuf + boot test | G3, G10 |
@@ -0,0 +1,18 @@
# AdGuard Home — E2 (J5040 .27 → .1) : DNS filtré LAN (migration depuis VM Freebox Delta).
# Stateless (config = AdGuardHome.yaml versionnée dans le kit) → Watchtower ON (S11.4).
# Image épinglée par digest. UI = 8081 LAN-only (ufw), DNS = 53 tcp/udp LAN-only.
services:
adguard:
image: adguard/adguardhome@sha256:aba9e3bf0613be3ba3755e1fc311b126e2c24bec25e18b6483894a88283074f0
container_name: adguard
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
volumes:
- /srv/adguard/work:/work
- /srv/adguard/data:/opt/adguardhome/conf
- /srv/adguard/state:/opt/adguardhome/work
ports:
- "0.0.0.0:53:53/tcp"
- "0.0.0.0:53:53/udp"
- "0.0.0.0:8081:80/tcp"
+57
View File
@@ -0,0 +1,57 @@
{
email admin@folzocorp.fr
# ACME staging désactivé = certs de production
}
(ovh_tls) {
tls {
dns ovh
}
}
(folzocorp) {
import ovh_tls
encode gzip
log {
output stdout
format console
}
}
cloud.folzocorp.fr {
import folzocorp
redir 308 {
/ /login*
}
reverse_proxy 192.168.1.24:8080
}
pass.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:8081
}
git.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:3000
}
media.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:8096
}
photos.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:2342
}
n8n.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:5678
}
matrix.folzocorp.fr {
import folzocorp
reverse_proxy 192.168.1.24:8008
}
@@ -0,0 +1,29 @@
# Edge J5040 (E4) : Caddy + plugin DNS OVH = Let's Encrypt par DNS-01 (achève T4.6/S9.1).
# Image épinglée (caddy 2.8.4 + dns.providers.ovh). Stateless → Watchtower ON.
# Remplace l'edge du G9 (CA interne) après bascule vérifiée. Secrets = ./secrets/ovh.env (600, hors git).
services:
caddy:
image: maxcryger/caddy-ovh@sha256:502996b5b60aec3ee565324143c14ee4a0c99ff55ace167629981c6c2f2eb597
container_name: caddy
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
env_file:
- ./secrets/ovh.env
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
ports:
- "0.0.0.0:80:80/tcp"
- "0.0.0.0:443:443/tcp"
- "0.0.0.0:443:443/udp"
healthcheck:
test: ["CMD", "caddy", "validate", "--config", "/etc/caddy/Caddyfile"]
interval: 60s
timeout: 10s
retries: 3
volumes:
caddy_data:
caddy_config:
@@ -0,0 +1,22 @@
# WireGuard — E4 (J5040) : accès VPN famille (portable). LAN-only (ufw 51820 udp WAN).
# Config client générée à l'activation (wg genkey, QR via `wg show`); secrets hors git.
services:
wireguard:
image: lscr.io/linuxserver/wireguard@sha256:437a490f7afc6969434251351ba47455a3829e71cef9c6172f3f533332d04063
container_name: wireguard
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
- net.ipv6.conf.all.forwarding=1
environment:
- WG_HOST=folzocorp.fr
- WG_PORT=51820
volumes:
- ./wg:/etc/wireguard
ports:
- "0.0.0.0:51820:51820/udp"
+20
View File
@@ -104,3 +104,23 @@
| 2026-10-11 14:10 | 4 (T4.6) | edge : Caddy déployé (reverse proxy LAN + HTTPS interne) | Briquage T4.6 : TLS interne pour vaultwarden/nextcloud/gitea/jellyfin/n8n/matrix, sous-domaines déjà configurés côté apps (DOMAIN/ROOT_URL/N8N_HOST) → aucune brique touchée | `caddy:2` (digest f2a1290d…) up healthy (healthcheck = `caddy validate`) ; **tls internal** = CA interne (LE + DNS challenge = suite S9.1 via AdGuard J5040) ; **PIÈGES mesurés** : (1) snippet `(name)` importé → « ambiguous site definition » si utilisé comme adresse ; (2) `request_id` nest PAS un directive site (validate KO) ; (3) Caddy logue les en-têtes **reçus du client**, pas ceux envoyés en amont → preuve X-Forwarded prise côté app ; ufw 443/tcp exposé WAN (seul, default deny protège les ports briques) + 80 redirect + 9980 LAN-only ; **Nextcloud** : merge config = `array_replace_recursive` (le dépôt dun 2e fichier *.config.php a été avalé par lentrée existante → résolu en écrivant g9-double-run.config.php = source de vérité du kit, monté :ro) → occ : overwritehost=cloud.folzocorp.fr, overwriteprotocol=https, trusted_proxies=[192.168.1.0/24, 172.16.0.0/12] ; **preuve côté app** : redirect `https://cloud.folzocorp.fr/` → Location `https://cloud.folzocorp.fr/index.php/login` (schéma https + host forwardé rendus par Nextcloud) ; curl -k via edge : pass 200 / git 200 / cloud 200 / media 200 / n8n 200 / matrix 302 / SNI IP 200 ; vaultwarden TRUSTED_HOSTS += 172.16.0.0/12 (aligné) | `docker compose -f /data/docker/edge/docker-compose.yml down` ; `ufw delete allow 443` ; supprimer g9-double-run.config.php (retour double-run http LAN) | | 2026-10-11 14:10 | 4 (T4.6) | edge : Caddy déployé (reverse proxy LAN + HTTPS interne) | Briquage T4.6 : TLS interne pour vaultwarden/nextcloud/gitea/jellyfin/n8n/matrix, sous-domaines déjà configurés côté apps (DOMAIN/ROOT_URL/N8N_HOST) → aucune brique touchée | `caddy:2` (digest f2a1290d…) up healthy (healthcheck = `caddy validate`) ; **tls internal** = CA interne (LE + DNS challenge = suite S9.1 via AdGuard J5040) ; **PIÈGES mesurés** : (1) snippet `(name)` importé → « ambiguous site definition » si utilisé comme adresse ; (2) `request_id` nest PAS un directive site (validate KO) ; (3) Caddy logue les en-têtes **reçus du client**, pas ceux envoyés en amont → preuve X-Forwarded prise côté app ; ufw 443/tcp exposé WAN (seul, default deny protège les ports briques) + 80 redirect + 9980 LAN-only ; **Nextcloud** : merge config = `array_replace_recursive` (le dépôt dun 2e fichier *.config.php a été avalé par lentrée existante → résolu en écrivant g9-double-run.config.php = source de vérité du kit, monté :ro) → occ : overwritehost=cloud.folzocorp.fr, overwriteprotocol=https, trusted_proxies=[192.168.1.0/24, 172.16.0.0/12] ; **preuve côté app** : redirect `https://cloud.folzocorp.fr/` → Location `https://cloud.folzocorp.fr/index.php/login` (schéma https + host forwardé rendus par Nextcloud) ; curl -k via edge : pass 200 / git 200 / cloud 200 / media 200 / n8n 200 / matrix 302 / SNI IP 200 ; vaultwarden TRUSTED_HOSTS += 172.16.0.0/12 (aligné) | `docker compose -f /data/docker/edge/docker-compose.yml down` ; `ufw delete allow 443` ; supprimer g9-double-run.config.php (retour double-run http LAN) |
| 2026-10-11 14:20 | 4 (T4.7) | watchtower : vérification scope stateless + dry-run | S11.4 : MAJ auto uniquement des briques stateless ; leçon incident Nextcloud 06/10 = CLEANUP=false | Conteneur up healthy, image **nickfedor/watchtower épinglée par digest** sha256:45b54f6d… (fork : containrrr/watchtower:1 n'existe pas) ; env mesuré = POLL=3600, LABEL_ENABLE=true, CLEANUP=false, INCLUDE_STOPPED=false ; **labels vérifiés sur les 16 conteneurs .24** : stateful enable=false = nextcloud, collabora, jellyfin, nextcloud_db, nextcloud_redis, vaultwarden, gitea, gitea-db, valheim, n8n, element, synapse, couchdb, obsidian ; enable=true = edge (stateless, T4.6) + watchtower ; **dry-run** `docker run --rm -v /var/run/docker.sock nickfedor/watchtower@sha256:45b54f6d… --label-enable --monitor-only -R` → « Only checking containers with label … enable », **scanned=2 updated=0 failed=0** = les 14 stateful sont ignorés ; notification : webhook n8n testé en POST depuis le réseau Docker → **200** (le compose affichait un placeholder https://n8n.folzocorp.fr → aligné sur generic+http://n8n:5678, nom de service : l'edge n'écoute que 443) ; nickfedor : SCHEDULE et POLL exclusifs, pas de --once (=-R) | `docker compose -f /data/docker/watchtower/docker-compose.yml down` ; aucun effet sur les briques (label-driven) | | 2026-10-11 14:20 | 4 (T4.7) | watchtower : vérification scope stateless + dry-run | S11.4 : MAJ auto uniquement des briques stateless ; leçon incident Nextcloud 06/10 = CLEANUP=false | Conteneur up healthy, image **nickfedor/watchtower épinglée par digest** sha256:45b54f6d… (fork : containrrr/watchtower:1 n'existe pas) ; env mesuré = POLL=3600, LABEL_ENABLE=true, CLEANUP=false, INCLUDE_STOPPED=false ; **labels vérifiés sur les 16 conteneurs .24** : stateful enable=false = nextcloud, collabora, jellyfin, nextcloud_db, nextcloud_redis, vaultwarden, gitea, gitea-db, valheim, n8n, element, synapse, couchdb, obsidian ; enable=true = edge (stateless, T4.6) + watchtower ; **dry-run** `docker run --rm -v /var/run/docker.sock nickfedor/watchtower@sha256:45b54f6d… --label-enable --monitor-only -R` → « Only checking containers with label … enable », **scanned=2 updated=0 failed=0** = les 14 stateful sont ignorés ; notification : webhook n8n testé en POST depuis le réseau Docker → **200** (le compose affichait un placeholder https://n8n.folzocorp.fr → aligné sur generic+http://n8n:5678, nom de service : l'edge n'écoute que 443) ; nickfedor : SCHEDULE et POLL exclusifs, pas de --once (=-R) | `docker compose -f /data/docker/watchtower/docker-compose.yml down` ; aucun effet sur les briques (label-driven) |
| 2026-10-11 14:25 | 4 (clôture) | Phase 4 consolidée + kit poussé sur Gitea | Clôture Phase 4 (critère 5) | `PHASE4-RECOLLEMENT.md` §0 ajouté : briques déployées + versions épinglées (vaultwarden 1.37.4, nextcloud 35.0.1/mariadb 10.11/redis 7/collabora 26.04.5.1.1, gitea 1.27.3/postgres 16, jellyfin 10.9.11, caddy 2 digest f2a1290d, watchtower digest 45b54f6d), **6 incidents + correctifs mesurés**, alignement proxy (trusted_proxies/overwrite/TRUSTED_HOSTS/ufw 443 seul WAN), état .25 (services relancés, dumps supprimés, rien de destructif) ; PhotoPrism T4.3 = **reportée** (script 07d prêt, copie 530 Go) ; README aligné (07e + compose/edge + jellyfin T4.5) ; **push gitea master : HEAD local 366c5ca == gitea/master 366c5ca** (API Gitea vérifiée) ; vault Obsidian = repo `g9ops/obsidian-vault` présent (47 Mo, import T4.4) ; **zéro secret dans git** : `git ls-files` (secrets/*.env/token/key/cred) = 0 fichier de secret, `git grep` des valeurs (token, hash jellyfin, Pw=) = 0, historique `-S token` = 0 ; durcissement : token retiré de l'URL du remote → `~/.git-credentials` 600 + credential.helper store | `git push gitea a1864c6:master` (fast-forward annulable) ; suppression de ~/.git-credentials | | 2026-10-11 14:25 | 4 (clôture) | Phase 4 consolidée + kit poussé sur Gitea | Clôture Phase 4 (critère 5) | `PHASE4-RECOLLEMENT.md` §0 ajouté : briques déployées + versions épinglées (vaultwarden 1.37.4, nextcloud 35.0.1/mariadb 10.11/redis 7/collabora 26.04.5.1.1, gitea 1.27.3/postgres 16, jellyfin 10.9.11, caddy 2 digest f2a1290d, watchtower digest 45b54f6d), **6 incidents + correctifs mesurés**, alignement proxy (trusted_proxies/overwrite/TRUSTED_HOSTS/ufw 443 seul WAN), état .25 (services relancés, dumps supprimés, rien de destructif) ; PhotoPrism T4.3 = **reportée** (script 07d prêt, copie 530 Go) ; README aligné (07e + compose/edge + jellyfin T4.5) ; **push gitea master : HEAD local 366c5ca == gitea/master 366c5ca** (API Gitea vérifiée) ; vault Obsidian = repo `g9ops/obsidian-vault` présent (47 Mo, import T4.4) ; **zéro secret dans git** : `git ls-files` (secrets/*.env/token/key/cred) = 0 fichier de secret, `git grep` des valeurs (token, hash jellyfin, Pw=) = 0, historique `-S token` = 0 ; durcissement : token retiré de l'URL du remote → `~/.git-credentials` 600 + credential.helper store | `git push gitea a1864c6:master` (fast-forward annulable) ; suppression de ~/.git-credentials |
| 2026-10-11 15:49 | 4 | migrate:photoprism : backup restic pré-migration | Garde-fou G2 avant import PhotoPrism | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 15:56 | 4 | migrate:photoprism : backup restic pré-migration | Garde-fou G2 avant import PhotoPrism | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 16:21 | 4 | migrate:photoprism : backup restic pré-migration | Garde-fou G2 avant import PhotoPrism | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-11 16:10 | J5040 (E1) | durcissement J5040 .27 (01-hardening paramétré) | E1 : edge Ubuntu 26.04 neuf, RAM 8 Go test (décision utilisateur) | SSH keys-only + ufw actif (LAN-only 53/3000/8081 + WAN 80/443/51820) + unattended-upgrades + fail2ban ; sudo NOPASSWD folzo ; clés SSH pilot installées ; portable à ajouter via ssh-copy-id | restaurer sshd_config .bak + ufw disable |
| 2026-10-11 16:12 | J5040 (E1) | Docker + compose installés sur .27 | Runtime edge | docker 29.1.3 + compose 2.40.3, folzo dans groupe docker | apt-get remove docker.io docker-compose-v2 |
| 2026-10-11 16:14 | 07d : copie SANS gzip (photos déjà compressées) | gzip sur CPU NAS = 10-13 Mo/s mesuré → flux brut ~17 Mo/s | tar c|x (sans compression), copie relancée propre (originals purgés, rien de destructif sur .25) | restaurer le script depuis git |
| 2026-10-11 16:20 | J5040 (E2) | AdGuard Home déployé sur .27 (migration depuis VM Freebox .1) | E2 : DNS filtré sur l'edge | config VM récupérée en LECTURE SEULE (ssh master@.1, AdGuardHome.yaml 415 l. : 2 filtres 182128+6540 règles, safe_search, services bloqués parents, users bcrypt conservé) ; image épinglée digest aba9e3bf ; UI 8081 LAN, DNS 53 LAN ; systemd-resolved désactivé sur .27 ; tests : doubleclick→0.0.0.0, example.org résolu, UI 302 | docker compose down -v ; VM .1 intacte ; resolv.conf .27 restauré |
| 2026-10-11 17:05 | J5040 (E3) | Préparation DHCP : baux statiques récupérés (38 appareils, capture Freebox via OCR utilisateur) + /srv/dnsmasq/leases (35 entrées, .1 réservé E4, VM adguard exclue) + 09-j5040-dhcp.sh | E3 : pré-bascule bridge, G6 = baux identiques (mêmes IP) | dnsmasq --test = syntax OK ; scan ARP cross-check (g9 48:ea:62, j5040 60:be:b4, nas 10:e7:c6, backup-218 .26 00:11:32) ; anomalie relevée : VM debian 02:11:32 en conflit sur .24 → à repositionner .23 côté Freebox (utilisateur) | rien (préparation, dnsmasq non démarré) |
| 2026-10-11 17:15 | J5040 (E2) | Bascule DNS : Freebox DHCP DNS1 .1 → .27 (GO utilisateur) + bail G9 renouvelé | E2 : AdGuard J5040 remplace la VM Freebox | resolvectl G9 = DNS .27 ; doubleclick→0.0.0.0, example.org résolu ; clients = renouvellement de bail progressif (baux 12 h) ; VM .1 conservée en secours (arrêt = action utilisateur G3 après stabilité) ; .254 freebox intact | remettre DNS1=.1 dans la Freebox (VM intacte, rollback immédiat) |
| 2026-10-11 18:05 | J5040 (E4) | Clé API OVH du NAS retrouvée (LE env, longueurs 16/32/32 OK) mais NON SOUSCRITE/EXPIRÉE (test /auth/credential → 404) | E4 : LE DNS-01 nécessite clé valide | secret copié en 600 sur J5040 (compose/edge-j5040/secrets/ovh.env, hors git) ; NAS : renewal LE cassée depuis expiration → nouvelle clé requise (droits API legacy /domain/zone/folzocorp.fr/*, utilisateur) | rien |
| 2026-10-11 18:40 | J5040 (E4) | Nouvelle clé API OVH créée+souscrite (droits legacy /domain/zone) ; clé NAS expirée confirmée (90 j) | E4 : LE DNS-01 | python-ovh GET /domain/zone/folzocorp.fr/record = 12 records OK ; secrets 600 sur J5040 (ovh.env hors git) ; NAS : mettra la nouvelle clé lors du renouvellement LE | révoquer la clé dans manager OVH |
| 2026-10-11 18:45 | J5040 (E4) | Caddy OVH déployé sur .27 : 7/7 certs Let's Encrypt émis (DNS-01) + proxy 6 briques | E4 : TLS auto achève T4.6/S9.1 | maxcryger/caddy-ovh digest 502996b5 healthy ; cloud 302/pass 200/git 200/media 302/photos 502 (stack en copie)/n8n 200/matrix 302, issuer = Let's Encrypt ; WAN (forward Freebox→G9) et démontage edge G9 = après bascule bridge E3 | docker compose down ; révoquer clé |
| 2026-10-11 18:10 | 07d : copie parallèle 4 flux (tar par dossier racine) | mono-flux SSH plafonné ~13 Mo/s (lecture HDD NAS, CPU NAS 7% = non saturé) | 29 dirs, xargs -P4 ; + chmod u+rwX post-copie ; intégrité/stack/index = 07d SKIP_COPY=1 | rien (source .25 lecture seule) |
| 2026-10-11 21:30 | 4 | décision chiffrement restic MAINTENU | Critère 3 : arbitrage chiffrement vs simplicité restore | Décision utilisateur : chiffrement restic conservé, clé de repo locale G9 (/etc/restic/repo.key 600, sauvegardée hors repo git), restore simple via sudo restic sur le G9 ; test restore photo prévu par scripts/10-verify-restore-photo.sh | révoquer/cloner la clé si changement de politique |
| 2026-10-12 13:40 | 4 | migrate:photoprism : backup restic pré-migration | Garde-fou G2 avant import PhotoPrism | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget <snapshot> && restic prune |
| 2026-10-12 14:06 | 4 | migrate:photoprism : intégration PhotoPrism | copie de 530 Go à vérifier avant indexation | manifeste chemins+tailles identique (433941 fichiers, 567394182751 o) + échantillon sha256 200/200 | re-copie ciblée via tar/ssh |
| 2026-10-12 14:11 | 4 | migrate:photoprism : intégration PhotoPrism | copie de 530 Go à vérifier avant indexation | manifeste chemins+tailles identique (433941 fichiers, 567394182751 o) + échantillon sha256 200/200 | re-copie ciblée via tar/ssh |
| 2026-10-12 14:17 | 4 | migrate:photoprism : intégration PhotoPrism | copie de 530 Go à vérifier avant indexation | manifeste chemins+tailles identique (433941 fichiers, 567394182751 o) + échantillon sha256 200/200 | re-copie ciblée via tar/ssh |
| 2026-10-12 14:18 | 4 | migrate:photoprism : stack PhotoPrism déployée + indexation lancée | T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9) | image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale | docker compose down + rm -rf /data/docker/photoprism |
| 2026-10-12 15:07 | 4 | test restauration photo depuis restic | Critère 3 : preuve restore du snapshot incluant les photos | snapshot 06a7e3b89ef3e97d5f5c1dc9098496fab8ad79d48f4455eabdde07255143e33b contient 505836 entrées sous /data/docker/photoprism/originals ; restore 1 fichier ; sha256 restauré == source .25 (00cbfed8da81…) ; chiffrement maintenu (clé locale G9) | rm -rf /tmp/restore-photo-test (fait) |
+17 -10
View File
@@ -68,22 +68,29 @@ ufw --force reset >/dev/null
ufw default deny incoming ufw default deny incoming
ufw default allow outgoing ufw default allow outgoing
ufw allow OpenSSH ufw allow OpenSSH
# Services LAN-only (G9 = LAN-only, l'exposition WAN passe par l'edge — S9.2/S9.3). # Services LAN-only (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) + # Listes paramétrables (défaut = G9 .24) : J5040 edge = TCP_LAN="53 3000 8081"
# briques famille futures + Valheim (TCP+UDP). Accessibles depuis le LAN uniquement. # UDP_LAN="53" WAN_TCP="80 443" WAN_UDP="51820".
for port in 2342 3000 3100 5678 5984 8008 8009 8080 8081 8096 9980; do TCP_LAN="${TCP_LAN:-2342 3000 3100 5678 5984 8008 8009 8080 8081 8096 9980}"
UDP_LAN="${UDP_LAN:-2456 2457 2458}"
WAN_TCP="${WAN_TCP:-443 80}"
WAN_UDP="${WAN_UDP:-}"
for port in $TCP_LAN; 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 tcp
done done
for port in 2456 2457 2458; do for port in $UDP_LAN; 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 tcp
ufw allow from 192.168.1.0/24 to any port "$port" proto udp ufw allow from 192.168.1.0/24 to any port "$port" proto udp
done done
# T4.6 edge (Caddy) : SEUL port exposé WAN = 443 (+ 80 = redirect ACME/HTTP→HTTPS). # Edge : ports exposés WAN (443 service + 80 redirect ACME/HTTP→HTTPS ; 51820 = WireGuard).
# L'exposition WAN des briques passe par l'edge uniquement (S9.2/S9.3, default deny incoming). for port in $WAN_TCP; do
ufw allow 443/tcp comment 'edge T4.6 (seul port exposé WAN)' ufw allow "$port/tcp" comment "edge WAN ($port)"
ufw allow 80/tcp comment 'edge T4.6 (redirect ACME)' done
for port in $WAN_UDP; do
ufw allow "$port/udp" comment "edge WAN ($port)"
done
ufw --force enable 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 9980 + Valheim 2456-2458 TCP/UDP) + edge 443/80 exposés WAN (T4.6)" "ufw disable" journal "ufw activé" "Firewall LAN-only (G6)" "deny incoming, allow outgoing, OpenSSH + TCP_LAN=[$TCP_LAN] + UDP_LAN=[$UDP_LAN] (LAN-only) + WAN_TCP=[$WAN_TCP] WAN_UDP=[$WAN_UDP]" "ufw disable"
# --- 3. unattended-upgrades --- # --- 3. unattended-upgrades ---
log "unattended-upgrades : activation" log "unattended-upgrades : activation"
+12 -7
View File
@@ -59,10 +59,11 @@ log "source .25 : $SRC_COUNT fichiers, $SRC_BYTES octets"
# ---------------------------------------------------------------- 1. backup G2 # ---------------------------------------------------------------- 1. backup G2
log "=== 1/5 BACKUP restic (G2) ===" log "=== 1/5 BACKUP restic (G2) ==="
if $SSH_G9 "sudo restic snapshots 2>/dev/null | grep -q '$(date '+%Y-%m-%d')'"; then SNAP_TODAY=$(date '+%Y-%m-%d')
if $SSH_G9 "sudo sh -c 'set -a; . /etc/restic.env; set +a; restic -r \"\$RESTIC_REPOSITORY\" snapshots 2>/dev/null' | grep -q '$SNAP_TODAY'"; then
log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)" log " ⏭ snapshot du jour déjà présent — backup complet sauté (idempotence)"
else else
$SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo restic snapshots 2>/dev/null | tail -2" $SSH_G9 "sudo /usr/local/bin/restic-daily.sh >/dev/null 2>&1; sudo sh -c 'set -a; . /etc/restic.env; set +a; restic -r \"\$RESTIC_REPOSITORY\" snapshots 2>/dev/null' | tail -2"
journal "backup restic pré-migration" "Garde-fou G2 avant import PhotoPrism" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune" journal "backup restic pré-migration" "Garde-fou G2 avant import PhotoPrism" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget <snapshot> && restic prune"
fi fi
@@ -75,10 +76,14 @@ if [[ "${SKIP_COPY:-0}" != "1" ]]; then
t0=$(date +%s) t0=$(date +%s)
# Progression : tar GNU du G9 émet un pointage sur stderr tous les 50 000 enregistrements # Progression : tar GNU du G9 émet un pointage sur stderr tous les 50 000 enregistrements
# (stderr relayé par ssh) → la copie de 530 Go n'est pas silencieuse pendant des heures. # (stderr relayé par ssh) → la copie de 530 Go n'est pas silencieuse pendant des heures.
if ! $SSH_XP "sudo tar cz -C '$XP_PHOTO' ." | $SSH_G9 "tar xz --checkpoint=50000 --checkpoint-action=echo='[copie] %n enregistrements' -C $G9_DIR/originals"; then # PAS de gzip : photos déjà compressées (JPEG/RAW), gzip sur le CPU du NAS = 10 Mo/s mesuré.
# Flux brut = lié par le réseau (~100 Mo/s).
if ! $SSH_XP "sudo tar c -C '$XP_PHOTO' ." | $SSH_G9 "tar x --checkpoint=50000 --checkpoint-action=echo='[copie] %n enregistrements' -C $G9_DIR/originals"; then
echo "ERREUR : flux tar interrompu"; exit 1 echo "ERREUR : flux tar interrompu"; exit 1
fi fi
t=$(( $(date +%s) - t0 )) t=$(( $(date +%s) - t0 ))
# DSM arrive avec des modes 0000/2700 (ACL Synology) → les dirs sont illisibles par folzo.
$SSH_G9 "sudo chmod -R u+rwX $G9_DIR/originals"
log " copie terminée en ${t} s" log " copie terminée en ${t} s"
journal "copie photos .25 → G9" "530 Go, source en lecture seule (aucune écriture sur .25)" "tar/ssh relayé par le pilot, ${t} s ; $SRC_COUNT fichiers source" "rm -rf $G9_DIR/originals (source .25 intacte)" journal "copie photos .25 → G9" "530 Go, source en lecture seule (aucune écriture sur .25)" "tar/ssh relayé par le pilot, ${t} s ; $SRC_COUNT fichiers source" "rm -rf $G9_DIR/originals (source .25 intacte)"
fi fi
@@ -115,13 +120,13 @@ $SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G
$SSH_G9 "sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db" $SSH_G9 "sudo chown -R 999:999 $G9_DIR/db && sudo chmod 750 $G9_DIR/db"
# PhotoPrism n'accepte pas les *_FILE : env_file construit à partir des secrets (600, root, hors git). # PhotoPrism n'accepte pas les *_FILE : env_file construit à partir des secrets (600, root, hors git).
$SSH_G9 "sudo sh -c 'umask 077; { echo PHOTOPRISM_ADMIN_PASSWORD=\$(cat $G9_DIR/secrets/admin); echo PHOTOPRISM_DATABASE_PASSWORD=\$(cat $G9_DIR/secrets/dbpass); } > $G9_DIR/secrets/photoprism.env' && sudo chown root:root $G9_DIR/secrets/photoprism.env" $SSH_G9 "sudo sh -c 'umask 077; { echo PHOTOPRISM_ADMIN_PASSWORD=\$(cat $G9_DIR/secrets/admin); echo PHOTOPRISM_DATABASE_PASSWORD=\$(cat $G9_DIR/secrets/dbpass); } > $G9_DIR/secrets/photoprism.env' && sudo chown root:root $G9_DIR/secrets/photoprism.env"
$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 30 && docker compose ps --format '{{.Name}} {{.Status}}'" $SSH_G9 "cd $G9_DIR && sudo docker compose up -d && sleep 30 && sudo docker compose ps --format '{{.Name}} {{.Status}}'"
# ---------------------------------------------------------------- 5. indexation + vérifs # ---------------------------------------------------------------- 5. indexation + vérifs
log "=== 5/5 INDEXATION (progressive, décision #4) + VÉRIFS ===" log "=== 5/5 INDEXATION (progressive, décision #4) + VÉRIFS ==="
$SSH_G9 "curl -sf -o /dev/null -w 'photoprism HTTP %{http_code}\n' http://127.0.0.1:2342/api/v1/session/health" $SSH_G9 "curl -s -o /dev/null -w 'photoprism HTTP %{http_code}\n' http://127.0.0.1:2342/api/v1/health"
$SSH_G9 "cd $G9_DIR && timeout $INDEX_TIMEOUT docker compose exec -T photoprism photoprism index 2>&1 | tail -6 < /dev/null || true" $SSH_G9 "cd $G9_DIR && timeout $INDEX_TIMEOUT sudo docker compose exec -T photoprism photoprism index < /dev/null 2>&1 | tail -6 || true"
$SSH_G9 "cd $G9_DIR && docker compose exec -T photoprism sh -c 'photoprism count 2>/dev/null | head -12' < /dev/null || true" $SSH_G9 "cd $G9_DIR && sudo docker compose exec -T db sh -c 'mariadb -u photoprism -p\"\$(cat /run/secrets/dbpass)\" photoprism -e \"SELECT COUNT(*) AS photos_indexes FROM photos\"' 2>/dev/null || true"
journal "stack PhotoPrism déployée + indexation lancée" "T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9)" "image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale" "docker compose down + rm -rf /data/docker/photoprism" journal "stack PhotoPrism déployée + indexation lancée" "T4.3 : stack saine, index progressif (photothèque 530 Go sur le G9)" "image épinglée 260919, MariaDB 10.11 (watchtower OFF), secrets via env_file 600, port 2342 LAN ; originals = copie locale" "docker compose down + rm -rf /data/docker/photoprism"
echo echo
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 09-j5040-dhcp.sh — E3 : DHCP sur le J5040 (dnsmasq) en préparation du mode bridge Freebox.
#
# RÔLE : dnsmasq = DHCP uniquement (DNS coupé : port=0, le DNS LAN = AdGuard .27:53).
# Les baux statiques sont lus depuis /srv/dnsmasq/leases (format: MAC,IP,nom — 1 par ligne),
# repris de la config DHCP Freebox (baux statiques existants, mêmes IP → G6 : pas de changement d'IP).
#
# ⚠️ BASCULE (G6) : ce script active le DHCP sur le J5040. La bascule Freebox (DHCP off /
# mode bridge) est une ACTION UTILISATEUR explicite. Rollback = couper dnsmasq + rallumer
# le DHCP Freebox (baux statiques Freebox intacts, rien n'y a été supprimé).
#
# Usage : sudo bash 09-j5040-dhcp.sh [interface_LAN] (défaut : enp1s0)
set -euo pipefail
IFACE="${1:-enp1s0}"
LEASES=/srv/dnsmasq/leases
log() { echo -e "\n[dhcp] $*"; }
[[ $EUID -eq 0 ]] || { echo "ERREUR : sudo"; exit 1; }
[[ -f "$LEASES" ]] || { echo "ERREUR : $LEASES absent (copier la liste des baux statiques Freebox, format MAC,IP,nom)"; exit 1; }
log "installation dnsmasq"
apt-get update -qq
apt-get install -y -qq dnsmasq
systemctl disable --now dnsmasq 2>/dev/null || true
log "config dnsmasq (DHCP only, DNS coupé)"
cat > /etc/dnsmasq.conf << EOF
interface=$IFACE
bind-interfaces
port=0
dhcp-range=192.168.1.100,192.168.1.240,12h
dhcp-option=6,192.168.1.27
dhcp-option=3,192.168.1.27
no-resolv
strict-order
dhcp-lease-max=256
dhcp-no-override
EOF
while IFS=, read -r mac ip name; do
[[ -z "${mac// }" || "$mac" == \#* ]] && continue
echo "dhcp-host=$mac,$ip,$name"
done < "$LEASES" >> /etc/dnsmasq.conf
dnsmasq --test && log "config dnsmasq valide"
systemctl enable --now dnsmasq
log "DHCP actif sur $IFACE (baux = $LEASES). Bascule Freebox = action utilisateur (mode bridge / DHCP off)."
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# 10-verify-restore-photo.sh — Critère 3 : preuve que le snapshot restic contient les photos
# et qu'un fichier photo se restaure à l'identique (sha256 vs source .25).
#
# Chiffrement MAINTENU (décision utilisateur 11/10) : clé de repo locale G9 (/etc/restic/repo.key),
# restore via sudo sur le G9. Source .25 en lecture seule (G4).
#
# Usage : sur la machine de pilotage (détenant la clé), ./10-verify-restore-photo.sh
set -euo pipefail
XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222
XP_PHOTO="/volume1/photo"
G9_USER="folzo"; G9_HOST="192.168.1.24"
KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}"
G9_ORIG="/data/docker/photoprism/originals"
TARGET="/tmp/restore-photo-test"
JOURNAL="$(cd "$(dirname "$0")/.." && pwd)/journal.md"
SSH_XP="ssh -p $XP_PORT -i $KEY -o ConnectTimeout=10 $XP_USER@$XP_HOST"
SSH_G9="ssh -i $KEY -o ConnectTimeout=10 $G9_USER@$G9_HOST"
RST_PRELUDE='set -a; . /etc/restic.env; set +a'
log() { echo -e "\n[restore-test] $*"; }
journal() {
printf '| %s | 4 | %s | %s | %s | %s |\n' \
"$(date '+%Y-%m-%d %H:%M')" "$1" "$2" "$3" "$4" >> "$JOURNAL"
}
log "=== 1/3 snapshot g9 le plus récent ==="
SNAP=$($SSH_G9 "sudo bash -s" <<EOF
$RST_PRELUDE
restic -r "\$RESTIC_REPOSITORY" snapshots --tag g9 --json 2>/dev/null | jq -r '.[-1].id'
EOF
)
[[ "$SNAP" =~ ^[0-9a-f]{8,}$ ]] || { echo "ERREUR : snapshot introuvable"; exit 1; }
log "snapshot = $SNAP"
N=$($SSH_G9 "sudo bash -s -- '$G9_ORIG' '$SNAP'" <<'EOF'
set -a; . /etc/restic.env; set +a
restic -r "$RESTIC_REPOSITORY" ls "$2" 2>/dev/null | grep -c "$1"
true
EOF
)
[[ "${N:-0}" -gt 0 ]] || { echo "ERREUR : le snapshot $SNAP ne contient pas $G9_ORIG"; exit 1; }
log " ✅ snapshot contient $N entrées sous $G9_ORIG"
log "=== 2/3 restauration d'un fichier photo ==="
G9_META="${HOME}/migration-staging/photoprism/g9.meta"
if [[ -f "$G9_META" ]]; then
REL=$(cut -f1 "$G9_META" | grep -i '\.jpg$' | LC_ALL=C sort | tail -1)
REL="${G9_ORIG}/${REL#./}"
else
REL=$($SSH_G9 "find '$G9_ORIG' -type f -iname '*.jpg' | LC_ALL=C sort | tail -1")
fi
log "fichier test = ${REL#$G9_ORIG/}"
B64=$(printf '%s' "$REL" | base64 -w0)
$SSH_G9 "sudo bash -s -- '$B64' '$SNAP' '$TARGET'" <<'EOF'
set -a; . /etc/restic.env; set +a
rel=$(printf '%s' "$1" | base64 -d)
rm -rf "$3"
restic -r "$RESTIC_REPOSITORY" restore --target "$3" --include "*${rel##*/}" "$2" >/dev/null
EOF
$SSH_G9 "sudo bash -s -- '$B64' '$TARGET'" <<'EOF' || { echo "ERREUR : fichier restauré absent"; exit 1; }
rel=$(printf '%s' "$1" | base64 -d)
test -f "$2$rel"
EOF
log "=== 3/3 sha256 restauré vs source .25 ==="
H_SRC=$($SSH_XP "sudo bash -s -- '$B64' '$XP_PHOTO'" <<'EOF'
f=$(printf '%s' "$1" | base64 -d)
sudo sha256sum "$2${f#/data/docker/photoprism/originals}" | cut -d' ' -f1
EOF
)
H_RST=$($SSH_G9 "sudo bash -s -- '$B64' '$TARGET'" <<'EOF'
f=$(printf '%s' "$1" | base64 -d)
sudo sha256sum "$2$f" | cut -d' ' -f1
EOF
)
$SSH_G9 "sudo rm -rf '$TARGET'"
if [[ "$H_SRC" == "$H_RST" && -n "$H_SRC" ]]; then
log "✅ sha256 identique : $H_RST"
journal "test restauration photo depuis restic" "Critère 3 : preuve restore du snapshot incluant les photos" "snapshot $SNAP contient $N entrées sous $G9_ORIG ; restore 1 fichier ; sha256 restauré == source .25 (${H_RST:0:12}…) ; chiffrement maintenu (clé locale G9)" "rm -rf $TARGET (fait)"
else
log "❌ sha256 divergents : src=$H_SRC rst=$H_RST"
exit 1
fi