T4.1 Vaultwarden : import coffre 953 ciphers + 2FA importé + ADMIN_TOKEN argon2 + emergency access activé

This commit is contained in:
Effi
2026-10-11 08:03:10 +02:00
parent e7727a6598
commit a1864c69e7
3 changed files with 146 additions and 2 deletions
@@ -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.
+5
View File
@@ -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 <snapshot> && 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 <snapshot> && 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 <snapshot> && restic prune |
| 2026-10-11 07:57 | 4 | migrate:vaultwarden : coffre importé + service lancé | T4.1 (double-run G4) | Source stoppée → tar/ssh → **source relancée healthy** ; 621 fichiers sha256 identiques (source == stage == G9) ; G9 = 953 ciphers / 1 user (identiques à .25, revérifié après coup : .25 healthy 953) ; migrations diesel OK (v.20260902120000) ; alive 200, WebVault 200 ; DOMAIN conservé = `pass.folzocorp.fr` (clients existants intacts) ; image épinglée 1.37.4 (génération source, image .25 pullée 2026-10-05) | docker compose -f /data/docker/vaultwarden/docker-compose.yml down + rm -rf /data/docker/vaultwarden ; .25 intact |
| 2026-10-11 08:01 | 4 | migrate:vaultwarden : 2FA + ADMIN_TOKEN + emergency access | T4.1 (critères 2FA + emergency access) | **2FA TOTP déjà actif côté source et importé avec le coffre** : twofactor(atype=0, enabled=1, last_used=59703916) identique sur le G9 → l'appli d'authentification de l'utilisateur continue sans re-scannage ; ADMIN_TOKEN **haché argon2id** (m=64MiB,t=3,p=4 — preset bitwarden, python3-argon2) : avis « plain text ADMIN_TOKEN » disparu, login admin 200 (dashboard Users/Invitations/Organizations/**Emergency** accessibles) ; token clair dans `/data/docker/vaultwarden/secrets/admin-plain` (600 root, à consigner dans le coffre puis supprimer) ; `EMERGENCY_ACCESS_ALLOWED=true` + `INVITATIONS_ALLOWED=true` posés | restaurer secrets/admin (token clair) + restart ; retirer les 2 env |
+131
View File
@@ -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 <snapshot> && 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."