sondes : les cinq qui manquaient le plus (autorite, base, annuaire, zones, edge)

CORRECTION DE COMPTE D ABORD. J avais annonce cinq groupes sans sonde. La
mesure en donne VINGT-SEPT : j avais compte ceux que j avais en tete, pas
ceux que le depot contient. Il en reste vingt-deux.

LES CINQ, PAR ORDRE DE DEGAT SILENCIEUX.

autorite (step_ca) — la plus urgente : nos certificats vivent 24 h, une AC
muette ne casse rien aujourd hui et casse TOUT demain, d un coup, sur les
21 machines. Elle surveille aussi l expiration de la RACINE, que personne
ne regarde parce qu elle vit des annees (relevee a 3642 jours).

base (postgresql) — une VRAIE requete, pas pg_isready : celui-ci dit que
le port repond, pas que la base sert. Plus le compte des connexions : a
saturation, chaque application tombe sans que la base ait l air morte.

annuaire (openldap) — elle COMPTE les entrees. Un annuaire vide repond
success a tout : le mensonge des sauvegardes vides, vert et sans contenu.

zones (powerdns) — un autoritatif sans zone repond NXDOMAIN a tout, ce qui
se lit comme « ce nom n existe pas ».

edge (nginx) — elle valide la configuration SUR DISQUE : nginx sert la
derniere valide, et une configuration cassee ne se voit qu au prochain
demarrage, souvent des mois plus tard.

Les cinq eprouvees vertes sur le sain puis rouges PAR PARAMETRE, sans
toucher a un service.

Etat : 20 sondes sur 19 roles, 23 services distincts, 70 instances, 67 au
vert. Les trois autres sont connues : deux sauvegardes sans donnee a
emporter, et Loki en delai de stabilisation apres redeploiement.

make prouver : CONFORME, 64 OK, 0 echec, 0 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-10 03:47:03 -04:00
parent a5d9040a11
commit 6e46ace4de
22 changed files with 358 additions and 1 deletions

View file

@ -1,5 +1,56 @@
# CHANGELOG — Set-OPS
## 2026-09-10 (3) — Les cinq sondes qui manquaient le plus
**20 sondes sur 19 roles. 23 services distincts, 70 instances, 67 au vert.**
### D'abord une correction de compte
J'avais annonce « cinq groupes sans sonde ». Mesure : **vingt-sept**. J'avais compte ceux
que j'avais en tete, pas ceux que le depot contient. Il en reste vingt-deux.
### Les cinq posees, par ordre de degat silencieux
**`autorite` (step_ca)** — la plus urgente de l'ecosysteme. Nos certificats vivent 24 h :
une AC muette ne casse rien aujourd'hui, elle casse TOUT demain, d'un coup, sur les
vingt-et-une machines a la fois. Elle surveille aussi **l'expiration de la RACINE**, que
personne ne regarde jamais parce qu'elle vit des annees — releve : 3642 jours. Le jour ou
elle expire, toute la confiance interne tombe d'un bloc, et aucun renouvellement de
certificat d'hote n'y change rien.
**`base` (postgresql)** — une VRAIE requete, pas `pg_isready`. Celui-ci ouvre une connexion
et la ferme : il dit que le port repond, pas que la base sert. Une base en recuperation, en
lecture seule ou a court de connexions le passe et refuse tout travail. La sonde compte
aussi les connexions : a saturation, chaque application tombe en meme temps sans que la
base ait l'air morte.
**`annuaire` (openldap)** — elle COMPTE les entrees. Un annuaire vide repond `success` a
tout, et plus personne ne s'authentifie nulle part : c'est exactement le mensonge des
sauvegardes vides, vert et sans contenu.
**`zones` (powerdns)** — un autoritatif sans zone repond NXDOMAIN a tout, ce qui se lit
comme « ce nom n'existe pas ». La panne la plus trompeuse du DNS.
**`edge` (nginx)** — elle valide la configuration SUR DISQUE. nginx garde la derniere
configuration valide et continue de servir ; une configuration cassee ne se voit qu'au
prochain demarrage, c'est-a-dire au pire moment, souvent des mois plus tard.
Les cinq eprouvees vertes sur le sain, puis rouges PAR PARAMETRE — port ferme, seuil
impossible, port qui n'ecoute pas — sans toucher a un seul service.
### Ce qui reste, et pourquoi ce n'est pas le meme genre
Vingt-deux groupes. Ils ne sont pas de la meme nature :
- **couverts ailleurs** : `client_metrique` (par `collecte` chez Prometheus),
`client_backup` (par `sauvegarde`), `client_sante` (sa fraicheur EST son `ttl`) ;
- **chemins de report** : `client_smtp`, `client_artefacts`, `client_journal`,
`client_resolveur` — leur panne se voit deja par le silence de ce qu'ils portent ;
- **du site** : `serveur_cache_site`, `serveur_forge_site`, `serveur_backup_site`,
`serveur_resolveur_site`, `serveur_ops_site` — a poser depuis l'instance du site ;
- **applications et socle** : `redis`, `rspamd`, `collabora`, `web_frontal`,
`web_dorsal`, `oauth2_proxy`, `icingaweb2`, `backup`, `debian`, `durci`.
## 2026-09-10 (2) — `cache-apt` scindee, et le defaut que la scission a revele
### La scission

View file

@ -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 | 15 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_artefacts/cache-apt-vol |
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 20 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_pki/certificat, serveur_artefacts/cache-apt, serveur_artefacts/cache-apt-vol |
## Couverture des affirmations ✅ du registre

View file

@ -45,3 +45,8 @@ serveur_nginx_publier_expositions: true
serveur_nginx_proxy_buffer_size: "16k"
serveur_nginx_proxy_buffers: "8 16k"
serveur_nginx_proxy_busy_buffers_size: "32k"
# --- Sonde de supervision -----------------------------------------------------------
# Les ports que l'edge DOIT servir. Variable, donc mise en defaut en nommant un port qui
# n'ecoute pas, sans toucher a nginx.
serveur_nginx_sonde_ports: "80 443"

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: edge
ttl: 5400
raison: 'L''edge ecoute-t-il, et sa configuration est-elle encore valide ? Un nginx dont la configuration
est cassee continue de servir l''ANCIENNE tant qu''on ne le recharge pas : il fonctionne, et le
prochain redemarrage le tuera.'

View file

@ -94,3 +94,22 @@
name: "{{ serveur_nginx_service_name }}"
enabled: true
state: started
# --- 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 « edge »
ansible.builtin.template:
src: sonde-edge.sh.j2
dest: /usr/local/lib/setops/sondes/edge.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,29 @@
#jinja2: comment_start_string:'{=#', comment_end_string:'#=}'
#!/bin/bash
# GENERE par Set-OPS (role serveur_nginx). Ne pas editer a la main.
#
# SONDE « edge » — l'edge ecoute et sa configuration tient au prochain demarrage
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_nginx_sonde_ports` (port qui n'ecoute pas).
set -uo pipefail
PORTS="{{ serveur_nginx_sonde_ports }}"
systemctl is-active --quiet nginx || { echo "L'edge (nginx) n'est pas actif."; exit 2; }
# LA CONFIGURATION SUR DISQUE, PAS SEULEMENT CELLE EN MEMOIRE. nginx garde la derniere
# configuration VALIDE et continue de servir ; une configuration cassee ne se voit qu'au
# prochain redemarrage — c'est-a-dire au pire moment, souvent des mois plus tard.
nginx -t >/dev/null 2>&1 \
|| { echo "L'edge sert, mais sa configuration sur disque est INVALIDE : le prochain demarrage echouera."; exit 2; }
manquants=()
for p in ${PORTS}; do
ss -ltn 2>/dev/null | grep -qE "[:.]${p}[[:space:]]" || manquants+=("${p}")
done
if (( ${#manquants[@]} > 0 )); then
echo "L'edge n'ecoute pas sur : ${manquants[*]} — ce qui est publie derriere est injoignable."
exit 2
fi
echo "Edge : configuration valide, ecoute sur ${PORTS}."

View file

@ -60,3 +60,9 @@ serveur_openldap_ppolicy_extra: {}
# « identifiants invalides » et attend la duree de verrouillage.
serveur_openldap_ppolicy_hash_cleartext: true
serveur_openldap_ppolicy_use_lockout: false
# --- Sonde de supervision -----------------------------------------------------------
serveur_openldap_sonde_base: "{{ serveur_openldap_base_dn | default('dc=chezlepro,dc=internal') }}"
# UN MINIMUM, PAS UNE EGALITE : l'annuaire grandit. Ce chiffre dit « il reste peuple »,
# pas « il contient exactement ceci ». Trop haut, il crierait a chaque menage.
serveur_openldap_sonde_minimum: 5

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: annuaire
ttl: 5400
raison: L'annuaire repond-il, et contient-il encore ses entrees ? Un annuaire vide repond parfaitement
a toutes les requetes — et plus personne ne s'authentifie nulle part. C'est le meme mensonge qu'une
sauvegarde vide.

View file

@ -288,3 +288,22 @@
- serveur_openldap_tls_actif | bool
- serveur_openldap_cert_pont.stat.exists | default(false)
notify: Redemarrer slapd
# --- 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 « annuaire »
ansible.builtin.template:
src: sonde-annuaire.sh.j2
dest: /usr/local/lib/setops/sondes/annuaire.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,25 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_openldap). Ne pas editer a la main.
#
# SONDE « annuaire » — l'annuaire repond ET contient ses entrees
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_openldap_sonde_minimum` (seuil impossible).
set -uo pipefail
BASE={{ serveur_openldap_sonde_base }}
MINIMUM={{ serveur_openldap_sonde_minimum }}
systemctl is-active --quiet slapd || { echo "L'annuaire (slapd) n'est pas actif."; exit 2; }
# ON COMPTE LES ENTREES, ON NE SE CONTENTE PAS D'UNE REPONSE. Un annuaire vide repond
# `success` a tout — c'est exactement le mensonge des sauvegardes vides : vert, et sans
# contenu. Sans ce comptage, la sonde dirait « tout va bien » sur un annuaire efface.
n=$(ldapsearch -x -H ldapi:/// -b "${BASE}" -s sub '(objectClass=*)' dn 2>/dev/null | grep -c '^dn:')
[[ "${n}" =~ ^[0-9]+$ ]] || { echo "L'annuaire ne repond pas sous ${BASE}."; exit 2; }
if (( n < MINIMUM )); then
echo "Annuaire a ${n} entree(s) sous ${BASE}, moins que le minimum attendu (${MINIMUM}) — plus personne ne s'authentifiera.|entrees=${n};${MINIMUM};;0;"
exit 2
fi
echo "Annuaire : ${n} entree(s) sous ${BASE}.|entrees=${n};${MINIMUM};;0;"

View file

@ -49,3 +49,7 @@ serveur_postgresql_tls_dir: "/var/lib/postgresql/tls"
# Verrou zero-confiance : hostssl dans pg_hba => refuse toute connexion NON-TLS.
# N'activer qu'apres avoir confirme que TOUS les clients sont en TLS (sinon coupure).
serveur_postgresql_tls_force: false
# --- Sonde de supervision -----------------------------------------------------------
serveur_postgresql_sonde_pct_avert: 70
serveur_postgresql_sonde_pct_crit: 90

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: base
ttl: 5400
raison: 'La base repond-elle a une VRAIE requete, et lui reste-t-il des connexions ? Un PostgreSQL
a court de connexions accepte encore le port et refuse tout le monde : chaque application tombe
en meme temps sans que la base ait l''air morte.'

View file

@ -215,3 +215,22 @@
loop_control:
label: "{{ item.value.base }}"
when: serveur_postgresql_registre | 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 « base »
ansible.builtin.template:
src: sonde-base.sh.j2
dest: /usr/local/lib/setops/sondes/base.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,34 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_postgresql). Ne pas editer a la main.
#
# SONDE « base » — la base repond a une requete et garde des connexions libres
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_postgresql_sonde_pct_avert` (seuil impossible).
set -uo pipefail
AVERT={{ serveur_postgresql_sonde_pct_avert }}
CRIT={{ serveur_postgresql_sonde_pct_crit }}
systemctl is-active --quiet postgresql || { echo "PostgreSQL n'est pas actif."; exit 2; }
# UNE REQUETE, PAS UN `pg_isready`. `pg_isready` ouvre une connexion et la ferme : il dit
# que le port repond, pas que la base sert. Une base en recuperation, en lecture seule ou
# a court de connexions passe `pg_isready` et refuse tout travail.
util=$(sudo -u postgres psql -Atc 'select 1' 2>&1)
[[ "${util}" == "1" ]] || { echo "La base n'execute plus de requete : ${util:0:70}"; exit 2; }
ouvertes=$(sudo -u postgres psql -Atc 'select count(*) from pg_stat_activity' 2>/dev/null)
max=$(sudo -u postgres psql -Atc 'show max_connections' 2>/dev/null)
[[ "${ouvertes}" =~ ^[0-9]+$ && "${max}" =~ ^[0-9]+$ ]] || { echo "Base vivante, comptes de connexions illisibles."; exit 1; }
pct=$(( ouvertes * 100 / max ))
if (( pct >= CRIT )); then
echo "Connexions a ${pct}% (${ouvertes}/${max}) — les applications vont etre refusees.|conn=${ouvertes};;;0;${max}"
exit 2
fi
if (( pct >= AVERT )); then
echo "Connexions a ${pct}% (${ouvertes}/${max}), seuil ${AVERT}%.|conn=${ouvertes};;;0;${max}"
exit 1
fi
echo "Base : requete servie, ${ouvertes}/${max} connexions (${pct}%).|conn=${ouvertes};;;0;${max}"

View file

@ -80,3 +80,7 @@ serveur_powerdns_zones_inverses: >-
{{ (serveur_powerdns_inventory_groups | map('extract', groups) | select | flatten
| map('extract', hostvars, 'ansible_host') | select | list)
| zones_inverses(setops_supernet | default('')) }}
# --- Sonde de supervision -----------------------------------------------------------
# La zone souveraine et son inverse : deux au minimum.
serveur_powerdns_sonde_minimum: 2

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: zones
ttl: 5400
raison: 'L''autoritatif repond-il, et sert-il encore ses zones ? Un PowerDNS qui a perdu ses zones
repond NXDOMAIN a tout : la resolution interne s''effondre pendant que le service a l''air parfaitement
vivant.'

View file

@ -126,3 +126,22 @@
# n'existe pas encore et systemd echouerait. On saute en mode check seulement ;
# en vrai deploiement, la tache s'execute normalement.
when: 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 « zones »
ansible.builtin.template:
src: sonde-zones.sh.j2
dest: /usr/local/lib/setops/sondes/zones.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,24 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_powerdns). Ne pas editer a la main.
#
# SONDE « zones » — l'autoritatif repond et sert ses zones
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_powerdns_sonde_minimum` (seuil impossible).
set -uo pipefail
MINIMUM={{ serveur_powerdns_sonde_minimum }}
systemctl is-active --quiet pdns || { echo "L'autoritatif (pdns) n'est pas actif."; exit 2; }
[[ "$(pdns_control rping 2>&1 | head -1)" == "PONG" ]] \
|| { echo "L'autoritatif ne repond pas a son propre controle."; exit 2; }
# LES ZONES, PAS SEULEMENT LE DEMON. Un PowerDNS sans zone repond NXDOMAIN a tout, ce qui
# se lit exactement comme « ce nom n'existe pas » — la panne la plus trompeuse du DNS.
n=$(pdnsutil list-all-zones 2>/dev/null | grep -vc '^All zonecount')
[[ "${n}" =~ ^[0-9]+$ ]] || { echo "Impossible de lister les zones."; exit 2; }
if (( n < MINIMUM )); then
echo "L'autoritatif ne sert que ${n} zone(s), moins que ${MINIMUM} — la resolution interne repondra NXDOMAIN.|zones=${n};${MINIMUM};;0;"
exit 2
fi
echo "Autoritatif : ${n} zone(s) servie(s).|zones=${n};${MINIMUM};;0;"

View file

@ -25,3 +25,10 @@ serveur_step_ca_acme: true
# Secrets (Ansible Vault) — obligatoires pour l'initialisation.
serveur_step_ca_password: "{{ vault_step_ca_password | default('') }}" # rempli depuis la voute (vault_step_ca_password)
serveur_step_ca_provisioner_password: "{{ vault_step_ca_provisioner_password | default('') }}" # rempli depuis la voute (vault_step_ca_provisioner_password)
# --- Sonde de supervision -----------------------------------------------------------
serveur_step_ca_sonde_url: "https://127.0.0.1:8443"
serveur_step_ca_sonde_racine: "/etc/step/certs/root_ca.crt"
# Une racine se remplace par une operation lourde et coordonnee : trois mois d'avance
# n'est pas de la prudence, c'est le minimum pour l'organiser.
serveur_step_ca_sonde_racine_jours: 90

View file

@ -0,0 +1,8 @@
---
# Supervision derivee du role. Voir docs/supervision-conception.md.
sondes:
- nom: autorite
ttl: 5400
raison: 'L''autorite de certification repond-elle ? C''est la sonde la plus urgente de l''ecosysteme
: nos certificats vivent 24 h. Une AC muette ne casse rien aujourd''hui — elle casse TOUT demain,
d''un coup, sur les vingt-et-une machines a la fois.'

View file

@ -138,3 +138,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 « autorite »
ansible.builtin.template:
src: sonde-autorite.sh.j2
dest: /usr/local/lib/setops/sondes/autorite.sh
owner: root
group: root
mode: "0750"

View file

@ -0,0 +1,33 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_step_ca). Ne pas editer a la main.
#
# SONDE « autorite » — l'autorite de certification repond et sa racine est valide
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_step_ca_sonde_url` (port ferme).
set -uo pipefail
URL={{ serveur_step_ca_sonde_url }}
RACINE={{ serveur_step_ca_sonde_racine }}
JOURS={{ serveur_step_ca_sonde_racine_jours }}
s=$(curl -sk --max-time 10 "${URL}/health" 2>/dev/null)
printf '%s' "${s}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"' \
|| { echo "L'autorite ne repond pas sur ${URL} — plus aucun certificat ne se renouvellera, et ils vivent 24 h."; exit 2; }
# LA RACINE, DONT PERSONNE NE SURVEILLE L'EXPIRATION. Elle vit des ANNEES, donc on n'y
# pense jamais — et le jour ou elle expire, toute la confiance interne tombe d'un bloc,
# sans qu'aucun renouvellement de certificat d'hote n'y change quoi que ce soit.
if [[ -r "${RACINE}" ]]; then
fin=$(openssl x509 -in "${RACINE}" -noout -enddate 2>/dev/null | cut -d= -f2)
if [[ -n "${fin}" ]]; then
reste=$(( ( $(date -d "${fin}" +%s) - $(date +%s) ) / 86400 ))
if (( reste <= JOURS )); then
echo "AUTORITE VIVANTE, mais sa RACINE expire dans ${reste} jour(s) — toute la confiance interne tombera d'un bloc.|racine=${reste}j;${JOURS};0;;"
exit 2
fi
echo "Autorite vivante ; racine valide ${reste} jour(s).|racine=${reste}j;${JOURS};0;;"
exit 0
fi
fi
echo "Autorite vivante (racine non lisible depuis cet hote)."