Paillasse/docs/runbooks/sauvegardes.md
Adam Belkacemi b054888f5a sauvegarde les bases d'OpenStack et teste leur restauration chaque semaine
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:01:50 +02:00

8.9 KiB

Runbook — Sauvegardes vers Dedibackup

Principe

Chaque jour à 03:15 (±5 min aléatoires), le timer systemd sauvegarde-dedibackup.timer déclenche sauvegarde-dedibackup.service, qui exécute /usr/local/sbin/sauvegarde-dedibackup.sh (déployé par le rôle Ansible sauvegardes, voir ansible/roles/sauvegardes/).

Le script :

  1. Archive /etc, /home, /root en tar.zst (streaming, sans fichier intermédiaire en clair), plus, depuis l'étape 13, le portail : export mariadb-dump --single-transaction de sa base (portail.sql, à la racine de l'archive), /var/www/portail/.env (APP_KEY) et /var/www/portail/storage/app. Avant le 2026-10-09, la base du portail n'était pas sauvegardée. Depuis le 2026-10-12, les bases d'OpenStack aussi : export mariadb-dump --single-transaction de toutes les bases de Kolla sauf mysql (openstack.sql, ≈ 30 Mo avant compression ; mot de passe lu dans /etc/kolla/passwords.yml, passé par l'entrée standard).
  2. Chiffre le flux avec age, avec la clé publique du serveur (/root/.age/cle-sauvegardes.txt).
  3. Envoie l'archive chiffrée en FTP vers Dedibackup (dedibackup-dc3.online.net, joignable uniquement depuis ce serveur).
  4. Le dimanche (jour 7), duplique aussi l'archive du jour comme hebdomadaire.
  5. Applique la rotation : garde les 7 quotidiennes et 4 hebdomadaires les plus récentes, supprime le reste sur Dedibackup (qui est limité à 1000 fichiers).

Le FTP de Dedibackup n'est pas chiffré en transport, mais le contenu de l'archive l'est déjà (age) avant l'envoi : une interception du transfert ne révèle rien.

Où est la clé de déchiffrement

  • Clé privée age : /root/.age/cle-sauvegardes.txt, permissions 600. Elle ne quitte le serveur que pour la copie de secours d'Adam (ci-dessous), jamais vers Git ni ailleurs.
  • Clé publique (sans risque à diffuser) : voir la sortie de age-keygen lors du déploiement, ou :
    sudo grep 'public key' /root/.age/cle-sauvegardes.txt
    

⚠️ Rappel pour Adam (pas fait automatiquement par Claude) : garder une copie de /root/.age/cle-sauvegardes.txt en dehors du serveur (gestionnaire de mots de passe, clé USB chiffrée, etc.). Si le serveur est perdu, cette clé est la seule façon de déchiffrer les sauvegardes existantes. Exemple pour la récupérer (à lancer depuis le Mac, le fichier arrive en clair sur le Mac donc à stocker immédiatement dans un endroit sûr puis à effacer du téléchargement) :

# La clé appartient à root : un simple scp échouerait. La redirection écrit
# le fichier sur le Mac sans jamais l'afficher.
ssh paillasse 'sudo cat /root/.age/cle-sauvegardes.txt' > ~/Desktop/cle-sauvegardes-syllabus.txt

Puis la ranger dans le gestionnaire de mots de passe (pièce jointe d'une fiche « Syllabus — clé des sauvegardes ») ou sur une clé USB chiffrée, et supprimer la copie du Bureau.

Prouver que la copie fonctionne (sans montrer la clé)

Sur le serveur, chiffrer un message de test avec la clé publique ; sur le Mac, le déchiffrer avec la copie (installer age : brew install age) :

ssh paillasse 'sudo bash -c "PUB=\$(grep -oP \"(?<=public key: )age1\\S+\" /root/.age/cle-sauvegardes.txt); echo \"copie OK \$(date +%F)\" | age -r \$PUB"' > ~/Desktop/test-cle.age
age -d -i <chemin de la copie> ~/Desktop/test-cle.age     # doit afficher « copie OK … »

Identifiants Dedibackup

Dans /root/.dedibackup (permissions 600, jamais dans Git) : DEDIBACKUP_HOST, DEDIBACKUP_USER, DEDIBACKUP_PASS — valeurs entre guillemets simples si elles contiennent des caractères spéciaux (#, $, espace…), sinon bash les tronque silencieusement en les lisant.

Lancer une sauvegarde manuellement

ssh paillasse
sudo systemctl start sauvegarde-dedibackup.service
sudo journalctl -u sauvegarde-dedibackup.service --no-pager | tail -20

Lister les sauvegardes présentes sur Dedibackup

sudo bash -c '
source /root/.dedibackup
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
  -e "set ftp:ssl-allow no; cls -1 *.tar.zst.age; quit"
'

Test de restauration du portail et d'OpenStack (automatique, chaque lundi 04:30)

/usr/local/sbin/syllabus-test-restauration (rôle sauvegardes, minuteur syllabus-test-restauration.timer) télécharge la dernière quotidienne, la déchiffre, restaure la base dans un conteneur MariaDB jetable (réseau portail_default, aucun port publié), fait tourner le code du portail contre cette copie (migrate:status, comptage), compare chaque table à la production, puis publie syllabus_derniere_restauration_reussie_timestamp. Alerte RestaurationNonTestee au-delà de 8 jours. Rien n'est écrit en production ; le conteneur et les fichiers temporaires sont supprimés à la fin.

ssh paillasse "sudo /usr/local/sbin/syllabus-test-restauration"     # à la demande
ssh paillasse "journalctl -u syllabus-test-restauration --no-pager | tail -40"

Des écarts de comptage sont normaux si la production a changé depuis la sauvegarde ; une table absente de la copie fait échouer le test.

Étape 6 du test (depuis le 2026-10-12) : openstack.sql est restauré dans un second conteneur jetable (mariadb:11.4, même version que Kolla). Le test échoue si une base ou un nombre de tables diffère de la production, ou si les tables stables (keystone.project, keystone.user, glance.images, nova_api.flavors) n'ont pas le même nombre de lignes.

Restaurer réellement le portail (sinistre)

  1. Extraire l'archive dans un dossier temporaire (voir plus bas), puis portail.sql et var/www/portail/.env.
  2. Redéployer le portail (ansible-playbook portail.yml) sur le serveur reconstruit ; remettre le .env restauré avant (APP_KEY identique, sinon sessions et données chiffrées perdues).
  3. Importer la base : docker exec -i portail_mariadb sh -c 'MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -uroot' < portail.sql (écrase la base portail).
  4. php artisan migrate --force, puis vérifier une connexion prof.

Restaurer réellement OpenStack (sinistre)

Procédure écrite, jamais jouée en vrai (il faudrait un second serveur) ; seule la restauration des bases dans un conteneur est testée chaque semaine.

  1. Serveur reconstruit : playbooks durcissement, reseau-openstack, puis remettre /etc/kolla/ depuis l'archive (passwords.yml identique, sinon les mots de passe en base ne correspondent plus) avant kolla.yml.
  2. kolla-ansible deploy crée des bases vides et les comptes MariaDB.
  3. Arrêter les services qui écrivent (docker stop des conteneurs nova_*, neutron_*, heat_*, glance_api, keystone*, placement_api), puis importer (le mot de passe ne passe ni en argument ni à l'écran) :
    sudo docker cp openstack.sql mariadb:/tmp/openstack.sql
    printf '[client]\nuser=root\npassword="%s"\nhost=10.10.0.1\n' \
      "$(sudo awk '/^database_password:/{print $2}' /etc/kolla/passwords.yml)" \
      | sudo docker exec -i mariadb sh -c 'umask 077; cat > /tmp/r.cnf;
          mariadb --defaults-extra-file=/tmp/r.cnf < /tmp/openstack.sql;
          rm -f /tmp/r.cnf /tmp/openstack.sql'
    
    Redémarrer les conteneurs arrêtés.
  4. Les fichiers des images Glance ne sont pas sauvegardés (seulement leur fiche) : reconstruire paillasse-debian (docs/runbooks/images.md) et supprimer l'ancienne fiche.
  5. Les VM d'élèves n'existent plus sur le nouvel hôte : supprimer toutes les stacks restantes (openstack stack list, puis stack delete) ; le stock chaud se reconstitue seul.

Restaurer un fichier (procédure testée le 2026-10-07)

ssh paillasse
sudo bash -c '
set -e
source /root/.dedibackup
RESTORE_DIR=/tmp/restauration-$(date +%Y%m%d-%H%M)
mkdir -p "$RESTORE_DIR"

# 1. Télécharger l'archive voulue depuis Dedibackup
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" \
  -e "set ftp:ssl-allow no; get quotidienne-AAAAMMJJ.tar.zst.age -o $RESTORE_DIR/archive.tar.zst.age; quit"

# 2. Déchiffrer puis extraire (tout le contenu, ou un chemin précis en argument de tar)
age -d -i /root/.age/cle-sauvegardes.txt "$RESTORE_DIR/archive.tar.zst.age" \
  | tar -I zstd -xf - -C "$RESTORE_DIR" etc/un/fichier/precis

ls -la "$RESTORE_DIR"
'

Pour tout restaurer (sans préciser de chemin après -xf -), ne jamais le faire directement vers /etc, /home ou /root sans réfléchir : ça écraserait l'état actuel. Toujours restaurer vers un répertoire temporaire (/tmp/...) puis copier à la main ce qui est nécessaire.

Les chemins dans l'archive n'ont pas de / ni de ./ au début (ex. etc/hostname, pas /etc/hostname ni ./etc/hostname) — tar les a retirés à la création.

Vérifié le 2026-10-07

  • Sauvegarde envoyée : quotidienne-20261007.tar.zst.age, confirmée présente sur Dedibackup via cls.
  • Restauration testée : etc/hostname extrait dans /tmp/test-restauration-20261007/, contenu conforme (sd-124443).