#!/usr/bin/env bash set -euo pipefail umask 077 ####################################### # Chemins fixes du script ####################################### SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" LOG_FILE="/var/log/vaultwarden_backup.log" mkdir -p "$(dirname "$LOG_FILE")" touch "$LOG_FILE" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } ####################################### # Vérification fichier .env ####################################### [[ -f "$ENV_FILE" ]] || { echo "ERROR: Fichier .env introuvable : $ENV_FILE" >&2 exit 1 } ####################################### # Chargement du .env ####################################### set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a ####################################### # Variables obligatoires ####################################### : "${DISCORD_WEBHOOK_URL:=}" : "${DATA_DIR:?Variable DATA_DIR manquante dans .env}" : "${LOCAL_BACKUP:?Variable LOCAL_BACKUP manquante dans .env}" : "${REMOTE_USER:?Variable REMOTE_USER manquante dans .env}" : "${REMOTE_HOST:?Variable REMOTE_HOST manquante dans .env}" : "${REMOTE_DIR:?Variable REMOTE_DIR manquante dans .env}" [[ "$REMOTE_DIR" =~ ^[a-zA-Z0-9/_.-]+$ ]] || { echo "ERROR: Variable REMOTE_DIR invalide dans .env" >&2 exit 1 } : "${SSH_KEY:?Variable SSH_KEY manquante dans .env}" : "${BACKUP_REMOTE_SSH_PORT:=22}" : "${SSH_CONNECT_TIMEOUT:=10}" : "${BACKUP_KNOWN_HOSTS_STRICT:=yes}" : "${BACKUP_KNOWN_HOSTS_FILE:=${HOME}/.ssh/known_hosts}" ####################################### # Variables backup ####################################### DATE="$(date +'%Y-%m-%d_%H-%M-%S')" BACKUP_PREFIX="vaultwarden-backup" BACKUP_NAME="${BACKUP_PREFIX}-${DATE}.tar.gz" LOCAL_BACKUP_FILE="${LOCAL_BACKUP}/${BACKUP_NAME}" RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-10}" [[ "$BACKUP_REMOTE_SSH_PORT" =~ ^[0-9]+$ ]] || { echo "ERROR: Variable BACKUP_REMOTE_SSH_PORT invalide dans .env" >&2 exit 1 } [[ "$SSH_CONNECT_TIMEOUT" =~ ^[0-9]+$ ]] || { echo "ERROR: Variable SSH_CONNECT_TIMEOUT invalide dans .env" >&2 exit 1 } [[ "$RETENTION_DAYS" =~ ^[0-9]+$ ]] || { echo "ERROR: Variable BACKUP_RETENTION_DAYS invalide dans .env" >&2 exit 1 } case "${BACKUP_KNOWN_HOSTS_STRICT,,}" in yes|y|oui|o|true|1) BACKUP_KNOWN_HOSTS_STRICT="yes" ;; no|n|non|false|0) BACKUP_KNOWN_HOSTS_STRICT="no" ;; *) echo "ERROR: Variable BACKUP_KNOWN_HOSTS_STRICT invalide dans .env" >&2 exit 1 ;; esac mkdir -p "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" chmod 700 "$(dirname "$BACKUP_KNOWN_HOSTS_FILE")" || true touch "$BACKUP_KNOWN_HOSTS_FILE" chmod 600 "$BACKUP_KNOWN_HOSTS_FILE" || true SSH_OPTS=( -i "$SSH_KEY" -p "$BACKUP_REMOTE_SSH_PORT" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" -o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT" -o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE" ) SCP_OPTS=( -i "$SSH_KEY" -P "$BACKUP_REMOTE_SSH_PORT" -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout="$SSH_CONNECT_TIMEOUT" -o StrictHostKeyChecking="$BACKUP_KNOWN_HOSTS_STRICT" -o UserKnownHostsFile="$BACKUP_KNOWN_HOSTS_FILE" ) mkdir -p "$LOCAL_BACKUP" ####################################### # Notification Discord ####################################### send_discord() { local success="$1" local details="${2:-}" local payload="" [[ -z "$DISCORD_WEBHOOK_URL" ]] && return 0 require_cmd jq || return 0 require_cmd curl || return 0 local icon status_line if [[ "$success" == "true" ]]; then icon="🟢" status_line="✅" ping="" else icon="🔴" status_line="❌" ping="@here " fi local msg msg="**${ping}Backup Vaultwarden ${icon}**\n" msg+="Backup: ${BACKUP_NAME}\n" msg+="Data transfer: ${status_line}\n" [[ -n "$details" ]] && msg+="Détails: ${details}" payload="$(jq -n --arg content "$msg" '{content: $content}')" curl -fsS -H "Content-Type: application/json" -d "$payload" "$DISCORD_WEBHOOK_URL" >/dev/null || true } ####################################### # Fonction erreur ####################################### fail() { local detail="$1" log "ERROR: $detail" send_discord "false" "$detail" exit 1 } require_cmd() { command -v "$1" >/dev/null 2>&1 || fail "commande requise absente : $1" } ####################################### # Verrou d'execution ####################################### LOCK_DIR="/tmp/vaultwarden_backup.lock.d" if ! mkdir "$LOCK_DIR" 2>/dev/null; then fail "Backup deja en cours" fi cleanup() { rm -f "${LOCAL_BACKUP_FILE:-}" rm -rf -- "$LOCK_DIR" } trap cleanup EXIT ####################################### # Vérifications préalables ####################################### [[ -d "$DATA_DIR" ]] || fail "Le dossier source n'existe pas : $DATA_DIR" [[ -f "$SSH_KEY" ]] || fail "La clé SSH est introuvable : $SSH_KEY" [[ -r "$SSH_KEY" ]] || fail "La clé SSH est non lisible : $SSH_KEY" [[ ! -L "$SSH_KEY" ]] || fail "La clé SSH ne doit pas être un lien symbolique : $SSH_KEY" chmod 600 "$SSH_KEY" || true for cmd in tar ssh scp jq curl find; do require_cmd "$cmd" done log "Début du backup Vaultwarden" log "Source : $DATA_DIR" log "Archive locale : $LOCAL_BACKUP_FILE" log "Destination distante : ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}" ####################################### # Création du backup ####################################### tar -czf "$LOCAL_BACKUP_FILE" -C "$(dirname "$DATA_DIR")" "$(basename "$DATA_DIR")" \ || fail "Erreur lors de la compression du dossier $DATA_DIR" log "Backup local créé : $LOCAL_BACKUP_FILE" ####################################### # Création dossier distant ####################################### ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "mkdir -p '$REMOTE_DIR'" \ || fail "Impossible de créer le dossier distant $REMOTE_DIR" ####################################### # Envoi du backup ####################################### scp "${SCP_OPTS[@]}" "$LOCAL_BACKUP_FILE" "$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/" \ || fail "Erreur lors de l'envoi du backup vers $REMOTE_HOST" log "Backup envoyé sur $REMOTE_HOST:$REMOTE_DIR" ####################################### # Rotation distante - suppression > 10 jours ####################################### ssh "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" \ "find '$REMOTE_DIR' -type f -name '${BACKUP_PREFIX}-*.tar.gz' -mtime +$RETENTION_DAYS -delete" \ || fail "Erreur lors de la rotation distante des sauvegardes" log "Rotation distante OK" ####################################### # Nettoyage local ####################################### rm -f "$LOCAL_BACKUP_FILE" || fail "Impossible de supprimer le backup local $LOCAL_BACKUP_FILE" ####################################### # Fin ####################################### log "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR" send_discord "true" "Backup envoyé avec succès vers $REMOTE_HOST" echo "Backup $BACKUP_NAME terminé et envoyé sur $REMOTE_HOST:$REMOTE_DIR"