From a1864c69e70fc45f257348438673aa6a7b588539 Mon Sep 17 00:00:00 2001 From: Effi Date: Sun, 11 Oct 2026 08:03:10 +0200 Subject: [PATCH] =?UTF-8?q?T4.1=20Vaultwarden=20:=20import=20coffre=20953?= =?UTF-8?q?=20ciphers=20+=202FA=20import=C3=A9=20+=20ADMIN=5FTOKEN=20argon?= =?UTF-8?q?2=20+=20emergency=20access=20activ=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../compose/vaultwarden/docker-compose.yml | 12 +- g9-migration/journal.md | 5 + .../scripts/07b-migrate-vaultwarden.sh | 131 ++++++++++++++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100755 g9-migration/scripts/07b-migrate-vaultwarden.sh diff --git a/g9-migration/compose/vaultwarden/docker-compose.yml b/g9-migration/compose/vaultwarden/docker-compose.yml index b7c4760..7bb6ec1 100644 --- a/g9-migration/compose/vaultwarden/docker-compose.yml +++ b/g9-migration/compose/vaultwarden/docker-compose.yml @@ -1,17 +1,25 @@ # 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). +# +# DOMAIN = **pass.folzocorp.fr** (mesuré 2026-10-11 sur .25) : c'est le domaine que les +# clients (apps + extensions) utilisent déjà. Le garder rend le double-run transparent : +# le jour de la bascule T4.6, le même domaine pointera vers le G9 sans toucher aux clients. +# Version épinglée sur la génération source (image .25 pulled 2026-10-05 ≈ 1.37.x). services: vaultwarden: - image: vaultwarden/server:1 + image: vaultwarden/server:1.37.4 container_name: vaultwarden restart: unless-stopped labels: - "com.centurylinklabs.watchtower.enable=false" # stateful → exclu environment: - TZ=Europe/Paris - - DOMAIN=https://vault.folzocorp.fr + - DOMAIN=https://pass.folzocorp.fr - SIGNUPS_ALLOWED=false + - EMERGENCY_ACCESS_ALLOWED=true # T4.1 : accès d'urgence (octroi via WebVault) + - INVITATIONS_ALLOWED=true # créer l'invitation du contact d'urgence + - ORG_POLICY_ENABLED=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. diff --git a/g9-migration/journal.md b/g9-migration/journal.md index 17f412a..7bf3a34 100644 --- a/g9-migration/journal.md +++ b/g9-migration/journal.md @@ -77,3 +77,8 @@ | 2026-10-11 07:50 | 4 | migrate:gitea : validation T4.4 (T4.4) | Critère C-T4.4 (service accessible + données importées) | Gitea G9 : 15 repos (13 importés + g9-migration + obsidian-vault), 4 users (3 importés + g9ops) ; git ls-remote OK sur Folzo/hermes (privé), Folzo/effi, Folzo/jean ; clone + push testés depuis le G9 ; vault Obsidian 80 Mo / 346 fichiers commité et poussé ; healthz 200 ; .25 intact (13 repos / 3 users revérifiés) | docker compose -f /data/docker/gitea/docker-compose.yml down + rm -rf /data/docker/gitea (G9) ; repos g9-migration/obsidian-vault supprimables via API | | 2026-10-11 07:47 | 4 | migrate:gitea : compte ops g9ops créé | API Gitea bloque les comptes « must_change_password » hérités de la source (master/antoine/folzo) → compte d'automatisation dédié nécessaire | gitea admin user create g9ops --admin --must-change-password=false (mot de passe dans /data/docker/gitea/secrets/g9ops-pass, 644 root, hors git) + token API « ops-phase4 » (read/write user+repository, 600, jamais affiché) ; endpoint 1.27 = POST /api/v1/user/repos | gitea admin user delete -u g9ops ; révocation du token via l'UI | | 2026-10-11 07:36 | 4 | kit : transport .25 → G9 = tar sur SSH (rsync/scp refusés) | Le sshd de DSM refuse `rsync --server` (« Permission denied, please try again. ») et scp (`subsystem request failed`, sftp = internal-sftp) | `ssh 'sudo tar cz'` + `ssh 'sudo cat'` fonctionnels ; 07a-migrate-gitea.sh réécrit sur ce transport ; manifestes sha256 avec **LC_ALL=C sort** (le tri local fr_FR ≠ tri DSM → faux échecs d'intégrité) | restaurer l'ancien transport depuis git (kit versionné) | +| 2026-10-11 07:55 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 07:55 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 07:56 | 4 | migrate:vaultwarden : backup restic pré-migration | Garde-fou G2 avant import Vaultwarden | snapshot restic Freebox (log /var/log/restic-daily.log) | restic forget && restic prune | +| 2026-10-11 07:57 | 4 | migrate:vaultwarden : coffre importé + service lancé | T4.1 (double-run G4) | Source stoppée → tar/ssh → **source relancée healthy** ; 621 fichiers sha256 identiques (source == stage == G9) ; G9 = 953 ciphers / 1 user (identiques à .25, revérifié après coup : .25 healthy 953) ; migrations diesel OK (v.20260902120000) ; alive 200, WebVault 200 ; DOMAIN conservé = `pass.folzocorp.fr` (clients existants intacts) ; image épinglée 1.37.4 (génération source, image .25 pullée 2026-10-05) | docker compose -f /data/docker/vaultwarden/docker-compose.yml down + rm -rf /data/docker/vaultwarden ; .25 intact | +| 2026-10-11 08:01 | 4 | migrate:vaultwarden : 2FA + ADMIN_TOKEN + emergency access | T4.1 (critères 2FA + emergency access) | **2FA TOTP déjà actif côté source et importé avec le coffre** : twofactor(atype=0, enabled=1, last_used=59703916) identique sur le G9 → l'appli d'authentification de l'utilisateur continue sans re-scannage ; ADMIN_TOKEN **haché argon2id** (m=64MiB,t=3,p=4 — preset bitwarden, python3-argon2) : avis « plain text ADMIN_TOKEN » disparu, login admin 200 (dashboard Users/Invitations/Organizations/**Emergency** accessibles) ; token clair dans `/data/docker/vaultwarden/secrets/admin-plain` (600 root, à consigner dans le coffre puis supprimer) ; `EMERGENCY_ACCESS_ALLOWED=true` + `INVITATIONS_ALLOWED=true` posés | restaurer secrets/admin (token clair) + restart ; retirer les 2 env | diff --git a/g9-migration/scripts/07b-migrate-vaultwarden.sh b/g9-migration/scripts/07b-migrate-vaultwarden.sh new file mode 100755 index 0000000..47dbe4d --- /dev/null +++ b/g9-migration/scripts/07b-migrate-vaultwarden.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# +# 07b-migrate-vaultwarden.sh — T4.1 : migration Vaultwarden (xpenology .25 → G9 .24) +# +# Garde-fous : G4 (double-run, .25 = source de vérité, AUCUNE suppression), G2 (backup restic +# déclenché avant import), G7 (rollback), G8 (journal), G9 (aucun secret loggué/commité). +# +# SOURCE mesurée (2026-10-11) : conteneur GUI DSM `vaultwarden-server-1` (vaultwarden/server:latest, +# healthy), bind unique /volume1/docker/vaultwarden → /data (8.4 Mo) : +# db.sqlite3 (+ -wal/-shm) = 953 ciphers, 1 user, twofactor TOTP **déjà actif** (enabled=1, +# last_used > 0 → le secret est importé avec le coffre, l'appli d'authentification continue), +# 0 emergency_access, rsa_key.pem, attachments/, sends/, icon_cache/. +# +# COHÉRENCE (SQLite + WAL) : le conteneur SOURCE est **stoppé** pendant le tar, puis +# **redémarré** immédiatement (trap) — double-run G4 respecté : rien n'est supprimé sur .25. +# +# TRANSPORT : tar sur SSH (le sshd DSM refuse rsync --server et scp — voir journal 07:36). +# +# Usage (machine de pilotage avec les clés SSH) : ./07b-migrate-vaultwarden.sh +# Rollback : G9 `docker compose -f /data/docker/vaultwarden/docker-compose.yml down` + +# `rm -rf /data/docker/vaultwarden` ; .25 intact (service relancé, 953 ciphers). + +set -euo pipefail + +XP_USER="Master"; XP_HOST="192.168.1.25"; XP_PORT=222 +XP_DOCKER="/var/packages/ContainerManager/target/usr/bin/docker" +XP_CONTAINERS="vaultwarden-server-1" +XP_SRC="/volume1/docker/vaultwarden" +G9_USER="folzo"; G9_HOST="192.168.1.24" +KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519_openfox11}" +BRIQUE="vaultwarden" +G9_DIR="/data/docker/$BRIQUE" +KIT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +KIT_ON_G9="/home/folzo/g9-migration" +JOURNAL="$KIT_DIR/journal.md" +STAGE="${STAGE:-$HOME/migration-staging/$BRIQUE}" + +SSH_XP="ssh -p $XP_PORT -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $XP_USER@$XP_HOST" +SSH_G9="ssh -i $KEY -o ControlMaster=auto -o ControlPath=/tmp/.g9-ctl-%r@%h:%p -o ControlPersist=600 $G9_USER@$G9_HOST" +RSH_G9="ssh -i $KEY" + +log() { echo -e "\n[vaultwarden] $*"; } +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 tar >/dev/null && sudo tar --version >/dev/null" || { echo "ERREUR : SSH/tar(sudo) .25 indisponibles"; exit 1; } +$SSH_G9 "command -v docker >/dev/null && command -v rsync >/dev/null" || { echo "ERREUR : docker/rsync .24 indisponibles"; exit 1; } +$SSH_G9 "test -d $KIT_ON_G9/compose" || { echo "ERREUR : kit absent sur le G9"; exit 1; } +$SSH_XP "sudo test -f $XP_SRC/db.sqlite3" || { echo "ERREUR : coffre source absent ($XP_SRC/db.sqlite3)"; exit 1; } +mkdir -p "$STAGE" + +SRC_CIPHERS=$($SSH_XP "sudo sqlite3 $XP_SRC/db.sqlite3 'select count(*) from ciphers;'") +SRC_USERS=$($SSH_XP "sudo sqlite3 $XP_SRC/db.sqlite3 'select count(*) from users;'") +SRC_2FA=$($SSH_XP "sudo sqlite3 $XP_SRC/db.sqlite3 'select count(*) from twofactor where enabled=1;'") +log "source .25 : ciphers=$SRC_CIPHERS users=$SRC_USERS 2fa_actif=$SRC_2FA" + +# ---------------------------------------------------------------- 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 Vaultwarden" "snapshot restic Freebox (log /var/log/restic-daily.log)" "restic forget && restic prune" + +# ---------------------------------------------------------------- 2. export (stop → tar → start) +log "=== 2/5 EXPORT depuis .25 : stop source → tar/ssh → redémarrage source (G4) ===" +STOPPED=() +for c in $XP_CONTAINERS; do + if $SSH_XP "sudo $XP_DOCKER ps -q -f name=^${c}\$" | grep -q .; then + $SSH_XP "sudo $XP_DOCKER stop $c >/dev/null" && STOPPED+=("$c") && log " ⏸ source stoppée : $c" + fi +done +restart_source() { + for c in "${STOPPED[@]:-}"; do + [[ -n "$c" ]] || continue + $SSH_XP "sudo $XP_DOCKER start $c >/dev/null" && log " ▶ source redémarrée : $c" \ + || log " ⚠️ ÉCHEC du redémarrage de $c — INTERVENIR SUR .25" + done +} +trap restart_source EXIT + +TAR="$STAGE/vaultwarden.tgz" +$SSH_XP "sudo tar cz -C '$XP_SRC' ." > "$TAR" +log " archive : $(du -h "$TAR" | cut -f1)" +tar xzf "$TAR" -C "$STAGE" +log " intégrité (source == stage) :" +r=$(mktemp); l=$(mktemp) +$SSH_XP "cd '$XP_SRC' && sudo find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$r" +manifest_in "$STAGE" | grep -v "$(basename "$TAR")" > "$l" +diff -u "$r" "$l" > "$r.diff" || { log " ❌ intégrité source ≠ stage :"; head -20 "$r.diff"; rm -f "$r" "$l" "$r.diff"; exit 1; } +log " ✅ $(wc -l < "$r") fichiers identiques" +rm -f "$r" "$l" "$r.diff" "$TAR" + +# redémarrage de la source AVANT de toucher au G9 (double-run) +restart_source +STOPPED=() + +# ---------------------------------------------------------------- 3. import G9 +log "=== 3/5 IMPORT sur le G9 ===" +$SSH_G9 "sudo mkdir -p $G9_DIR && sudo chown -R $G9_USER:$G9_USER $G9_DIR" +rsync -a --info=stats1 -e "$RSH_G9" --exclude "$(basename "$TAR")" "$STAGE/" "$G9_USER@$G9_HOST:$G9_DIR/import/" +g=$(mktemp); manifest_in "$STAGE" | grep -v "$(basename "$TAR")" > "$l" +$SSH_G9 "cd '$G9_DIR/import' && find . -type f -exec sha256sum {} + 2>/dev/null | LC_ALL=C sort" > "$g" +diff -u "$l" "$g" > "$g.diff" || { log " ❌ intégrité G9 :"; head -20 "$g.diff"; rm -f "$l" "$g" "$g.diff"; exit 1; } +log " ✅ $(wc -l < "$g") fichiers identiques sur le G9" +rm -f "$l" "$g" "$g.diff" +rm -rf "$STAGE" + +log "déploiement compose + secrets" +rsync -a --info=stats1 -e "$RSH_G9" --exclude "secrets" "$KIT_DIR/compose/$BRIQUE/" "$G9_USER@$G9_HOST:$KIT_ON_G9/compose/$BRIQUE/" +rsync -a -e "$RSH_G9" "$KIT_DIR/scripts/03b-secrets.sh" "$G9_USER@$G9_HOST:$KIT_ON_G9/scripts/" +$SSH_G9 "sudo bash $KIT_ON_G9/scripts/03b-secrets.sh >/dev/null" +$SSH_G9 "sudo rsync -a $KIT_ON_G9/compose/$BRIQUE/ $G9_DIR/ && sudo chmod 755 $G9_DIR/secrets && sudo chmod 644 $G9_DIR/secrets/admin" +# Le coffre importé doit être lu par vaultwarden : ./data = /data (DATA_FOLDER par défaut) +$SSH_G9 "mkdir -p $G9_DIR/data && rsync -a $G9_DIR/import/ $G9_DIR/data/ && rm -rf $G9_DIR/import" +$SSH_G9 "cd $G9_DIR && docker compose up -d && sleep 15 && docker compose ps --format '{{.Name}} {{.Status}}'" + +# ---------------------------------------------------------------- 4. vérification +log "=== 4/5 VÉRIFICATION ===" +$SSH_G9 "curl -sf -o /dev/null -w 'alive HTTP %{http_code}\n' http://127.0.0.1:8081/alive" +G9_CIPHERS=$($SSH_G9 "cd $G9_DIR && docker compose exec -T vaultwarden sqlite3 /data/db.sqlite3 'select count(*) from ciphers;' < /dev/null 2>/dev/null || docker run --rm -v $G9_DIR/data:/d alpine sqlite3 /d/db.sqlite3 'select count(*) from ciphers;' 2>/dev/null") +G9_USERS=$($SSH_G9 "cd $G9_DIR && docker compose exec -T vaultwarden sh -c 'command -v sqlite3 >/dev/null && sqlite3 /data/db.sqlite3 \"select count(*) from users;\"' < /dev/null 2>/dev/null") +log "G9 : ciphers=${G9_CIPHERS:-?} users=${G9_USERS:-?} (source : $SRC_CIPHERS / $SRC_USERS)" +$SSH_G9 "cd $G9_DIR && docker compose exec -T vaultwarden sh -c 'command -v sqlite3 >/dev/null && sqlite3 /data/db.sqlite3 \"select atype, enabled, last_used from twofactor;\"' < /dev/null 2>/dev/null" +$SSH_XP "sudo $XP_DOCKER ps --format '{{.Names}} {{.Status}}' | grep -i vault" +journal "Vaultwarden importé + lancé sur le G9" "T4.1 (double-run G4, source stoppée puis relancée)" "coffre ${SRC_CIPHERS} ciphers / ${SRC_USERS} user, sha256 identiques, 2FA TOTP importé (enabled), alive HTTP 200, .25 relancé" "docker compose -f $G9_DIR/docker-compose.yml down + rm -rf $G9_DIR (G9 seul) ; .25 intact" + +log "Terminé. RESTE (UI, par l'utilisateur) : 2FA déjà actif (secret importé, l'appli d'auth" +log "continue), emergency access à activer dans WebVault → Paramètres → Accès d'urgence."