Sauvegarde Vaultwarden
Script : /usr/local/bin/backup_vaultwarden.sh — tous les jours à 03:45, rétention 30 jours.
| Conteneur | vaultwarden (détecté automatiquement) |
| Dossier compose | /srv/vaultwarden |
| Base | SQLite (/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 :
| Fichier | Contenu |
|---|---|
db.sqlite3 | Copie cohérente à chaud de la base (commande intégrée vaultwarden backup) |
data.tar.gz | Pièces jointes, Sends, rsa_key* (signature des sessions), config.json — sans le cache d'icônes |
compose.tar.gz | docker-compose.yml et .env (contient le jeton admin) |
image.txt | Image et version de Vaultwarden |
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
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