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