diff --git a/CHANGELOG.md b/CHANGELOG.md index 6683e85..864926f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # CHANGELOG — Set-OPS +## 2026-08-26 — `serveur_ops_tenant` : le runner d'un tenant reçoit enfin sa voûte + +**47 preuves.** La doctrine des runners décrit trois portées depuis le 2026-08-22 : + +``` +calculer plan → inventaire aucune voûte serveur_ops +configurer rôles sur ses machines voûte du TENANT ← revendiquée, jamais reçue +matérialiser créer/détruire des VM voûte du SITE serveur_ops_site +``` + +La deuxième ligne était un trou. **Rien ne déposait jamais la voûte d'un tenant sur son +runner.** Un runner pouvait dériver son inventaire et ne rien pouvoir en faire : chaque +rôle qui demande un secret échouait sur son assertion, et l'échec ne disait pas qu'il +manquait un *fichier*, seulement que les valeurs étaient vides. + +Découvert en préparant la reconstruction de Chezlepro. Le runner du site peut matérialiser +ses quinze machines ; il ne peut pas les configurer, parce que Chezlepro a sa **propre** +autorité de certification — donc `client_pki` y réclame un secret de Chezlepro. Le runner +du site ne l'a pas, et **ne doit pas l'avoir** : c'est la ligne qui rend l'hébergement +mutualisé défendable. + +`serveur_ops_tenant` est le symétrique exact de `serveur_ops_site` : un **marqueur**, pas +un installateur. Il dépose la voûte `decrypt: false`, puis **relit l'en-tête** du fichier +déposé — une voûte déchiffrée par accident est une fuite silencieuse. Le dossier +d'inventaire (`principal` ou `production`) se **découvre** sur la machine. + +*Pourquoi un rôle à part et non une option : donner sa voûte à un runner est un pouvoir, +pas un réglage. Le déclarer au plan force à répondre à « cette machine a-t-elle le droit de +configurer cet écosystème ? » — une option activée par défaut y répondrait à notre place.* + +### Chezlepro lisait encore son génome chez patient 0 + +Trouvé au passage, et bloquant pour la reconstruction : `serveur_ops_forge_amont` pointait +sur `10.29.16.11` — la forge de patient 0, l'amont d'avant que le site ait la sienne. +**D-81** a tranché depuis. Laissé tel quel, Chezlepro se serait reconstruit depuis un +moteur périmé, sur un réseau que le découpage en zones a de toute façon déplacé. + +Corrigé vers la forge du site, **par son IP** — `forge.genese.internal` appartient à la +zone souveraine du site, et un tenant ne résout que la sienne ; le certificat servi porte +bien `IP Address:10.0.33.11`. La racine de l'AC du site est désormais versionnée à côté de +la carte de la fabric à laquelle elle appartient, et son chemin se **dérive** du symlink +`underlay.yml` — il vaut donc depuis le poste comme depuis un runner. + ## 2026-08-26 — Le runner du site travaille, et le génome remonte chez lui **46 preuves.** `serveur_ops` et `serveur_ops_site` sont déployés sur `site-ops-01` : le diff --git a/docs/audit/preuve-2026-08-26.md b/docs/audit/preuve-2026-08-26.md index e5281ad..235b499 100644 --- a/docs/audit/preuve-2026-08-26.md +++ b/docs/audit/preuve-2026-08-26.md @@ -20,8 +20,8 @@ | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | -| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 36 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 34 rôles, 92 flux, schéma + matrice OK. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 37 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 35 rôles, 92 flux, schéma + matrice OK. | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_oauth2_proxy | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | @@ -41,16 +41,16 @@ | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 5 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | -| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 28 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 17, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 29 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 18, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 50 scripts expliques et atteignables, 104 cibles make documentees, 60 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 50 scripts expliques et atteignables, 104 cibles make documentees, 61 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 14 exigence(s) de role, toutes satisfaites (39 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (16 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (24 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 0 application(s) exigeant une base l'ont toutes (0 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 2 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | -| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 35 role(s) serveur/client tous nommes, 36 groupe(s) cite(s) en table existent tous. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 36 role(s) serveur/client tous nommes, 37 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 46 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 9d224b4..934bc2a 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -23,8 +23,8 @@ README de rôles). Cette page comble ces deux trous. | Ce qu'on compte | Combien | Comment on le mesure | |---|---|---| -| rôles | 60 | `roles/*/` | -| README de rôles | 60 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | +| rôles | 61 | `roles/*/` | +| README de rôles | 61 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | documents | 38 | `docs/*.md` | | pièces d'audit | 27 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index a73dc85..ddf4154 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -51,6 +51,7 @@ Ce que la reconstruction couvre, par capacité : | Sauvegardes | `serveur_backup`, `client_backup` | restic hors-nœud, **restauration éprouvée** (2026-08-12 : la donnée revient) | | Exploitation | `serveur_ops` | le poste depuis lequel l'ecosysteme se reconstruit : Ansible epingle, genome clone depuis **sa propre forge**, cle SSH propre — **sans** la voute ni son mot de passe | | Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment | +| Runner de tenant | `serveur_ops_tenant` | le pouvoir de **configurer** : la voute de l'ecosysteme, deposee CHIFFREE sur son propre runner. Sans elle un runner calcule son inventaire et ne peut rien en faire — chaque role qui demande un secret echoue sur son assertion. N'atteint ni la fabric ni la frontiere | | Runner de site | `serveur_ops_site` | le pouvoir de **materialiser** : creer et detruire des VM sur la fabric. Detient la voute du SITE, chiffree, et n'entre JAMAIS chez un tenant — reserve a l'ecosysteme de l'hebergeur | | Resolution | `serveur_resolveur`, `client_resolveur` | UN resolveur recursif par tenant (Unbound), qui recurse depuis la racine et delegue la zone souveraine a PowerDNS. Remplace les N demons locaux d'avant le 2026-08-24 | | Cache du site | `serveur_cache_site` | designe LE cache que les ecosystemes voisins prennent comme amont : Debian telecharge une fois pour toute la fabric, et le cache ne voit que des requetes agregees. Reserve a l'ecosysteme de l'hebergeur | diff --git a/docs/couches-deploiement.yml b/docs/couches-deploiement.yml index 37a9a72..3cd3e86 100644 --- a/docs/couches-deploiement.yml +++ b/docs/couches-deploiement.yml @@ -76,6 +76,10 @@ couches: # Le poste d'exploitation vient APRES la forge : il clone le genome depuis # elle. Le placer plus tot le laisserait sans source. - serveur_ops + # Le runner de TENANT vient APRES le poste : il suppose les depots clones et le lien + # `instance` pose. Il n'ajoute qu'un pouvoir -- celui de CONFIGURER cet ecosysteme, + # par sa voute deposee chiffree. + - serveur_ops_tenant # Le runner de SITE vient APRES le runner de tenant : il suppose les depots # clones. Il n'ajoute qu'un pouvoir -- celui de materialiser sur la fabric. - serveur_ops_site diff --git a/docs/dependances-groupes.yml b/docs/dependances-groupes.yml index 14c8fa2..7258e6e 100644 --- a/docs/dependances-groupes.yml +++ b/docs/dependances-groupes.yml @@ -118,6 +118,16 @@ groupes: raison: "Un cache apt ne depend d'aucun service de l'ecosysteme : il ne fait que relayer et retenir." surveillance: "Verifier l'ecoute sur 3142, le taux de service depuis le journal, et l'espace du cache." + serveur_ops_tenant: + requiert_groupes_actifs: + - serveur_ops + # LE RUNNER DE TENANT EST ADDITIF, comme celui du site : il suppose le poste + # d'exploitation en place, dont il reutilise la racine, l'utilisateur, les depots + # clones et le lien `instance`. Seul, il ne ferait que deposer un secret sur une + # machine qui n'a pas le plan qu'il ouvre. + raison: "Le runner de tenant n'ajoute qu'un pouvoir a un poste d'exploitation existant : sans lui, rien a quoi l'attacher." + surveillance: "Verifier que la voute de l'ecosysteme est presente ET CHIFFREE sous son dossier d'inventaire." + serveur_ops_site: requiert_groupes_actifs: - serveur_ops diff --git a/playbooks/groupes/serveur_ops_tenant.yml b/playbooks/groupes/serveur_ops_tenant.yml new file mode 100644 index 0000000..9e5825e --- /dev/null +++ b/playbooks/groupes/serveur_ops_tenant.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe serveur_ops_tenant + hosts: serveur_ops_tenant + become: true + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - serveur_ops_tenant diff --git a/roles/serveur_ops_tenant/README.md b/roles/serveur_ops_tenant/README.md new file mode 100644 index 0000000..a6c7bab --- /dev/null +++ b/roles/serveur_ops_tenant/README.md @@ -0,0 +1,48 @@ +# serveur_ops_tenant — donner à un runner la voûte de SON écosystème + +> **Pour qui :** l'exploitant qui monte le runner d'un tenant. + +Marqueur, pas installateur. `serveur_ops` installe le poste — Ansible, les collections, les +dépôts du génome, les symlinks. Ce rôle-ci y ajoute **un pouvoir** : la voûte de +l'écosystème, déposée **chiffrée**, sans laquelle le runner peut tout calculer et ne rien +configurer. + +## Les trois portées d'un runner + +``` +calculer plan → inventaire aucune voûte serveur_ops +configurer rôles sur ses machines voûte du TENANT serveur_ops_tenant +matérialiser créer/détruire des VM voûte du SITE serveur_ops_site +``` + +Aucun runner n'est omnipotent. Le runner d'un **site** matérialise le terrain et n'entre +jamais chez un tenant ; le runner d'un **tenant** configure son écosystème et ne touche +jamais la fabric. + +## Pourquoi un rôle à part + +Donner sa voûte à un runner est un **pouvoir**, pas un réglage. Le déclarer au plan force à +répondre à « cette machine a-t-elle le droit de configurer cet écosystème ? » — une option +activée par défaut y répondrait à notre place. + +## Ce qu'il exige + +| Variable | Ce que c'est | +|---|---| +| `serveur_ops_tenant_depot` | le dossier de cet écosystème chez le runner ; vaut `serveur_ops_instance` par défaut | +| `serveur_ops_tenant_voute_source` | le chemin du `vault.yml` **sur le contrôleur** | + +Le dossier d'inventaire (`principal` ou `production`) se **découvre** sur la machine : il +n'est écrit nulle part. + +## Ce qu'il garantit + +`decrypt: false` sur la copie, puis **relecture de l'en-tête** du fichier déposé. Une voûte +déchiffrée par accident est une fuite silencieuse — le fichier existe, le rôle se dirait +satisfait, et les secrets dormiraient en clair sur une machine. Le rôle refuse plutôt. + +## Voir aussi + +- `roles/serveur_ops/README.md` — l'installateur +- `roles/serveur_ops_site/README.md` — le pouvoir symétrique, côté fabric +- `docs/implanter-un-tenant-sur-un-site.md` — la séquence complète diff --git a/roles/serveur_ops_tenant/defaults/main.yml b/roles/serveur_ops_tenant/defaults/main.yml new file mode 100644 index 0000000..422a663 --- /dev/null +++ b/roles/serveur_ops_tenant/defaults/main.yml @@ -0,0 +1,58 @@ +--- +# LE RUNNER D'UN TENANT — celui qui CONFIGURE. +# +# Le travail d'un runner se divise en trois portées, et chacune a son rôle : +# +# calculer plan -> inventaire aucune voûte `serveur_ops` +# configurer rôles sur ses machines voûte du TENANT CE RÔLE +# matérialiser créer/détruire des VM voûte du SITE `serveur_ops_site` +# +# CE QUI MANQUAIT (2026-08-26). La portée « configurer » était attribuée à `serveur_ops` +# dans la doctrine — mais rien ne déposait jamais la voûte du tenant sur son runner. Un +# runner pouvait donc dériver son inventaire et ne rien pouvoir en faire : chaque rôle qui +# demande un secret échouait sur son assertion, et l'échec ne disait pas qu'il manquait un +# FICHIER, seulement que les valeurs étaient vides. +# +# Découvert en préparant la reconstruction de Chezlepro : le runner du SITE peut +# matérialiser ses quinze machines, mais ne peut pas les configurer — Chezlepro a sa +# PROPRE autorité de certification, donc `client_pki` y réclame un secret de Chezlepro. +# Le runner du site ne l'a pas, et ne doit pas l'avoir : c'est la ligne qui rend +# l'hébergement mutualisé défendable. +# +# POURQUOI UN RÔLE À PART, et non une option de `serveur_ops`. Donner sa voûte à un runner +# est un POUVOIR, pas un réglage. Le déclarer explicitement au plan force à répondre à la +# question « cette machine a-t-elle le droit de configurer cet écosystème ? » — alors +# qu'une option activée par défaut y répondrait à notre place. C'est le même patron que +# `serveur_artefacts` + `serveur_cache_site` : un installateur, puis un marqueur qui +# ajoute un pouvoir. +# +# QUI PEUT LE DÉCLARER : l'écosystème lui-même, pour SA machine. Un runner de SITE ne le +# déclare jamais — il porte les plans des tenants en `role: tenant` précisément pour dire +# qu'il prépare leur terrain sans les piloter. + +serveur_ops_tenant_utilisateur: "setops" +serveur_ops_tenant_racine: "/opt/setops" + +# Le dossier du dépôt de CET écosystème chez le runner, cloné par `serveur_ops` +# (entrée `role: instance` de `serveur_ops_depots`). Par défaut : celui que le lien +# `instance` désigne déjà — le pilote et sa voûte ne peuvent pas viser deux écosystèmes. +serveur_ops_tenant_depot: "{{ serveur_ops_instance | default('') }}" + +# --- LA VOÛTE DU TENANT ------------------------------------------------------ +# +# Déposée CHIFFRÉE, jamais en clair. Le mot de passe n'est pas stocké : l'exploitant le +# tape au moment d'agir. +# +# `decrypt: false` EST OBLIGATOIRE sur la copie. Sans lui, Ansible DÉCHIFFRE la source +# quand il détient le mot de passe — mesuré le 2026-08-24 sur la voûte du site, 776 octets +# en clair au lieu de 3465 chiffrés, sur une machine où ils n'avaient rien à faire. +serveur_ops_tenant_voute_source: "" +serveur_ops_tenant_voute_deposer: true + +# LE DOSSIER D'INVENTAIRE SE DÉCOUVRE, il ne s'écrit pas. Le dépôt en connaît deux noms +# — `principal` et `production` — et l'ordre de préférence est celui de +# `inventory_rules.dossier_inventaire`. Écrire le nom en dur ici le ferait mentir pour +# l'écosystème qui emploie l'autre. +serveur_ops_tenant_inventaires_connus: + - principal + - production diff --git a/roles/serveur_ops_tenant/meta/authentification.yml b/roles/serveur_ops_tenant/meta/authentification.yml new file mode 100644 index 0000000..7d3e211 --- /dev/null +++ b/roles/serveur_ops_tenant/meta/authentification.yml @@ -0,0 +1,12 @@ +--- +# Position de ce role dans la directive d'authentification (D-38..D-41). +# Voir docs/authentification.md. Gardee par la preuve P29. +authentification: + portee: sans-auth-humaine + mecanisme: aucun + formulaire_local: sans-objet + secours: "Acces SSH a l'hote, puis `sudo -iu setops`" + raison: >- + Ce role n'expose aucune interface : il DETIENT un pouvoir au lieu d'en offrir un. + Ce qui le protege n'est pas une authentification mais le fait que la voute deposee + reste CHIFFREE et que son mot de passe soit tape a l'execution, jamais stocke. diff --git a/roles/serveur_ops_tenant/meta/empreinte.yml b/roles/serveur_ops_tenant/meta/empreinte.yml new file mode 100644 index 0000000..fb53456 --- /dev/null +++ b/roles/serveur_ops_tenant/meta/empreinte.yml @@ -0,0 +1,7 @@ +--- +# Empreinte ressources — ce rôle n'ajoute qu'un fichier. Il se colocalise avec +# `serveur_ops`, dont il partage la racine et l'utilisateur. +setops_empreinte: + coeurs: 0 + memoire_mo: 0 + disque_go: 0 diff --git a/roles/serveur_ops_tenant/meta/flux.yml b/roles/serveur_ops_tenant/meta/flux.yml new file mode 100644 index 0000000..1fde4bd --- /dev/null +++ b/roles/serveur_ops_tenant/meta/flux.yml @@ -0,0 +1,11 @@ +--- +# Flux réseau du runner de TENANT. Voir docs/flux-conception.md. +# +# AUCUN FLUX PROPRE, ET C'EST LE POINT. Ce rôle ne fait que déposer un fichier ; ce qui +# consomme la voûte, c'est `serveur_ops` quand il configure les machines de l'écosystème, +# et ses flux sont déclarés chez lui. Ce rôle n'atteint NI la fabric NI la frontière — +# c'est exactement ce qui le sépare de `serveur_ops_site`. +# +# Le dire explicitement plutôt que d'omettre le fichier : une absence de déclaration se +# lit comme un oubli, une déclaration vide se lit comme une décision. +flux: [] diff --git a/roles/serveur_ops_tenant/tasks/main.yml b/roles/serveur_ops_tenant/tasks/main.yml new file mode 100644 index 0000000..b66b657 --- /dev/null +++ b/roles/serveur_ops_tenant/tasks/main.yml @@ -0,0 +1,113 @@ +--- +- name: Exiger les intrants du runner de tenant + ansible.builtin.assert: + that: + - serveur_ops_tenant_depot | length > 0 + - (not (serveur_ops_tenant_voute_deposer | bool)) + or (serveur_ops_tenant_voute_source | length > 0) + fail_msg: >- + serveur_ops_tenant exige `serveur_ops_tenant_depot` (le dossier de CET écosystème + chez le runner, cloné par serveur_ops — il vaut `serveur_ops_instance` par défaut) + et, si le dépôt de la voûte est demandé, `serveur_ops_tenant_voute_source` + (le chemin de son `vault.yml` sur le contrôleur). + +- name: Le plan de l'écosystème est-il bien là ? + ansible.builtin.stat: + path: "{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/plan/serveurs.yml" + register: serveur_ops_tenant_plan + +- name: Refuser si le plan de l'écosystème manque + ansible.builtin.assert: + that: + - serveur_ops_tenant_plan.stat.exists + fail_msg: >- + {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/plan/serveurs.yml est + absent. Déclarer ce dépôt dans `serveur_ops_depots` (role: instance) et rejouer + serveur_ops : donner sa voûte à un runner qui n'a pas le plan qu'elle ouvre ne + configure rien — ça ne fait que déposer un secret. + when: not ansible_check_mode + +# LE DOSSIER D'INVENTAIRE SE DÉCOUVRE. Deux noms existent dans le dépôt (`principal`, +# `production`) et l'écrire en dur le ferait mentir pour l'écosystème qui emploie l'autre. +- name: Chercher le dossier d'inventaire de cet écosystème + ansible.builtin.stat: + path: "{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{ item }}" + register: serveur_ops_tenant_candidats + loop: "{{ serveur_ops_tenant_inventaires_connus }}" + loop_control: + label: "{{ item }}" + +- name: Retenir le premier qui existe + ansible.builtin.set_fact: + serveur_ops_tenant_inventaire: >- + {{ (serveur_ops_tenant_candidats.results | selectattr('stat.exists') + | map(attribute='item') | list | first) | default('') }} + +- name: Refuser si aucun inventaire n'est reconnu + ansible.builtin.assert: + that: + - serveur_ops_tenant_inventaire | length > 0 + fail_msg: >- + Aucun dossier d'inventaire trouvé sous + {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/ parmi + {{ serveur_ops_tenant_inventaires_connus | join(', ') }}. Sans lui, on ne saurait pas + où déposer la voûte — et la déposer au mauvais endroit revient à ne pas la déposer, + en laissant croire le contraire. + when: not ansible_check_mode + +- name: Préparer le dossier des variables communes + ansible.builtin.file: + path: >- + {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{ + serveur_ops_tenant_inventaire }}/group_vars/all + state: directory + owner: "{{ serveur_ops_tenant_utilisateur }}" + group: "{{ serveur_ops_tenant_utilisateur }}" + mode: "0700" + when: + - serveur_ops_tenant_voute_deposer | bool + - not ansible_check_mode + +# LES SECRETS DE L'ÉCOSYSTÈME, DE DROIT ET NON PAR EMPRUNT. +# +# `decrypt: false` : voir defaults/main.yml. Le fichier reste chiffré ; seul le mot de +# passe, tapé à l'exécution, l'ouvre. +- name: Déposer la voûte de l'écosystème (chiffrée) + ansible.builtin.copy: + src: "{{ serveur_ops_tenant_voute_source }}" + dest: >- + {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{ + serveur_ops_tenant_inventaire }}/group_vars/all/vault.yml + owner: "{{ serveur_ops_tenant_utilisateur }}" + group: "{{ serveur_ops_tenant_utilisateur }}" + mode: "0600" + decrypt: false + when: + - serveur_ops_tenant_voute_deposer | bool + - not ansible_check_mode + +# ÉCRIRE, PUIS RELIRE (D-68). Une voûte déchiffrée par accident est une fuite silencieuse : +# le fichier existe, le rôle se dit satisfait, et les secrets de l'écosystème dorment en +# clair sur une machine. +- name: Relire l'en-tête de la voûte déposée + ansible.builtin.command: + cmd: >- + head -c 21 {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot + }}/inventories/{{ serveur_ops_tenant_inventaire }}/group_vars/all/vault.yml + register: serveur_ops_tenant_entete + changed_when: false + when: + - serveur_ops_tenant_voute_deposer | bool + - not ansible_check_mode + +- name: Exiger que la voûte déposée soit CHIFFRÉE + ansible.builtin.assert: + that: + - "'$ANSIBLE_VAULT' in serveur_ops_tenant_entete.stdout" + fail_msg: >- + La voûte déposée n'est PAS chiffrée — elle a été déchiffrée en transit. + Vérifier `decrypt: false` sur la copie. Détruire le fichier au shred et considérer + les secrets de cet écosystème comme exposés. + when: + - serveur_ops_tenant_voute_deposer | bool + - not ansible_check_mode