observatoire : un seul nom pour la console, des deux cotes

grafana.chezlepro.internal et tableaux.genese.internal nommaient le meme
service de deux facons. Les deux deviennent observatoire.

Un nom de produit dans une URL se grave aussi dans les SAN du certificat et
dans les URI de redirection du SSO : remplacer Grafana obligerait alors a
renommer le service. Le nom dit desormais la fonction.

Le renommage a revele que les URI des clients Keycloak repetent a la main les
FQDN declares dans expose:. Rien ne les reliait. P66 garde ce lien, ecrite en
meme temps que le premier renommage.

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 20:21:13 -04:00
parent c733d2df1c
commit 2450c9ea65
9 changed files with 111 additions and 13 deletions

View file

@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
## Écrire, puis relire (D-68)
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
C'est aussi ce que font les 65 preuves de `make prouver` : elles lisent le dépôt, sans le
C'est aussi ce que font les 66 preuves de `make prouver` : elles lisent le dépôt, sans le
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
dépôt annonce.**

View file

@ -1,5 +1,46 @@
# CHANGELOG — Set-OPS
## 2026-09-10 (17) — La console d'observabilite s'appelle pareil des deux cotes
Deux ecosystemes, deux noms pour le meme service : `grafana.chezlepro.internal` chez le
locataire, `tableaux.genese.internal` au site. Le second etait de notre fait, pose le jour
meme en montant la pile d'observabilite du site.
grafana.chezlepro.internal -> observatoire.chezlepro.internal
tableaux.genese.internal -> observatoire.genese.internal
### Pourquoi pas `grafana`
Un nom de produit dans une URL se grave ailleurs qu'a l'ecran : dans les SAN du certificat,
dans les URI de redirection du SSO, dans les signets de l'exploitant. Remplacer Grafana
obligerait alors a renommer le service — donc a refaire le certificat et le client Keycloak
pour une raison qui n'a rien a voir avec eux.
Le plan du locataire nommait deja quatre services par leur FONCTION (`auth`, `cloud`,
`bureau`, `forge`) et deux par leur PRODUIT (`grafana`, `icinga`). Celui-ci rejoint le
registre majoritaire. `icinga` reste, pour l'instant : c'est un autre geste.
### Ce que le renommage a revele
Les URI de redirection des clients Keycloak **repetent a la main** les FQDN que
`plan/applications.yml` declare dans `expose:`. Rien ne les relie. Renommer l'exposition
regenere `hosts.yml`, le certificat et la zone DNS — et laisse le client OIDC viser l'ancien
nom. Keycloak ne proteste pas : c'est l'utilisateur qui le decouvre au retour du SSO.
**P66** (`preuve_clients_oidc_vises_sur_une_exposition`) : chaque `redirect_uris` et chaque
`web_origins` doit viser un FQDN qu'une application expose. Ecrite EN MEME TEMPS que le
renommage, parce que c'est le seul moment ou l'on sait encore que les deux listes existent.
P66 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose
### Une erreur de lecture, au passage
En cherchant pourquoi `tableaux.genese.internal` ne resolvait pas depuis le poste, on a
conclu que le nom n'etait pas dans la zone. Il y etait — PowerDNS le publiait correctement,
comme `sauvegarde.genese.internal`. Ce qui ne les connaissait pas, c'est le RESOLVEUR DU
POSTE (`192.168.10.10`), qui porte trois entrees faites a la main et aucune delegation de
`genese.internal`. L'index consulte n'etait pas la source.
## 2026-09-10 (16) — La frontiere journalise a nouveau, et une garde veille cette fois
Doctrine posee par l'exploitant : *des lors que le site a son Loki, la journalisation de la

View file

@ -88,7 +88,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Vis le SSO | Ouvre https://grafana.chezlepro.internal → « *Se connecter avec Chezlepro* » → testmail. Puis ouvre Forgejo, puis Icinga : tu n'es reconnecté nulle part. | 👁 observe | — |
| 1 | Vis le SSO | Ouvre https://observatoire.chezlepro.internal → « *Se connecter avec Chezlepro* » → testmail. Puis ouvre Forgejo, puis Icinga : tu n'es reconnecté nulle part. | 👁 observe | — |
| 2 | Observe le flux | Rouvre Grafana en navigation privée, ouvre les outils dév (F12 → Réseau) : repère la redirection vers Keycloak, puis le retour avec un code=. C'est ① en action. | 👁 observe | — |
| 3 | Interroge l'annuaire (la source de vérité) | Sur un nœud avec ldap-utils : « commande » Tu vois l'entrée que Keycloak fédère — il ne l'a pas recopiée. | 👁 observe | — |
| 4 | Casse & répare (la fédération) | Dans la console admin Keycloak → *User Federation* → désactive le fournisseur LDAP. Reconnecte-toi : échec (l'IdP ne voit plus l'annuaire). Réactive : ça remarche. Tu viens de *sentir* la dépendance requise entre l'IdP et l'annuaire. | 🔨 casse-répare | — |
@ -184,7 +184,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
|---|---|---|---|---|
| 1 | Interroge les métriques | (sur obs-01) — combien de nœuds scrapés, tous UP ? « commande » | 👁 observe | — |
| 2 | Vois les journaux | : curl -s http://localhost:3100/loki/api/v1/label/host/values → les nœuds qui expédient leurs logs. | 👁 observe | — |
| 3 | Ouvre Grafana | (https://grafana.chezlepro.internal) — métriques *et* logs au même endroit. | 👁 observe | — |
| 3 | Ouvre Grafana | (https://observatoire.chezlepro.internal) — métriques *et* logs au même endroit. | 👁 observe | — |
| 4 | Casse & répare | Arrête prometheus-node-exporter sur un nœud : dans Prometheus, sa cible passe up=0 (DOWN). Redémarre : elle repasse UP. Tu *sens* que c'est l'agent qui nourrit le serveur (modèle pull). | 🔨 casse-répare | — |
*Source : [Métriques & journaux](../../wiki/M%C3%A9triques-et-journaux.md) · § À toi de jouer.*
@ -205,7 +205,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Route par nom | Deux noms, un seul edge (10.17.16.11) : « commande » Change grafana en forge : même IP, backend différent. C'est le routage par SNI. | 👁 observe | — |
| 2 | Vois la terminaison TLS | Le certificat présenté est celui de l'edge (avec les SAN des exposés) : openssl s_client -connect 10.17.16.11:443 -servername grafana.chezlepro.internal \| openssl x509 -noout -text \| grep -A1 'Subject Alternative'. | 👁 observe | — |
| 2 | Vois la terminaison TLS | Le certificat présenté est celui de l'edge (avec les SAN des exposés) : openssl s_client -connect 10.17.16.11:443 -servername observatoire.chezlepro.internal \| openssl x509 -noout -text \| grep -A1 'Subject Alternative'. | 👁 observe | — |
| 3 | Casse & répare | Arrête le backend (ex. systemctl stop grafana-server sur obs-01) et rouvre Grafana : l'edge répond 502 Bad Gateway (le proxy est là, le service non). Redémarre : ça remarche. Tu distingues le proxy de ce qu'il sert. | 🔨 casse-répare | — |
*Source : [Reverse-proxy & TLS](../../wiki/Reverse-proxy-et-TLS.md) · § À toi de jouer.*

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (65 OK · 0 echec · 0 saute)
- **Verdict** : ✅ CONFORME (65 OK · 0 echec · 1 saute)
## Preuves
@ -28,7 +28,7 @@
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 34 groupes (inventaire dechiffre et parse). |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
@ -69,7 +69,7 @@
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (65 preuves, 67 roles, 41 groupes). |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (66 preuves, 67 roles, 41 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `2d9dc86` (publie le 2026-09-09). |
@ -78,6 +78,7 @@
| 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 | 24 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
## Couverture des affirmations ✅ du registre

View file

@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
## Le trou qu'il comble
`scripts/prouver.py` porte 65 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
`scripts/prouver.py` porte 66 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
propriétaire, que rien n'est codé en dur.

View file

@ -3103,6 +3103,60 @@ def preuve_depots_tiers_par_le_cache() -> tuple[bool, str]:
f"vise en https:// ecrit en dur.")
def preuve_clients_oidc_vises_sur_une_exposition() -> tuple[bool, str]:
"""Chaque client OIDC pointe un nom que le plan EXPOSE vraiment.
UNE LISTE QUI SUIT UNE AUTRE PREND DU RETARD (2026-09-10). Les URI de redirection des
clients Keycloak repetent, a la main, les FQDN que `plan/applications.yml` declare dans
`expose:`. Rien ne les relie : renommer une exposition regenere `hosts.yml`, le
certificat et la zone DNS — et laisse le client OIDC viser l'ancien nom.
Le defaut ne se voit pas au deploiement. Keycloak accepte sans broncher une URI de
redirection vers un nom qui ne resout plus ; c'est l'utilisateur qui le decouvre, au
retour du SSO, devant un `invalid_redirect_uri` ou une page morte.
Cette garde est ecrite EN MEME TEMPS que le premier renommage (`grafana` ->
`observatoire`), parce que c'est le seul moment ou l'on sait encore que les deux listes
existent.
"""
instance = instance_courante()
f_app = instance / "plan" / "applications.yml"
if not f_app.is_file():
return True, "Aucun plan d'applications : pas de client OIDC a verifier."
apps = (yaml.safe_load(f_app.read_text(encoding="utf-8")) or {}).get("applications") or {}
exposes = {str(f).strip().lower()
for a in apps.values() if isinstance(a, dict)
for f in (a.get("expose") or [])}
from urllib.parse import urlsplit
fautifs: list[str] = []
clients = 0
for gv in sorted((instance / "inventories").glob("*/group_vars/*.yml")):
data = yaml.safe_load(gv.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
continue
for cl in (data.get("serveur_keycloak_clients") or []):
if not isinstance(cl, dict):
continue
clients += 1
nom = str(cl.get("clientId") or "?")
for champ in ("redirect_uris", "web_origins"):
for uri in (cl.get(champ) or []):
hote = urlsplit(str(uri)).hostname or ""
if hote in ("", "*"):
continue
if hote.lower() not in exposes:
fautifs.append(f"{gv.name}: client `{nom}` {champ} vise `{hote}`, "
f"qu'aucune application n'expose")
if not clients:
return True, "Aucun client OIDC declare dans cette instance."
if fautifs:
return False, ("Client OIDC vise un nom non expose — le SSO renverra l'utilisateur "
"dans le vide :\n - " + "\n - ".join(sorted(set(fautifs))))
return True, (f"{clients} client(s) OIDC, toutes leurs URI visent un FQDN que le plan "
f"expose ({len(exposes)} exposition(s)).")
PREUVES: list[dict] = [
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
"cmds": [["ansible-lint", "-q"]]},
@ -3253,6 +3307,8 @@ PREUVES: list[dict] = [
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
{"id": "P65", "titre": "Depots tiers : demandes au cache, jamais en HTTPS direct",
"refs": [], "func": preuve_depots_tiers_par_le_cache},
{"id": "P66", "titre": "Clients OIDC : chaque URI vise un nom que le plan expose",
"refs": [], "func": preuve_clients_oidc_vises_sur_une_exposition},
]

View file

@ -84,7 +84,7 @@ Change les produits, le **schéma reste**. C'est ça, un savoir générique.
> Prérequis : accès au lab (VPN), `/etc/hosts` pointant les services sur l'edge.
1. **Vis le SSO.** Ouvre `https://grafana.chezlepro.internal` → « *Se connecter avec
1. **Vis le SSO.** Ouvre `https://observatoire.chezlepro.internal` → « *Se connecter avec
Chezlepro* » → `testmail`. Puis ouvre Forgejo, puis Icinga : **tu n'es reconnecté nulle part**.
2. **Observe le flux.** Rouvre Grafana en navigation privée, ouvre les outils dév (F12 → Réseau) :
repère la **redirection vers Keycloak**, puis le **retour avec un `code=`**. C'est ① en action.

View file

@ -65,7 +65,7 @@ Tu as appris **métriques vs logs, pull vs push, le motif agent** — pas « Pro
```
2. **Vois les journaux** : `curl -s http://localhost:3100/loki/api/v1/label/host/values` → les nœuds
qui expédient leurs logs.
3. **Ouvre Grafana** (`https://grafana.chezlepro.internal`) — métriques *et* logs au même endroit.
3. **Ouvre Grafana** (`https://observatoire.chezlepro.internal`) — métriques *et* logs au même endroit.
4. **Casse & répare.** Arrête `prometheus-node-exporter` sur un nœud : dans Prometheus, sa cible passe
**`up=0`** (DOWN). Redémarre : elle repasse UP. Tu *sens* que c'est **l'agent** qui nourrit le
serveur (modèle pull).

View file

@ -51,12 +51,12 @@ Tu as appris **le reverse-proxy, le routage par nom, la terminaison TLS** — pa
1. **Route par nom.** Deux noms, un seul edge (`10.17.16.11`) :
```bash
curl -sI --resolve grafana.chezlepro.internal:443:10.17.16.11 \
--cacert /etc/step/certs/root_ca.crt https://grafana.chezlepro.internal/ | head -1
curl -sI --resolve observatoire.chezlepro.internal:443:10.17.16.11 \
--cacert /etc/step/certs/root_ca.crt https://observatoire.chezlepro.internal/ | head -1
```
Change `grafana` en `forge` : même IP, **backend différent**. C'est le routage par SNI.
2. **Vois la terminaison TLS.** Le certificat présenté est celui de l'**edge** (avec les SAN des
exposés) : `openssl s_client -connect 10.17.16.11:443 -servername grafana.chezlepro.internal | openssl x509 -noout -text | grep -A1 'Subject Alternative'`.
exposés) : `openssl s_client -connect 10.17.16.11:443 -servername observatoire.chezlepro.internal | openssl x509 -noout -text | grep -A1 'Subject Alternative'`.
3. **Casse & répare.** Arrête le backend (ex. `systemctl stop grafana-server` sur obs-01) et rouvre
Grafana : l'edge répond **502 Bad Gateway** (le proxy est là, le service non). Redémarre : ça
remarche. Tu distingues **le proxy** de **ce qu'il sert**.