sso : l'issuer et le nom d'hote se derivent de l'exposition declaree
oauth2-proxy fonctionne devant Icinga Web 2 : 302 vers Keycloak, qui federe LDAP. Quatre defauts leves, dont trois du meme motif — un nom construit par convention d'un cote, declare de l'autre. 1. Le secret de cookie etait en base64 STANDARD. oauth2-proxy decode en url-safe : le decodage echoue, il retombe sur la chaine brute et se plaint de sa LONGUEUR, jamais de l'encodage. Garde ajoutee qui refuse + et /. 2. L'issuer etait fabrique (`keycloak.<domaine>`), un nom que rien ne publie. Le plan expose `auth.<domaine>` — c'est ce nom que le DNS resout et que nginx sert. Derive desormais du champ `expose`. 3. Keycloak s'annoncait sous ce meme nom invente : « issuer did not match ». Meme correctif a la source. 4. Le pare-feu est-ouest bloquait l'edge : nginx ne declarait son 443 qu'avec `pair: externe`, que le devis est-ouest saute (il releve de la frontiere). Le flux existait d'un seul cote et la matrice etait satisfaite. nginx declare maintenant aussi son 443 depuis la flotte. obs-01 et mon-01 deployees. 7 cibles Prometheus up, scrutees en TLS a travers cinq zones du VRF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7cb358dab6
commit
249fba702a
6 changed files with 135 additions and 1 deletions
42
CHANGELOG.md
42
CHANGELOG.md
|
|
@ -2,6 +2,48 @@
|
|||
|
||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
||||
|
||||
### Le SSO fonctionne — quatre défauts, un seul motif
|
||||
|
||||
`obs-01` et `mon-01` déployées : Loki, Prometheus, Grafana d'un côté ; Icinga, Icinga Web 2 et
|
||||
oauth2-proxy de l'autre. **Sept VM debout**, et la supervision est réelle — 7 cibles Prometheus
|
||||
*up*, une par hôte vivant, scrutées en TLS à travers cinq zones de sécurité du VRF.
|
||||
|
||||
La preuve du SSO :
|
||||
|
||||
```
|
||||
GET http://127.0.0.1:4180/ → 302
|
||||
https://auth.chezlepro.internal/realms/chezlepro/protocol/openid-connect/auth
|
||||
?client_id=icingaweb2&redirect_uri=https://icinga.chezlepro.internal/oauth2/callback
|
||||
```
|
||||
|
||||
C'est le patron générique : Keycloak devant une application sans OIDC natif, fédérant LDAP.
|
||||
|
||||
Il a fallu quatre corrections, et l'erreur changeait à chaque fois — signe qu'on avançait.
|
||||
|
||||
**Le secret de cookie était en base64 standard.** oauth2-proxy décode en base64 *url-safe* ; un
|
||||
secret contenant `+` ou `/` fait échouer le décodage, il retombe sur la chaîne brute et se
|
||||
plaint de sa **longueur** — « is 44 bytes » — sans jamais mentionner l'encodage. Régénéré en
|
||||
url-safe dans les deux tenants, avec une garde qui refuse `+` et `/` en nommant la cause.
|
||||
|
||||
**L'issuer était inventé.** Le rôle fabriquait `https://keycloak.<domaine>` par convention — un
|
||||
nom que rien ne publie. Le plan expose Keycloak sous `auth.<domaine>`, et c'est ce nom que
|
||||
PowerDNS résout et que nginx sert.
|
||||
|
||||
**Keycloak s'annonçait sous ce même nom inventé**, à la source : `issuer did not match the
|
||||
issuer returned by provider`. Même correctif — le nom d'hôte se dérive de l'exposition.
|
||||
|
||||
**Le pare-feu est-ouest bloquait l'edge.** Le flux existait d'un seul côté : `oauth2-proxy`
|
||||
déclarait `egress 443 → edge`, la matrice était satisfaite (nginx déclare bien 443) — mais avec
|
||||
`pair: externe`, que le devis est-ouest **saute volontairement**, puisqu'il relève de la
|
||||
frontière. Aucune règle d'hyperviseur n'était émise, et la connexion expirait. `serveur_nginx`
|
||||
déclare désormais aussi son 443 depuis la `flotte` : les FQDN publiés vivent à l'edge, et un
|
||||
service interne qui appelle un autre service passe par son nom publié.
|
||||
|
||||
**Trois de ces quatre sont le motif du jour** : un nom construit par convention d'un côté,
|
||||
déclaré de l'autre. Le champ `expose` du plan a maintenant **cinq** consommateurs — PowerDNS,
|
||||
nginx, `/etc/hosts`, oauth2-proxy et Keycloak — pour une seule source.
|
||||
|
||||
|
||||
### Le supernet du tenant devient un intrant dérivé
|
||||
|
||||
Keycloak ne démarrait pas :
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ serveur_keycloak_utilisateur: "keycloak"
|
|||
serveur_keycloak_service: "keycloak"
|
||||
|
||||
# Publication (derriere serveur_nginx, TLS termine a l'edge).
|
||||
# DERIVE de l'exposition que le plan declare (voir tasks/main.yml). Ce nom est celui que
|
||||
# Keycloak ANNONCE dans son issuer OIDC : il doit etre exactement celui que l'edge sert et
|
||||
# que le DNS resout, sinon tout client OIDC refuse le jeton — « issuer did not match ».
|
||||
# La valeur ci-dessous n'est qu'un REPLI, si l'application n'expose rien.
|
||||
serveur_keycloak_hostname_derive: true
|
||||
serveur_keycloak_hostname: "keycloak.{{ domaine_interne }}"
|
||||
|
||||
# Base de donnees : resolue depuis le registre par le groupe consommateur.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,25 @@
|
|||
---
|
||||
|
||||
# Le nom d'hote de Keycloak EST son identite OIDC. Le fabriquer par convention
|
||||
# (`keycloak.<domaine>`) produisait un nom que rien ne publie : le plan expose `auth`,
|
||||
# l'edge sert `auth`, le DNS resout `auth` — et Keycloak s'annoncait `keycloak`. Tout
|
||||
# client OIDC comparait les deux et refusait. Constate le 2026-08-07 sur oauth2-proxy :
|
||||
# « issuer did not match the issuer returned by provider ».
|
||||
- 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"
|
||||
when: serveur_keycloak_hostname_derive | bool
|
||||
|
||||
- name: Dériver le nom d'hôte public depuis l'exposition déclarée
|
||||
ansible.builtin.set_fact:
|
||||
serveur_keycloak_hostname: >-
|
||||
{{ (applications | default({})).get('keycloak', {}).get('expose', []) | first }}
|
||||
when:
|
||||
- serveur_keycloak_hostname_derive | bool
|
||||
- ((applications | default({})).get('keycloak', {}).get('expose', []) | length) > 0
|
||||
- name: Exiger le mot de passe admin Keycloak (Vault)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
|
|
|
|||
|
|
@ -15,6 +15,22 @@ flux:
|
|||
pair: externe
|
||||
chiffrement: tls-requis
|
||||
raison: "HTTPS entrant — services exposés (terminaison TLS à l'edge)."
|
||||
# L'edge sert AUSSI les hôtes du tenant : les FQDN publiés (`expose`) n'existent qu'ici,
|
||||
# et un service interne qui doit joindre un autre service passe par son nom publié — pas
|
||||
# par une adresse. C'est le cas de toute découverte OIDC : oauth2-proxy interroge
|
||||
# `https://auth.<domaine>/.well-known/…`, servi par l'edge.
|
||||
#
|
||||
# Sans cette déclaration, le flux existait d'un seul côté : `serveur_oauth2_proxy`
|
||||
# déclarait `egress 443 → edge`, la matrice était satisfaite (nginx déclare bien 443),
|
||||
# mais le devis est-ouest SAUTE `pair: externe` — il relève de la frontière. Aucune règle
|
||||
# d'hyperviseur n'était donc émise, et oauth2-proxy expirait sur `10.27.16.11:443`.
|
||||
# Constaté le 2026-08-07.
|
||||
- sens: ingress
|
||||
port: 443
|
||||
protocole: tcp
|
||||
pair: flotte
|
||||
chiffrement: tls-requis
|
||||
raison: "HTTPS depuis le tenant : les FQDN publiés vivent à l'edge (découverte OIDC, appels inter-services par nom)."
|
||||
- sens: egress
|
||||
port: derive
|
||||
protocole: tcp
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ serveur_oauth2_proxy_ecoute: "0.0.0.0:4180"
|
|||
|
||||
# OIDC (Keycloak) — realm applicatif.
|
||||
serveur_oauth2_proxy_realm: "{{ identite_realm | default('chezlepro') }}"
|
||||
# DERIVE de l'exposition que le plan declare pour l'IdP (voir tasks/main.yml). La valeur
|
||||
# ci-dessous n'est qu'un REPLI, utilise si l'application n'expose rien : un nom fabrique
|
||||
# par convention n'est resolu par personne.
|
||||
serveur_oauth2_proxy_issuer_derive: true
|
||||
serveur_oauth2_proxy_app_idp: "keycloak"
|
||||
serveur_oauth2_proxy_issuer: "https://keycloak.{{ domaine_interne }}/realms/{{ serveur_oauth2_proxy_realm }}"
|
||||
serveur_oauth2_proxy_client_id: "" # OBLIGATOIRE (par instance)
|
||||
serveur_oauth2_proxy_client_secret: "" # OBLIGATOIRE (voûte)
|
||||
|
|
@ -26,7 +31,14 @@ serveur_oauth2_proxy_redirect_url: "" # OBLIGATOIRE (https://<app>/oauth2/ca
|
|||
# Application protégée (upstream local).
|
||||
serveur_oauth2_proxy_upstream: "" # OBLIGATOIRE (ex. http://127.0.0.1:8080)
|
||||
|
||||
# Secret de cookie (voûte) — 32 octets base64.
|
||||
# Secret de cookie (voûte) — 32 octets en base64 URL-SAFE (alphabet `-_`, RFC 4648 §5).
|
||||
#
|
||||
# Le base64 STANDARD ne convient PAS, et l'erreur ne le dit pas : oauth2-proxy décode en
|
||||
# `RawURLEncoding`, et un secret contenant `+` ou `/` fait échouer le décodage. Il retombe
|
||||
# alors sur la chaîne BRUTE et se plaint de sa longueur — « is 44 bytes » — sans jamais
|
||||
# mentionner l'encodage. Constaté le 2026-08-07 : le service redémarrait en boucle.
|
||||
#
|
||||
# Générer avec : `openssl rand -base64 32 | tr '+/' '-_'`
|
||||
serveur_oauth2_proxy_cookie_secret: "{{ vault_oauth2_cookie | default('') }}"
|
||||
# * = tout utilisateur authentifié par Keycloak.
|
||||
serveur_oauth2_proxy_email_domains: "*"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,29 @@
|
|||
---
|
||||
# L'ISSUER SE DERIVE DE L'EXPOSITION DECLAREE, il ne s'invente pas. Le defaut fabriquait
|
||||
# `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-la que PowerDNS resout et
|
||||
# que nginx sert. Resultat le 2026-08-07 : `lookup keycloak.chezlepro.internal : no such
|
||||
# host`, et oauth2-proxy redemarrait en boucle.
|
||||
#
|
||||
# Le champ `expose` est deja la source pour PowerDNS, nginx et /etc/hosts. C'est la
|
||||
# quatrieme fois qu'on le lit : une convention de nommage se contredit, une declaration
|
||||
# se corrige.
|
||||
- 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"
|
||||
when: serveur_oauth2_proxy_issuer_derive | bool
|
||||
|
||||
- name: Dériver l'issuer depuis l'exposition de Keycloak
|
||||
ansible.builtin.set_fact:
|
||||
serveur_oauth2_proxy_issuer: >-
|
||||
https://{{ (applications | default({})).get(serveur_oauth2_proxy_app_idp, {}).get('expose', []) | first }}/realms/{{ serveur_oauth2_proxy_realm }}
|
||||
when:
|
||||
- serveur_oauth2_proxy_issuer_derive | bool
|
||||
- ((applications | default({})).get(serveur_oauth2_proxy_app_idp, {}).get('expose', []) | length) > 0
|
||||
|
||||
- name: Exiger la configuration OIDC (client, secret, redirect, upstream, cookie)
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
|
|
@ -9,6 +34,19 @@
|
|||
- serveur_oauth2_proxy_cookie_secret | length > 0
|
||||
fail_msg: "client_id, client_secret (voûte), redirect_url, upstream et cookie_secret (voûte) requis."
|
||||
|
||||
# Garde d'ENCODAGE, distincte de la garde de présence : un secret présent mais en base64
|
||||
# standard fait redémarrer oauth2-proxy en boucle, avec un message qui parle de longueur
|
||||
# et jamais d'encodage. Mieux vaut refuser ici, où la cause est nommable.
|
||||
- name: Refuser un cookie_secret qui n'est pas du base64 url-safe
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'+' not in serveur_oauth2_proxy_cookie_secret"
|
||||
- "'/' not in serveur_oauth2_proxy_cookie_secret"
|
||||
fail_msg: >-
|
||||
`vault_oauth2_cookie` contient `+` ou `/` : c'est du base64 STANDARD. oauth2-proxy
|
||||
décode en base64 URL-SAFE et retombera sur la chaîne brute, dont la longueur sera
|
||||
refusée. Régénérer : openssl rand -base64 32 | tr '+/' '-_'
|
||||
|
||||
- name: Créer l'utilisateur système oauth2-proxy
|
||||
ansible.builtin.user:
|
||||
name: "{{ serveur_oauth2_proxy_utilisateur }}"
|
||||
|
|
|
|||
Loading…
Reference in a new issue