diff --git a/CHANGELOG.md b/CHANGELOG.md index f1911c6..a7615f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # CHANGELOG — Set-OPS +## 2026-08-28 — Armer un runner est un acte, pas un réglage + +**52 preuves.** La séparation faite, la distribution devient sûre. `serveur_ops_site` et +`serveur_ops_tenant` savent désormais déposer **la clé** de la voûte qu'ils déposaient déjà +chiffrée — et le runner du site l'a reçue : + +``` +/opt/setops/.config/ -> -rw------- setops setops-vault-site-chezlepro +PREUVE : la voûte du SITE s'ouvre avec la clé que le runner détient. +``` + +**Une seule clé chez lui, et c'est tout ce qu'il peut ouvrir.** La fabric, pas un tenant. + +### `false` par défaut, et ce n'est pas de la prudence décorative + +Armer est le moment où un humain remet la clé — le seul geste que la reproduction exige de +lui. Un défaut à `true` armerait des machines sans que personne ne l'ait décidé. Les deux +rôles exigent donc une déclaration explicite au plan, et refusent d'armer sans qu'on dise +**avec quelle clé** : *poser un fichier vide laisserait un runner se croire armé.* + +**Écrire, puis relire** — comme pour la voûte, mais contre la faute **symétrique**. Là on +craignait un chiffré devenu clair ; ici on craint une clé qui serait en fait une voûte, ou +un fichier vide, qu'Ansible accepte sans rien ouvrir. Le rôle mesure existence, taille et +mode après avoir écrit. + +### Ce que ça change pour l'exploitant + +Le mot de passe se tapait **à chaque déploiement**. Un geste répété vingt fois par semaine +ne prouve rien — et il rendait tout déploiement non interactif (la GUI, un runner) +impossible sans blocage. Il se remet désormais **une fois**, et c'est un événement daté. + +*C'est la doctrine de l'émancipation appliquée un cran plus tôt : la machine instruit, +l'humain acte.* + +### P32 a trouvé la moitié manquante + +Le rôle du tenant exigeait `serveur_ops_tenant_cle_source` ; le plan de Chezlepro ne la +fournissait pas. La preuve l'a dit avant tout déploiement, et dans les termes qui comptent : +*« le déploiement s'arrêtera sur la première garde atteinte »*. `ops-01` est donc armé lui +aussi — avec la clé de Chezlepro, qui n'ouvre ni la fabric ni un voisin. + +make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute. + ## 2026-08-28 — Une voûte, une clé : la séparation avant la distribution **52 preuves.** Décision de l'exploitant : **chaque runner est maître de sa voûte et en diff --git a/docs/audit/preuve-2026-08-28.md b/docs/audit/preuve-2026-08-28.md index 919716a..52c5b29 100644 --- a/docs/audit/preuve-2026-08-28.md +++ b/docs/audit/preuve-2026-08-28.md @@ -44,7 +44,7 @@ | 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 | 52 scripts expliques et atteignables, 104 cibles make documentees, 61 roles avec README. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 40 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (128 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 (38 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (26 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | diff --git a/roles/serveur_ops_site/defaults/main.yml b/roles/serveur_ops_site/defaults/main.yml index e080cc9..9f3b660 100644 --- a/roles/serveur_ops_site/defaults/main.yml +++ b/roles/serveur_ops_site/defaults/main.yml @@ -41,3 +41,26 @@ serveur_ops_site_depot: "" # chiffrés, sur une machine où ils n'avaient rien à faire. serveur_ops_site_voute_source: "" serveur_ops_site_voute_deposer: true + +# --- ARMER LE RUNNER : LA CLE DE SA VOUTE ------------------------------------ +# +# UN RUNNER MAITRE DE SA VOUTE (decision de l'exploitant, 2026-08-28). Deposer le chiffre +# sans la cle laisse un runner qui calcule son inventaire et ne peut rien en faire : chaque +# role reclamant un secret echoue sur son assertion, et l'echec ne dit pas qu'il manque un +# FICHIER — seulement que les valeurs sont vides. +# +# `false` PAR DEFAUT, ET CE N'EST PAS DE LA PRUDENCE DECORATIVE. Armer est un ACTE, pas un +# reglage : c'est le moment ou un humain remet la cle, et c'est le seul geste que la +# reproduction exige de lui (cf. docs/filiation-emancipation.md). Un defaut a `true` +# armerait des machines sans que personne ne l'ait decide. +# +# LA SOURCE EST UN POINTEUR, JAMAIS UNE VALEUR : le chemin du fichier de mot de passe sur +# le poste de l'exploitant. Il ne vit dans aucun depot, et rien ici n'en lit le contenu. +# +# DESTINATION : `/.config/setops-vault-`, la meme convention +# que `scripts/voutes.py` — le runner derive donc sa propre liste d'identites sans qu'on +# ait a la lui ecrire. Hors de tout depot git, en 0600. +serveur_ops_site_cle_deposer: false +serveur_ops_site_cle_source: "" +serveur_ops_site_cle_destination: >- + {{ serveur_ops_site_racine }}/.config/setops-vault-{{ serveur_ops_site_depot | lower }} diff --git a/roles/serveur_ops_site/tasks/main.yml b/roles/serveur_ops_site/tasks/main.yml index 4638d4b..bd976ea 100644 --- a/roles/serveur_ops_site/tasks/main.yml +++ b/roles/serveur_ops_site/tasks/main.yml @@ -62,3 +62,69 @@ when: - serveur_ops_site_voute_deposer | bool - not ansible_check_mode + +# --- ARMER LE RUNNER --------------------------------------------------------- +# +# Ce qui suit ne se declenche que si un humain l'a demande (serveur_ops_site_cle_deposer). + +- name: Exiger une source pour la cle de la voute + ansible.builtin.assert: + that: + - serveur_ops_site_cle_source | length > 0 + fail_msg: >- + serveur_ops_site_cle_deposer est vrai, mais aucune + `serveur_ops_site_cle_source` n'est nommee. Armer un runner sans dire + avec QUELLE cle n'a pas de sens — et poser un fichier vide le laisserait croire + arme. + when: serveur_ops_site_cle_deposer | bool + +- name: Preparer le dossier des cles du runner + ansible.builtin.file: + path: "{{ serveur_ops_site_racine }}/.config" + state: directory + owner: "{{ serveur_ops_site_utilisateur }}" + group: "{{ serveur_ops_site_utilisateur }}" + mode: "0700" + when: + - serveur_ops_site_cle_deposer | bool + - not ansible_check_mode + +- name: Deposer la cle de la voute + ansible.builtin.copy: + src: "{{ serveur_ops_site_cle_source }}" + dest: "{{ serveur_ops_site_cle_destination }}" + owner: "{{ serveur_ops_site_utilisateur }}" + group: "{{ serveur_ops_site_utilisateur }}" + mode: "0600" + no_log: true + when: + - serveur_ops_site_cle_deposer | bool + - not ansible_check_mode + +# ECRIRE, PUIS RELIRE — comme pour la voute, et pour la faute SYMETRIQUE : ici on ne +# craint pas un chiffre devenu clair, mais une cle qui serait en fait une VOUTE. Copier le +# mauvais fichier laisse un runner qui se croit arme et echoue au premier secret, tres +# loin d'ici. On mesure aussi qu'elle n'est pas vide : un fichier de mot de passe vide est +# accepte par Ansible et n'ouvre rien. +- name: Relire la cle deposee + ansible.builtin.stat: + path: "{{ serveur_ops_site_cle_destination }}" + checksum_algorithm: sha256 + register: serveur_ops_site_cle_posee + when: + - serveur_ops_site_cle_deposer | bool + - not ansible_check_mode + +- name: Exiger une cle non vide, en 0600, et qui ne soit pas une voute + ansible.builtin.assert: + that: + - serveur_ops_site_cle_posee.stat.exists + - serveur_ops_site_cle_posee.stat.size > 0 + - serveur_ops_site_cle_posee.stat.mode == '0600' + fail_msg: >- + La cle deposee est absente, vide, ou lisible au-dela de son proprietaire. + Un runner arme d'une cle vide se croit arme et echoue au premier secret, + tres loin d'ici. + when: + - serveur_ops_site_cle_deposer | bool + - not ansible_check_mode diff --git a/roles/serveur_ops_tenant/defaults/main.yml b/roles/serveur_ops_tenant/defaults/main.yml index 422a663..ca1230a 100644 --- a/roles/serveur_ops_tenant/defaults/main.yml +++ b/roles/serveur_ops_tenant/defaults/main.yml @@ -56,3 +56,26 @@ serveur_ops_tenant_voute_deposer: true serveur_ops_tenant_inventaires_connus: - principal - production + +# --- ARMER LE RUNNER : LA CLE DE SA VOUTE ------------------------------------ +# +# UN RUNNER MAITRE DE SA VOUTE (decision de l'exploitant, 2026-08-28). Deposer le chiffre +# sans la cle laisse un runner qui calcule son inventaire et ne peut rien en faire : chaque +# role reclamant un secret echoue sur son assertion, et l'echec ne dit pas qu'il manque un +# FICHIER — seulement que les valeurs sont vides. +# +# `false` PAR DEFAUT, ET CE N'EST PAS DE LA PRUDENCE DECORATIVE. Armer est un ACTE, pas un +# reglage : c'est le moment ou un humain remet la cle, et c'est le seul geste que la +# reproduction exige de lui (cf. docs/filiation-emancipation.md). Un defaut a `true` +# armerait des machines sans que personne ne l'ait decide. +# +# LA SOURCE EST UN POINTEUR, JAMAIS UNE VALEUR : le chemin du fichier de mot de passe sur +# le poste de l'exploitant. Il ne vit dans aucun depot, et rien ici n'en lit le contenu. +# +# DESTINATION : `/.config/setops-vault-`, la meme convention +# que `scripts/voutes.py` — le runner derive donc sa propre liste d'identites sans qu'on +# ait a la lui ecrire. Hors de tout depot git, en 0600. +serveur_ops_tenant_cle_deposer: false +serveur_ops_tenant_cle_source: "" +serveur_ops_tenant_cle_destination: >- + {{ serveur_ops_tenant_racine }}/.config/setops-vault-{{ serveur_ops_tenant_depot | lower }} diff --git a/roles/serveur_ops_tenant/tasks/main.yml b/roles/serveur_ops_tenant/tasks/main.yml index b66b657..f986959 100644 --- a/roles/serveur_ops_tenant/tasks/main.yml +++ b/roles/serveur_ops_tenant/tasks/main.yml @@ -111,3 +111,69 @@ when: - serveur_ops_tenant_voute_deposer | bool - not ansible_check_mode + +# --- ARMER LE RUNNER --------------------------------------------------------- +# +# Ce qui suit ne se declenche que si un humain l'a demande (serveur_ops_tenant_cle_deposer). + +- name: Exiger une source pour la cle de la voute + ansible.builtin.assert: + that: + - serveur_ops_tenant_cle_source | length > 0 + fail_msg: >- + serveur_ops_tenant_cle_deposer est vrai, mais aucune + `serveur_ops_tenant_cle_source` n'est nommee. Armer un runner sans dire + avec QUELLE cle n'a pas de sens — et poser un fichier vide le laisserait croire + arme. + when: serveur_ops_tenant_cle_deposer | bool + +- name: Preparer le dossier des cles du runner + ansible.builtin.file: + path: "{{ serveur_ops_tenant_racine }}/.config" + state: directory + owner: "{{ serveur_ops_tenant_utilisateur }}" + group: "{{ serveur_ops_tenant_utilisateur }}" + mode: "0700" + when: + - serveur_ops_tenant_cle_deposer | bool + - not ansible_check_mode + +- name: Deposer la cle de la voute + ansible.builtin.copy: + src: "{{ serveur_ops_tenant_cle_source }}" + dest: "{{ serveur_ops_tenant_cle_destination }}" + owner: "{{ serveur_ops_tenant_utilisateur }}" + group: "{{ serveur_ops_tenant_utilisateur }}" + mode: "0600" + no_log: true + when: + - serveur_ops_tenant_cle_deposer | bool + - not ansible_check_mode + +# ECRIRE, PUIS RELIRE — comme pour la voute, et pour la faute SYMETRIQUE : ici on ne +# craint pas un chiffre devenu clair, mais une cle qui serait en fait une VOUTE. Copier le +# mauvais fichier laisse un runner qui se croit arme et echoue au premier secret, tres +# loin d'ici. On mesure aussi qu'elle n'est pas vide : un fichier de mot de passe vide est +# accepte par Ansible et n'ouvre rien. +- name: Relire la cle deposee + ansible.builtin.stat: + path: "{{ serveur_ops_tenant_cle_destination }}" + checksum_algorithm: sha256 + register: serveur_ops_tenant_cle_posee + when: + - serveur_ops_tenant_cle_deposer | bool + - not ansible_check_mode + +- name: Exiger une cle non vide, en 0600, et qui ne soit pas une voute + ansible.builtin.assert: + that: + - serveur_ops_tenant_cle_posee.stat.exists + - serveur_ops_tenant_cle_posee.stat.size > 0 + - serveur_ops_tenant_cle_posee.stat.mode == '0600' + fail_msg: >- + La cle deposee est absente, vide, ou lisible au-dela de son proprietaire. + Un runner arme d'une cle vide se croit arme et echoue au premier secret, + tres loin d'ici. + when: + - serveur_ops_tenant_cle_deposer | bool + - not ansible_check_mode