Set-OPS-Public/roles/serveur_backup_site/tasks/main.yml
Daniel Allaire 3ea0c95492
Some checks are pending
verifier / verifier (push) Waiting to run
sauvegarde : l etat d un locataire quitte enfin sa propre flotte
Chezlepro rangeait ses instantanes sur une VM DE SA PROPRE FLOTTE. Raser
l ecosysteme pour le reconstruire, c etait raser le filet avec.

Le site a son depot ; les neuf detenteurs d etat y deposent ; une
restitution est sortie (annuaire LDAP lisible, hors flotte).

Isolation par compte Unix, pas par convention : home 0700, cle exclusive,
racine partagee a root en 0711 (traversable, non listable). Les deux refus
constates. Le site heberge du chiffre : il ne peut ni lire ni ouvrir, d ou
la verification deplacee chez le locataire qui detient la cle.

Quatre defauts reveles par ce deuxieme usage :
- la racine des depots ne peut etre le home de personne (StrictModes rendait
  Permission denied publickey pour un refus de CHEMIN)
- la racine nie le TLD internal, et harden-below-nxdomain etendait ce non a
  toute la zone sans jamais interroger l autoritatif : aucun locataire ne
  pouvait nommer un service du site
- le gabarit transporte des fichiers de durcissement perimes, et les machines
  du site ne recoivent jamais ssh_hardening
- MaxStartups compte les connexions non authentifiees : un depot de site en
  voit la somme de ses locataires

Constat non corrige : les machines du site ne sont pas durcies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-09-01 22:16:20 -04:00

168 lines
7.8 KiB
YAML

---
# CE RÔLE MARQUE UN DÉPÔT ; IL N'EN INSTALLE PAS.
#
# Il présuppose `serveur_backup` sur le même hôte et lit ses variables. Le message qu'on
# obtiendrait sans lui — « variable non définie » — envoie chercher une faute de frappe
# alors que la cause est une déclaration incomplète, un cran au-dessus.
- name: Le rôle qui installe le dépôt est-il bien là ?
ansible.builtin.assert:
that:
- serveur_backup_site_racine | length > 0
- serveur_backup_site_utilisateur | length > 0
fail_msg: >-
`serveur_backup_site` marque un dépôt existant, il n'en installe aucun.
Cet hôte doit AUSSI porter `serveur_backup`. Ajouter les deux rôles à sa
déclaration, dans cet ordre.
# ÉCRIRE, PUIS RELIRE (D-68). La seule prise de ce rôle sur le réel est de vérifier que le
# dépôt qu'il désigne peut RÉELLEMENT recevoir. Sans ça, il attribuerait la sauvegarde de
# tous les locataires à un répertoire qui n'existe pas, ou à un disque plein — et la panne
# arriverait au moment de l'écriture, chez le locataire, en ressemblant à autre chose.
- name: La racine des dépôts existe-t-elle vraiment ?
ansible.builtin.stat:
path: "{{ serveur_backup_site_racine }}"
register: serveur_backup_site_racine_etat
- name: Combien de place reste-t-il devant les locataires ?
ansible.builtin.command:
argv: ["df", "-BG", "--output=avail", "{{ serveur_backup_site_racine }}"]
register: serveur_backup_site_place
changed_when: false
failed_when: false
when: serveur_backup_site_racine_etat.stat.exists
- name: Exiger un dépôt qui peut recevoir
ansible.builtin.assert:
that:
- serveur_backup_site_racine_etat.stat.exists
- serveur_backup_site_racine_etat.stat.isdir
- serveur_backup_site_espace_libre_go | int >= serveur_backup_site_espace_minimal_go | int
fail_msg: >-
{{ serveur_backup_site_racine }}
{{ serveur_backup_site_racine_etat.stat.exists
| ternary('n a plus que ' ~ serveur_backup_site_espace_libre_go ~ ' Go libres (minimum '
~ serveur_backup_site_espace_minimal_go ~ ')', 'n existe pas') }}.
Un dépôt qui se remplit n'échoue pas à l'ouverture : il échoue AU MILIEU d'un
instantané, chez le locataire, et la panne se lit comme une erreur réseau.
success_msg: >-
Dépôt du site prêt : {{ serveur_backup_site_racine }},
{{ serveur_backup_site_espace_libre_go }} Go libres.
vars:
# `df --output=avail -BG` rend « 123G » en deuxieme ligne. On decoupe plutot que
# d'employer une expression a groupe : celles-ci rendent `None` quand rien ne
# correspond, et le gabarit explose au lieu de se taire — paye trois fois cette
# semaine.
serveur_backup_site_espace_libre_go: >-
{{ (serveur_backup_site_place.stdout_lines | default(['', '0G']) | last
| default('0G', true)) | trim | replace('G', '') | default('0', true) }}
when: not ansible_check_mode
# --- UN COMPTE PAR LOCATAIRE ---------------------------------------------------
#
# POURQUOI PAS UN SEUL COMPTE AVEC PLUSIEURS CLES.
#
# `serveur_backup` pose UN compte `restic` et UNE clé : c'est juste pour le dépôt d'un
# écosystème, où il n'y a qu'un déposant. Ici il y en a plusieurs, et la même clé pour
# tous donnerait à chaque locataire la lecture — et l'effacement — des instantanés de
# tous les autres. Une sauvegarde qui expose ce qu'elle protège ne protège rien ; une
# sauvegarde qu'un voisin peut effacer ne protège rien non plus.
#
# La séparation par répertoire seule ne suffit pas : elle tient tant que personne ne
# tape `cd ..`. Le compte Unix, lui, est une frontière que le noyau fait respecter.
#
# Ce que ça ne protège PAS, et qu'il faut savoir : le site voit les octets. Il ne peut
# pas les lire — restic chiffre CHEZ LE CLIENT, avec un mot de passe qui reste dans la
# voûte du locataire, que le site ne détient pas. Le site héberge du chiffré ; il ne
# peut ni le lire, ni le déchiffrer, ni le reconstituer. C'est la même frontière que
# pour les voûtes : le site sert, il ne sait pas.
# LA RACINE PARTAGÉE — À ROOT, TRAVERSABLE, NON LISTABLE.
#
# 0711 et non 0755 : personne n'a besoin d'ÉNUMÉRER les locataires. Un locataire
# traverse pour atteindre son propre répertoire, et ne peut pas apprendre qui d'autre
# dépose ici. La liste des voisins est déjà une information.
- name: La racine partagée n'appartient à aucun déposant
ansible.builtin.file:
path: "{{ serveur_backup_site_parent }}"
state: directory
owner: root
group: root
mode: "0711"
# Deux littéraux doivent s'accorder : le parent déclaré ici, et le `serveur_backup_racine`
# que `meta/main.yml` passe à l'installateur. S'ils divergent, le site déposerait hors de
# sa propre racine et plus rien ne le dirait.
- name: Le dépôt du site vit-il bien sous la racine partagée ?
ansible.builtin.assert:
that:
- serveur_backup_site_racine is match(serveur_backup_site_parent ~ "/.+")
fail_msg: >-
Le dépôt du site ({{ serveur_backup_site_racine }}) n'est pas sous la racine
partagée ({{ serveur_backup_site_parent }}). `meta/main.yml` et
`defaults/main.yml` ne s'accordent plus.
- name: Ouvrir un compte pour chaque locataire du dépôt
ansible.builtin.user:
name: "{{ item.compte }}"
home: "{{ serveur_backup_site_parent }}/{{ item.compte }}"
shell: /bin/bash
create_home: true
system: true
loop: "{{ serveur_backup_site_locataires }}"
loop_control:
label: "{{ item.nom }} -> {{ item.compte }}"
# 0700 : le home d'un locataire n'est lisible que par lui. Sans ça, le compte Unix
# séparerait l'écriture mais pas la lecture — la moitié du problème résolue se lirait
# comme le problème résolu.
- name: Refermer le dépôt de chaque locataire sur lui-même
ansible.builtin.file:
path: "{{ serveur_backup_site_parent }}/{{ item.compte }}"
state: directory
owner: "{{ item.compte }}"
group: "{{ item.compte }}"
mode: "0700"
loop: "{{ serveur_backup_site_locataires }}"
loop_control:
label: "{{ item.compte }}"
# `exclusive: true` : le site est AUTORITÉ sur qui entre chez lui. Une clé ajoutée à la
# main — par dépannage, par un ancien locataire, par erreur — disparaît au prochain
# passage. Sans ce drapeau, `authorized_key` n'ajoute jamais que des lignes : une clé
# retirée du plan resterait valide pour toujours, et rien ne le dirait.
- name: N'autoriser chez chaque locataire que SA clé
ansible.posix.authorized_key:
user: "{{ item.compte }}"
key: "{{ item.pubkey }}"
exclusive: true
state: present
loop: "{{ serveur_backup_site_locataires }}"
loop_control:
label: "{{ item.nom }}"
# ÉCRIRE, PUIS RELIRE (D-68). On vient de déclarer une isolation ; on la constate.
# Deux locataires qui partageraient un home, ou un home lisible par le groupe, se
# liraient dans le journal comme un succès.
- name: Relire ce que le noyau applique vraiment
ansible.builtin.stat:
path: "{{ serveur_backup_site_parent }}/{{ item.compte }}"
register: serveur_backup_site_homes
loop: "{{ serveur_backup_site_locataires }}"
loop_control:
label: "{{ item.compte }}"
- name: Exiger une isolation réelle entre locataires
ansible.builtin.assert:
that:
- item.stat.exists
- item.stat.pw_name == item.item.compte
- item.stat.mode == "0700"
fail_msg: >-
Le dépôt de {{ item.item.nom }} n'est pas isolé :
propriétaire « {{ item.stat.pw_name | default('?') }} »,
permissions « {{ item.stat.mode | default('?') }} » (attendu : {{ item.item.compte }}, 0700).
Tant que ce n'est pas vrai, un locataire peut lire — ou effacer — les instantanés
d'un autre.
loop: "{{ serveur_backup_site_homes.results | default([]) }}"
loop_control:
label: "{{ item.item.compte }}"
when: not ansible_check_mode