# 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. 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 ~/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 (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. ## 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 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`).