Aller au contenu

Vérifier qu'une archive de sauvegarde de fichiers est restaurable

Cette page part d'un dossier à sauvegarder et va jusqu'à la preuve : l'archive produite avec sa somme de contrôle, la vérification faite une fois à la main, puis la même vérification rejouée chaque nuit sans personne.

Ce qui casse une archive sans le dire

Une archive de fichiers a ceci de traître qu'elle ressemble toujours à une sauvegarde réussie : un fichier, une taille, une date. Voici ce qui se passe vraiment quand elle est cassée.

  • Le disque était plein. tar écrit jusqu'à la dernière page libre, puis s'arrête. Le fichier obtenu est une archive tronquée : ses premiers fichiers se déplient parfaitement, les derniers manquent, et aucune ligne de journal ne le dit si personne n'a lu le code de sortie.
  • La copie a été interrompue. Un transfert coupé vers le NAS ou vers S3 laisse un objet plus court que l'original. La liste des sauvegardes le montre avec la bonne date et le bon nom.
  • Le code de sortie n'a pas été lu. C'est le cas le plus fréquent, et le plus facile à corriger : un tar en échec dans un script qui ne s'arrête pas sur l'erreur rend un succès au planificateur.
  • L'archive est chiffrée et personne n'a la clé. Elle est intacte, elle est récente, elle est illisible. Une clé de chiffrement qui n'est sauvegardée nulle part ailleurs que dans le serveur chiffré est une clé perdue.

Les trois premiers se détectent sans rien déplier, à condition d'avoir écrit à côté de l'archive de quoi les détecter. Le quatrième ne se détecte que par un déchiffrement réel, fait ailleurs que sur la machine d'origine.

Produire une archive qu'on peut vérifier

Deux commandes, pas une :

tar -czf /backups/app_data_20260918_010000.tar.gz -C /srv/app data config
sha256sum /backups/app_data_20260918_010000.tar.gz \
  > /backups/app_data_20260918_010000.tar.gz.sha256

-C /srv/app data config entre dans le répertoire avant d'archiver, et n'enregistre donc que les chemins data/ et config/. Sans -C, l'archive contient srv/app/data/... et se déplie dans une arborescence que personne n'attend.

La deuxième commande est celle qui change tout. Une somme de contrôle écrite à côté de l'archive, au moment où l'archive est produite, permet de répondre plus tard à une question qu'aucune inspection du fichier seul ne permet de trancher : ces octets sont-ils bien ceux qui ont été écrits ? Une archive tronquée ou une copie interrompue donnent un fichier qui s'ouvre partiellement, mais jamais la bonne empreinte.

Elle doit être calculée sur le serveur d'origine, avant le transfert. Une somme recalculée après la copie, sur la copie, ne prouve rien d'autre que la copie est égale à elle-même.

Une somme de contrôle n'est pas une signature

sha256sum détecte une corruption accidentelle, pas une modification délibérée : qui peut réécrire l'archive peut réécrire le fichier .sha256 posé à côté. Contre un rançongiciel, ce qui protège est ailleurs — un stockage que la machine sauvegardée ne peut pas effacer, versionné ou en écriture unique.

Le script de sauvegarde

#!/bin/bash
set -euo pipefail

ts=$(date +%Y%m%d_%H%M%S)
archive="/backups/app_data_${ts}.tar.gz"

tar -cf - -C /srv/app data config | gzip > "${archive}"
sha256sum "${archive}" > "${archive}.sha256"

aws s3 cp "${archive}" s3://sauvegardes-shopdemo/files/
aws s3 cp "${archive}.sha256" s3://sauvegardes-shopdemo/files/

Sans pipefail, une archive ratée ressort en succès

Dans tar | gzip, le shell ne regarde que le code de sortie du dernier maillon. gzip réussit à compresser une sortie vide ou tronquée, donc le script rend 0 et le planificateur est content. set -o pipefail — inclus dans le set -euo pipefail ci-dessus — fait échouer la ligne entière dès que tar échoue. C'est la première cause de sauvegardes vides qui passent au vert pendant des mois.

L'horodatage dans le nom du fichier n'est pas une coquetterie : une archive toujours écrite au même nom ne laisse aucune chance de revenir à la veille, et une archive corrompue écrase la dernière bonne. Pour la rétention, une règle de cycle de vie sur le stockage supprime les objets de plus de N jours sans script à maintenir.

Deux précautions pour le lancement nocturne : que la sortie d'erreur aille quelque part où quelqu'un la lit, et qu'une exécution qui s'éternise n'en croise pas une autre — flock -n sur un fichier verrou règle le second point en une ligne.

Vérifier l'archive sans la déplier

Trois questions, de la moins chère à la plus chère, dans cet ordre :

cd /backups
sha256sum -c app_data_20260918_010000.tar.gz.sha256
gzip -t app_data_20260918_010000.tar.gz
tar -tzf app_data_20260918_010000.tar.gz | wc -l
  1. Les octets sont-ils les bons ? sha256sum -c relit le fichier et compare à l'empreinte enregistrée. C'est la seule des trois qui attrape une copie silencieusement altérée.
  2. Le flux compressé se lit-il jusqu'au bout ? gzip -t décompresse sans rien écrire et vérifie le contrôle de redondance que gzip place à la fin du flux. Une archive tronquée échoue ici.
  3. Le catalogue est-il cohérent ? tar -tzf liste les en-têtes sans extraire un seul octet sur le disque. C'est aussi le moyen le plus rapide de voir ce que l'archive contient réellement à sa racine.

Extraire pour de vrai, dans un répertoire jetable

Lister n'est pas extraire. Les droits, les liens symboliques, les chemins inattendus ne se manifestent qu'à l'extraction.

essai=$(mktemp -d)
tar -xzf /backups/app_data_20260918_010000.tar.gz -C "${essai}"

Prenez le temps que ça a pris, et la place que ça a occupée. C'est votre durée de restauration réelle et votre besoin d'espace réel, les seuls chiffres à comparer à ce que vous avez promis.

Ce qu'on vérifie après extraction

find "${essai}" -type f | wc -l
du -sb "${essai}"
test -s "${essai}/config/app.yaml"
grep -q "database_url" "${essai}/config/app.yaml"
rm -rf "${essai}"

Les deux premières lignes donnent le volume : combien de fichiers, combien d'octets. Comparez à l'ordre de grandeur de la veille, pas à un chiffre exact. C'est ce qui attrape la sauvegarde qui rétrécit en silence — celle qui contient encore quarante fichiers au lieu de douze mille parce qu'un point de montage n'était pas monté au moment du tar.

Mais un comptage ne suffit pas, et il faut voir précisément pourquoi. Le nombre de fichiers et la taille totale sont des agrégats : ils ne savent pas de quels fichiers ils parlent. Une archive prise sur le mauvais répertoire, une arborescence où les données ont été remplacées par des fichiers de cache, un fichier de configuration tronqué à zéro octet au milieu de douze mille fichiers intacts — tout cela passe un seuil de volume sans broncher.

D'où les deux dernières lignes : les fichiers témoins. Un chemin que vous nommez, dont vous savez qu'il doit exister après l'extraction, et dont vous vérifiez qu'il contient quelque chose que vous reconnaissez. test -s refuse un fichier vide, grep -q refuse un fichier au bon nom dont le contenu n'est pas le bon. Choisissez-en deux ou trois : le fichier de configuration de l'application, un fichier de données au fond de l'arborescence, et le fichier le plus récemment écrit par la production.

Automatiser cette vérification

Ce qui précède coûte une heure, à chaque fois. C'est pour cette raison que ces vérifications, faites à la main, finissent par ne plus être faites.

RestoreProof rejoue exactement ces commandes en tâche planifiée, sur votre infrastructure : un runner récupère l'archive, la déplie dans un espace de travail jetable, pose les mêmes questions, détruit tout, et signe le résultat. Les données ne sortent pas de chez vous.

Déclarez d'abord l'archive comme source. Un partage NAS monté sur la machine du runner se déclare comme un dossier local : le chemin tel que le runner le voit, le motif app_data_*.tar.gz, et la stratégie le plus récemment modifié. Pour S3, ce sont le bucket et le préfixe ; les clés d'accès ne se saisissent pas, le plan porte une référence, env://AWS_ACCESS_KEY_ID, que le runner résout dans son propre environnement. Voir adapter le bloc fetch et les références de secrets.

Le modèle Archive Files écrit ensuite le plan, et ce plan est la manipulation que vous venez de faire à la main, ligne pour ligne :

À la main Dans le plan
cp depuis le NAS, ou aws s3 cp fetch, qui prend l'archive la plus récente
tar -xzf dans un répertoire jetable unpack, avec le format auto
find \| wc -l RESTOREPROOF_CANARY_MIN_FILES
du -sb RESTOREPROOF_CANARY_MIN_TOTAL_SIZE
test -s et grep -q RESTOREPROOF_CANARY_FILES, avec min_size et contains
sha256sum d'un fichier témoin la clé expected_hash du même fichier témoin
rm -rf du répertoire d'essai le nettoyage, toujours exécuté

Le seul ajout est max_age: 26h, et c'est celui qu'une extraction ne peut pas déduire du contenu : il fait échouer l'exécution quand l'archive la plus récente trouvée à la source est plus vieille que ce délai. Une sauvegarde qui a cessé d'être produite il y a trois mois se déplie parfaitement. Le plan complet, prêt à coller dans l'éditeur, est dans la recette fichiers et archives.

Les deux sondes du catalogue répondent à deux questions différentes, et le choix entre elles dépend de l'étape unpack :

  • filesystem-canary regarde l'arborescence sortie de l'extraction. C'est elle qui porte les planchers de volume et les fichiers témoins, et c'est le seul moyen de prouver qu'un chemin nommé existe et contient ce qu'il doit contenir. Elle a besoin d'une étape unpack avant elle. Sans aucun seuil ni chemin nommé, elle échoue exprès plutôt que de signer un vert vide.
  • archive-integrity ouvre l'archive de bout en bout sans rien restaurer, ce que fait gzip -t. Elle n'a de sens que dans un plan sans étape unpack : dès qu'un plan déplie, l'extraction a déjà tout parcouru et une archive corrompue a déjà fait échouer l'étape. Son usage, c'est de confirmer qu'une archive reste ouvrable sans payer l'espace disque d'une extraction complète. Voir vérifier une archive sans la décompresser.

Les seuils ne se recopient pas depuis cette page. Un essai déplie votre archive, compte les fichiers et les octets qu'elle contient réellement, et propose chaque plancher 5 % sous la valeur mesurée, avec l'opérateur gte.

L'écran de fin d'essai : chaque sonde avec la valeur mesurée et le plancher proposé cinq pour cent en dessous

Ce que le comptage inclut

Le décompte de filesystem-canary porte sur tout l'espace de travail, y compris l'archive téléchargée qui s'y trouve encore, et les dossiers ne comptent pas comme des fichiers. Les chiffres proposés par l'essai en tiennent compte ; un chiffre compté à la main sur votre serveur, non.

Il reste à choisir une fréquence. Chaque nuit place l'exécution à 2 h du matin : votre script de sauvegarde tourne à 1 h, l'archive a donc une heure quand elle est éprouvée. L'autre déclenchement possible est un appel HTTP à la fin de ce script — la vérification porte alors exactement sur l'archive qui vient d'être produite.

Chaque exécution laisse un rapport horodaté, signé, qui nomme l'archive éprouvée et ce que chaque sonde a mesuré.

Ce que cette chaîne ne prouve pas

Elle prouve qu'une archive récente s'ouvre, se déplie entièrement, et que l'arborescence obtenue contient les fichiers que vous avez nommés avec le contenu que vous attendez. Elle ne prouve pas que votre application fonctionne sur ces fichiers : pour cela, il faut la démarrer contre l'arborescence restaurée et ajouter une sonde http. Elle ne dit rien des droits et des propriétaires d'origine, que tar n'enregistre que si vous le lui avez demandé et que l'extraction ne restitue que si elle tourne en root. Et elle ne dit rien de ce qui a été écrit depuis la dernière archive — cet écart, c'est votre RPO, et il se règle avec la fréquence des sauvegardes, pas avec les vérifications.

Le plan complet

À coller dans l'éditeur de plan, vue JSON (expert), en remplaçant l'identifiant de source et les chemins témoins par les vôtres.

{
  "steps": [
    {
      "type": "fetch",
      "name": "Récupérer l'archive la plus récente",
      "config": {
        "source": "source_profile",
        "source_id": "<identifiant de votre source>",
        "max_age": "26h",
        "output": "app_data.tar.gz"
      }
    },
    {
      "type": "unpack",
      "name": "Déplier l'archive",
      "config": {
        "format": "auto",
        "input": "app_data.tar.gz",
        "output": "app_data"
      }
    }
  ],
  "probes": [
    {
      "name": "fichiers_temoins",
      "type": "filesystem-canary",
      "timeout": 60,
      "config": {
        "RESTOREPROOF_CANARY_FILES": "[{\"path\":\"app_data/config/app.yaml\",\"contains\":\"database_url\",\"min_size\":100},{\"path\":\"app_data/data\"}]",
        "RESTOREPROOF_CANARY_MIN_FILES": "1",
        "RESTOREPROOF_CANARY_MIN_TOTAL_SIZE": "1024"
      }
    }
  ]
}

Les valeurs 1 et 1024 sont des planchers de démonstration, choisis pour ne jamais passer au rouge à tort. Remplacez-les par ceux que l'essai propose.

Et ensuite