nom public : il vient du plan, plus de la devinette du role
Le renommage deploye, nginx servait observatoire, le certificat le portait, les deux zones le publiaient — et Grafana fabriquait toujours son URL de retour OIDC avec l ancien nom. Quatre roles portaient en defaut une devinette du nom sous lequel ils sont servis. Tant que le plan suit la meme convention, la devinette tombe juste et rien ne revele qu il y a deux sources. Keycloak avait deja son remede, dans le role — donc trois roles sans remede. instancier derive desormais <groupe>_hostname de l exposition unique declaree par le plan. Six services en heritent. P67 garde la derivation. Les quatre instances sont regenerees, 67 preuves vertes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
f5b5899284
commit
d1ae41cfdf
6 changed files with 110 additions and 5 deletions
|
|
@ -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 66 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||
C'est aussi ce que font les 67 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.**
|
||||
|
||||
|
|
|
|||
19
CHANGELOG.md
19
CHANGELOG.md
|
|
@ -33,6 +33,25 @@ renommage, parce que c'est le seul moment ou l'on sait encore que les deux liste
|
|||
|
||||
P66 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose
|
||||
|
||||
### Et une TROISIEME liste, que le deploiement a revelee
|
||||
|
||||
Le renommage deploye, nginx servait le nouveau nom, le certificat le portait, les deux zones
|
||||
le publiaient — et Grafana fabriquait toujours son URL de retour OIDC avec l'ancien :
|
||||
|
||||
redirect_uri=https%3A%2F%2Fgrafana.chezlepro.internal%2Flogin%2Fgeneric_oauth
|
||||
|
||||
Quatre roles — grafana, forgejo, nextcloud, keycloak — portaient en defaut une DEVINETTE du
|
||||
nom sous lequel ils sont servis (`grafana.{{ domaine_interne }}`). Tant que le plan suit la
|
||||
meme convention, la devinette tombe juste et rien ne revele qu'il y a deux sources. Keycloak
|
||||
avait deja son remede, dans le role, en relisant le plan a l'execution — un remede par role,
|
||||
donc trois roles sans remede.
|
||||
|
||||
`instancier` derive desormais `<groupe>_hostname` de l'exposition declaree, quand elle est
|
||||
UNIQUE (deux expositions ne designent aucun nom canonique : le role garde alors la main).
|
||||
Six services en heritent : collabora, forgejo, grafana, keycloak, nextcloud, oauth2_proxy.
|
||||
|
||||
**P67** garde la derivation. Les quatre instances ont ete regenerees.
|
||||
|
||||
### Une erreur de lecture, au passage
|
||||
|
||||
En cherchant pourquoi `tableaux.genese.internal` ne resolvait pas depuis le poste, on a
|
||||
|
|
|
|||
|
|
@ -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 · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (66 OK · 0 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 59 scripts expliques et atteignables, 116 cibles make documentees, 67 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (132 cle(s) declaree(s) par l'instance). |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (138 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (36 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
|
|
@ -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 (66 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 (67 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 `2450c9e` (publie le 2026-09-10). |
|
||||
|
|
@ -79,6 +79,7 @@
|
|||
| 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)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 6 service(s) expose(s) portent le nom du plan (6 groupe(s) derive(s)). |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -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 66 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||
`scripts/prouver.py` porte 67 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.
|
||||
|
|
|
|||
|
|
@ -198,6 +198,25 @@ def generer() -> dict:
|
|||
domaines = charger_domaines(FICHIER_DOMAINES)
|
||||
expositions = expositions_des_applications({"applications": apps}, domaines)
|
||||
|
||||
# LE NOM PUBLIC D'UN SERVICE VIENT DU PLAN, PAS DU ROLE (2026-09-10).
|
||||
#
|
||||
# Quatre roles — grafana, forgejo, nextcloud, keycloak — portaient en defaut une
|
||||
# DEVINETTE du nom sous lequel ils sont servis (`grafana.{{ domaine_interne }}`).
|
||||
# Tant que le plan suit la meme convention, la devinette tombe juste et personne ne
|
||||
# voit qu'il y a deux sources. Renommer une exposition la fait mentir : nginx sert le
|
||||
# nouveau nom, le certificat le porte, la zone le publie — et le service continue de
|
||||
# fabriquer ses URL de retour avec l'ancien. Le SSO renvoie alors dans le vide.
|
||||
#
|
||||
# On derive donc `<groupe>_hostname` de l'exposition declaree, quand elle est UNIQUE
|
||||
# (deux expositions ne designent aucun nom canonique : on laisse le role choisir).
|
||||
# `setdefault` cote hote : une valeur posee a la main dans les group_vars gagne encore.
|
||||
hostname_par_groupe: dict = {}
|
||||
for app in apps.values():
|
||||
exp = [str(f).strip() for f in (app.get("expose") or []) if str(f).strip()]
|
||||
groupe = str(app.get("groupe") or "")
|
||||
if groupe and len(exp) == 1:
|
||||
hostname_par_groupe[groupe] = exp[0]
|
||||
|
||||
services_par_hote: dict = {}
|
||||
for app in apps.values():
|
||||
services_par_hote.setdefault(app.get("hote"), set()).add(app.get("groupe"))
|
||||
|
|
@ -311,6 +330,10 @@ def generer() -> dict:
|
|||
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
||||
if sans:
|
||||
hostvars["sans_exposition"] = sans
|
||||
# Nom public de chaque service porte par cet hote, tel que le plan l'expose.
|
||||
for groupe_expose in sorted(groupes & set(hostname_par_groupe)):
|
||||
hostvars.setdefault(f"{groupe_expose}_hostname",
|
||||
hostname_par_groupe[groupe_expose])
|
||||
# Le resolveur de l'ecosysteme doit savoir a qui demander la zone de l'hebergeur.
|
||||
if "serveur_resolveur" in groupes and zones_deleguees:
|
||||
hostvars["serveur_resolveur_zones_deleguees"] = zones_deleguees
|
||||
|
|
|
|||
|
|
@ -3157,6 +3157,66 @@ def preuve_clients_oidc_vises_sur_une_exposition() -> tuple[bool, str]:
|
|||
f"expose ({len(exposes)} exposition(s)).")
|
||||
|
||||
|
||||
def preuve_nom_public_vient_du_plan() -> tuple[bool, str]:
|
||||
"""Un service servi sous un nom expose porte CE nom, pas celui que son role devine.
|
||||
|
||||
LE DEFAUT, MESURE LE 2026-09-10. Quatre roles — grafana, forgejo, nextcloud, keycloak —
|
||||
portaient en defaut une devinette du nom sous lequel ils sont servis, de la forme
|
||||
`grafana.{{ domaine_interne }}`. Tant que le plan suit la meme convention, la devinette
|
||||
tombe juste et rien ne revele qu'il y a deux sources.
|
||||
|
||||
Renommer une exposition l'a revele d'un coup : nginx servait `observatoire`, le
|
||||
certificat le portait, les deux zones le publiaient — et Grafana continuait de fabriquer
|
||||
son URL de retour OIDC avec `grafana`. Le SSO renvoyait l'utilisateur sur un nom qui
|
||||
n'existait plus.
|
||||
|
||||
`instancier` derive donc `<groupe>_hostname` de l'exposition UNIQUE que le plan declare.
|
||||
Cette preuve garde la derivation : elle refuse qu'un hote serve un groupe expose sans
|
||||
porter le nom du plan. Elle ne juge pas les groupes a deux expositions — aucun nom
|
||||
canonique n'en sort, et le role garde alors la main.
|
||||
"""
|
||||
inv = RACINE / INV
|
||||
if not inv.is_file():
|
||||
return True, "Aucun inventaire monte : rien a verifier."
|
||||
instance = instance_courante()
|
||||
f_app = instance / "plan" / "applications.yml"
|
||||
if not f_app.is_file():
|
||||
return True, "Aucun plan d'applications : rien a verifier."
|
||||
apps = (yaml.safe_load(f_app.read_text(encoding="utf-8")) or {}).get("applications") or {}
|
||||
attendu: dict[str, str] = {}
|
||||
for a in apps.values():
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
exp = [str(f).strip() for f in (a.get("expose") or []) if str(f).strip()]
|
||||
g = str(a.get("groupe") or "")
|
||||
if g and len(exp) == 1:
|
||||
attendu[g] = exp[0]
|
||||
if not attendu:
|
||||
return True, "Aucun groupe expose par le plan : rien a deriver."
|
||||
|
||||
data = yaml.safe_load(inv.read_text(encoding="utf-8")) or {}
|
||||
enfants = (data.get("all") or {}).get("children") or {}
|
||||
hostvars: dict = {}
|
||||
for etat in ("hotes_actifs", "hotes_planifies"):
|
||||
for nom, v in ((enfants.get(etat) or {}).get("hosts") or {}).items():
|
||||
hostvars[nom] = v or {}
|
||||
|
||||
fautes: list[str] = []
|
||||
verifies = 0
|
||||
for groupe, fqdn in sorted(attendu.items()):
|
||||
for hote in sorted((enfants.get(groupe) or {}).get("hosts") or {}):
|
||||
vu = str((hostvars.get(hote) or {}).get(f"{groupe}_hostname") or "")
|
||||
verifies += 1
|
||||
if vu != fqdn:
|
||||
fautes.append(f"{hote} porte {groupe} sans `{groupe}_hostname` = "
|
||||
f"`{fqdn}` (inventaire : {vu or 'absent'})")
|
||||
if fautes:
|
||||
return False, ("Un service ne porte pas le nom que le plan lui donne — ses URL de "
|
||||
"retour viseront l'ancien :\n - " + "\n - ".join(fautes[:6]))
|
||||
return True, (f"{verifies} service(s) expose(s) portent le nom du plan "
|
||||
f"({len(attendu)} groupe(s) derive(s)).")
|
||||
|
||||
|
||||
PREUVES: list[dict] = [
|
||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||
"cmds": [["ansible-lint", "-q"]]},
|
||||
|
|
@ -3309,6 +3369,8 @@ PREUVES: list[dict] = [
|
|||
"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},
|
||||
{"id": "P67", "titre": "Nom public : le service porte celui du plan, pas celui du role",
|
||||
"refs": [], "func": preuve_nom_public_vient_du_plan},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue