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