Set-OPS-Public/roles/serveur_backup_site/tasks/main.yml
Daniel Allaire aac74f6043 supervision : les huit derniers roles, et deux defauts que l epreuve a trouves
Les 33 roles serveur_* declarent maintenant une sonde. Les huit qui manquaient
sont ceux dont la verite ne ressemble pas a « ce service repond-il ».

Quatre marqueurs du site : ce que tasks/main.yml verifie UNE FOIS au deploiement
cesse d etre vrai sans que rien ne tombe. La racine du cache se retrouve chainee,
la forge du genome repond en n ayant plus rien dedans, un locataire n est plus
admis a resoudre, l isolation d un depot glisse.

serveur_ops_site ne sert rien : il detient un pouvoir. La sonde verifie que la
carte est la, que la voute du site est chiffree et que sa cle est en 0600 — sans
lire le contenu d aucun des trois.

serveur_icingaweb2 surveille la vitrine de la supervision elle-meme : si la
console meurt, tout reste vert et l exploitant est aveugle.

DEFAUT 1 — quatre gabarits qu Ansible aurait refuse de rendre. Jinja lit le
{# de ${#tableau[@]} comme un debut de commentaire. Le depot connaissait le
remede et l appliquait la ou quelqu un s etait fait prendre, nulle part ailleurs.
P76 rend desormais chaque gabarit de role, avec les delimiteurs qu Ansible en
tirerait — pas une recherche de motif.

DEFAUT 2 — la doctrine promettait 54 greffons et citait check_pgsql. La flotte a
monitoring-plugins-basic : 53, sans check_pgsql ni check_dns ni check_ldap. Le
paquet qui les porte traine samba et snmp sur chaque machine. Un greffon absent
sort en 127, qui n est pas un code Nagios.

21 controles negatifs sur les machines reelles du site. ansible-lint production
0/91, harnais 75 OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-14 15:43:52 -04:00

191 lines
8.6 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
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et
# pousse le resultat.
#
# LES DEUX ASSERTIONS CI-DESSUS SONT DES PHOTOS. L'espace se consomme tous les jours et
# l'etanchéite peut glisser a la premiere main qui passe — or aucune de ces deux derives
# ne fait echouer quoi que ce soit avant qu'il ne soit trop tard. La sonde les rejoue
# toutes les quinze minutes.
- name: Assurer le repertoire des sondes de supervision
ansible.builtin.file:
path: /usr/local/lib/setops/sondes
state: directory
owner: root
group: root
mode: "0755"
- name: Deposer la sonde « depot-locataires »
ansible.builtin.template:
src: sonde-depot-locataires.sh.j2
dest: /usr/local/lib/setops/sondes/depot-locataires.sh
owner: root
group: root
mode: "0750"