resoudre_idp : le nom invente etait dans QUATRE roles

`forge-01` a echoue sur une URL de decouverte pointant
`https://keycloak.<domaine>` — le nom que rien ne publie. J'avais corrige
exactement ca dans `serveur_oauth2_proxy` quelques heures plus tot, en croyant
regler un cas isole.

Il etait dans quatre roles : forgejo, grafana, nextcloud, oauth2-proxy. Chacun
fabriquait le meme nom par la meme convention. Une cinquieme correction a la main
aurait diverge comme les quatre autres.

`roles/resoudre_idp` lit l'exposition declaree au plan et rend hote, base et
discovery. Les roles n'en gardent qu'un REPLI nomme, jamais la valeur de travail.

Verifie en base sur forge-01 : discovery = auth.chezlepro.internal,
GroupClaimName = groups, AdminGroup = sysadmin.

Meme lecon que `resoudre_annuaire` hier : corriger la valeur la ou elle echoue ne
corrige que la. Ce sont les copies silencieuses qui coutent la journee suivante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-07 15:33:32 -04:00
parent a08c61bb4e
commit 6699935dc7
10 changed files with 139 additions and 3 deletions

View file

@ -2,6 +2,35 @@
## 2026-08-06 — le chemin nord-sud devient dérivable
### `resoudre_idp` — le nom inventé était dans **quatre** rôles
`forge-01` a échoué sur `serveur_forgejo_oidc_discovery`, qui pointait
`https://keycloak.<domaine>` — le nom que rien ne publie. J'avais corrigé exactement ça dans
`serveur_oauth2_proxy` quelques heures plus tôt, en croyant régler un cas isolé.
Il était en fait dans **quatre** rôles : forgejo, grafana, nextcloud, oauth2-proxy. Chacun
fabriquait le même nom par la même convention, et chacun aurait échoué au même endroit —
`oauth2-proxy` l'a fait le premier parce qu'il a été déployé le premier.
Une cinquième copie corrigée à la main aurait divergé comme les quatre autres. D'où
`roles/resoudre_idp` : il lit l'exposition déclarée au plan et rend `resoudre_idp_hote`,
`resoudre_idp_base`, `resoudre_idp_discovery`. Les trois rôles restants l'appellent ; leur
défaut ne garde plus qu'un **repli** nommé, jamais la valeur de travail.
Vérifié en base sur `forge-01` :
```
OpenIDConnectAutoDiscoveryURL : https://auth.chezlepro.internal/realms/chezlepro/…
GroupClaimName : 'groups' AdminGroup : 'sysadmin'
```
Le câblage par groupe et l'URL correcte, sur un service déployé pour la première fois.
**La leçon est la même que pour `resoudre_annuaire` hier**, et elle mérite d'être dite deux
fois : corriger la valeur là où elle échoue ne corrige que là. Ce sont les autres copies,
silencieuses, qui coûtent la journée suivante.
### Forgejo et Nextcloud câblés — avant d'être déployés
Les deux services n'existaient pas encore : les câbler maintenant vaut mieux que les corriger

View file

@ -0,0 +1,23 @@
# resoudre_idp — l'URL publique du fournisseur d'identite
Resout `auth.<domaine>` depuis l'exposition que le plan declare, au lieu de fabriquer
`keycloak.<domaine>` par convention.
## Pourquoi
Quatre roles construisaient le nom a la main. Aucun ne le publiait : le DNS resout
`auth`, nginx sert `auth`, et Keycloak s'annonce sous `auth`. Un client qui interroge
`keycloak.<domaine>` echoue — ou recoit un issuer qui ne correspond pas au sien
(« issuer did not match the issuer returned by provider »).
Corrige une premiere fois dans `serveur_oauth2_proxy`, puis retrouve a l'identique
dans trois autres roles. Une convention de nommage se contredit ; une declaration se
corrige.
## Sorties
| Fait | Exemple |
|---|---|
| `resoudre_idp_hote` | `auth.chezlepro.internal` |
| `resoudre_idp_base` | `https://auth.chezlepro.internal/realms/chezlepro` |
| `resoudre_idp_discovery` | `…/.well-known/openid-configuration` |

View file

@ -0,0 +1,5 @@
---
# Application du plan qui EST le fournisseur d'identite.
resoudre_idp_app: "keycloak"
# Realm applicatif — le meme pour tous les consommateurs d'une instance.
resoudre_idp_realm: "{{ identite_realm | default('chezlepro') }}"

View file

@ -0,0 +1,42 @@
---
# Role utilitaire (pas un groupe deployable) : resout l'URL PUBLIQUE du fournisseur
# d'identite depuis l'exposition que le plan declare.
#
# Sorties (facts) :
# resoudre_idp_hote — auth.<domaine> (le FQDN publie, pas un nom fabrique)
# resoudre_idp_base — https://<hote>/realms/<realm>
# resoudre_idp_discovery — .../.well-known/openid-configuration
#
# POURQUOI CE ROLE. Quatre roles fabriquaient `https://keycloak.<domaine>` par
# convention de nommage — un nom que RIEN ne publie : le plan expose Keycloak sous
# `auth.<domaine>`, et c'est ce nom que le DNS resout, que nginx sert et que
# Keycloak annonce dans son issuer. Un client OIDC qui interroge l'autre nom
# echoue, ou recoit un issuer qui ne correspond pas au sien.
#
# Corrige une premiere fois dans `serveur_oauth2_proxy` le 2026-08-07, puis
# retrouve a l'identique dans forgejo, grafana et nextcloud. Une quatrieme copie
# aurait diverge comme les trois premieres : d'ou ce role.
- name: Charger les registres applications et domaines (expositions)
ansible.builtin.include_vars:
file: "{{ item }}"
loop:
- "{{ setops_plan_dir }}/applications.yml"
- "{{ setops_plan_dir }}/domaines.yml"
- name: Exiger que l'IdP declare une exposition
ansible.builtin.assert:
that:
- ((applications | default({})).get(resoudre_idp_app, {}).get('expose', []) | length) > 0
fail_msg: >-
L'application `{{ resoudre_idp_app }}` n'expose aucun FQDN dans
instance/plan/applications.yml. Un client OIDC ne peut pas joindre un
fournisseur qui n'est publie nulle part.
- name: Deriver l'URL publique du fournisseur d'identite
ansible.builtin.set_fact:
resoudre_idp_hote: >-
{{ (applications[resoudre_idp_app].expose | first) }}
resoudre_idp_base: >-
https://{{ (applications[resoudre_idp_app].expose | first) }}/realms/{{ resoudre_idp_realm }}
resoudre_idp_discovery: >-
https://{{ (applications[resoudre_idp_app].expose | first) }}/realms/{{ resoudre_idp_realm }}/.well-known/openid-configuration

View file

@ -60,7 +60,12 @@ serveur_forgejo_oidc_nom: "{{ organisation | default('organisation') | lower | r
serveur_forgejo_oidc_client_id: "forgejo"
serveur_forgejo_oidc_client_secret: "{{ vault_forgejo_oidc | default('') }}" # voûte (vault_forgejo_oidc)
serveur_forgejo_oidc_realm: "{{ identite_realm | default('chezlepro') }}"
serveur_forgejo_oidc_discovery: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_forgejo_oidc_realm }}/.well-known/openid-configuration"
# DERIVE de l'exposition declaree au plan (role `resoudre_idp`). La valeur
# ci-dessous n'est qu'un REPLI : `keycloak.<domaine>` n'est publie nulle part.
serveur_forgejo_oidc_repli: >-
https://keycloak.{{ domaine_interne }}/realms/{{ serveur_forgejo_oidc_realm }}/.well-known/openid-configuration
serveur_forgejo_oidc_discovery: >-
{{ resoudre_idp_discovery | default(serveur_forgejo_oidc_repli) }}
# TLS vers PostgreSQL (zero-confiance). 'disable' = compat ; 'verify-full' = chiffre + verifie
# le cert serveur contre le root_ca step-ca (via PGSSLROOTCERT dans l'unite systemd).

View file

@ -1,4 +1,12 @@
---
# L'URL PUBLIQUE de l'IdP se derive de l'exposition declaree au plan, elle ne se
# fabrique pas : `keycloak.<domaine>` n'est publie nulle part (voir resoudre_idp).
- name: Resoudre le fournisseur d'identite (role partage)
ansible.builtin.include_role:
name: resoudre_idp
vars:
resoudre_idp_realm: "{{ serveur_forgejo_oidc_realm }}"
- name: Refuser de fermer la connexion locale sur un Forgejo anterieur a la v10
# `ENABLE_INTERNAL_SIGNIN` a ete ajoute en v10 (suivi amont, issue 7476). Sur une
# version anterieure il est ignore EN SILENCE : le role croirait avoir ferme la porte

View file

@ -36,7 +36,11 @@ serveur_grafana_oidc_nom: "{{ organisation | default('Organisation') }}"
serveur_grafana_oidc_client_id: "grafana"
serveur_grafana_oidc_client_secret: "{{ vault_grafana_oidc | default('') }}" # voûte (vault_grafana_oidc)
serveur_grafana_oidc_realm: "{{ identite_realm | default('chezlepro') }}"
serveur_grafana_oidc_base: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_grafana_oidc_realm }}/protocol/openid-connect"
# DERIVE de l'exposition declaree au plan (role `resoudre_idp`) ; repli seulement.
serveur_grafana_oidc_repli: >-
https://keycloak.{{ domaine_interne }}/realms/{{ serveur_grafana_oidc_realm }}
serveur_grafana_oidc_base: >-
{{ (resoudre_idp_base | default(serveur_grafana_oidc_repli)) }}/protocol/openid-connect
serveur_grafana_oidc_scopes: "openid email profile"
# RBAC via SSO : mappe le claim 'roles' (rôles de realm Keycloak) vers le niveau Grafana.
# Défaut : grafana-admin→Admin, grafana-editor→Editor, sinon Viewer. Vider pour désactiver.

View file

@ -1,4 +1,12 @@
---
# L'URL PUBLIQUE de l'IdP se derive de l'exposition declaree au plan, elle ne se
# fabrique pas : `keycloak.<domaine>` n'est publie nulle part (voir resoudre_idp).
- name: Resoudre le fournisseur d'identite (role partage)
ansible.builtin.include_role:
name: resoudre_idp
vars:
resoudre_idp_realm: "{{ serveur_grafana_oidc_realm }}"
- name: Exiger le mot de passe admin Grafana (Vault)
ansible.builtin.assert:
that:

View file

@ -62,7 +62,11 @@ serveur_nextcloud_oidc_nom: "{{ organisation | default('Organisation') }}"
serveur_nextcloud_oidc_client_id: "nextcloud"
serveur_nextcloud_oidc_client_secret: "{{ vault_nextcloud_oidc | default('') }}"
serveur_nextcloud_oidc_realm: "{{ identite_realm | default('chezlepro') }}"
serveur_nextcloud_oidc_discovery: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_nextcloud_oidc_realm }}/.well-known/openid-configuration"
# DERIVE de l'exposition declaree au plan (role `resoudre_idp`) ; repli seulement.
serveur_nextcloud_oidc_repli: >-
https://keycloak.{{ domaine_interne }}/realms/{{ serveur_nextcloud_oidc_realm }}/.well-known/openid-configuration
serveur_nextcloud_oidc_discovery: >-
{{ resoudre_idp_discovery | default(serveur_nextcloud_oidc_repli) }}
# --- Collabora Online (WOPI) — app richdocuments ---
serveur_nextcloud_office_actif: true

View file

@ -1,4 +1,12 @@
---
# L'URL PUBLIQUE de l'IdP se derive de l'exposition declaree au plan, elle ne se
# fabrique pas : `keycloak.<domaine>` n'est publie nulle part (voir resoudre_idp).
- name: Resoudre le fournisseur d'identite (role partage)
ansible.builtin.include_role:
name: resoudre_idp
vars:
resoudre_idp_realm: "{{ serveur_nextcloud_oidc_realm }}"
- name: Exiger les secrets Nextcloud (Vault)
ansible.builtin.assert:
that: