From a60b69f0722d7df82bd05cde1f56dd7466a2df81 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 10 Sep 2026 17:26:35 -0400 Subject: [PATCH] un fichier vide existe, et une sonde pour les correctifs LA GARDE FICHIER ENTIER, en deux corrections. Un telechargement interrompu laisse un fichier de zero octet QUI EXISTE, et toutes les gardes demandaient seulement s il etait la. infra-mail-01 a garde une cle smallstep de 0 octet apres l epreuve hors ligne. La premiere correction n a pas suffi. Ajouter le controle de taille faisait bien s executer la tache - et le fichier faisait toujours 0 octet au passage suivant. get_url sur une destination existante emet une requete CONDITIONNELLE : l amont repond non modifie, le module rend ok, la ruine reste. Le play etait vert et ne reparait rien. Il faut effacer avant de redemander. Controle negatif : 0 -> 1022 octets, 0 erreur apt. Cinq roles. LA SONDE CORRECTIFS, 23e. Set-OPS desarme unattended-upgrades et applique les correctifs au deploiement - choix defendable, le verrou dpkg a fait decrocher une machine d une reconstruction entiere le matin meme. Mais rien ne disait QUAND le geste etait du : une flotte pouvait deriver des mois en restant verte. Elle mesure les paquets de securite en attente ET depuis quand. Elle ne lance pas apt-get update - une sonde qui rafraichit l index toutes les quinze minutes deviendrait la cause de la panne qu elle surveille. Et le seuil de 72 h est un choix d exploitation, pas une derivation : le mecanisme qui applique les correctifs est un geste humain. P64 REFUSAIT UNE DECLARATION CORRECTE. serveur_debian et serveur_durci sont des roles de declaration pure, sans une tache ; le travail est fait par les roles que leur playbook applique. La preuve exigeait declaration et depot dans le meme role - vrai des vingt-deux premieres sondes, faux des qu une sonde appartient au socle. Une garde qui force a contourner ce qu elle protege est un defaut. Elle suit desormais le playbook du groupe. Mesure : 21/21 machines vertes, 65 preuves, 23 sondes, 0 echec. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 73 +++++++++++++++++++ docs/audit/preuve-2026-09-10.md | 2 +- paquets-tiers.yml | 2 +- roles/client_journal/tasks/main.yml | 39 +++++++++- roles/client_pki/tasks/main.yml | 39 +++++++++- roles/common_packages/defaults/main.yml | 17 +++++ roles/common_packages/tasks/main.yml | 29 ++++++++ .../templates/sonde-correctifs.sh.j2 | 62 ++++++++++++++++ roles/serveur_collabora/tasks/main.yml | 39 +++++++++- roles/serveur_debian/meta/supervision.yml | 34 +++++++++ roles/serveur_grafana/tasks/main.yml | 39 +++++++++- roles/serveur_icinga/tasks/main.yml | 20 ++++- roles/serveur_loki/tasks/main.yml | 39 +++++++++- scripts/prouver.py | 27 ++++++- 14 files changed, 452 insertions(+), 9 deletions(-) create mode 100644 roles/common_packages/templates/sonde-correctifs.sh.j2 create mode 100644 roles/serveur_debian/meta/supervision.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2d6c3..c2768e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,78 @@ # CHANGELOG — Set-OPS +## 2026-09-10 (12) — Un fichier vide existe, et une sonde pour les correctifs + +### La garde « fichier entier » — et pourquoi il a fallu DEUX corrections + +Un telechargement interrompu laisse un fichier de zero octet, **qui existe**. Toutes les +gardes de ce depot demandaient *« ce fichier est-il la ? »* : + + - name: Cette ressource est-elle deja recuperee ? + ansible.builtin.stat: ... + - name: Telecharger la cle + when: not ..._present.stat.exists + +`infra-mail-01` a garde une cle smallstep de **0 octet** apres l'epreuve hors ligne. Treize +machines portaient 1022 octets, elle portait le vide — et `apt` refusait le depot. + +**La premiere correction n'a pas suffi.** Ajouter le controle de taille a bien fait +s'executer la tache (`ok: [infra-mail-01]`) — et le fichier faisait **toujours 0 octet au +passage suivant**. `get_url` sur une destination existante emet une requete CONDITIONNELLE : +l'amont repond « non modifie », le module rend `ok`, la ruine reste. *Le play etait vert et +ne reparait rien.* + +Il faut donc **effacer avant de redemander**. `state: absent` ne mord que sur un fichier +vide : une cle valide n'est jamais retiree. + +Controle negatif : fichier vide volontairement, role rejoue → **0 → 1022 octets, 0 erreur +apt**. Cinq roles portent le patron complet. + +*Cette etape intermediaire est la lecon de la journee en miniature : un `failed=0` ne dit +pas que quelque chose a ete fait.* + +### La sonde `correctifs` — 23e sonde + +Set-OPS **desarme** `unattended-upgrades` (masque sur les vingt et une machines) et applique +les correctifs par `upgrade: full` au passage du socle. Choix defendable — un minuteur de +fond qui se dispute le verrou `dpkg` avec un deploiement est un tirage au sort, et il a fait +decrocher `infra-mail-01` d'une reconstruction entiere le matin meme. + +Mais rien ne disait **quand le geste etait du**. Vingt-deux sondes, aucune sur le retard de +securite : une flotte pouvait deriver des mois en restant verte. + +Elle mesure les paquets en attente venant d'un depot de SECURITE, **et depuis quand** — le +retard seul ne dit rien, c'est sa duree qui transforme un correctif publie en exposition +acceptee. Trois etats eprouves sur une copie : `rc=0` a jour, `rc=1` des le premier +correctif, `rc=2` au-dela du seuil. + +**Deux choix dits franchement.** Elle ne lance PAS `apt-get update` : une sonde qui +rafraichit l'index toutes les quinze minutes deviendrait la cause de la panne qu'elle +surveille. Et le seuil de 72 h est un CHOIX D'EXPLOITATION, pas une derivation — les autres +seuils se deduisent d'un mecanisme (le certificat vit 24 h, donc on alerte a 6 h) ; ici le +mecanisme est un geste humain, il n'y a rien a en deduire. + +### P64 refusait une declaration correcte + +Declaree dans `common_packages`, la sonde etait **invisible d'Icinga** : la derivation croise +les GROUPES, et ce role n'en est pas un. Pire, `client_sante` l'aurait retiree comme +orpheline au passage suivant — la garde ecrite le matin meme. + +Mais la declarer dans `serveur_debian` faisait echouer P64, qui exigeait declaration et depot +dans le MEME role. Or `serveur_debian` et `serveur_durci` sont des roles de **declaration +pure** : ils portent `flux.yml`, `authentification.yml`, `supervision.yml`, et pas une tache. +Le travail est fait par les roles que leur playbook applique. + +**Une garde qui force a contourner ce qu'elle protege est un defaut.** P64 suit desormais le +playbook du groupe : ce qu'il applique compte comme depose. Elle ne s'affaiblit pas — elle +apprend ou le depot a le droit de vivre. Controle negatif refait : depot desactive → refus, +restaure → 65 OK. + +### Etat mesure + + sonde `correctifs` 21 / 21 machines vertes + prouver 65 preuves, 23 sondes, 0 echec + ansible-lint 0 defaut + ## 2026-09-10 (11) — Le trou reste ouvert derriere la porte qu'on croyait fermee Troisieme reconstruction complete de Chezlepro : **32 minutes, un seul echec — le mien**, diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md index 8390446..4ec76a9 100644 --- a/docs/audit/preuve-2026-09-10.md +++ b/docs/audit/preuve-2026-09-10.md @@ -76,7 +76,7 @@ | 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 | 22 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 23 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. | ## Couverture des affirmations ✅ du registre diff --git a/paquets-tiers.yml b/paquets-tiers.yml index 8938bc4..ff35d86 100644 --- a/paquets-tiers.yml +++ b/paquets-tiers.yml @@ -55,5 +55,5 @@ depots: # `alloy` est universel comme `step-cli` : `client_metrique` le pose partout. paquets: alloy: 1.19.2-1 - grafana: 13.2.0 + grafana: 13.2.1 loki: 3.7.7 diff --git a/roles/client_journal/tasks/main.yml b/roles/client_journal/tasks/main.yml index b28b376..c541c28 100644 --- a/roles/client_journal/tasks/main.yml +++ b/roles/client_journal/tasks/main.yml @@ -16,13 +16,50 @@ # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Gr) ansible.builtin.stat: path: "{{ client_journal_depot_cle_fichier }}" register: telecharger_la_cle_de_signature_grafana_present +# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). +# +# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui +# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend +# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien +# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un +# `force: yes` manuel l'a reparee en une seconde. +# +# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un +# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge +# pas ce qu'on possede deja » reste intacte. +- name: Retirer une ressource VIDE avant de la redemander + ansible.builtin.file: + path: "{{ client_journal_depot_cle_fichier }}" + state: absent + when: + - telecharger_la_cle_de_signature_grafana_present.stat.exists + - (telecharger_la_cle_de_signature_grafana_present.stat.size | default(0)) == 0 + - name: Telecharger la cle de signature Grafana - when: not telecharger_la_cle_de_signature_grafana_present.stat.exists + when: not (telecharger_la_cle_de_signature_grafana_present.stat.exists and telecharger_la_cle_de_signature_grafana_present.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ client_journal_depot_cle_url }}" dest: "{{ client_journal_depot_cle_fichier }}" diff --git a/roles/client_pki/tasks/main.yml b/roles/client_pki/tasks/main.yml index f5d9a46..59f9530 100644 --- a/roles/client_pki/tasks/main.yml +++ b/roles/client_pki/tasks/main.yml @@ -58,6 +58,24 @@ # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Sm) ansible.builtin.stat: path: "{{ client_pki_depot_cle_fichier }}" @@ -66,8 +84,27 @@ # Un serveur tiers intermittent ne doit pas arreter un deploiement de quarante # minutes (mesure du 2026-08-23 : la meme URL pend, puis rend 200 en 0,48 s au # second essai). Defaut de `get_url` : 10 s et aucune reprise. +# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). +# +# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui +# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend +# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien +# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un +# `force: yes` manuel l'a reparee en une seconde. +# +# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un +# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge +# pas ce qu'on possede deja » reste intacte. +- name: Retirer une ressource VIDE avant de la redemander + ansible.builtin.file: + path: "{{ client_pki_depot_cle_fichier }}" + state: absent + when: + - telecharger_la_cle_de_signature_smallste_present.stat.exists + - (telecharger_la_cle_de_signature_smallste_present.stat.size | default(0)) == 0 + - name: Telecharger la cle de signature Smallstep - when: not telecharger_la_cle_de_signature_smallste_present.stat.exists + when: not (telecharger_la_cle_de_signature_smallste_present.stat.exists and telecharger_la_cle_de_signature_smallste_present.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ client_pki_depot_cle_url }}" dest: "{{ client_pki_depot_cle_fichier }}" diff --git a/roles/common_packages/defaults/main.yml b/roles/common_packages/defaults/main.yml index 2719ed9..24eacd7 100644 --- a/roles/common_packages/defaults/main.yml +++ b/roles/common_packages/defaults/main.yml @@ -75,3 +75,20 @@ common_packages_unites_maj_auto: - apt-daily.timer - apt-daily-upgrade.timer - unattended-upgrades.service + + +# --- Sonde de supervision (declaree dans roles/serveur_debian/meta/supervision.yml) --- +common_packages_sonde_correctifs_etat: "/var/lib/setops/correctifs-vus" + +# LE SEUIL N'EST PAS DERIVE D'UN MECANISME, ET IL FAUT LE DIRE. +# +# Les autres seuils du depot se derivent de ce que la machine fait : le certificat vit +# 24 h, donc on alerte a 6 h ; le porteur passe toutes les 15 min, donc le `ttl` vaut six +# periodes. Ici, le mecanisme qui applique les correctifs est un DEPLOIEMENT — un geste +# humain. Aucune periode a en deriver. +# +# 72 h est donc un CHOIX D'EXPLOITATION, pas une deduction : trois jours laissent passer +# un week-end sans crier, et au-dela un correctif publie devient une exposition connue et +# acceptee. En dessous du seuil la sonde AVERTIT deja — elle ne se tait jamais sur un +# correctif de securite en attente, elle gradue seulement l'urgence. +common_packages_sonde_seuil_heures: 72 diff --git a/roles/common_packages/tasks/main.yml b/roles/common_packages/tasks/main.yml index 17495cc..be64939 100644 --- a/roles/common_packages/tasks/main.yml +++ b/roles/common_packages/tasks/main.yml @@ -68,3 +68,32 @@ ansible.builtin.apt: autoremove: true lock_timeout: "{{ common_packages_lock_timeout }}" + + +# LA SONDE SE DEPOSE ICI, ELLE SE DECLARE AILLEURS (`roles/serveur_debian/meta/`). +# Ce role est celui qui APPLIQUE les mises a jour ; il est donc le bon porteur. Mais la +# derivation des services d'Icinga croise les GROUPES, et `common_packages` n'en est pas +# un — d'ou la separation, expliquee dans la declaration. +- 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: Assurer le repertoire d etat des sondes + ansible.builtin.file: + path: /var/lib/setops + state: directory + owner: root + group: root + mode: "0750" + +- name: Deposer la sonde des correctifs de securite + ansible.builtin.template: + src: sonde-correctifs.sh.j2 + dest: /usr/local/lib/setops/sondes/correctifs.sh + owner: root + group: root + mode: "0750" diff --git a/roles/common_packages/templates/sonde-correctifs.sh.j2 b/roles/common_packages/templates/sonde-correctifs.sh.j2 new file mode 100644 index 0000000..2a4d59a --- /dev/null +++ b/roles/common_packages/templates/sonde-correctifs.sh.j2 @@ -0,0 +1,62 @@ +#!/bin/bash +# GENERE par Set-OPS (role common_packages). Ne pas editer a la main. +# +# SONDE « correctifs » — des correctifs de securite attendent-ils, et depuis quand ? +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `common_packages_sonde_seuil_heures` (mettre 0). +set -uo pipefail + +ETAT={{ common_packages_sonde_correctifs_etat }} +SEUIL_H={{ common_packages_sonde_seuil_heures }} + +# ON NE MET PAS A JOUR L'INDEX ICI, ET C'EST VOULU. Une sonde qui declenche un +# `apt-get update` a chaque passage ferait tourner le reseau et le verrou dpkg toutes les +# quinze minutes — elle deviendrait la cause de la panne qu'elle surveille. L'index est +# rafraichi par le deploiement ; la sonde lit ce que la machine SAIT. +simule=$(LC_ALL=C apt-get -s dist-upgrade 2>/dev/null | grep '^Inst') || true +total=$(printf '%s\n' "${simule}" | grep -c . ) +[[ -n "${simule}" ]] || total=0 + +# UN CORRECTIF DE SECURITE SE RECONNAIT A SON DEPOT D'ORIGINE, pas a son nom. `apt-get -s` +# rend l'origine entre parentheses ; on y cherche le mot que Debian y met. +secu=$(printf '%s\n' "${simule}" | grep -ci 'security' || true) +: "${secu:=0}" + +maintenant=$(date +%s) +depuis=0 +if [[ -r "${ETAT}" ]]; then + depuis=$(tr -cd '0-9' < "${ETAT}" 2>/dev/null) +fi +: "${depuis:=0}" + +perf="en_attente=${total} securite=${secu}" + +if (( secu == 0 )); then + # PLUS RIEN EN ATTENTE : on oublie la date, sinon le prochain correctif heriterait de + # l'anciennete du precedent et naitrait deja en retard. + rm -f "${ETAT}" 2>/dev/null || true + if (( total > 0 )); then + echo "Aucun correctif de securite en attente (${total} mise(s) a jour ordinaire(s)). | ${perf}" + exit 0 + fi + echo "A jour : aucune mise a jour en attente. | ${perf}" + exit 0 +fi + +# PREMIERE FOIS QU'ON LES VOIT : on note la date, on ne crie pas encore. Un correctif +# publie il y a trois minutes n'est pas une negligence. +if (( depuis == 0 )); then + printf '%s' "${maintenant}" > "${ETAT}" 2>/dev/null || true + depuis=${maintenant} +fi + +age_h=$(( (maintenant - depuis) / 3600 )) +perf="${perf} age_heures=${age_h}" + +if (( age_h >= SEUIL_H )); then + echo "CORRECTIFS DE SECURITE EN ATTENTE depuis ${age_h} h : ${secu} paquet(s). Deployer le socle. | ${perf}" + exit 2 +fi +echo "${secu} correctif(s) de securite en attente depuis ${age_h} h (seuil ${SEUIL_H} h). | ${perf}" +exit 1 diff --git a/roles/serveur_collabora/tasks/main.yml b/roles/serveur_collabora/tasks/main.yml index 0225e7b..4f835d2 100644 --- a/roles/serveur_collabora/tasks/main.yml +++ b/roles/serveur_collabora/tasks/main.yml @@ -29,13 +29,50 @@ # Récupérée UNE FOIS, comme la clé Smallstep : une plateforme souveraine ne recontacte pas # un serveur étranger à chaque déploiement pour une clé déjà installée. Une rotation amont # ne passe pas inaperçue pour autant — `apt` refuse alors le dépôt, bruyamment. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (cle Collabora) ansible.builtin.stat: path: "{{ serveur_collabora_depot_cle_fichier }}" register: serveur_collabora_cle_presente +# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). +# +# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui +# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend +# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien +# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un +# `force: yes` manuel l'a reparee en une seconde. +# +# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un +# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge +# pas ce qu'on possede deja » reste intacte. +- name: Retirer une ressource VIDE avant de la redemander + ansible.builtin.file: + path: "{{ serveur_collabora_depot_cle_fichier }}" + state: absent + when: + - serveur_collabora_cle_presente.stat.exists + - (serveur_collabora_cle_presente.stat.size | default(0)) == 0 + - name: Telecharger la cle de signature Collabora - when: not serveur_collabora_cle_presente.stat.exists + when: not (serveur_collabora_cle_presente.stat.exists and serveur_collabora_cle_presente.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ serveur_collabora_depot_cle_url }}" dest: "{{ serveur_collabora_depot_cle_fichier }}" diff --git a/roles/serveur_debian/meta/supervision.yml b/roles/serveur_debian/meta/supervision.yml new file mode 100644 index 0000000..7d15bb8 --- /dev/null +++ b/roles/serveur_debian/meta/supervision.yml @@ -0,0 +1,34 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +# +# CE QUE CETTE SONDE MESURE, ET POURQUOI ELLE MANQUAIT. +# +# Set-OPS DESARME les mises a jour automatiques de Debian — `unattended-upgrades` est +# masque sur les vingt et une machines — et les applique lui-meme, par `upgrade: full` +# au passage du socle. C'est un choix defendable : un minuteur de fond qui se dispute le +# verrou `dpkg` avec un deploiement est un tirage au sort, et il a fait decrocher +# `infra-mail-01` d'une reconstruction entiere le 2026-09-10. +# +# Mais ce choix a une consequence que rien ne rattrapait : **les correctifs n'arrivent +# que si quelqu'un deploie**. Vingt-deux sondes couvraient les certificats, les journaux, +# les metriques, les sauvegardes, les bases, l'annuaire, les boites — aucune ne disait +# qu'une machine prenait du retard de securite. Une flotte pouvait deriver des mois en +# restant verte. +# +# CE QU'ON NE MESURE PAS, ET C'EST DELIBERE : +# +# - « le minuteur a-t-il tourne ? » — il est masque, exprès ; mesurer son inactivite +# serait mesurer une decision, pas un risque. +# - « la machine est-elle a jour ? » — trop vague : un paquet d'agrement en retard +# n'est pas un correctif de securite en attente. +# +# CE QU'ON MESURE : les paquets en attente venant d'un depot de SECURITE, et depuis +# COMBIEN DE TEMPS. Le retard seul ne dit rien — c'est sa DUREE qui transforme un +# correctif publie en exposition acceptee. +sondes: + - nom: correctifs + ttl: 5400 + raison: >- + Des correctifs de securite attendent-ils d'etre appliques, et depuis quand ? + Set-OPS desarme les mises a jour automatiques et les applique au deploiement : + sans cette mesure, rien ne dit quand le geste est du. diff --git a/roles/serveur_grafana/tasks/main.yml b/roles/serveur_grafana/tasks/main.yml index 8d430fa..7301da2 100644 --- a/roles/serveur_grafana/tasks/main.yml +++ b/roles/serveur_grafana/tasks/main.yml @@ -48,13 +48,50 @@ # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Gr) ansible.builtin.stat: path: "{{ serveur_grafana_depot_cle_fichier }}" register: telecharger_la_cle_de_signature_grafana_present +# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). +# +# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui +# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend +# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien +# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un +# `force: yes` manuel l'a reparee en une seconde. +# +# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un +# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge +# pas ce qu'on possede deja » reste intacte. +- name: Retirer une ressource VIDE avant de la redemander + ansible.builtin.file: + path: "{{ serveur_grafana_depot_cle_fichier }}" + state: absent + when: + - telecharger_la_cle_de_signature_grafana_present.stat.exists + - (telecharger_la_cle_de_signature_grafana_present.stat.size | default(0)) == 0 + - name: Telecharger la cle de signature Grafana - when: not telecharger_la_cle_de_signature_grafana_present.stat.exists + when: not (telecharger_la_cle_de_signature_grafana_present.stat.exists and telecharger_la_cle_de_signature_grafana_present.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ serveur_grafana_depot_cle_url }}" dest: "{{ serveur_grafana_depot_cle_fichier }}" diff --git a/roles/serveur_icinga/tasks/main.yml b/roles/serveur_icinga/tasks/main.yml index 4c6c7e0..470f4ab 100644 --- a/roles/serveur_icinga/tasks/main.yml +++ b/roles/serveur_icinga/tasks/main.yml @@ -8,13 +8,31 @@ # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger le trousseau de cles I) ansible.builtin.stat: path: "/tmp/icinga-archive-keyring.deb" register: telecharger_le_trousseau_de_cles_icinga_present - name: Telecharger le trousseau de cles Icinga - when: not telecharger_le_trousseau_de_cles_icinga_present.stat.exists + when: not (telecharger_le_trousseau_de_cles_icinga_present.stat.exists and telecharger_le_trousseau_de_cles_icinga_present.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ serveur_icinga_keyring_url }}" dest: "/tmp/icinga-archive-keyring.deb" diff --git a/roles/serveur_loki/tasks/main.yml b/roles/serveur_loki/tasks/main.yml index d47ed03..81b4d88 100644 --- a/roles/serveur_loki/tasks/main.yml +++ b/roles/serveur_loki/tasks/main.yml @@ -16,13 +16,50 @@ # CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle # ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour # forcer le rafraichissement : supprimer le fichier et rejouer le role. +# UN FICHIER VIDE EXISTE (mesure du 2026-09-10). +# +# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503, +# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache +# est donc sautee a tous les passages suivants, et la machine garde une ressource morte +# pour toujours, en silence. +# +# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une +# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide : +# +# E: Le depot http://packages.smallstep.com/... n'est pas signe. +# +# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332 +# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il +# se fait passer pour un succes. +# +# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? », +# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours. - name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Gr) ansible.builtin.stat: path: "{{ serveur_loki_depot_cle_fichier }}" register: telecharger_la_cle_de_signature_grafana_present +# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10). +# +# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui +# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend +# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien +# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un +# `force: yes` manuel l'a reparee en une seconde. +# +# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un +# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge +# pas ce qu'on possede deja » reste intacte. +- name: Retirer une ressource VIDE avant de la redemander + ansible.builtin.file: + path: "{{ serveur_loki_depot_cle_fichier }}" + state: absent + when: + - telecharger_la_cle_de_signature_grafana_present.stat.exists + - (telecharger_la_cle_de_signature_grafana_present.stat.size | default(0)) == 0 + - name: Telecharger la cle de signature Grafana - when: not telecharger_la_cle_de_signature_grafana_present.stat.exists + when: not (telecharger_la_cle_de_signature_grafana_present.stat.exists and telecharger_la_cle_de_signature_grafana_present.stat.size | default(0) > 0) ansible.builtin.get_url: url: "{{ serveur_loki_depot_cle_url }}" dest: "{{ serveur_loki_depot_cle_fichier }}" diff --git a/scripts/prouver.py b/scripts/prouver.py index b6a4716..fb2fd5b 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -2209,6 +2209,29 @@ def preuve_sondes_declarees_et_deposees() -> tuple[bool, str]: continue taches = (roles / role / "tasks" / "main.yml") corps = taches.read_text(encoding="utf-8") if taches.is_file() else "" + # UN ROLE DE GROUPE N'A PAS DE TACHES, ET C'EST LA STRUCTURE DU DEPOT (2026-09-10). + # + # `serveur_debian` et `serveur_durci` sont des roles de DECLARATION : ils portent + # `flux.yml`, `authentification.yml`, `supervision.yml` — et pas une seule tache. + # Le travail est fait par les roles que leur playbook de groupe APPLIQUE + # (`common_packages`, `hardening_packages`, `auditd`…). + # + # Cette preuve exigeait que la declaration et le depot vivent dans le MEME role. + # C'etait vrai des vingt-deux premieres sondes — toutes portees par des roles qui + # sont a la fois groupe et travail — et faux des qu'une sonde appartient au SOCLE. + # Elle refusait alors une declaration correcte, ce qui est le pire defaut d'une + # garde : forcer a contourner ce qu'elle protege. + # + # On suit donc le playbook du groupe : ce qu'il applique compte comme depose. La + # garde ne s'affaiblit pas — elle apprend ou le depot a le droit de vivre. + if not corps.strip(): + playbook = RACINE / "playbooks" / "groupes" / f"{role}.yml" + if playbook.is_file(): + for applique in re.findall(r"^\s+- ([a-z][a-z0-9_]*)\s*$", + playbook.read_text(encoding="utf-8"), re.M): + t = roles / applique / "tasks" / "main.yml" + if t.is_file(): + corps += t.read_text(encoding="utf-8") for s in sondes: nom = s.get("nom") if not nom: @@ -2228,6 +2251,8 @@ def preuve_sondes_declarees_et_deposees() -> tuple[bool, str]: mesures.append(f"{role}/{nom}") # L'AUTRE SENS : un script depose que rien ne declare. + # Les sondes declarees PARTOUT, y compris par un role de groupe qui delegue son depot. + _declarees_globalement = {m.split("/", 1)[1] for m in mesures} for taches in sorted(roles.glob("*/tasks/main.yml")): role = taches.parent.parent.name corps = taches.read_text(encoding="utf-8") @@ -2237,7 +2262,7 @@ def preuve_sondes_declarees_et_deposees() -> tuple[bool, str]: if meta.is_file(): data = yaml.safe_load(meta.read_text(encoding="utf-8")) or {} declarees = {s.get("nom") for s in (data.get("sondes") or [])} - for orpheline in sorted(deposees - declarees): + for orpheline in sorted(deposees - declarees - _declarees_globalement): fautes.append(f"{role}/{orpheline} : deposee mais absente de " f"`meta/supervision.yml` — le compte d'API la refuserait")