From 2450c9ea654ac55c388f6b53fc061de3ce4d7c3c Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 10 Sep 2026 20:21:13 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q --- AGENTS.md | 2 +- CHANGELOG.md | 41 ++++++++++++++++++++++++ docs/audit/plan-de-recette.md | 6 ++-- docs/audit/preuve-2026-09-10.md | 7 +++-- docs/devis-services.md | 2 +- scripts/prouver.py | 56 +++++++++++++++++++++++++++++++++ wiki/Identité-et-SSO.md | 2 +- wiki/Métriques-et-journaux.md | 2 +- wiki/Reverse-proxy-et-TLS.md | 6 ++-- 9 files changed, 111 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 198d276..484152e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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.** diff --git a/CHANGELOG.md b/CHANGELOG.md index beeb1bf..a04b11f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/audit/plan-de-recette.md b/docs/audit/plan-de-recette.md index a9e502b..84c453e 100644 --- a/docs/audit/plan-de-recette.md +++ b/docs/audit/plan-de-recette.md @@ -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.* diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md index 7ababe9..02cd292 100644 --- a/docs/audit/preuve-2026-09-10.md +++ b/docs/audit/preuve-2026-09-10.md @@ -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 diff --git a/docs/devis-services.md b/docs/devis-services.md index af2b61d..3b779fa 100644 --- a/docs/devis-services.md +++ b/docs/devis-services.md @@ -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. diff --git a/scripts/prouver.py b/scripts/prouver.py index fb2fd5b..4a9a64d 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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}, ] diff --git a/wiki/Identité-et-SSO.md b/wiki/Identité-et-SSO.md index 8d93854..b93c95c 100644 --- a/wiki/Identité-et-SSO.md +++ b/wiki/Identité-et-SSO.md @@ -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. diff --git a/wiki/Métriques-et-journaux.md b/wiki/Métriques-et-journaux.md index a677310..b943e0f 100644 --- a/wiki/Métriques-et-journaux.md +++ b/wiki/Métriques-et-journaux.md @@ -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). diff --git a/wiki/Reverse-proxy-et-TLS.md b/wiki/Reverse-proxy-et-TLS.md index 71fec56..c877f8d 100644 --- a/wiki/Reverse-proxy-et-TLS.md +++ b/wiki/Reverse-proxy-et-TLS.md @@ -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**.