Aller au contenu principal

Sauvegarde Vaultwarden

Script : /usr/local/bin/backup_vaultwarden.sh — tous les jours à 03:45, rétention 30 jours.

Conteneurvaultwarden (détecté automatiquement)
Dossier compose/srv/vaultwarden
BaseSQLite (/data/db.sqlite3)
Archives/srv/backups/vaultwarden/vaultwarden_AAAA-MM-JJ_HHMM.tar.gpg

Ce qui est sauvegardé​

L'archive (chiffrée GPG AES256) contient :

FichierContenu
db.sqlite3Copie cohérente à chaud de la base (commande intégrée vaultwarden backup)
data.tar.gzPièces jointes, Sends, rsa_key* (signature des sessions), config.json — sans le cache d'icônes
compose.tar.gzdocker-compose.yml et .env (contient le jeton admin)
image.txtImage et version de Vaultwarden
info

Les coffres sont chiffrés par le mot de passe maître de chaque utilisateur : l'IT ne peut pas les lire, même avec la sauvegarde. Le chiffrement GPG protège le reste (jeton admin, clés, config).

Installation​

sudo install -m 750 backup_vaultwarden.sh /usr/local/bin/backup_vaultwarden.sh
sudo sh -c 'openssl rand -base64 32 > /root/.vw_backup_pass && chmod 600 /root/.vw_backup_pass'
sudo sed -i 's|^GPG_PASSFILE=.*|GPG_PASSFILE="/root/.vw_backup_pass"|' /usr/local/bin/backup_vaultwarden.sh
sudo /usr/local/bin/backup_vaultwarden.sh

Planifier (sudo crontab -e) :

45 3 * * * /usr/local/bin/backup_vaultwarden.sh >> /var/log/backup_vaultwarden.log 2>&1
danger

Noter la phrase de passe (sudo cat /root/.vw_backup_pass) hors ligne. La garder uniquement dans Vaultwarden la rendrait inaccessible au moment où on en a besoin.

Vérifier une archive​

sudo gpg --batch --pinentry-mode loopback --passphrase-file /root/.vw_backup_pass \
-d /srv/backups/vaultwarden/vaultwarden_AAAA-MM-JJ_HHMM.tar.gpg | tar tvf -

On doit voir db.sqlite3 (non vide), data.tar.gz, compose.tar.gz, image.txt.

Restauration​

Renseigner GPG_PASSFILE en tête du script, puis :

sudo ./restore_vaultwarden.sh /srv/backups/vaultwarden/vaultwarden_AAAA-MM-JJ_HHMM.tar.gpg

Le script arrête Vaultwarden, remplace la base et les fichiers, puis le redémarre. La config compose est extraite dans ./vaultwarden_compose_restored.tar.gz.

Scripts​

backup_vaultwarden.sh
#!/usr/bin/env bash
# Sauvegarde Vaultwarden (docker, base SQLite par défaut) :
# base de données (copie cohérente à chaud), pièces jointes, Sends, clés RSA, config.json,
# + docker-compose.yml / .env du dossier compose.
#
# Installation :
# sudo install -m 750 backup_vaultwarden.sh /usr/local/bin/backup_vaultwarden.sh
# sudo crontab -e puis ajouter :
# 45 3 * * * /usr/local/bin/backup_vaultwarden.sh >> /var/log/backup_vaultwarden.log 2>&1
set -euo pipefail

# ---------- À adapter ----------
VW_CONTAINER="" # vide = détection auto (image vaultwarden/server)
BACKUP_DIR="/srv/backups/vaultwarden"
KEEP_DAYS=30
GPG_PASSFILE="" # ex. /root/.vw_backup_pass -> archive chiffrée (recommandé)
RCLONE_REMOTE="" # ex. "gdrive-crypt:backups/vaultwarden" (vide = pas de copie hors site)
# --------------------------------

STAMP="$(date +%F_%H%M)"
WORK="$BACKUP_DIR/.tmp_$STAMP"
log() { echo "[$(date '+%F %T')] $*"; }
fail() { log "ERREUR : $*"; rm -rf "$WORK"; exit 1; }

exec 9>/var/lock/backup_vaultwarden.lock
flock -n 9 || fail "une sauvegarde est déjà en cours"

# Conteneur
if [ -z "$VW_CONTAINER" ]; then
VW_CONTAINER="$(docker ps --filter ancestor=vaultwarden/server --format '{{.Names}}' | head -1)"
[ -n "$VW_CONTAINER" ] || VW_CONTAINER="$(docker ps --format '{{.Names}} {{.Image}}' | awk '/vaultwarden/{print $1; exit}')"
fi
[ -n "$VW_CONTAINER" ] || fail "conteneur Vaultwarden introuvable (renseigner VW_CONTAINER)"
log "Conteneur : $VW_CONTAINER"

# Base externe (MySQL/PostgreSQL) non gérée par ce script
if docker exec "$VW_CONTAINER" printenv DATABASE_URL 2>/dev/null | grep -qE '^(mysql|postgres)'; then
fail "DATABASE_URL pointe vers MySQL/PostgreSQL : ce script ne gère que SQLite"
fi

# Dossier /data (volume nommé ou dossier monté)
DATA_SRC="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/data"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' "$VW_CONTAINER")"
[ -n "$DATA_SRC" ] || fail "montage /data introuvable"

mkdir -p "$WORK"; chmod 700 "$BACKUP_DIR" "$WORK"

# 1. Copie cohérente de la base, à chaud
log "Sauvegarde de la base..."
if docker exec "$VW_CONTAINER" /vaultwarden backup >/dev/null 2>&1; then
# La commande intégrée crée /data/db_<date>.sqlite3 : on le déplace dans la sauvegarde
docker run --rm -v "$DATA_SRC:/data" -v "$WORK:/backup" alpine sh -c \
'f=$(ls -t /data/db_*.sqlite3 2>/dev/null | head -1); [ -n "$f" ] && mv "$f" /backup/db.sqlite3' \
|| fail "fichier de sauvegarde de la base introuvable"
else
log "Commande intégrée indisponible, utilisation de sqlite3 (.backup)"
docker run --rm -v "$DATA_SRC:/data" -v "$WORK:/backup" alpine sh -c \
'apk add --no-cache sqlite >/dev/null && sqlite3 /data/db.sqlite3 ".backup /backup/db.sqlite3"' \
|| fail "copie SQLite échouée"
fi
[ -s "$WORK/db.sqlite3" ] || fail "copie de la base vide"

# 2. Le reste de /data (sans le cache d'icônes ni les fichiers de base en cours d'utilisation)
log "Archive des pièces jointes, Sends, clés et config..."
docker run --rm -v "$DATA_SRC:/data:ro" -v "$WORK:/backup" alpine \
tar czf /backup/data.tar.gz -C /data \
--exclude='./icon_cache' --exclude='./db.sqlite3*' --exclude='./db_*.sqlite3' --exclude='./tmp' . \
|| fail "archive /data échouée"

# 3. Fichiers compose (si lancé via docker compose)
COMPOSE_DIR="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "$VW_CONTAINER" 2>/dev/null || true)"
if [ -n "$COMPOSE_DIR" ] && [ -d "$COMPOSE_DIR" ]; then
log "Copie de la config compose ($COMPOSE_DIR)..."
tar czf "$WORK/compose.tar.gz" -C "$COMPOSE_DIR" \
$(cd "$COMPOSE_DIR" && ls -A | grep -E '^(docker-)?compose\.ya?ml$|^\.env$' || true) 2>/dev/null || true
fi
docker inspect -f '{{.Config.Image}}' "$VW_CONTAINER" > "$WORK/image.txt"

# 4. Archive finale (chiffrée si GPG_PASSFILE est défini)
OUT="$BACKUP_DIR/vaultwarden_$STAMP.tar"
tar cf "$OUT" -C "$WORK" .
rm -rf "$WORK"
if [ -n "$GPG_PASSFILE" ]; then
gpg --batch --yes --pinentry-mode loopback --passphrase-file "$GPG_PASSFILE" \
--symmetric --cipher-algo AES256 -o "$OUT.gpg" "$OUT" || fail "chiffrement échoué"
rm -f "$OUT"; OUT="$OUT.gpg"
fi
chmod 600 "$OUT"
sha256sum "$OUT" > "$OUT.sha256"
log "Archive : $OUT ($(du -h "$OUT" | cut -f1))"

# 5. Copie hors site
if [ -n "$RCLONE_REMOTE" ]; then
log "Copie vers $RCLONE_REMOTE ..."
rclone copy "$OUT" "$RCLONE_REMOTE" && rclone copy "$OUT.sha256" "$RCLONE_REMOTE" || fail "copie rclone échouée"
rclone delete "$RCLONE_REMOTE" --min-age "${KEEP_DAYS}d" || log "ATTENTION : nettoyage distant échoué"
fi

# 6. Rotation locale
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'vaultwarden_*' -mtime +"$KEEP_DAYS" -delete
log "Sauvegarde terminée."
restore_vaultwarden.sh
#!/usr/bin/env bash
# Restaure une archive faite par backup_vaultwarden.sh.
# ATTENTION : remplace la base et les fichiers actuels de Vaultwarden.
#
# Usage : sudo ./restore_vaultwarden.sh /srv/backups/vaultwarden/vaultwarden_2026-11-03_0345.tar[.gpg]
set -euo pipefail

VW_CONTAINER="" # vide = détection auto
GPG_PASSFILE="" # le même que pour la sauvegarde, si l'archive est chiffrée

ARCHIVE="${1:?Usage : $0 /chemin/vers/vaultwarden_xxx.tar[.gpg]}"
[ -f "$ARCHIVE.sha256" ] && sha256sum -c --quiet "$ARCHIVE.sha256"

if [ -z "$VW_CONTAINER" ]; then
VW_CONTAINER="$(docker ps -a --format '{{.Names}} {{.Image}}' | awk '/vaultwarden/{print $1; exit}')"
fi
[ -n "$VW_CONTAINER" ] || { echo "Conteneur Vaultwarden introuvable"; exit 1; }
DATA_SRC="$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/data"}}{{if .Name}}{{.Name}}{{else}}{{.Source}}{{end}}{{end}}{{end}}' "$VW_CONTAINER")"

read -r -p "Restaurer dans $VW_CONTAINER ($DATA_SRC) ? Les données actuelles seront ÉCRASÉES. Taper OUI : " ok
[ "$ok" = "OUI" ] || { echo "Annulé."; exit 1; }

TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
if [[ "$ARCHIVE" == *.gpg ]]; then
gpg --batch --pinentry-mode loopback --passphrase-file "${GPG_PASSFILE:?GPG_PASSFILE requis}" -d "$ARCHIVE" | tar xf - -C "$TMP"
else
tar xf "$ARCHIVE" -C "$TMP"
fi

echo "Arrêt de Vaultwarden..."
docker stop "$VW_CONTAINER"
docker run --rm -v "$DATA_SRC:/data" -v "$TMP:/backup:ro" alpine sh -c '
rm -f /data/db.sqlite3 /data/db.sqlite3-wal /data/db.sqlite3-shm
cp /backup/db.sqlite3 /data/db.sqlite3
tar xzf /backup/data.tar.gz -C /data'
docker start "$VW_CONTAINER"
echo "Terminé. Connecte-toi et vérifie un coffre et une pièce jointe."
if [ -f "$TMP/compose.tar.gz" ]; then
cp "$TMP/compose.tar.gz" "./vaultwarden_compose_restored.tar.gz"
echo "Config compose (docker-compose.yml/.env) copiée dans ./vaultwarden_compose_restored.tar.gz"
fi