Vérifier les sauvegardes qui disent « réussi » : fraîcheur, intégrité et un test de restauration qui se journalise lui-même
Une tâche de sauvegarde qui renvoie exit 0 prouve que la tâche a tourné — pas qu'il y a quelque chose d'utilisable dans l'archive. Trois contrôles à automatiser : fraîcheur (mtime), intégrité (restic/borg check) et un test de restauration mensuel qui se journalise lui-même. Plus le piège de rsync qui préserve les mtimes.
Sommaire
Toute organisation qui a perdu des données avait des sauvegardes. La tâche tournait chaque nuit, le mail disait « Backup completed successfully », et le jour où c'était nécessaire, l'archive s'est révélée vide, vieille de trois semaines, chiffrée avec une clé que personne n'avait, ou tout simplement impossible à ouvrir. Cet article automatise les trois questions que vous devriez en réalité vous poser chaque jour : est-elle fraîche, est-elle intacte, et puis-je en sortir quelque chose ?
Étape 1 : la fraîcheur — quelque chose est-il vraiment arrivé cette nuit ?
Le contrôle le plus simple fonctionne pour tout outil de sauvegarde, y compris le cron avec tar d'il y a dix ans : regardez le fichier le plus récent à la destination.
# Fichier le plus récent sous le chemin de sauvegarde, avec l'âge en heures
DEST=/srv/backup # ou un montage de votre NAS, ou le répertoire du dépôt restic/borg
newest=$(find "$DEST" -type f -printf '%T@ %p\n' 2>/dev/null | sort -n | tail -1)
ts=${newest%% *}; file=${newest#* }
age_h=$(( ( $(date +%s) - ${ts%.*} ) / 3600 ))
echo "newest: $file age: ${age_h}h"
[ "$age_h" -gt 26 ] && echo "STALE: aucune nouvelle sauvegarde depuis ${age_h} heures"
Le seuil de 26 heures est délibéré : une tâche quotidienne plus deux heures de marge. Pour une tâche horaire, prenez 3.
Avec restic et borg, il existe une meilleure source que le mtime — la liste des snapshots du dépôt lui-même :
# restic
restic -r "$DEST" snapshots --latest 1 --json | jq -r '.[0] | "\(.time) \(.hostname) \(.paths|join(","))"'
# borg
borg list "$DEST" --last 1 --format '{time} {hostname} {name}{NL}'
Comparez l'heure avec maintenant, et surveillez aussi le hostname : un dépôt dans lequel un autre hôte écrit par accident a l'air frais alors que le vôtre ne sauvegarde plus depuis des semaines.
Étape 2 : l'intégrité — l'archive est-elle lisible ?
« Il y a un fichier de 40 Go » ne dit rien sur le contenu. restic et borg peuvent vérifier leur propre dépôt ; faites-le chaque semaine sur un échantillon et chaque mois en entier.
# restic : structure + lire réellement 5 % des données (hebdomadaire)
restic -r "$DEST" check --read-data-subset=5%
# restic : tout (mensuel ; peut prendre des heures sur les gros dépôts)
restic -r "$DEST" check --read-data
# borg
borg check --verify-data "$DEST" # complet
borg check "$DEST" # métadonnées uniquement, rapide
Pour les archives tar/zip sans vérification propre : tar -tzf archive.tgz >/dev/null lit toute l'archive et échoue en cas de corruption. Pour les dumps de bases de données : pg_restore --list dump.pgc >/dev/null (format custom PostgreSQL) ou gzip -t dump.sql.gz (couche de compression seulement).
Étape 3 : le test de restauration qui se journalise lui-même
C'est l'étape que tout le monde saute, et la seule qui compte. Une fois par mois, vous remettez quelque chose de réel à un autre endroit et vous vérifiez que c'est correct. Pas tout le serveur — un ensemble ciblé et représentatif : la configuration de votre service le plus important et un dump de base de données.
sudo tee /usr/local/sbin/restore-test.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Test de restauration mensuel. Restaure un ensemble connu dans un répertoire temporaire,
# compare avec l'original, et journalise le résultat avec la date.
set -u
DEST=/srv/backup
LOG=/srv/inventory/restore-tests.log
NTFY="https://ntfy.example.be/backup"
T=$(mktemp -d /tmp/restore-test.XXXX)
HOST=$(hostname -s)
result=OK; detail=""
# 1. Fichiers de configuration : restaurer et comparer octet par octet avec le live
if restic -r "$DEST" restore latest --target "$T" --include /etc/nginx --include /etc/ssh/sshd_config.d >/dev/null 2>&1; then
if ! diff -rq /etc/nginx "$T/etc/nginx" >/dev/null 2>&1; then
result=FAIL; detail+="config nginx différente du live; "
fi
else
result=FAIL; detail+="restic restore a échoué; "
fi
# 2. Dump de base de données : restaurer dans une base jetable et compter
if [ -x /usr/bin/psql ]; then
dump=$(ls -t "$T"/srv/dumps/*.pgc 2>/dev/null | head -1)
if [ -n "$dump" ]; then
sudo -u postgres createdb restoretest 2>/dev/null
if sudo -u postgres pg_restore -d restoretest "$dump" >/dev/null 2>&1; then
tables=$(sudo -u postgres psql -tAc "select count(*) from information_schema.tables where table_schema='public'" restoretest)
[ "${tables:-0}" -gt 0 ] || { result=FAIL; detail+="pg_restore: 0 table; "; }
else
result=FAIL; detail+="pg_restore a échoué; "
fi
sudo -u postgres dropdb restoretest 2>/dev/null
fi
fi
rm -rf "$T"
line="$(date -Is) restore-test $HOST $result ${detail:-config+db restaurées et comparées}"
echo "$line" | sudo tee -a "$LOG" >/dev/null
[ "$result" = FAIL ] && echo "$line" | curl -s -H "Title: RESTORE-TEST FAILED $HOST" -H "Priority: urgent" --data-binary @- "$NTFY" >/dev/null
echo "$line"
EOF
sudo chmod 0755 /usr/local/sbin/restore-test.sh
echo '0 4 1 * * root /usr/local/sbin/restore-test.sh' | sudo tee /etc/cron.d/restore-test
La ligne de journal — 2026-10-01T04:00:12+02:00 restore-test web-01 OK config+db restaurées et comparées — est exactement ce qu'un auditeur entend par « preuve de tests de reprise ». Douze lignes par an, par hôte.
Adaptez les chemins --include et l'emplacement du dump à votre situation ; le principe est : restaurer quelque chose que vous pouvez comparer, et une base de données que vous pouvez interroger.
Étape 4 : le contrôle quotidien, avec alerte
Les étapes 1 et 2 dans un script qui tourne chaque matin et ne signale que ce qui ne va pas :
sudo tee /usr/local/sbin/backup-check.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
DEST=/srv/backup; MAX_H=26
NTFY="https://ntfy.example.be/backup"
HOST=$(hostname -s); MSG=()
# Fraîcheur via le dépôt lui-même (restic) ; repli sur le mtime si restic manque
if command -v restic >/dev/null; then
last=$(restic -r "$DEST" snapshots --latest 1 --json 2>/dev/null | jq -r '.[0].time // empty')
[ -n "$last" ] && age_h=$(( ( $(date +%s) - $(date -d "$last" +%s) ) / 3600 )) || age_h=9999
else
ts=$(find "$DEST" -type f -printf '%T@\n' 2>/dev/null | sort -n | tail -1); ts=${ts%.*}
age_h=$(( ( $(date +%s) - ${ts:-0} ) / 3600 ))
fi
[ "$age_h" -gt "$MAX_H" ] && MSG+=("STALE: dernière sauvegarde vieille de ${age_h}h (max ${MAX_H}h)")
# Intégrité, le dimanche seulement (échantillon)
if [ "$(date +%u)" = 7 ] && command -v restic >/dev/null; then
restic -r "$DEST" check --read-data-subset=5% >/tmp/restic-check.log 2>&1 || MSG+=("CHECK FAILED: $(tail -1 /tmp/restic-check.log)")
fi
# Espace à la destination
pct=$(df --output=pcent "$DEST" 2>/dev/null | tail -1 | tr -dc '0-9')
[ "${pct:-0}" -gt 85 ] && MSG+=("destination pleine à ${pct}%")
if [ ${#MSG[@]} -gt 0 ]; then
printf '%s\n' "${MSG[@]}" | curl -s -H "Title: backup@$HOST" -H "Priority: high" --data-binary @- "$NTFY" >/dev/null
fi
EOF
sudo chmod 0755 /usr/local/sbin/backup-check.sh
echo '15 7 * * * root /usr/local/sbin/backup-check.sh' | sudo tee /etc/cron.d/backup-check
Pièges
- rsync préserve les mtimes.
rsync -acopie les fichiers avec leur date de modification d'origine. Une vérification mtime sur une destination rsync voit donc la date du fichier source, pas de la copie. Pour les sauvegardes rsync, utilisez un marqueur :rsync ... && date -Is > "$DEST/.last-run"et vérifiez ce fichier. - La tâche qui « réussit » avec zéro fichier. Un chemin mal monté, un répertoire de volume Docker vide, un
--excludetrop large : exit 0, archive de 12 Ko. Outre l'âge, vérifiez aussi la taille du dernier snapshot (restic stats latest) et alertez sur une baisse de plus de 50 % par rapport au précédent. - Une rétention qui nettoie tout.
restic forget --keep-daily 7 --prunesans--keep-monthlysignifie : après une semaine sans nouvelles sauvegardes, le dépôt est vide. Combinez toujours avec un contrôle de fraîcheur, sinon personne ne le remarque. - La clé sur le même serveur. Un dépôt restic chiffré dont le mot de passe n'existe que dans
/etc/restic/passwordsur le serveur sauvegardé ne vaut rien lors d'un incident ransomware. Conservez la clé à un second endroit (coffre de mots de passe, papier dans un coffre-fort). - Les bases de données comme fichiers. Une copie de
/var/lib/postgresqld'une base en cours d'exécution a de fortes chances d'être incohérente. Utilisezpg_dump/pg_basebackup,mariadb-dump --single-transaction, ou un snapshot au niveau du système de fichiers avec un état cohérent. - Test de restauration sur la production. Le script restaure dans
/tmpet une base jetable. Jamaisrestore latest --target /. Cela semble évident jusqu'à ce que quelqu'un fasse autrement à 04:00.
Ce qu'il vous manque encore
- Une vue d'ensemble par hôte. Trente hôtes, trente
restore-tests.log. « Quels hôtes n'ont pas eu de test de restauration réussi au T3 ? » est un script sur ssh. - Des alertes qui s'arrêtent quand c'est résolu. Le push ntfy revient chaque jour jusqu'à ce que quelqu'un corrige, et encore un jour parce que le contrôle tourne à 07:15.
- La preuve pour ISO 27001 A.8.13. La ligne de journal est bien ; un auditeur la veut sous une forme signée et immuable, sur toute la flotte, et sur douze mois.
Comment monsys fait
Dans monsys, vous enregistrez par hôte une surveillance de sauvegarde (backup watch) : le chemin où atterrit la sauvegarde et l'âge maximal. L'agent vérifie le chemin toutes les 30 minutes (stat uniquement, il n'ouvre pas les archives — les dépôts chiffrés fonctionnent donc sans droits de lecture) et rapporte le mtime et la taille les plus récents. Le hub alerte en cas de dépassement (warning) et de double dépassement (critical), dédupliqué en une seule alerte ouverte qui se ferme d'elle-même quand la sauvegarde repart. Le contrôle ISO 27001 A.8.13 compte automatiquement les surveillances dans leur âge maximal et l'intègre à l'audit pack mensuel. Voir vérification des sauvegardes dans la documentation. Le test de restauration reste un travail humain — mais sa ligne de journal peut être jointe comme preuve.
FAQ
À quelle fréquence dois-je faire un test de restauration ?
Mensuellement pour un ensemble ciblé (config + base de données), et une fois par an une reprise complète d'un vrai service vers un environnement de test, chronomètre en main. Ce dernier est aussi le seul moyen de connaître votre RTO au lieu de l'estimer.
La vérification de l'outil de sauvegarde ne suffit-elle pas ?
restic check prouve que le dépôt est cohérent en interne. Il ne prouve pas que les bons chemins ont été sauvegardés, que le dump de base de données est utilisable, ou que vous avez encore la clé. C'est à ça que sert le test de restauration.
Et si ma sauvegarde est dans le cloud (S3, Backblaze, Hetzner Storage Box) ?
Toutes les commandes fonctionnent de la même façon : restic et borg parlent directement à ces backends. La vérification mtime de l'étape 1 ne fonctionne pas sur l'object storage ; utilisez là la liste des snapshots de l'outil. Et testez la restauration depuis l'emplacement cloud, pas depuis un cache local — la bande passante fait partie de votre RTO.
Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.
Fait à la main ? Laissez monsys s'en charger en continu.
Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.