#!/bin/bash # GENERE par Set-OPS (role serveur_icingaweb2). Ne pas editer a la main. # # SONDE « vigie » — la vigie (Icinga Web 2) repond-elle, et PEUT-ELLE MONTRER quelque chose ? # # « vigie », PAS « console » (2026-09-28). Elle s'appelait `console`, alors que le # vocabulaire des plans reserve « console » a la console d'exploitation Set-OPS # (`serveur_ops`, sonde `console-ops`) et nomme ce service-ci `vigie`. Dans Icinga, # `mon-01!console` parlait donc de la vigie : le nom dit ce qu'on vient y faire. # # Mise en defaut PAR PARAMETRE : `serveur_icingaweb2_sonde_codes` pour la premiere etape, # `serveur_icingaweb2_sonde_table` (table inexistante) pour la seconde. set -uo pipefail # PREMIERE ETAPE — LA PILE WEB SERT. `check_http` vient de `monitoring-plugins-basic` : il # parle deja l'API des greffons Nagios. Sur la boucle locale et sans s'authentifier : en # mode `external`, l'ecoute est restreinte a 127.0.0.1 et une passerelle SSO se tient # devant ; la sonde frappe la ou l'application ecoute vraiment. # # `-e` PLUTOT QU'UN 200 IMPLICITE : Icinga Web 2 renvoie une REDIRECTION vers son # formulaire de connexion quand personne n'est authentifie. Exiger un 200 ferait crier la # sonde sur le comportement NORMAL d'une vigie saine. http=$(/usr/lib/nagios/plugins/check_http -H 127.0.0.1 -p {{ serveur_icingaweb2_http_port }} -u '/' \ -e '{{ serveur_icingaweb2_sonde_codes }}' -t 10 2>&1) rc=$? if (( rc != 0 )); then echo "La vigie ne sert plus ses pages : ${http:0:160}" exit "${rc}" fi # SECONDE ETAPE — CE QU'ELLE LIT (2026-09-28). « La pile PHP repond » n'etait pas la # question : une vigie dont la base est injoignable (mot de passe tourne d'un seul cote, # `pg_hba`, certificat), dont l'annuaire refuse sa liaison, ou dont le Redis est tombe, # sert tres bien son formulaire de connexion — puis une page d'erreur, ou des tableaux # vides, ou un compte connecte SANS AUCUN DROIT parce que ses groupes ne se lisent plus. # Meme lecon que `tableaux` chez Grafana. # # ON LIT LA CONFIGURATION DEPLOYEE, PAS CELLE QU'ON CROIT AVOIR POSEE : chaque ressource # que la vigie UTILISE (module icingadb, authentification, groupes) est eprouvee avec SES # identifiants, par le MEME pilote PHP (PDO pgsql, ext-ldap). Un tenant (annuaire) et le # site (base locale) passent par le meme code sans qu'on ait a le lui dire. # Les secrets restent dans les fichiers (0640 root:icingaweb2) ; rien n'est affiche. TABLE='{{ serveur_icingaweb2_sonde_table }}' php -d display_errors=stderr -- '{{ serveur_icingaweb2_config_dir }}' <<'PHP' a quoi elle sert (moteur, comptes, groupes) foreach (['modules/icingadb/config.ini' => 'moteur', 'authentication.ini' => 'comptes', 'groups.ini' => 'groupes'] as $f => $usage) { foreach ($lire($f) as $s) { if (!empty($s['resource'])) $utilisees[$s['resource']][$usage] = $usage; } } if (!array_filter($utilisees, fn($u) => isset($u['moteur']))) { echo "La vigie sert ses pages, mais aucune base du moteur n'est configuree (modules/icingadb/config.ini).\n"; exit(2); } $ko = []; $vu = []; $perf = ''; foreach ($utilisees as $nom => $usages) { $role = implode(' et ', $usages); $r = $res[$nom] ?? null; if ($r === null) { $ko[] = "$nom ($role) : ressource absente de resources.ini"; continue; } if (($r['type'] ?? '') === 'db') { $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $r['host'], $r['port'] ?? 5432, $r['dbname']); if (!empty($r['ssl_mode'])) $dsn .= ';sslmode=' . $r['ssl_mode']; if (!empty($r['ssl_ca'])) $dsn .= ';sslrootcert=' . $r['ssl_ca']; try { $db = new PDO($dsn, $r['username'], $r['password'] ?? '', [PDO::ATTR_TIMEOUT => 5, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); if (isset($usages['moteur'])) { $n = (int) $db->query('SELECT count(*) FROM ' . preg_replace('/[^a-z_]/', '', getenv('TABLE')))->fetchColumn(); if ($n === 0) { $ko[] = "$nom : la base du moteur ne contient AUCUN hote"; continue; } $vu[] = "base du moteur ($n hotes)"; $perf = " | hotes=$n"; } else { $db->query('SELECT 1')->fetchColumn(); $vu[] = "base des $role"; } } catch (Throwable $e) { $ko[] = "$nom ($role, {$r['host']}) : " . substr(preg_replace('/\s+/', ' ', $e->getMessage()), 0, 110); } } elseif (($r['type'] ?? '') === 'ldap') { $enc = $r['encryption'] ?? ''; $url = ($enc === 'ldaps' ? 'ldaps://' : 'ldap://') . $r['hostname'] . ':' . ($r['port'] ?? 389); $l = @ldap_connect($url); ldap_set_option($l, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($l, LDAP_OPT_NETWORK_TIMEOUT, (int) ($r['timeout'] ?? 5)); if ($enc === 'starttls' && !@ldap_start_tls($l)) { $ko[] = "$nom ($role) : STARTTLS refuse — " . ldap_error($l); continue; } if (!@ldap_bind($l, $r['bind_dn'] ?? null, $r['bind_pw'] ?? null)) { $ko[] = "$nom ($role, {$r['hostname']}) : liaison refusee — " . ldap_error($l); continue; } if (!@ldap_read($l, $r['root_dn'], '(objectClass=*)', ['dn'])) { $ko[] = "$nom ($role) : liee, mais {$r['root_dn']} illisible — " . ldap_error($l); continue; } $vu[] = "annuaire ($role)"; } } // L'ETAT EN DIRECT : le module icingadb lit les etats volatils dans Redis. Sans lui, la // vigie affiche une banniere d'erreur et des resultats de controle perimes. foreach ($lire('modules/icingadb/redis.ini') as $sec => $r) { if (empty($r['host'])) continue; $s = @fsockopen($r['host'], (int) ($r['port'] ?? 6380), $errno, $errstr, 3); if (!$s) { $ko[] = "Redis $sec ({$r['host']}:{$r['port']}) : $errstr"; continue; } stream_set_timeout($s, 3); fwrite($s, "PING\r\n"); $rep = trim((string) fgets($s)); fclose($s); if ($rep !== '+PONG') { $ko[] = "Redis $sec : rend « " . substr($rep, 0, 40) . " » au lieu de PONG"; continue; } $vu[] = 'Redis'; } if ($ko) { echo "La vigie sert ses pages, mais NE PEUT PLUS MONTRER ce qu'elle doit : " . implode(' ; ', $ko) . $perf . "\n"; exit(2); } echo "Vigie : pages servies ; lit " . implode(', ', $vu) . '.' . $perf . "\n"; exit(0); PHP