diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d2731..b63c854 100644 --- a/CHANGELOG.md +++ b/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/`). `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` diff --git a/playbooks/groupes/client_backup.yml b/playbooks/groupes/client_backup.yml new file mode 100644 index 0000000..73a5b62 --- /dev/null +++ b/playbooks/groupes/client_backup.yml @@ -0,0 +1,7 @@ +--- +- name: Appliquer l'intégration client_backup + hosts: client_backup + become: true + gather_facts: true + roles: + - client_backup diff --git a/playbooks/groupes/serveur_backup.yml b/playbooks/groupes/serveur_backup.yml new file mode 100644 index 0000000..dcb05ab --- /dev/null +++ b/playbooks/groupes/serveur_backup.yml @@ -0,0 +1,7 @@ +--- +- name: Appliquer le groupe serveur_backup + hosts: serveur_backup + become: true + gather_facts: true + roles: + - serveur_backup diff --git a/roles/client_backup/defaults/main.yml b/roles/client_backup/defaults/main.yml new file mode 100644 index 0000000..27e32c4 --- /dev/null +++ b/roles/client_backup/defaults/main.yml @@ -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: [] diff --git a/roles/client_backup/meta/empreinte.yml b/roles/client_backup/meta/empreinte.yml new file mode 100644 index 0000000..0e472fa --- /dev/null +++ b/roles/client_backup/meta/empreinte.yml @@ -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 diff --git a/roles/client_backup/tasks/main.yml b/roles/client_backup/tasks/main.yml new file mode 100644 index 0000000..42f57db --- /dev/null +++ b/roles/client_backup/tasks/main.yml @@ -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 diff --git a/roles/client_backup/templates/sauvegarder.sh.j2 b/roles/client_backup/templates/sauvegarder.sh.j2 new file mode 100644 index 0000000..498f40d --- /dev/null +++ b/roles/client_backup/templates/sauvegarder.sh.j2 @@ -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 diff --git a/roles/client_backup/templates/setops-sauvegarde.service.j2 b/roles/client_backup/templates/setops-sauvegarde.service.j2 new file mode 100644 index 0000000..9c41cd2 --- /dev/null +++ b/roles/client_backup/templates/setops-sauvegarde.service.j2 @@ -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 diff --git a/roles/client_backup/templates/setops-sauvegarde.timer.j2 b/roles/client_backup/templates/setops-sauvegarde.timer.j2 new file mode 100644 index 0000000..dcd487d --- /dev/null +++ b/roles/client_backup/templates/setops-sauvegarde.timer.j2 @@ -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 diff --git a/roles/serveur_backup/defaults/main.yml b/roles/serveur_backup/defaults/main.yml new file mode 100644 index 0000000..b586cfb --- /dev/null +++ b/roles/serveur_backup/defaults/main.yml @@ -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: "" diff --git a/roles/serveur_backup/meta/empreinte.yml b/roles/serveur_backup/meta/empreinte.yml new file mode 100644 index 0000000..1972a5a --- /dev/null +++ b/roles/serveur_backup/meta/empreinte.yml @@ -0,0 +1,6 @@ +--- +# Empreinte ressources — cible de sauvegarde (surtout du disque). +setops_empreinte: + coeurs: 1 + memoire_mo: 512 + disque_go: 64 diff --git a/roles/serveur_backup/tasks/main.yml b/roles/serveur_backup/tasks/main.yml new file mode 100644 index 0000000..57de9dd --- /dev/null +++ b/roles/serveur_backup/tasks/main.yml @@ -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