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