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
|
||||
|
||||
### 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`
|
||||
|
|
|
|||
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