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:
parent
0a0563c226
commit
7476a54f22
12 changed files with 253 additions and 0 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
|
@ -3,6 +3,19 @@
|
||||||
## 2026-07-03
|
## 2026-07-03
|
||||||
|
|
||||||
### Ajouté
|
### 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.**
|
- **Consolidation — binding `annuaire` (`resoudre_annuaire`) + retrait de la cruft.**
|
||||||
*Cruft* : supprimés les 8 dossiers-catégories inertes (`roles/{applications,backup,database,
|
*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`
|
identity,monitoring,proxmox,storage,web}/`, README seuls) et les 5 playbooks-échafaudages `debug`
|
||||||
|
|
|
||||||
7
playbooks/groupes/client_backup.yml
Normal file
7
playbooks/groupes/client_backup.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
- name: Appliquer l'intégration client_backup
|
||||||
|
hosts: client_backup
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- client_backup
|
||||||
7
playbooks/groupes/serveur_backup.yml
Normal file
7
playbooks/groupes/serveur_backup.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
- name: Appliquer le groupe serveur_backup
|
||||||
|
hosts: serveur_backup
|
||||||
|
become: true
|
||||||
|
gather_facts: true
|
||||||
|
roles:
|
||||||
|
- serveur_backup
|
||||||
30
roles/client_backup/defaults/main.yml
Normal file
30
roles/client_backup/defaults/main.yml
Normal 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: []
|
||||||
6
roles/client_backup/meta/empreinte.yml
Normal file
6
roles/client_backup/meta/empreinte.yml
Normal 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
|
||||||
97
roles/client_backup/tasks/main.yml
Normal file
97
roles/client_backup/tasks/main.yml
Normal 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
|
||||||
30
roles/client_backup/templates/sauvegarder.sh.j2
Normal file
30
roles/client_backup/templates/sauvegarder.sh.j2
Normal 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
|
||||||
11
roles/client_backup/templates/setops-sauvegarde.service.j2
Normal file
11
roles/client_backup/templates/setops-sauvegarde.service.j2
Normal 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
|
||||||
11
roles/client_backup/templates/setops-sauvegarde.timer.j2
Normal file
11
roles/client_backup/templates/setops-sauvegarde.timer.j2
Normal 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
|
||||||
7
roles/serveur_backup/defaults/main.yml
Normal file
7
roles/serveur_backup/defaults/main.yml
Normal 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: ""
|
||||||
6
roles/serveur_backup/meta/empreinte.yml
Normal file
6
roles/serveur_backup/meta/empreinte.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
# Empreinte ressources — cible de sauvegarde (surtout du disque).
|
||||||
|
setops_empreinte:
|
||||||
|
coeurs: 1
|
||||||
|
memoire_mo: 512
|
||||||
|
disque_go: 64
|
||||||
28
roles/serveur_backup/tasks/main.yml
Normal file
28
roles/serveur_backup/tasks/main.yml
Normal 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
|
||||||
Loading…
Reference in a new issue