diff --git a/AGENTS.md b/AGENTS.md index 4bcbbea..0620321 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente ## Écrire, puis relire (D-68) `--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**. -C'est aussi ce que font les 67 preuves de `make prouver` : elles lisent le dépôt, sans le +C'est aussi ce que font les 68 preuves de `make prouver` : elles lisent le dépôt, sans le moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le dépôt annonce.** diff --git a/CHANGELOG.md b/CHANGELOG.md index ad2e212..7dc3fb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,57 @@ # CHANGELOG — Set-OPS +## 2026-09-11 (1) — Reconstruction a froid : les noms derives naissent justes + +Quatrieme reconstruction depuis zero de Chezlepro, demandee pour une raison precise : trois +roles dependent desormais d'une variable que le GENERATEUR pose. En incrementiel ils avaient +marche parce que la configuration existait deja. A froid, si `instancier` ne posait pas +`_hostname` sur un chemin quelconque, le defaut du role reprendrait la main **en +silence** — et il tomberait juste pour `forge` et `cloud` (la convention coincide), faux pour +`observatoire`. + + 14/14 VM rasees puis recreees 32 min 14 s + un seul echec, et il n'a rien a voir avec les noms + +Le certificat ne A FROID porte `observatoire` et `vigie`, et aucun ancien nom. Les deux +retours SSO derivent juste, sans reprise : + + observatoire redirect_uri=https%3A%2F%2Fobservatoire.chezlepro.internal%2Flogin%2Fgeneric_oauth + vigie redirect_uri=https%3A%2F%2Fvigie.chezlepro.internal%2Foauth2%2Fcallback + +En incrementiel, le SAN avait demande un second passage de `client_pki`. A froid, il naît +juste du premier coup — la sequence penible n'existe qu'en incrementiel. + +### Une reussite n'est pas un contenu + +L'echec unique, sur `infra-mail-01` et sur elle seule : `get_url` a rendu **0 octet sans +erreur**. Le cache a servi un 200 au corps vide. Le role a continue, satisfait. + +Le defaut ne s'est pas lu la. Il s'est lu deux cents lignes plus loin, dans un `apt` qui +accusait la SIGNATURE : + + Missing key 35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3, which is needed to verify signature + +Un message qui envoie chercher une cle revoquee chez le fournisseur, alors que le fichier +local faisait zero octet. Meme famille que l'index tronque du cache la veille : l'octet +manquant se denonce toujours ailleurs qu'ou il manque. + +**Et la garde posee hier ne mordait pas.** « Retirer une ressource VIDE avant de la +redemander » ne vaut qu'au passage SUIVANT : au premier, le fichier n'existe pas encore, il +n'y a rien a retirer. Elle repare le second essai, elle ne protege pas le premier. + +Les cinq roles qui telechargent une cle la MESURENT maintenant dans la meme execution — le +`until` exige une taille non nulle (cinq tentatives), puis une assertion nomme la vraie +cause. **P68** garde le motif. Les roles qui deposent une cle embarquee (`copy` depuis +`files/`) n'entrent pas dans le compte : rien de reseau ne s'interpose. + +`infra-mail-01` redeployee : la cle fait 1022 octets, comme les treize autres. + +### Les trois bruits connus + +Revenus comme prevu, et ce ne sont pas des regressions : `auditd` livre sans regles au +gabarit, la course sur le verrou `dpkg`, l'index d'`apt.grafana.com` absent du cache +hors-ligne. + ## 2026-09-10 (18) — `vigie` pour Icinga, et la quatrieme liste tombe Suite du renommage precedent, decide par l'exploitant : `vigie` est le nom de la console de diff --git a/docs/audit/preuve-2026-09-11.md b/docs/audit/preuve-2026-09-11.md new file mode 100644 index 0000000..a7f2c3f --- /dev/null +++ b/docs/audit/preuve-2026-09-11.md @@ -0,0 +1,104 @@ +# Preuve de conformite — Set-OPS — 2026-09-11 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (67 OK · 0 echec · 1 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | +| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| 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), 1 nom(s) surveille(s) sans reference orpheline. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 107 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_resolveur_site | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). | +| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | +| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | +| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 49 groupe(s), 87 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 6 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, 34 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, 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 | 59 scripts expliques et atteignables, 116 cibles make documentees, 67 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (137 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (37 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). | +| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site 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 : 40 role(s) serveur/client tous nommes, 41 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 : 55 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | +| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 155 regle(s) du site. | +| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | +| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). | +| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos | +| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). | +| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | +| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (125 lignes). | +| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 217 regles `pass`), tous non consignes et tous motives. | +| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | +| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. | +| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | +| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. | +| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. | +| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. | +| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (68 preuves, 67 roles, 41 groupes). | +| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. | +| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). | +| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `53e2f78` (publie le 2026-09-11). | +| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | +| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | +| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 24 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ | +| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. | +| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). | +| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 6 service(s) expose(s) portent le nom du plan (6 groupe(s) derive(s)). | +| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-09-11._ diff --git a/docs/audit/wiki-publie.yml b/docs/audit/wiki-publie.yml index 75f2dfe..cc51b7b 100644 --- a/docs/audit/wiki-publie.yml +++ b/docs/audit/wiki-publie.yml @@ -1,5 +1,5 @@ --- # Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main. remote: ssh://git@forge.genese.internal/genome/set-ops-public.wiki.git -source: d1ae41c -date: 2026-09-10 +source: 53e2f78 +date: 2026-09-11 diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 165a1e1..491e729 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous. | rôles | 67 | `roles/*/` | | README de rôles | 67 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | documents | 40 | `docs/*.md` | -| pièces d'audit | 41 | `docs/audit/*` | +| pièces d'audit | 42 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | | décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` | | décisions renversées | 3 | lignes `\| **D-nn** —` du même document | diff --git a/docs/devis-services.md b/docs/devis-services.md index 9067d78..5bfa0eb 100644 --- a/docs/devis-services.md +++ b/docs/devis-services.md @@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met ## Le trou qu'il comble -`scripts/prouver.py` porte 67 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le +`scripts/prouver.py` porte 68 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est cohérent **avec lui-même** — que les handlers existent, que les intrants ont un propriétaire, que rien n'est codé en dur. diff --git a/roles/client_journal/tasks/main.yml b/roles/client_journal/tasks/main.yml index c541c28..c0ca39b 100644 --- a/roles/client_journal/tasks/main.yml +++ b/roles/client_journal/tasks/main.yml @@ -88,6 +88,10 @@ http_proxy: >- {{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }} https_proxy: "" + register: client_journal_cle_depot + retries: 5 + delay: 6 + until: client_journal_cle_depot is succeeded and (client_journal_cle_depot.size | default(0)) > 0 # `apt_repository` AJOUTE, IL NE REMPLACE PAS (mesure du 2026-09-10). # @@ -100,6 +104,29 @@ # # On retire donc explicitement la forme precedente. `state: absent` ne mord que si la # ligne existe : sur une machine neuve, cette tache ne fait rien. +- name: Mesurer la cle de signature Grafana qui vient d'etre posee + ansible.builtin.stat: + path: "{{ client_journal_depot_cle_fichier }}" + register: cle_grafana_posee + +- name: Exiger une cle NON VIDE avant de s'en servir + # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a + # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache + # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au + # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. + # + # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un + # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify + # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, + # alors que le fichier local fait zero octet. + ansible.builtin.assert: + that: + - (cle_grafana_posee.stat.size | default(0)) > 0 + fail_msg: >- + La cle de signature Grafana ({{ client_journal_depot_cle_fichier }}) fait 0 octet. + Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera + plus loin en accusant la signature : c'est ICI qu'il faut regarder. + - name: Retirer la forme HTTPS du depot (elle contournait le cache) ansible.builtin.apt_repository: repo: "{{ client_journal_depot_source | replace('http://', 'https://') }}" diff --git a/roles/client_pki/tasks/main.yml b/roles/client_pki/tasks/main.yml index 59f9530..8f7946f 100644 --- a/roles/client_pki/tasks/main.yml +++ b/roles/client_pki/tasks/main.yml @@ -115,7 +115,7 @@ register: client_pki_cle_depot_telechargee retries: 5 delay: 6 - until: client_pki_cle_depot_telechargee is succeeded + until: client_pki_cle_depot_telechargee is succeeded and (client_pki_cle_depot_telechargee.size | default(0)) > 0 # LA CLE PASSE PAR LE CACHE, ELLE AUSSI (mesure du 2026-09-10). # # `get_url` ignore la configuration d'apt : le mandataire pose dans @@ -139,6 +139,29 @@ {{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }} https_proxy: "" +- name: Mesurer la cle de signature Smallstep qui vient d'etre posee + ansible.builtin.stat: + path: "{{ client_pki_depot_cle_fichier }}" + register: cle_smallstep_posee + +- name: Exiger une cle NON VIDE avant de s'en servir + # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a + # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache + # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au + # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. + # + # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un + # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify + # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, + # alors que le fichier local fait zero octet. + ansible.builtin.assert: + that: + - (cle_smallstep_posee.stat.size | default(0)) > 0 + fail_msg: >- + La cle de signature Smallstep ({{ client_pki_depot_cle_fichier }}) fait 0 octet. + Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera + plus loin en accusant la signature : c'est ICI qu'il faut regarder. + - name: Ajouter le depot apt Smallstep ansible.builtin.template: src: smallstep.sources.j2 diff --git a/roles/serveur_collabora/tasks/main.yml b/roles/serveur_collabora/tasks/main.yml index 4f835d2..02284fd 100644 --- a/roles/serveur_collabora/tasks/main.yml +++ b/roles/serveur_collabora/tasks/main.yml @@ -83,7 +83,7 @@ register: serveur_collabora_cle retries: 5 delay: 6 - until: serveur_collabora_cle is succeeded + until: serveur_collabora_cle is succeeded and (serveur_collabora_cle.size | default(0)) > 0 # LA CLE PASSE PAR LE CACHE, ELLE AUSSI (mesure du 2026-09-10). # # `get_url` ignore la configuration d'apt : le mandataire pose dans @@ -108,6 +108,29 @@ https_proxy: "" # DEPOT PLAT : `Suites: ./`, aucun composant. Une source classique ne le trouverait pas. +- name: Mesurer la cle de signature Collabora qui vient d'etre posee + ansible.builtin.stat: + path: "{{ serveur_collabora_depot_cle_fichier }}" + register: cle_collabora_posee + +- name: Exiger une cle NON VIDE avant de s'en servir + # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a + # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache + # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au + # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. + # + # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un + # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify + # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, + # alors que le fichier local fait zero octet. + ansible.builtin.assert: + that: + - (cle_collabora_posee.stat.size | default(0)) > 0 + fail_msg: >- + La cle de signature Collabora ({{ serveur_collabora_depot_cle_fichier }}) fait 0 octet. + Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera + plus loin en accusant la signature : c'est ICI qu'il faut regarder. + - name: Ajouter le depot apt Collabora ansible.builtin.template: src: collabora.sources.j2 diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index 7301da2..b014950 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -120,6 +120,10 @@ http_proxy: >- {{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }} https_proxy: "" + register: serveur_grafana_cle_depot + retries: 5 + delay: 6 + until: serveur_grafana_cle_depot is succeeded and (serveur_grafana_cle_depot.size | default(0)) > 0 # `apt_repository` AJOUTE, IL NE REMPLACE PAS (mesure du 2026-09-10). # @@ -132,6 +136,29 @@ # # On retire donc explicitement la forme precedente. `state: absent` ne mord que si la # ligne existe : sur une machine neuve, cette tache ne fait rien. +- name: Mesurer la cle de signature Grafana qui vient d'etre posee + ansible.builtin.stat: + path: "{{ serveur_grafana_depot_cle_fichier }}" + register: cle_grafana_posee + +- name: Exiger une cle NON VIDE avant de s'en servir + # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a + # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache + # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au + # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. + # + # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un + # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify + # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, + # alors que le fichier local fait zero octet. + ansible.builtin.assert: + that: + - (cle_grafana_posee.stat.size | default(0)) > 0 + fail_msg: >- + La cle de signature Grafana ({{ serveur_grafana_depot_cle_fichier }}) fait 0 octet. + Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera + plus loin en accusant la signature : c'est ICI qu'il faut regarder. + - name: Retirer la forme HTTPS du depot (elle contournait le cache) ansible.builtin.apt_repository: repo: "{{ serveur_grafana_depot_source | replace('http://', 'https://') }}" diff --git a/roles/serveur_loki/tasks/main.yml b/roles/serveur_loki/tasks/main.yml index 243c15e..2dd1848 100644 --- a/roles/serveur_loki/tasks/main.yml +++ b/roles/serveur_loki/tasks/main.yml @@ -88,6 +88,10 @@ http_proxy: >- {{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }} https_proxy: "" + register: serveur_loki_cle_depot + retries: 5 + delay: 6 + until: serveur_loki_cle_depot is succeeded and (serveur_loki_cle_depot.size | default(0)) > 0 # `apt_repository` AJOUTE, IL NE REMPLACE PAS (mesure du 2026-09-10). # @@ -100,6 +104,29 @@ # # On retire donc explicitement la forme precedente. `state: absent` ne mord que si la # ligne existe : sur une machine neuve, cette tache ne fait rien. +- name: Mesurer la cle de signature Grafana qui vient d'etre posee + ansible.builtin.stat: + path: "{{ serveur_loki_depot_cle_fichier }}" + register: cle_grafana_posee + +- name: Exiger une cle NON VIDE avant de s'en servir + # UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a + # froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache + # a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au + # passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer. + # + # Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un + # `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify + # signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur, + # alors que le fichier local fait zero octet. + ansible.builtin.assert: + that: + - (cle_grafana_posee.stat.size | default(0)) > 0 + fail_msg: >- + La cle de signature Grafana ({{ serveur_loki_depot_cle_fichier }}) fait 0 octet. + Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera + plus loin en accusant la signature : c'est ICI qu'il faut regarder. + - name: Retirer la forme HTTPS du depot (elle contournait le cache) ansible.builtin.apt_repository: repo: "{{ serveur_loki_depot_source | replace('http://', 'https://') }}" diff --git a/scripts/prouver.py b/scripts/prouver.py index 03cffb0..52aa07d 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -3217,6 +3217,67 @@ def preuve_nom_public_vient_du_plan() -> tuple[bool, str]: f"({len(attendu)} groupe(s) derive(s)).") +def preuve_cle_telechargee_non_vide() -> tuple[bool, str]: + """Une cle de depot TELECHARGEE est mesuree avant d'etre utilisee. + + UNE REUSSITE N'EST PAS UN CONTENU (mesure le 2026-09-11). Sur une reconstruction a + froid, `get_url` a rendu 0 octet SANS ERREUR sur une machine des quatorze : le cache a + servi un 200 au corps vide. Le role a continue, satisfait. + + Le defaut ne s'est pas lu la. Il s'est lu deux cents lignes plus loin, dans un `apt` qui + accusait la SIGNATURE : « Missing key 35BAA0B3..., which is needed to verify signature ». + Un message qui envoie chercher une cle revoquee chez le fournisseur, alors que le + fichier local faisait zero octet. C'est la meme famille que l'index tronque du cache la + veille : l'octet manquant se denonce toujours ailleurs qu'ou il manque. + + La garde posee d'abord — « retirer une ressource VIDE avant de la redemander » — ne + mordait qu'au passage SUIVANT : au premier, le fichier n'existe pas encore, il n'y a + rien a retirer. Elle repare le second essai, elle ne protege pas le premier. + + Cette preuve exige donc que tout role qui TELECHARGE une cle la MESURE dans la meme + execution. Les roles qui deposent une cle embarquee (`copy` depuis `files/`) n'entrent + pas dans le compte : rien de reseau ne s'interpose, l'octet ne peut pas manquer. + """ + manquants: list[str] = [] + verifies = 0 + for chemin in sorted((RACINE / "roles").glob("*/tasks/main.yml")): + role = chemin.parent.parent.name + try: + taches = yaml.safe_load(chemin.read_text(encoding="utf-8")) or [] + except Exception: + continue + if not isinstance(taches, list): + continue + + def _mod(t: dict, nom: str) -> dict | None: + for cle in (nom, f"ansible.builtin.{nom}"): + if isinstance(t.get(cle), dict): + return t[cle] + return None + + telecharge = [t for t in taches if isinstance(t, dict) + and (_mod(t, "get_url") or {}).get("dest", "") + and "depot_cle_fichier" in str((_mod(t, "get_url") or {}).get("dest"))] + if not telecharge: + continue + verifies += 1 + mesure = any( + isinstance(t, dict) and _mod(t, "assert") + and any("stat.size" in str(c) and ">" in str(c) + for c in (_mod(t, "assert") or {}).get("that") or []) + for t in taches) + if not mesure: + manquants.append(f"{role} telecharge une cle de depot sans mesurer sa taille") + if not verifies: + return False, ("Aucun role ne telecharge de cle de depot : la preuve a perdu son " + "objet, ou le motif a change.") + if manquants: + return False, ("Cle de depot telechargee sans mesure — un corps vide passera, et " + "apt accusera la signature :\n - " + "\n - ".join(manquants)) + return True, (f"{verifies} role(s) telechargent une cle de depot, tous la mesurent " + f"avant de s'en servir.") + + PREUVES: list[dict] = [ {"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"], "cmds": [["ansible-lint", "-q"]]}, @@ -3371,6 +3432,8 @@ PREUVES: list[dict] = [ "refs": [], "func": preuve_clients_oidc_vises_sur_une_exposition}, {"id": "P67", "titre": "Nom public : le service porte celui du plan, pas celui du role", "refs": [], "func": preuve_nom_public_vient_du_plan}, + {"id": "P68", "titre": "Cle de depot telechargee : mesuree avant d'etre utilisee", + "refs": [], "func": preuve_cle_telechargee_non_vide}, ]