T4.1 Vaultwarden : import coffre 953 ciphers + 2FA importé + ADMIN_TOKEN argon2 + emergency access activé
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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."
|
||||
Reference in New Issue
Block a user