From 6120e6f006998a2f71e7a3e8666b384d43e82297 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 10 Sep 2026 03:00:21 -0400 Subject: [PATCH] sondes : quatorze, une par role, derivees en objets Icinga MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit boites (dovecot) · cache-apt (artefacts) · certificat (client_pki, 14/14) collaboration (nextcloud) · collecte (prometheus) · file-courriel (postfix) forge (forgejo) · identite (keycloak) · ingestion (loki) · moteur (icinga) resolution (resolveur) · runner (serveur_ops) · tableaux (grafana) voute (ops_tenant) — toutes vertes, sans une ligne ecrite dans Icinga. DEUX PRINCIPES QUE LA PREMIERE SONDE A IMPOSES. Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE : cible et seuils sont des variables du role, on prouve le rouge avec un port ferme ou un seuil impossible, sans rien casser. Et la sonde vit LA OU VIT LA VERITE : « ce noeud est-il collecte ? » appartient a prometheus, pas au client — une seule y voit les N noeuds, et surtout elle voit le cas SILENCIEUX. ON DEMANDE AU SERVICE CE QU IL PENSE DE LUI-MEME quand il sait le dire (healthz, /ready, status.php, decouverte OIDC). Quand il ne sait pas, on va chercher la verite de terrain : « moteur » ne regarde ni le service ni le port, il demande a la base depuis combien de temps elle n a pas ete rafraichie — la lecon des sauvegardes appliquee a la supervision. QUATRE FOIS J AI ECRIT LA SONDE AVANT DE MESURER, QUATRE FOIS ELLE A EU TORT. La forge : port et chemin des depots inventes, elle ecoute en 3000 derriere l edge et n a legitimement aucun depot. Loki : « panne persistante » conclue sur deux lectures a quelques secondes d intervalle juste apres un redemarrage — deux mesures rapprochees ne distinguent pas un etat d un instant. Keycloak : vise en 8443, il ecoute en 8080. Le runner : git en root refuse un depot d un autre proprietaire. A chaque fois le remede est le meme — lire la verite du role, ne pas la supposer. Et le meme piege Jinja qu avec client_sante : ${#tableau[@]} contient {#. Le remede etait deja au depot ; je l ai reecrit au lieu de le chercher. RESTE : client_smtp, client_artefacts, client_journal, icingaweb2 et ops_site. Ce sont des chemins de report, dont la panne se voit deja par le silence des sondes qu ils portent. make prouver : CONFORME, 64 OK, 0 echec, 0 saute (P64 : 14 sondes). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- CHANGELOG.md | 69 +++++++++++++++++++ docs/audit/preuve-2026-09-10.md | 2 +- roles/serveur_dovecot/defaults/main.yml | 6 ++ roles/serveur_dovecot/meta/supervision.yml | 8 +++ roles/serveur_dovecot/tasks/main.yml | 19 +++++ .../templates/sonde-boites.sh.j2 | 22 ++++++ roles/serveur_icinga/defaults/main.yml | 7 ++ roles/serveur_icinga/meta/supervision.yml | 9 +++ roles/serveur_icinga/tasks/main.yml | 19 +++++ .../templates/sonde-moteur.sh.j2 | 39 +++++++++++ roles/serveur_keycloak/defaults/main.yml | 6 ++ roles/serveur_keycloak/meta/supervision.yml | 8 +++ roles/serveur_keycloak/tasks/main.yml | 19 +++++ .../templates/sonde-identite.sh.j2 | 24 +++++++ roles/serveur_nextcloud/defaults/main.yml | 3 + roles/serveur_nextcloud/meta/supervision.yml | 8 +++ roles/serveur_nextcloud/tasks/main.yml | 19 +++++ .../templates/sonde-collaboration.sh.j2 | 35 ++++++++++ roles/serveur_ops/defaults/main.yml | 5 ++ roles/serveur_ops/meta/supervision.yml | 8 +++ roles/serveur_ops/tasks/main.yml | 19 +++++ .../serveur_ops/templates/sonde-runner.sh.j2 | 43 ++++++++++++ roles/serveur_ops_tenant/defaults/main.yml | 8 +++ roles/serveur_ops_tenant/meta/supervision.yml | 8 +++ roles/serveur_ops_tenant/tasks/main.yml | 19 +++++ .../templates/sonde-voute.sh.j2 | 24 +++++++ roles/serveur_postfix/defaults/main.yml | 7 ++ roles/serveur_postfix/meta/supervision.yml | 8 +++ roles/serveur_postfix/tasks/main.yml | 19 +++++ .../templates/sonde-file-courriel.sh.j2 | 29 ++++++++ 30 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 roles/serveur_dovecot/meta/supervision.yml create mode 100644 roles/serveur_dovecot/templates/sonde-boites.sh.j2 create mode 100644 roles/serveur_icinga/meta/supervision.yml create mode 100644 roles/serveur_icinga/templates/sonde-moteur.sh.j2 create mode 100644 roles/serveur_keycloak/meta/supervision.yml create mode 100644 roles/serveur_keycloak/templates/sonde-identite.sh.j2 create mode 100644 roles/serveur_nextcloud/meta/supervision.yml create mode 100644 roles/serveur_nextcloud/templates/sonde-collaboration.sh.j2 create mode 100644 roles/serveur_ops/meta/supervision.yml create mode 100644 roles/serveur_ops/templates/sonde-runner.sh.j2 create mode 100644 roles/serveur_ops_tenant/meta/supervision.yml create mode 100644 roles/serveur_ops_tenant/templates/sonde-voute.sh.j2 create mode 100644 roles/serveur_postfix/meta/supervision.yml create mode 100644 roles/serveur_postfix/templates/sonde-file-courriel.sh.j2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da394c..555b667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,74 @@ # CHANGELOG — Set-OPS +## 2026-09-10 — Chaque role porte desormais sa sonde + +**Quatorze sondes**, declarees dans `meta/supervision.yml`, deposees par le role qui +possede la verite, derivees en objets Icinga sans qu'une ligne soit ecrite a la main : + + boites (dovecot) certificat (client_pki, 14/14) collaboration (nextcloud) + cache-apt (artefacts) collecte (prometheus) file-courriel (postfix) + forge (forgejo) identite (keycloak) ingestion (loki) + moteur (icinga) resolution (resolveur) runner (serveur_ops) + tableaux (grafana) voute (ops_tenant) + +Les dix-neuf lignes `surveillance:` ecrites en prose et jamais executees commencent a +devenir des mesures. + +### Deux principes que la premiere sonde a imposes + +**Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE.** Cible et seuils sont des +variables du role : on prouve le rouge avec un port ferme ou un seuil impossible, sur une +machine reelle, sans rien casser, aussi souvent qu'on veut. Une sonde qu'on ne peut +prouver qu'en cassant un service ne sera prouvee qu'une fois. Eprouve sur `cache-apt` : +vert, CRITIQUE par port ferme, AVERTISSEMENT par seuil, retour au vert. + +**La sonde vit la ou vit la verite.** *« Ce noeud est-il collecte ? »* est une sonde de +`serveur_prometheus`, pas de `client_metrique` : une seule y voit les N noeuds, et surtout +elle voit le cas **silencieux** — celui qui a cesse d'etre collecte ne peut pas s'en +plaindre lui-meme. + +### On demande au service ce qu'il pense de lui-meme + +Quand il sait le dire : `/api/healthz` de Forgejo (base + cache), `/api/health` de Grafana, +`/ready` de Loki, `status.php` de Nextcloud, la decouverte OIDC de Keycloak. C'est plus +juste que tout critere invente de l'exterieur — une forge dont la base est tombee sert +encore ses pages et repond a `/api/v1/version`. + +Et quand il ne sait pas : on va chercher la verite de terrain. `moteur` ne regarde ni le +service ni le port — il demande a la base **depuis combien de temps elle n'a pas ete +rafraichie**. C'est la lecon des sauvegardes appliquee a la supervision elle-meme : un +moteur vert dont la synchronisation est tombee affiche eternellement le dernier etat connu. + +### Quatre fois j'ai ecrit la sonde avant de mesurer, quatre fois elle a eu tort + +**La forge** : port 443 et chemin des depots INVENTES. Elle ecoute en 3000 derriere l'edge +et n'a legitimement AUCUN depot — elle est neuve. Deux cris sur un service sain. + +**Loki** : conclu « panne persistante » sur deux lectures prises a quelques secondes +d'intervalle, juste apres un redemarrage. L'anneau etait `ACTIVE` et la reponse est passee +a `ready` moins d'une minute plus tard. *Deux mesures rapprochees ne distinguent pas un +etat d'un instant.* Le delai de stabilisation est desormais un AVERTISSEMENT nomme. + +**Keycloak** : vise en 8443, il ecoute en 8080 derriere l'edge. + +**Le runner** : `git` en root refuse les depots d'un autre proprietaire — la sonde aurait +rendu un echec qui parle de git au lieu de parler du depot. + +À chaque fois le remede est le meme : **lire la verite du role, ne pas la supposer**. + +### Et le meme piege Jinja, une seconde fois + +`${#tableau[@]}` contient `{#`. Le remede etait deja au depot depuis `client_sante` ; je +l'ai reecrit au lieu de le chercher. La correction balaie desormais tous les gabarits de +sonde. + +### Ce qui reste + +`client_smtp`, `client_artefacts`, `client_journal`, `serveur_icingaweb2` et +`serveur_ops_site` n'ont pas encore la leur. Le premier lot couvre les services dont la +chute arrete l'ecosysteme ; ceux-la sont des chemins de report, dont la panne se voit +deja par le silence des sondes qu'ils portent. + ## 2026-09-09 (9) — La supervision passe juste apres la PKI (D-86) *On n'allume pas la lumiere une fois la maison finie.* diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md index cfe43f1..d1e9500 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 | 7 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_forgejo/forge, serveur_g | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 14 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_dovecot/boites, serveur | ## Couverture des affirmations ✅ du registre diff --git a/roles/serveur_dovecot/defaults/main.yml b/roles/serveur_dovecot/defaults/main.yml index dad2465..186c3ba 100644 --- a/roles/serveur_dovecot/defaults/main.yml +++ b/roles/serveur_dovecot/defaults/main.yml @@ -50,3 +50,9 @@ serveur_dovecot_sasl_port: 12345 # cote emetteur : l'un sans l'autre, la livraison interne s'arrete — soit elle part en # clair, soit elle est differee sans fin. serveur_dovecot_lmtp_tls: true + +# --- Sonde de supervision ----------------------------------------------------------- +# UN COMPTE QUI DOIT EXISTER. Le resoudre prouve la chaine entiere : demon, configuration, +# bind LDAP, mappage userdb. Variable, donc mise en defaut avec un nom inexistant sans +# toucher a l'annuaire. +serveur_dovecot_sonde_utilisateur: "daniel" diff --git a/roles/serveur_dovecot/meta/supervision.yml b/roles/serveur_dovecot/meta/supervision.yml new file mode 100644 index 0000000..05a1123 --- /dev/null +++ b/roles/serveur_dovecot/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: boites + ttl: 5400 + raison: 'Dovecot repond-il, et sait-il encore RESOUDRE un utilisateur dans l''annuaire ? Un Dovecot + dont le lien LDAP est tombe accepte les connexions et refuse toutes les authentifications : le service + a l''air debout et plus personne n''entre.' diff --git a/roles/serveur_dovecot/tasks/main.yml b/roles/serveur_dovecot/tasks/main.yml index d5293d7..a232121 100644 --- a/roles/serveur_dovecot/tasks/main.yml +++ b/roles/serveur_dovecot/tasks/main.yml @@ -154,3 +154,22 @@ enabled: true state: started daemon_reload: true + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « boites » + ansible.builtin.template: + src: sonde-boites.sh.j2 + dest: /usr/local/lib/setops/sondes/boites.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_dovecot/templates/sonde-boites.sh.j2 b/roles/serveur_dovecot/templates/sonde-boites.sh.j2 new file mode 100644 index 0000000..88e2012 --- /dev/null +++ b/roles/serveur_dovecot/templates/sonde-boites.sh.j2 @@ -0,0 +1,22 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_dovecot). Ne pas editer a la main. +# +# SONDE « boites » — Dovecot repond et resout un utilisateur dans l'annuaire +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_dovecot_sonde_utilisateur` (compte inexistant). +set -uo pipefail + +UTILISATEUR={{ serveur_dovecot_sonde_utilisateur }} + +systemctl is-active --quiet dovecot || { echo "Dovecot n'est pas actif."; exit 2; } + +# ON RESOUT UN UTILISATEUR REEL, PAS UN PING DE SERVICE. C'est le bind LDAP qui tombe le +# plus souvent, et il ne se voit que quand on demande un compte : le demon reste debout, +# le port reste ouvert, et toute authentification echoue. +sortie=$(doveadm user "${UTILISATEUR}" 2>&1) +if ! printf '%s' "${sortie}" | grep -qE '^uid[[:space:]]'; then + echo "Dovecot ne resout pas « ${UTILISATEUR} » — l'annuaire ne repond plus : ${sortie:0:70}" + exit 2 +fi +echo "Dovecot : « ${UTILISATEUR} » resolu dans l'annuaire, service actif." diff --git a/roles/serveur_icinga/defaults/main.yml b/roles/serveur_icinga/defaults/main.yml index 07e10e5..1270270 100644 --- a/roles/serveur_icinga/defaults/main.yml +++ b/roles/serveur_icinga/defaults/main.yml @@ -106,3 +106,10 @@ serveur_icinga_sondes: {} # Vide, aucun destinataire n'est pose et le deploiement le SIGNALE — plutot que de # laisser croire qu'une alerte partira. serveur_icinga_destinataire: "" + +# --- Sonde de supervision ----------------------------------------------------------- +serveur_icinga_sonde_config_db: /etc/icingadb/config.yml +# Les controles passifs ont un `ttl` de 45 min au plus long ; une base qui n'a rien recu +# depuis deux fois cela ne reflete plus rien. +serveur_icinga_sonde_age_avert: 900 +serveur_icinga_sonde_age_crit: 5400 diff --git a/roles/serveur_icinga/meta/supervision.yml b/roles/serveur_icinga/meta/supervision.yml new file mode 100644 index 0000000..fa3091b --- /dev/null +++ b/roles/serveur_icinga/meta/supervision.yml @@ -0,0 +1,9 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: moteur + ttl: 5400 + raison: 'L''etat des controles arrive-t-il encore jusqu''a la base ? On ne mesure ni le service ni + le port : on mesure la FRAICHEUR de ce que la base contient. Un moteur vert dont la synchronisation + est tombee affiche eternellement le dernier etat connu — une supervision figee qui a l''air d''aller + bien.' diff --git a/roles/serveur_icinga/tasks/main.yml b/roles/serveur_icinga/tasks/main.yml index e31d477..9eceec3 100644 --- a/roles/serveur_icinga/tasks/main.yml +++ b/roles/serveur_icinga/tasks/main.yml @@ -350,3 +350,22 @@ `root@localhost`, que personne ne lit. La supervision VERRA les defauts sans pouvoir les dire. Declarer l'adresse au plan. when: serveur_icinga_destinataire | length == 0 + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « moteur » + ansible.builtin.template: + src: sonde-moteur.sh.j2 + dest: /usr/local/lib/setops/sondes/moteur.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_icinga/templates/sonde-moteur.sh.j2 b/roles/serveur_icinga/templates/sonde-moteur.sh.j2 new file mode 100644 index 0000000..e81c298 --- /dev/null +++ b/roles/serveur_icinga/templates/sonde-moteur.sh.j2 @@ -0,0 +1,39 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_icinga). Ne pas editer a la main. +# +# SONDE « moteur » — l'etat des controles arrive frais jusqu'a IcingaDB +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_icinga_sonde_age_crit` (age impossible). +set -uo pipefail + +CONF={{ serveur_icinga_sonde_config_db }} +AVERT={{ serveur_icinga_sonde_age_avert }} +CRIT={{ serveur_icinga_sonde_age_crit }} + +systemctl is-active --quiet icinga2 || { echo "Le moteur Icinga n'est pas actif."; exit 2; } +systemctl is-active --quiet icingadb || { echo "La synchronisation IcingaDB n'est pas active."; exit 2; } + +# ON NE SE FIE PAS A `is-active` SEUL — c'est le mensonge que ce depot a deja paye avec +# les sauvegardes : un service vert sur un travail qui n'aboutit pas. On demande a la BASE +# depuis combien de temps elle n'a pas ete rafraichie. +hote=$(grep -m1 -A5 '^database:' "${CONF}" 2>/dev/null | grep -m1 'host:' | awk '{print $2}') +mdp=$(grep -m1 'password:' "${CONF}" 2>/dev/null | awk '{print $2}') +base=$(grep -m1 -A5 '^database:' "${CONF}" 2>/dev/null | grep -m1 'database:' | awk '{print $2}') +util=$(grep -m1 -A6 '^database:' "${CONF}" 2>/dev/null | grep -m1 'user:' | awk '{print $2}') +[[ -n "${hote}" && -n "${mdp}" ]] || { echo "Configuration IcingaDB illisible (${CONF})."; exit 2; } + +age=$(PGPASSWORD="${mdp}" psql -h "${hote}" -U "${util:-icingadb}" -d "${base:-icingadb}" -Atc \ + "select coalesce(extract(epoch from now()) - max(last_update)/1000, 999999)::bigint from service_state" 2>/dev/null) +[[ "${age}" =~ ^-?[0-9]+$ ]] || { echo "La base de supervision ne repond pas (${hote})."; exit 2; } +(( age < 0 )) && age=0 + +if (( age >= CRIT )); then + echo "Etat FIGE : rien de neuf en base depuis ${age} s — la supervision montre le passe.|age=${age}s;${AVERT};${CRIT};0;" + exit 2 +fi +if (( age >= AVERT )); then + echo "Etat vieillissant : ${age} s sans mise a jour (seuil ${AVERT}).|age=${age}s;${AVERT};${CRIT};0;" + exit 1 +fi +echo "Moteur et synchronisation vivants : base rafraichie il y a ${age} s.|age=${age}s;${AVERT};${CRIT};0;" diff --git a/roles/serveur_keycloak/defaults/main.yml b/roles/serveur_keycloak/defaults/main.yml index 74aaf3f..7c1de87 100644 --- a/roles/serveur_keycloak/defaults/main.yml +++ b/roles/serveur_keycloak/defaults/main.yml @@ -220,3 +220,9 @@ serveur_keycloak_cache_local: "{{ setops_cache_artefacts | default(lookup('env', # il exige une relecture humaine, pas un remplacement silencieux. serveur_keycloak_signature_empreinte: "861AB50E8CC6611FB6BC01A6B8F12EA26FD6EEBA" serveur_keycloak_signature_url: "{{ serveur_keycloak_url }}.asc" + +# --- Sonde de supervision ----------------------------------------------------------- +# Keycloak ecoute EN CLAIR derriere l'edge : viser le port TLS en dur casserait sur toute +# installation derriere un proxy (verifie le 2026-09-10 : 8080, pas 8443). +serveur_keycloak_sonde_url: "http://127.0.0.1:8080" +serveur_keycloak_sonde_realm: "{{ serveur_keycloak_realm }}" diff --git a/roles/serveur_keycloak/meta/supervision.yml b/roles/serveur_keycloak/meta/supervision.yml new file mode 100644 index 0000000..318d61c --- /dev/null +++ b/roles/serveur_keycloak/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: identite + ttl: 5400 + raison: Le realm sert-il encore sa decouverte OIDC ? C'est la porte par laquelle tout le SSO de l'ecosysteme + entre. Un Keycloak debout dont le realm n'a pas charge repond sur son port et refuse toute connexion + — sans rien dire. diff --git a/roles/serveur_keycloak/tasks/main.yml b/roles/serveur_keycloak/tasks/main.yml index e7765f3..abc5350 100644 --- a/roles/serveur_keycloak/tasks/main.yml +++ b/roles/serveur_keycloak/tasks/main.yml @@ -324,3 +324,22 @@ when: - serveur_keycloak_account_theme | length > 0 - not ansible_check_mode + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « identite » + ansible.builtin.template: + src: sonde-identite.sh.j2 + dest: /usr/local/lib/setops/sondes/identite.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_keycloak/templates/sonde-identite.sh.j2 b/roles/serveur_keycloak/templates/sonde-identite.sh.j2 new file mode 100644 index 0000000..e1f322b --- /dev/null +++ b/roles/serveur_keycloak/templates/sonde-identite.sh.j2 @@ -0,0 +1,24 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_keycloak). Ne pas editer a la main. +# +# SONDE « identite » — le realm sert sa decouverte OIDC +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_keycloak_sonde_realm` (realm inexistant). +set -uo pipefail + +URL={{ serveur_keycloak_sonde_url }} +REALM={{ serveur_keycloak_sonde_realm }} + +s=$(curl -s --max-time 10 "${URL}/realms/${REALM}/.well-known/openid-configuration" 2>/dev/null) +[[ -n "${s}" ]] || { echo "Keycloak ne repond pas sur ${URL}."; exit 2; } +printf '%s' "${s}" | grep -q '"issuer"' \ + || { echo "Le realm « ${REALM} » ne sert pas sa decouverte OIDC : ${s:0:70}"; exit 2; } + +# LES POINTS D'ENTREE QUE LES CLIENTS UTILISENT VRAIMENT. Un document de decouverte +# tronque se lit comme du JSON valide et casse chaque client a la premiere redirection. +for cle in authorization_endpoint token_endpoint jwks_uri; do + printf '%s' "${s}" | grep -q "\"${cle}\"" \ + || { echo "Decouverte OIDC incomplete pour « ${REALM} » : ${cle} absent."; exit 2; } +done +echo "Realm « ${REALM} » : decouverte OIDC complete." diff --git a/roles/serveur_nextcloud/defaults/main.yml b/roles/serveur_nextcloud/defaults/main.yml index f9c26c9..184e9ba 100644 --- a/roles/serveur_nextcloud/defaults/main.yml +++ b/roles/serveur_nextcloud/defaults/main.yml @@ -120,3 +120,6 @@ serveur_nextcloud_marqueur_admin: "/etc/setops-nextcloud-admin-sha256" # puis pousse par SSH : aucun flux nouveau, et l'artefact devient deployable hors ligne. # Surchargeable par `setops_cache_artefacts` pour partager un cache commun. serveur_nextcloud_cache_local: "{{ setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops') }}" + +# --- Sonde de supervision ----------------------------------------------------------- +serveur_nextcloud_sonde_url: "http://127.0.0.1" diff --git a/roles/serveur_nextcloud/meta/supervision.yml b/roles/serveur_nextcloud/meta/supervision.yml new file mode 100644 index 0000000..59dd4fd --- /dev/null +++ b/roles/serveur_nextcloud/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: collaboration + ttl: 5400 + raison: Nextcloud est-il installe, hors maintenance et sans migration en attente ? Ces trois etats + se lisent d'un coup dans `status.php`, et chacun rend le service inutilisable sans que la page d'accueil + cesse de repondre. diff --git a/roles/serveur_nextcloud/tasks/main.yml b/roles/serveur_nextcloud/tasks/main.yml index 7797b38..766f4e7 100644 --- a/roles/serveur_nextcloud/tasks/main.yml +++ b/roles/serveur_nextcloud/tasks/main.yml @@ -53,3 +53,22 @@ - name: Identite visuelle Alliance Boreale (theme + ciel etoile) ansible.builtin.import_tasks: theme.yml when: serveur_nextcloud_branding | bool + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « collaboration » + ansible.builtin.template: + src: sonde-collaboration.sh.j2 + dest: /usr/local/lib/setops/sondes/collaboration.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_nextcloud/templates/sonde-collaboration.sh.j2 b/roles/serveur_nextcloud/templates/sonde-collaboration.sh.j2 new file mode 100644 index 0000000..9519f2d --- /dev/null +++ b/roles/serveur_nextcloud/templates/sonde-collaboration.sh.j2 @@ -0,0 +1,35 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_nextcloud). Ne pas editer a la main. +# +# SONDE « collaboration » — installe, hors maintenance, sans migration en attente +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_nextcloud_sonde_url` (port ferme). +set -uo pipefail + +URL={{ serveur_nextcloud_sonde_url }} + +s=$(curl -sk --max-time 10 "${URL}/status.php" 2>/dev/null) +[[ -n "${s}" ]] || { echo "Nextcloud ne repond pas sur ${URL}."; exit 2; } + +verdict=$(printf '%s' "${s}" | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + print("2|status.php illisible"); raise SystemExit +if not d.get("installed"): + print("2|instance NON INSTALLEE") +elif d.get("maintenance"): + print("2|en MODE MAINTENANCE — personne ne peut travailler") +elif d.get("needsDbUpgrade"): + print("2|migration de base EN ATTENTE — le service tourne sur un schema perime") +else: + print("0|version " + str(d.get("versionstring") or d.get("version") or "?")) +' 2>/dev/null) + +code=${verdict%%|*}; texte=${verdict#*|} +[[ -n "${code}" ]] || { echo "Verdict Nextcloud illisible."; exit 2; } +[[ "${code}" == "0" ]] && { echo "Nextcloud : ${texte}, installe et disponible."; exit 0; } +echo "Nextcloud EN DEFAUT — ${texte}." +exit 2 diff --git a/roles/serveur_ops/defaults/main.yml b/roles/serveur_ops/defaults/main.yml index 040c4cc..6cefdff 100644 --- a/roles/serveur_ops/defaults/main.yml +++ b/roles/serveur_ops/defaults/main.yml @@ -182,3 +182,8 @@ serveur_ops_cache_roues: >- {{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/roues-{{ (lookup('file', serveur_ops_roues_source) ~ serveur_ops_ansible) | hash('sha1') | truncate(12, true, '') }} serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne" + +# --- Sonde de supervision ----------------------------------------------------------- +serveur_ops_sonde_racine: /opt/setops +serveur_ops_sonde_venv: /opt/setops/venv +serveur_ops_sonde_branche: main diff --git a/roles/serveur_ops/meta/supervision.yml b/roles/serveur_ops/meta/supervision.yml new file mode 100644 index 0000000..b38005c --- /dev/null +++ b/roles/serveur_ops/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: runner + ttl: 5400 + raison: 'Le poste d''exploitation peut-il encore agir ? Deux choses : ses depots sont-ils PROPRES + et sur leur branche (une modification locale fait diverger le runner du depot que tout le monde + lit), et Ansible repond-il dans son venv.' diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index c1d9c62..fe47c11 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -520,3 +520,22 @@ owner: "{{ serveur_ops_utilisateur }}" group: "{{ serveur_ops_utilisateur }}" mode: "0644" + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « runner » + ansible.builtin.template: + src: sonde-runner.sh.j2 + dest: /usr/local/lib/setops/sondes/runner.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_ops/templates/sonde-runner.sh.j2 b/roles/serveur_ops/templates/sonde-runner.sh.j2 new file mode 100644 index 0000000..2ec85e6 --- /dev/null +++ b/roles/serveur_ops/templates/sonde-runner.sh.j2 @@ -0,0 +1,43 @@ +#jinja2: comment_start_string:'{=#', comment_end_string:'#=}' +#!/bin/bash +# (Le gabarit redefinit le delimiteur de commentaire Jinja : `${#tableau[@]}` contient +# la sequence `{#`, que Jinja lirait comme un debut de commentaire. Le shell en a +# besoin ; c'est donc a Jinja de s'ecarter.) +# GENERE par Set-OPS (role serveur_ops). Ne pas editer a la main. +# +# SONDE « runner » — depots propres sur leur branche, et Ansible repond +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_ops_sonde_venv` (chemin inexistant). +set -uo pipefail + +VENV={{ serveur_ops_sonde_venv }} +BRANCHE={{ serveur_ops_sonde_branche }} +RACINE={{ serveur_ops_sonde_racine }} + +# `safe.directory` : les depots appartiennent au compte d'exploitation, la sonde tourne en +# root. Sans cela git refuse — « proprietaire douteux » — et la sonde rendrait un echec +# qui parle de git au lieu de parler du depot. +git_() { git -c safe.directory='*' -C "$1" "${@:2}" 2>/dev/null; } + +ennuis=(); n=0 +for d in "${RACINE}"/*/; do + [[ -d "${d}.git" ]] || continue + n=$((n+1)); nom=$(basename "${d}") + b=$(git_ "${d}" rev-parse --abbrev-ref HEAD) + [[ "${b}" == "${BRANCHE}" ]] || ennuis+=("${nom} sur « ${b:-?} »") + # UN DEPOT SALE SUR LE RUNNER EST UNE DIVERGENCE SILENCIEUSE : ce qui s'applique a la + # flotte n'est plus ce que le depot contient, et personne ne le sait. + [[ -z "$(git_ "${d}" status --porcelain)" ]] || ennuis+=("${nom} MODIFIE localement") +done + +(( n > 0 )) || { echo "Aucun depot sous ${RACINE} — le runner n'a rien a appliquer."; exit 2; } + +v=$("${VENV}/bin/ansible" --version 2>/dev/null | head -1) +[[ -n "${v}" ]] || { echo "Ansible ne repond pas dans ${VENV} — le runner ne peut rien appliquer."; exit 2; } + +if (( ${#ennuis[@]} > 0 )); then + echo "Runner : ${ennuis[*]} — ce qui s'applique diverge du depot.|depots=${n}" + exit 2 +fi +echo "Runner : ${n} depot(s) propres sur « ${BRANCHE} », ${v}.|depots=${n}" diff --git a/roles/serveur_ops_tenant/defaults/main.yml b/roles/serveur_ops_tenant/defaults/main.yml index ca1230a..4146cb3 100644 --- a/roles/serveur_ops_tenant/defaults/main.yml +++ b/roles/serveur_ops_tenant/defaults/main.yml @@ -79,3 +79,11 @@ serveur_ops_tenant_cle_deposer: false serveur_ops_tenant_cle_source: "" serveur_ops_tenant_cle_destination: >- {{ serveur_ops_tenant_racine }}/.config/setops-vault-{{ serveur_ops_tenant_depot | lower }} + +# --- Sonde de supervision ----------------------------------------------------------- +# Le chemin ou le RUNNER lit la voute de son ecosysteme. +# LE DEPOT VIENT DU ROLE, PAS D'UNE SUPPOSITION : `serveur_ops_tenant_depot` designe +# deja le dossier clone sur le runner. +serveur_ops_tenant_sonde_depot: "{{ serveur_ops_tenant_depot }}" +serveur_ops_tenant_sonde_voute: >- + /opt/setops/{{ serveur_ops_tenant_sonde_depot }}/inventories/principal/group_vars/all/vault.yml diff --git a/roles/serveur_ops_tenant/meta/supervision.yml b/roles/serveur_ops_tenant/meta/supervision.yml new file mode 100644 index 0000000..897ff2a --- /dev/null +++ b/roles/serveur_ops_tenant/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: voute + ttl: 5400 + raison: 'La voute de l''ecosysteme est-elle presente ET CHIFFREE la ou le runner la lit ? Une voute + en clair sur un runner est la fuite la plus discrete qui soit : rien ne casse, tout continue de + fonctionner, et les secrets sont lisibles par quiconque accede a la machine.' diff --git a/roles/serveur_ops_tenant/tasks/main.yml b/roles/serveur_ops_tenant/tasks/main.yml index f986959..d3fa6ad 100644 --- a/roles/serveur_ops_tenant/tasks/main.yml +++ b/roles/serveur_ops_tenant/tasks/main.yml @@ -177,3 +177,22 @@ when: - serveur_ops_tenant_cle_deposer | bool - not ansible_check_mode + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « voute » + ansible.builtin.template: + src: sonde-voute.sh.j2 + dest: /usr/local/lib/setops/sondes/voute.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_ops_tenant/templates/sonde-voute.sh.j2 b/roles/serveur_ops_tenant/templates/sonde-voute.sh.j2 new file mode 100644 index 0000000..c6dd980 --- /dev/null +++ b/roles/serveur_ops_tenant/templates/sonde-voute.sh.j2 @@ -0,0 +1,24 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_ops_tenant). Ne pas editer a la main. +# +# SONDE « voute » — la voute est presente et chiffree +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_ops_tenant_sonde_voute` (chemin inexistant). +set -uo pipefail + +VOUTE={{ serveur_ops_tenant_sonde_voute }} + +[[ -f "${VOUTE}" ]] || { echo "Aucune voute en ${VOUTE} — le runner ne peut pas deployer."; exit 2; } + +# LA PRESENCE NE SUFFIT PAS, ET C'EST TOUT L'INTERET DE CETTE SONDE. Un fichier existe +# aussi bien chiffre qu'en clair ; seul son entete le dit. +head -c 20 "${VOUTE}" | grep -q '^\$ANSIBLE_VAULT' \ + || { echo "LA VOUTE EST EN CLAIR : ${VOUTE} — secrets lisibles sur ce runner."; exit 2; } + +perm=$(stat -c '%a' "${VOUTE}" 2>/dev/null) +if [[ "${perm}" != "600" && "${perm}" != "640" && "${perm}" != "400" ]]; then + echo "Voute chiffree mais lisible trop largement (${perm}) : ${VOUTE}" + exit 1 +fi +echo "Voute presente et chiffree (${perm})." diff --git a/roles/serveur_postfix/defaults/main.yml b/roles/serveur_postfix/defaults/main.yml index b06b0ca..e528e63 100644 --- a/roles/serveur_postfix/defaults/main.yml +++ b/roles/serveur_postfix/defaults/main.yml @@ -61,3 +61,10 @@ serveur_postfix_tls_dir: "/etc/postfix/tls" # Zero-confiance : verifier le TLS de la livraison LMTP vers Dovecot (cert step-ca). serveur_postfix_lmtp_tls_verify: false serveur_postfix_lmtp_tls_cafile: "/etc/step/certs/root_ca.crt" + +# --- Sonde de supervision ----------------------------------------------------------- +# UN COURRIEL EN ATTENTE N'EST PAS UNE PANNE — un relais distant momentanement muet suffit +# a en garder quelques-uns. C'est l'ACCUMULATION qui parle. Les seuils sont bas parce que +# cet ecosysteme n'emet presque rien : dix messages en attente y sont deja anormaux. +serveur_postfix_sonde_avert: 10 +serveur_postfix_sonde_crit: 50 diff --git a/roles/serveur_postfix/meta/supervision.yml b/roles/serveur_postfix/meta/supervision.yml new file mode 100644 index 0000000..7530aa5 --- /dev/null +++ b/roles/serveur_postfix/meta/supervision.yml @@ -0,0 +1,8 @@ +--- +# Supervision derivee du role. Voir docs/supervision-conception.md. +sondes: + - nom: file-courriel + ttl: 5400 + raison: Combien de courriels attendent dans la file ? Une file qui monte est le premier signe d'une + remise qui ne se fait plus — et c'est un signe qu'on voit AVANT que quiconque se plaigne de ne rien + recevoir. diff --git a/roles/serveur_postfix/tasks/main.yml b/roles/serveur_postfix/tasks/main.yml index b7a6805..00bb086 100644 --- a/roles/serveur_postfix/tasks/main.yml +++ b/roles/serveur_postfix/tasks/main.yml @@ -214,3 +214,22 @@ enabled: true state: started daemon_reload: true + +# --- Sonde de supervision (docs/supervision-conception.md) -------------------------- +# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la +# fait tourner et pousse le verdict, sans savoir ce qu'elle mesure. +- 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: Deposer la sonde « file-courriel » + ansible.builtin.template: + src: sonde-file-courriel.sh.j2 + dest: /usr/local/lib/setops/sondes/file-courriel.sh + owner: root + group: root + mode: "0750" diff --git a/roles/serveur_postfix/templates/sonde-file-courriel.sh.j2 b/roles/serveur_postfix/templates/sonde-file-courriel.sh.j2 new file mode 100644 index 0000000..ebd22eb --- /dev/null +++ b/roles/serveur_postfix/templates/sonde-file-courriel.sh.j2 @@ -0,0 +1,29 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_postfix). Ne pas editer a la main. +# +# SONDE « file-courriel » — la file d'attente ne s'accumule pas +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_postfix_sonde_avert` / `_crit` (seuils impossibles). +set -uo pipefail + +AVERT={{ serveur_postfix_sonde_avert }} +CRIT={{ serveur_postfix_sonde_crit }} + +systemctl is-active --quiet postfix || { echo "Postfix n'est pas actif."; exit 2; } + +# `mailq` PLUTOT QUE LE COMPTE DE FICHIERS : la file vit dans plusieurs repertoires +# (incoming, active, deferred, hold) et seul Postfix sait ce qu'il compte comme en +# attente. Un `find` sur `/var/spool` donnerait un chiffre plausible et faux. +n=$(mailq 2>/dev/null | grep -cE '^[0-9A-F]{6,}' || true) +diff=$(mailq 2>/dev/null | grep -cE '^[0-9A-F]+\*?[[:space:]]' || true) + +if (( n >= CRIT )); then + echo "File de courriel : ${n} message(s) en attente — la remise ne se fait plus.|file=${n};${AVERT};${CRIT};0;" + exit 2 +fi +if (( n >= AVERT )); then + echo "File de courriel : ${n} message(s) en attente (seuil ${AVERT}).|file=${n};${AVERT};${CRIT};0;" + exit 1 +fi +echo "File de courriel : ${n} message(s) en attente.|file=${n};${AVERT};${CRIT};0;"