ajoute les sauvegardes chiffrées vers Dedibackup (rôle Ansible, timer quotidien, rotation)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
f5a5e8d8a9
commit
e879a214a9
9 changed files with 328 additions and 0 deletions
13
ansible/roles/sauvegardes/defaults/main.yml
Normal file
13
ansible/roles/sauvegardes/defaults/main.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
sauvegardes_retention_quotidiennes: 7
|
||||||
|
sauvegardes_retention_hebdomadaires: 4
|
||||||
|
# Jour de la semaine (1=lundi .. 7=dimanche, format `date +%u`) où la quotidienne
|
||||||
|
# du jour est aussi conservée comme hebdomadaire.
|
||||||
|
sauvegardes_jour_hebdomadaire: "7"
|
||||||
|
sauvegardes_heure_timer: "03:15"
|
||||||
|
sauvegardes_repertoires:
|
||||||
|
- /etc
|
||||||
|
- /home
|
||||||
|
- /root
|
||||||
|
sauvegardes_age_dir: /root/.age
|
||||||
|
sauvegardes_age_cle: /root/.age/cle-sauvegardes.txt
|
||||||
4
ansible/roles/sauvegardes/handlers/main.yml
Normal file
4
ansible/roles/sauvegardes/handlers/main.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
- name: recharger systemd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
70
ansible/roles/sauvegardes/tasks/main.yml
Normal file
70
ansible/roles/sauvegardes/tasks/main.yml
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
---
|
||||||
|
- name: installer age et lftp
|
||||||
|
ansible.builtin.apt:
|
||||||
|
name:
|
||||||
|
- age
|
||||||
|
- lftp
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
cache_valid_time: 3600
|
||||||
|
|
||||||
|
- name: créer le répertoire de la clé age
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ sauvegardes_age_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: générer la paire de clés age (si absente)
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "age-keygen -o {{ sauvegardes_age_cle }}"
|
||||||
|
creates: "{{ sauvegardes_age_cle }}"
|
||||||
|
|
||||||
|
- name: restreindre les permissions de la clé privée age
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ sauvegardes_age_cle }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
|
||||||
|
- name: lire la clé publique age (pour affichage, jamais la clé privée)
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: "grep -oP '(?<=public key: )age1\\S+' {{ sauvegardes_age_cle }}"
|
||||||
|
register: sauvegardes_age_pub
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: déployer le script de sauvegarde
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sauvegarde-dedibackup.sh.j2
|
||||||
|
dest: /usr/local/sbin/sauvegarde-dedibackup.sh
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0700"
|
||||||
|
|
||||||
|
- name: déployer le service systemd
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sauvegarde-dedibackup.service.j2
|
||||||
|
dest: /etc/systemd/system/sauvegarde-dedibackup.service
|
||||||
|
mode: "0644"
|
||||||
|
notify: recharger systemd
|
||||||
|
|
||||||
|
- name: déployer le timer systemd
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sauvegarde-dedibackup.timer.j2
|
||||||
|
dest: /etc/systemd/system/sauvegarde-dedibackup.timer
|
||||||
|
mode: "0644"
|
||||||
|
notify: recharger systemd
|
||||||
|
|
||||||
|
- name: appliquer les changements systemd
|
||||||
|
ansible.builtin.meta: flush_handlers
|
||||||
|
|
||||||
|
- name: activer et démarrer le timer de sauvegarde
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sauvegarde-dedibackup.timer
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
- name: afficher la clé publique age (à connaître, jamais la privée)
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Clé publique age du serveur : {{ sauvegardes_age_pub.stdout }}"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Sauvegarde quotidienne chiffrée vers Dedibackup
|
||||||
|
# Géré par Ansible (rôle sauvegardes) — ne pas modifier à la main.
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/sauvegarde-dedibackup.sh
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Géré par Ansible (rôle sauvegardes) — ne pas modifier à la main.
|
||||||
|
#
|
||||||
|
# Archive {{ sauvegardes_repertoires | join(', ') }} en tar.zst, chiffre avec age
|
||||||
|
# (clé publique du serveur — la clé privée ne quitte jamais le serveur, voir
|
||||||
|
# {{ sauvegardes_age_cle }}), envoie vers Dedibackup en FTP, puis applique la
|
||||||
|
# rotation ({{ sauvegardes_retention_quotidiennes }} quotidiennes,
|
||||||
|
# {{ sauvegardes_retention_hebdomadaires }} hebdomadaires).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Les identifiants ne sont jamais affichés ni journalisés : on désactive
|
||||||
|
# l'option xtrace pour cette ligne précise, par précaution.
|
||||||
|
set +x
|
||||||
|
source /root/.dedibackup
|
||||||
|
|
||||||
|
AGE_KEY="{{ sauvegardes_age_cle }}"
|
||||||
|
AGE_PUB=$(grep -oP '(?<=public key: )age1\S+' "$AGE_KEY")
|
||||||
|
|
||||||
|
DATE=$(date +%Y%m%d)
|
||||||
|
DOW=$(date +%u)
|
||||||
|
SEMAINE=$(date +%G-W%V)
|
||||||
|
|
||||||
|
WORKDIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORKDIR"' EXIT
|
||||||
|
|
||||||
|
ARCHIVE_QUOT="quotidienne-${DATE}.tar.zst.age"
|
||||||
|
|
||||||
|
tar --exclude="{{ sauvegardes_age_dir }}" --exclude='/root/.dedibackup' \
|
||||||
|
-I zstd -cf - {{ sauvegardes_repertoires | join(' ') }} \
|
||||||
|
| age -r "$AGE_PUB" -o "$WORKDIR/$ARCHIVE_QUOT"
|
||||||
|
|
||||||
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" -e "
|
||||||
|
set ftp:ssl-allow no
|
||||||
|
put $WORKDIR/$ARCHIVE_QUOT
|
||||||
|
quit
|
||||||
|
"
|
||||||
|
logger -t sauvegarde-dedibackup "Quotidienne envoyée : $ARCHIVE_QUOT"
|
||||||
|
|
||||||
|
if [ "$DOW" = "{{ sauvegardes_jour_hebdomadaire }}" ]; then
|
||||||
|
ARCHIVE_HEBDO="hebdomadaire-${SEMAINE}.tar.zst.age"
|
||||||
|
cp "$WORKDIR/$ARCHIVE_QUOT" "$WORKDIR/$ARCHIVE_HEBDO"
|
||||||
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" -e "
|
||||||
|
set ftp:ssl-allow no
|
||||||
|
put $WORKDIR/$ARCHIVE_HEBDO
|
||||||
|
quit
|
||||||
|
"
|
||||||
|
logger -t sauvegarde-dedibackup "Hebdomadaire envoyée : $ARCHIVE_HEBDO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Rotation : on ne garde que les N plus récentes de chaque catégorie. ---
|
||||||
|
rotation() {
|
||||||
|
local motif="$1" garder="$2"
|
||||||
|
local liste a_supprimer
|
||||||
|
liste=$(lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" -e "
|
||||||
|
set ftp:ssl-allow no
|
||||||
|
cls -1 ${motif}
|
||||||
|
quit
|
||||||
|
" 2>/dev/null | sort)
|
||||||
|
a_supprimer=$(echo "$liste" | head -n -"$garder")
|
||||||
|
if [ -n "$a_supprimer" ]; then
|
||||||
|
local commandes=""
|
||||||
|
while IFS= read -r fichier; do
|
||||||
|
[ -n "$fichier" ] && commandes+="rm $fichier;"
|
||||||
|
done <<< "$a_supprimer"
|
||||||
|
lftp -u "$DEDIBACKUP_USER,$DEDIBACKUP_PASS" "$DEDIBACKUP_HOST" -e "
|
||||||
|
set ftp:ssl-allow no
|
||||||
|
$commandes
|
||||||
|
quit
|
||||||
|
"
|
||||||
|
logger -t sauvegarde-dedibackup "Rotation $motif : supprimé $(echo "$a_supprimer" | wc -l) fichier(s)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
rotation "quotidienne-*.tar.zst.age" {{ sauvegardes_retention_quotidiennes }}
|
||||||
|
rotation "hebdomadaire-*.tar.zst.age" {{ sauvegardes_retention_hebdomadaires }}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Déclenche la sauvegarde quotidienne vers Dedibackup
|
||||||
|
# Géré par Ansible (rôle sauvegardes) — ne pas modifier à la main.
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* {{ sauvegardes_heure_timer }}:00
|
||||||
|
Persistent=true
|
||||||
|
RandomizedDelaySec=300
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
8
ansible/sauvegardes.yml
Normal file
8
ansible/sauvegardes.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
# Étape 3 du déploiement Syllabus : sauvegardes chiffrées vers Dedibackup.
|
||||||
|
- name: Sauvegardes du serveur Syllabus
|
||||||
|
hosts: syllabus
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- sauvegardes
|
||||||
|
|
@ -66,3 +66,42 @@ Durcir un serveur, c'est fondamentalement risqué pour l'accès qu'on a déjà :
|
||||||
**Suite**
|
**Suite**
|
||||||
|
|
||||||
Étape 3 : sauvegardes vers Dedibackup (archives `tar.zst` chiffrées `age`, rotation 7 quotidiennes + 4 hebdomadaires, timer systemd).
|
Étape 3 : sauvegardes vers Dedibackup (archives `tar.zst` chiffrées `age`, rotation 7 quotidiennes + 4 hebdomadaires, timer systemd).
|
||||||
|
|
||||||
|
## 2026-10-07 — Étape 3 : sauvegardes vers Dedibackup
|
||||||
|
|
||||||
|
**Fait**
|
||||||
|
|
||||||
|
- Rôle Ansible `sauvegardes` (`ansible/roles/sauvegardes/`) + playbook `ansible/sauvegardes.yml`. Pas de risque réseau/pare-feu/SSH ici, donc appliqué directement sans attendre d'accord explicite (règle 3).
|
||||||
|
- Paire de clés **age** générée sur le serveur (`/root/.age/cle-sauvegardes.txt`, `600`) : la clé privée ne quitte jamais le serveur.
|
||||||
|
- Script `/usr/local/sbin/sauvegarde-dedibackup.sh` : archive `/etc`, `/home`, `/root` en `tar.zst`, chiffre en flux avec `age`, envoie en FTP vers Dedibackup avec `lftp`, duplique en hebdomadaire le dimanche, applique la rotation (7 quotidiennes + 4 hebdomadaires, suppression du surplus sur Dedibackup).
|
||||||
|
- Timer systemd `sauvegarde-dedibackup.timer` : tous les jours à 03:15 (±5 min).
|
||||||
|
- `docs/runbooks/sauvegardes.md` écrit (déclenchement manuel, liste, restauration, rappel sur la clé age).
|
||||||
|
|
||||||
|
**Commandes clés**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ansible && script -q /dev/null ansible-playbook sauvegardes.yml -i inventory.ini
|
||||||
|
ssh paillasse "sudo systemctl start sauvegarde-dedibackup.service"
|
||||||
|
# restauration testée : voir docs/runbooks/sauvegardes.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problèmes rencontrés et solution**
|
||||||
|
|
||||||
|
- 1er envoi FTP : `530 Bad credentials`. Diagnostic sans jamais afficher les identifiants (longueur et forme des variables seulement). Cause probable : valeurs non protégées par des guillemets dans `/root/.dedibackup`, risque de troncature silencieuse par `bash` si le mot de passe contient `#`, `$` ou une espace. **Adam a corrigé le fichier lui-même** (jamais Claude, le fichier contient un secret). Après correction, l'envoi a réussi.
|
||||||
|
- Extraction de test avec un chemin `./etc/...` : échec (`Not found in archive`), car `tar` retire le `/` initial à la création (`etc/...`, sans `./`). Corrigé dans le runbook.
|
||||||
|
- `etc/os-release` n'a pas pu être lu après extraction : c'est un lien symbolique vers `../usr/lib/os-release`, hors du périmètre sauvegardé (`/etc`, `/home`, `/root` seulement) — comportement normal, pas un bug.
|
||||||
|
|
||||||
|
**Critère vérifié**
|
||||||
|
|
||||||
|
- Sauvegarde envoyée : `quotidienne-20261007.tar.zst.age`, confirmée présente sur Dedibackup (`lftp cls`).
|
||||||
|
- Fichier restauré avec succès dans `/tmp` : `etc/hostname` extrait et relu, contenu conforme (`sd-124443`). Dossier de test nettoyé après coup.
|
||||||
|
|
||||||
|
**Comprendre**
|
||||||
|
|
||||||
|
Chiffrer avant d'envoyer (plutôt que de compter sur un transport chiffré) a un avantage : Dedibackup n'a accès qu'à un FTP en clair, mais comme le contenu est déjà chiffré par `age` avant de partir, intercepter le transfert ne révèle rien d'utile. La contrepartie, c'est qu'il ne faut jamais perdre la clé privée `age` : sans elle, toutes les sauvegardes passées deviennent illisibles pour toujours, même si elles sont bien là sur Dedibackup. D'où le rappel à Adam de garder une copie de cette clé ailleurs que sur le serveur.
|
||||||
|
|
||||||
|
**⚠️ Reste à faire par Adam** : copier `/root/.age/cle-sauvegardes.txt` en dehors du serveur (gestionnaire de mots de passe, clé USB chiffrée…) — voir `docs/runbooks/sauvegardes.md`.
|
||||||
|
|
||||||
|
**Suite**
|
||||||
|
|
||||||
|
Étape 4 : réseau pour OpenStack (`dummy0`, paire veth, NAT). Touche le réseau → accord explicite d'Adam obligatoire avant d'appliquer, filet anti-coupure, et redémarrage du serveur seulement avec son accord.
|
||||||
|
|
|
||||||
101
docs/runbooks/sauvegardes.md
Normal file
101
docs/runbooks/sauvegardes.md
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 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).
|
||||||
|
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`, **ne quitte jamais le serveur**.
|
||||||
|
- 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) :
|
||||||
|
```
|
||||||
|
scp paillasse:/root/.age/cle-sauvegardes.txt ~/Desktop/
|
||||||
|
# puis la mettre immédiatement dans un gestionnaire de mots de passe,
|
||||||
|
# et supprimer la copie en clair sur le Desktop.
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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`).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue