Sauvegardes applicatives (restic) : serveur_backup + client_backup — Tier 0 prouvé

Choix : sauvegarder la donnée d'état (non régénérable), pas les VM
(reconstructibles par le code). restic (chiffré, dédupliqué, rétention).
serveur_backup = cible SFTP (backup-01). client_backup = intégration par
nœud : restic + jobs déclaratifs + timer systemd + rétention, secrets voûte.

Prouvé de bout en bout sur le Tier 0 : infra-pki-01 → /etc/step-ca sauvegardé
hors-nœud vers backup-01, restauration byte-identique des clés CA.

Reste : jobs Tier 1 (pg_dump/slapcat/vmail/forgejo) + offsite (3-2-1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-03 22:49:26 -04:00
parent 0a0563c226
commit 7476a54f22
12 changed files with 253 additions and 0 deletions

View file

@ -3,6 +3,19 @@
## 2026-07-03
### Ajouté
- **Sauvegardes applicatives (logiques) — `serveur_backup` + `client_backup` (restic), Tier 0 prouvé.**
Choix : sauvegarder la **donnée d'état** (non régénérable) plutôt que les VM (reconstructibles par
le code + le template). Outil **restic** (chiffrement côté client, déduplication, rétention).
`serveur_backup` (nœud `backup-01`) = cible SFTP/SSH (utilisateur `restic`, clé autorisée, dépôts
sous `/srv/restic/<nœud>`). `client_backup` (intégration par nœud) = restic + **jobs déclaratifs**
(`client_backup_jobs` : `{nom, commande?, chemins}`), clé SSH + mot de passe restic en voûte,
script + **timer systemd** (quotidien) + rétention `forget --prune`. **Prouvé de bout en bout**
sur le **Tier 0** (`infra-pki-01` → `/etc/step-ca`, l'ancre de confiance) : sauvegarde **hors-nœud**
vers `backup-01`, puis **restauration byte-identique** des clés CA (`root_ca_key`,
`intermediate_ca_key`, `ca.json`). Piège corrigé : le plancher `/etc/hosts` d'un nœud existant
ignore un nœud nouvellement ajouté → rafraîchir le socle. Reste : généraliser aux Tier 1
(`pg_dump`, `slapcat`, `/var/vmail`, `forgejo dump` — juste des jobs à déclarer) + cible offsite
(3-2-1, Étape B).
- **Consolidation — binding `annuaire` (`resoudre_annuaire`) + retrait de la cruft.**
*Cruft* : supprimés les 8 dossiers-catégories inertes (`roles/{applications,backup,database,
identity,monitoring,proxmox,storage,web}/`, README seuls) et les 5 playbooks-échafaudages `debug`

View file

@ -0,0 +1,7 @@
---
- name: Appliquer l'intégration client_backup
hosts: client_backup
become: true
gather_facts: true
roles:
- client_backup

View file

@ -0,0 +1,7 @@
---
- name: Appliquer le groupe serveur_backup
hosts: serveur_backup
become: true
gather_facts: true
roles:
- serveur_backup

View file

@ -0,0 +1,30 @@
---
# Sauvegarde applicative (logique) d'un nœud vers la cible restic hors-nœud.
# Chaque nœud déclare ses "jobs" ; restic chiffre côté client + applique la rétention.
client_backup_paquets:
- restic
# Cible (nœud serveur_backup) + transport SSH.
client_backup_cible: "backup-01.{{ domaine_interne }}"
client_backup_utilisateur_distant: "restic"
client_backup_ssh_key: "/etc/setops/backup_ed25519"
# Dépôt restic (un sous-dossier par nœud, relatif au home de l'utilisateur distant).
client_backup_repo: "sftp:{{ client_backup_utilisateur_distant }}@{{ client_backup_cible }}:{{ inventory_hostname }}"
client_backup_password: "{{ vault_restic_password | default('') }}"
client_backup_ssh_privkey: "{{ vault_backup_ssh_privkey | default('') }}"
# Dossier de préparation des dumps (pg_dump, slapcat, forgejo dump...).
client_backup_staging: "/var/backups/setops"
# Rétention (restic forget).
client_backup_retention: "--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
# Planification (timer systemd).
client_backup_horaire: "*-*-* 02:30:00"
# Jobs déclaratifs. Chaque job :
# nom : identifiant
# commande : (optionnel) dump écrivant dans {{ staging }}/{{ nom }}
# chemins : liste de chemins à inclure dans le snapshot restic
# Défini par nœud (group_vars). Ex. step_ca : { nom: step_ca, chemins: [/etc/step-ca] }.
client_backup_jobs: []

View file

@ -0,0 +1,6 @@
---
# Empreinte ressources — intégration légère (restic + staging des dumps).
setops_empreinte:
coeurs: 0
memoire_mo: 64
disque_go: 2

View file

@ -0,0 +1,97 @@
---
- name: Exiger les secrets de sauvegarde (Vault)
ansible.builtin.assert:
that:
- client_backup_password | length > 0
- client_backup_ssh_privkey | length > 0
fail_msg: "vault_restic_password et vault_backup_ssh_privkey requis."
- name: Installer restic
ansible.builtin.apt:
name: "{{ client_backup_paquets }}"
state: present
update_cache: true
- name: Créer le répertoire des secrets Set-OPS
ansible.builtin.file:
path: /etc/setops
state: directory
owner: root
group: root
mode: "0700"
- name: Déposer la clé SSH privée de sauvegarde
ansible.builtin.copy:
content: "{{ client_backup_ssh_privkey }}"
dest: "{{ client_backup_ssh_key }}"
owner: root
group: root
mode: "0600"
no_log: true
- name: Déposer le mot de passe restic
ansible.builtin.copy:
content: "{{ client_backup_password }}\n"
dest: /etc/setops/restic.pass
owner: root
group: root
mode: "0600"
no_log: true
- name: Assurer /root/.ssh
ansible.builtin.file:
path: /root/.ssh
state: directory
owner: root
group: root
mode: "0700"
- name: Configurer l'accès SSH à la cible de sauvegarde
ansible.builtin.blockinfile:
path: /root/.ssh/config
create: true
owner: root
group: root
mode: "0600"
marker: "# {mark} SETOPS BACKUP {{ client_backup_cible }}"
block: |
Host {{ client_backup_cible }}
User {{ client_backup_utilisateur_distant }}
IdentityFile {{ client_backup_ssh_key }}
IdentitiesOnly yes
StrictHostKeyChecking accept-new
- name: Assurer le répertoire de préparation des dumps
ansible.builtin.file:
path: "{{ client_backup_staging }}"
state: directory
owner: root
group: root
mode: "0700"
- name: Déployer le script de sauvegarde
ansible.builtin.template:
src: sauvegarder.sh.j2
dest: /usr/local/sbin/setops-sauvegarder.sh
owner: root
group: root
mode: "0700"
- name: Déployer l'unité et le timer systemd
ansible.builtin.template:
src: "{{ item.s }}"
dest: "/etc/systemd/system/{{ item.d }}"
owner: root
group: root
mode: "0644"
loop:
- { s: setops-sauvegarde.service.j2, d: setops-sauvegarde.service }
- { s: setops-sauvegarde.timer.j2, d: setops-sauvegarde.timer }
- name: Activer le timer de sauvegarde
when: not ansible_check_mode
ansible.builtin.systemd:
name: setops-sauvegarde.timer
enabled: true
state: started
daemon_reload: true

View file

@ -0,0 +1,30 @@
#!/bin/bash
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
# Sauvegarde applicative du nœud -> dépôt restic hors-nœud (chiffré, dédupliqué).
set -euo pipefail
export RESTIC_REPOSITORY="{{ client_backup_repo }}"
export RESTIC_PASSWORD_FILE="/etc/setops/restic.pass"
# 1. Dumps applicatifs (pg_dump, slapcat, forgejo dump...) vers le staging.
{% for job in client_backup_jobs %}
{% if job.commande is defined %}
install -d -m 0700 "{{ client_backup_staging }}/{{ job.nom }}"
{{ job.commande }}
{% endif %}
{% endfor %}
# 2. Initialiser le dépôt s'il n'existe pas encore.
restic snapshots >/dev/null 2>&1 || restic init
# 3. Snapshot de tous les chemins déclarés.
CHEMINS=(
{% for job in client_backup_jobs %}
{% for c in job.chemins %}
"{{ c }}"
{% endfor %}
{% endfor %}
)
restic backup --host "{{ inventory_hostname }}" --tag setops "${CHEMINS[@]}"
# 4. Rétention.
restic forget {{ client_backup_retention }} --prune

View file

@ -0,0 +1,11 @@
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
[Unit]
Description=Sauvegarde applicative Set-OPS (restic hors-nœud)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/setops-sauvegarder.sh
Nice=10
IOSchedulingClass=idle

View file

@ -0,0 +1,11 @@
# Géré par Set-OPS (rôle client_backup). Ne pas éditer à la main.
[Unit]
Description=Planification de la sauvegarde applicative Set-OPS
[Timer]
OnCalendar={{ client_backup_horaire }}
RandomizedDelaySec=900
Persistent=true
[Install]
WantedBy=timers.target

View file

@ -0,0 +1,7 @@
---
# Cible des sauvegardes : héberge les dépôts restic (un sous-dossier par nœud source).
# Les clients y accèdent en SFTP/SSH avec la clé de sauvegarde. Chiffrement côté client (restic).
serveur_backup_utilisateur: "restic"
serveur_backup_racine: "/srv/restic"
# Clé PUBLIQUE de sauvegarde autorisée (la privée est dans la voûte, côté client_backup).
serveur_backup_pubkey: ""

View file

@ -0,0 +1,6 @@
---
# Empreinte ressources — cible de sauvegarde (surtout du disque).
setops_empreinte:
coeurs: 1
memoire_mo: 512
disque_go: 64

View file

@ -0,0 +1,28 @@
---
- name: Exiger la clé publique de sauvegarde
ansible.builtin.assert:
that:
- serveur_backup_pubkey | length > 0
fail_msg: "serveur_backup_pubkey requis (clé publique de la paire de sauvegarde)."
- name: Créer l'utilisateur de sauvegarde
ansible.builtin.user:
name: "{{ serveur_backup_utilisateur }}"
home: "{{ serveur_backup_racine }}"
shell: /bin/bash
create_home: true
system: true
- name: Sécuriser la racine des dépôts
ansible.builtin.file:
path: "{{ serveur_backup_racine }}"
state: directory
owner: "{{ serveur_backup_utilisateur }}"
group: "{{ serveur_backup_utilisateur }}"
mode: "0700"
- name: Autoriser la clé SSH de sauvegarde
ansible.posix.authorized_key:
user: "{{ serveur_backup_utilisateur }}"
key: "{{ serveur_backup_pubkey }}"
state: present