diff --git a/CHANGELOG.md b/CHANGELOG.md index 5611956..248cb75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### Administrer le realm par appartenance — le dernier `porte_par: aucun` + +`realm-admin` (rôle du client `realm-management`) est attaché au **groupe** `sysadmin`. +Administrer le realm ne passe plus par le compte local : il suffit d'appartenir au groupe dans +l'annuaire. + +**Portée : ce realm seulement, jamais `master`.** Le compte `admin` reste hors d'atteinte du +groupe, et c'est délibéré — un accès de secours qui dépendrait des habilitations qu'il doit +pouvoir réparer n'en serait pas un (D-40). + +La console à utiliser est celle du realm — `/admin//console/` — pas la racine `/admin/`, +qui est celle de `master`. Le runbook nomme désormais les **trois** portes et dit ce que +chacune gouverne. + +**Les rôles de client sont un espace de noms distinct** des rôles de realm ; la déclaration +gagne un champ `roles_client`. Et l'API attend l'**UUID** du client, pas son `clientId` : +interroger par le nom rendait une erreur, la vérification échouait toujours, et la tâche se +déclarait `changed` à chaque passage alors que le rôle était déjà posé. Corrigé — deux passages +consécutifs à `changed=0`. + +### La boucle de changement de mot de passe + +Après `editMode=WRITABLE`, le changement partait mais **rebouclait sans fin**. Cause : +`pwdMustChange: TRUE` signifie « quand un *administrateur* pose un mot de passe, l'utilisateur +doit le changer ». Or Keycloak écrit en tant qu'administrateur (`cn=admin`) — chaque changement +relayé était donc vu comme une réinitialisation, et OpenLDAP reposait `pwdReset` aussitôt. + +**C'est incompatible par construction avec un IdP qui relaie le changement.** La contrainte a +été déplacée là où l'utilisateur la voit : `pwdMustChange: FALSE` côté annuaire, et Keycloak +pose l'action requise `UPDATE_PASSWORD` tant que `pwdReset` est vrai — un écran qui explique, +au lieu d'un refus muet au niveau du protocole. + +Je ne l'avais pas vu parce que j'avais éprouvé `pwdReset` **au niveau LDAP**, où il fonctionne +parfaitement, sans jamais parcourir le chemin complet à travers Keycloak. L'opérateur l'a dit +avant moi : « ce n'est pas du tout explicite » — c'était le symptôme de deux mécanismes qui ne +se parlent pas. + + ### « Federated storage is not writable » — la fédération était en lecture seule Le changement de mot de passe imposé échouait : `editMode=READ_ONLY` était **codé en dur** diff --git a/docs/autorisation.md b/docs/autorisation.md index 06f0672..01368cd 100644 --- a/docs/autorisation.md +++ b/docs/autorisation.md @@ -122,14 +122,20 @@ est exact et parfaitement trompeur — le mot de passe est bon, c'est la porte q | Ce que tu veux faire | URL | Compte | |---|---|---| | **ton compte, tes accès** | `https://auth./realms//account/` | `sysadmin` + jeton d'amorçage | -| administrer Keycloak lui-même | `https://auth./admin/` | `admin` + `vault_keycloak_admin` | +| **administrer ton realm** | `https://auth./admin//console/` | `sysadmin` — par le groupe | +| Keycloak lui-même (secours) | `https://auth./admin/` | `admin` + `vault_keycloak_admin` | La première ligne est celle de la reprise. C'est là que le changement de mot de passe te sera imposé, et c'est cette connexion qui **importe ton groupe dans le realm** — sans elle, les habilitations sur Grafana, Forgejo et Nextcloud restent câblées mais jamais exercées. -La seconde reste le compte local de secours : administrer le realm ne passe pas encore par le -groupe (`meta/acces.yml` de `serveur_keycloak` le signale, `porte_par: aucun`). +**Administrer ton realm passe désormais par le groupe** : `realm-admin` (rôle du client +`realm-management`) est attaché à `sysadmin`. La console est celle du realm — +`/admin//console/` — pas la racine `/admin/`, qui est celle de `master`. + +La dernière ligne reste le **compte de secours** (D-40). Sa portée est `master`, hors d'atteinte +du groupe : c'est voulu. Un accès de secours qui dépendrait des habilitations qu'il doit pouvoir +réparer ne serait pas un accès de secours. ### 6.3 Faire confiance à l'AC interne diff --git a/roles/serveur_keycloak/defaults/main.yml b/roles/serveur_keycloak/defaults/main.yml index d8206a9..adc47dd 100644 --- a/roles/serveur_keycloak/defaults/main.yml +++ b/roles/serveur_keycloak/defaults/main.yml @@ -112,3 +112,7 @@ serveur_keycloak_groupes_mapper_clients: [] # Constate le 2026-08-07 : READ_ONLY etait code en dur, et la premiere reprise du # sysadmin butait dessus. serveur_keycloak_ldap_edit_mode: "WRITABLE" + +# Compte d'amorcage dont le changement de mot de passe doit etre EXPLICITE dans +# Keycloak. Vide = aucun alignement. Voir roles/amorcage_acces (D-67). +serveur_keycloak_amorcage_uid: "{{ amorcage_acces_uid | default('sysadmin') }}" diff --git a/roles/serveur_keycloak/meta/acces.yml b/roles/serveur_keycloak/meta/acces.yml index 3362465..4b016e2 100644 --- a/roles/serveur_keycloak/meta/acces.yml +++ b/roles/serveur_keycloak/meta/acces.yml @@ -20,10 +20,9 @@ acces_projection: acces: - groupe: sysadmin accorde: realm-admin - # /!\ Non cable : la console d'administration de Keycloak reste accessible au - # seul compte local `admin` (voute). C'est l'acces de secours de D-40 — mais il - # devrait AUSSI etre atteignable par le groupe, sans quoi administrer le realm - # oblige a passer par le compte de secours. - porte_par: aucun + # Role de CLIENT (`realm-management`), attache au GROUPE. Portee : ce realm + # seulement, jamais `master` — le compte de secours reste hors de portee du + # groupe, ce qui est le sens meme d'un acces de secours (D-40). + porte_par: role-client raison: >- - Administration du realm : clients, mappers, politiques. + Administration du realm : clients, mappers, politiques, utilisateurs. diff --git a/roles/serveur_keycloak/tasks/groupes-ldap.yml b/roles/serveur_keycloak/tasks/groupes-ldap.yml index 1a4965c..7be11bd 100644 --- a/roles/serveur_keycloak/tasks/groupes-ldap.yml +++ b/roles/serveur_keycloak/tasks/groupes-ldap.yml @@ -100,13 +100,36 @@ echo "SETOPS_ERREUR: groupe '{{ lien.groupe }}' absent du realm (synchronise ?)" >&2 exit 4 fi - {% for role in lien.roles %} + {% for role in lien.roles | default([]) %} if ! "$KC" get "groups/$GID/role-mappings/realm" -r {{ serveur_keycloak_realm }} \ --fields name --format csv --noquotes 2>/dev/null | grep -q '^{{ role }}$'; then "$KC" add-roles -r {{ serveur_keycloak_realm }} --gid "$GID" --rolename {{ role }} >/dev/null CHANGED=1 fi {% endfor %} + {# Roles de CLIENT — `realm-admin` du client `realm-management` en est un. Ils + vivent dans un espace de noms distinct des roles de realm : `add-roles` les + distingue par `--cclientid`. Sans ca, administrer le realm resterait le + privilege du seul compte local de secours. #} + {% for cl in lien.roles_client | default([]) %} + {% for role in cl.roles %} + # L'API attend l'UUID du client, PAS son clientId : interroger par le nom rend + # une erreur, la verification echoue toujours, et la tache se declare `changed` + # a chaque passage alors que le role est deja la. Constate le 2026-08-07. + CCID=$("$KC" get clients -r {{ serveur_keycloak_realm }} -q clientId={{ cl.client }} \ + --fields id --format csv --noquotes 2>/dev/null | head -1 || true) + if [ -z "$CCID" ]; then + echo "SETOPS_ERREUR: client '{{ cl.client }}' introuvable dans le realm" >&2; exit 6 + fi + if ! "$KC" get "groups/$GID/role-mappings/clients/$CCID" \ + -r {{ serveur_keycloak_realm }} --fields name --format csv --noquotes 2>/dev/null \ + | grep -q '^{{ role }}$'; then + "$KC" add-roles -r {{ serveur_keycloak_realm }} --gid "$GID" \ + --cclientid {{ cl.client }} --rolename {{ role }} >/dev/null + CHANGED=1 + fi + {% endfor %} + {% endfor %} {% endfor %} [ "$CHANGED" = 1 ] && echo SETOPS_CHANGED || echo SETOPS_OK environment: @@ -157,3 +180,68 @@ when: - serveur_keycloak_groupes_mapper_clients | length > 0 - not ansible_check_mode + +# --- Rendre le changement de mot de passe EXPLICITE --------------------------- +# OpenLDAP marque `pwdReset` et refuse les operations au niveau du PROTOCOLE ; +# Keycloak a son propre systeme d'actions requises et NE LIT PAS `pwdReset`. Les +# deux mecanismes ne se parlent pas : l'utilisateur entre avec son jeton, atterrit +# dans la console de compte, et rien ne lui dit pourquoi ni quoi faire. Constate le +# 2026-08-07 par l'operateur : « ce n'est pas du tout explicite ». +# +# On aligne donc Keycloak sur l'annuaire : tant que `pwdReset` est vrai, l'action +# `UPDATE_PASSWORD` est posee et Keycloak affiche un ecran qui l'explique. Elle +# disparait d'elle-meme quand le mot de passe change — l'annuaire retire `pwdReset`, +# et la condition cesse d'etre vraie. +- name: Lire `pwdReset` sur le compte d'amorçage (délégué à l'annuaire) + ansible.builtin.command: + argv: + - ldapsearch + - "-x" + - "-H" + - "ldapi:///" + - "-D" + - "{{ resoudre_annuaire_bind_dn }}" + - "-w" + - "{{ resoudre_annuaire_bind_password }}" + - "-b" + - "uid={{ serveur_keycloak_amorcage_uid }},{{ resoudre_annuaire_users_dn }}" + - "+" + delegate_to: "{{ groups['serveur_openldap'][0] }}" + register: serveur_keycloak_pwdreset + changed_when: false + failed_when: false + no_log: true + when: + - serveur_keycloak_amorcage_uid | length > 0 + - (groups['serveur_openldap'] | default([])) | length > 0 + - not ansible_check_mode + +- name: Exiger le changement de mot de passe dans Keycloak aussi + ansible.builtin.shell: + executable: /bin/bash + cmd: | + set -euo pipefail + KC={{ serveur_keycloak_home }}/bin/kcadm.sh + "$KC" config credentials --server http://localhost:8080 --realm master \ + --user {{ serveur_keycloak_admin_user }} --password "$KC_ADMIN_PW" >/dev/null + UID_KC=$("$KC" get users -r {{ serveur_keycloak_realm }} \ + -q username={{ serveur_keycloak_amorcage_uid }} \ + --fields id --format csv --noquotes 2>/dev/null | head -1 || true) + if [ -z "$UID_KC" ]; then echo "SETOPS_OK (pas encore importe)"; exit 0; fi + if "$KC" get users/"$UID_KC" -r {{ serveur_keycloak_realm }} 2>/dev/null \ + | grep -q 'UPDATE_PASSWORD'; then + echo SETOPS_OK + else + "$KC" update users/"$UID_KC" -r {{ serveur_keycloak_realm }} \ + -s 'requiredActions=["UPDATE_PASSWORD"]' >/dev/null + echo SETOPS_CHANGED + fi + environment: + KC_ADMIN_PW: "{{ serveur_keycloak_admin_password }}" + register: serveur_keycloak_req_action + changed_when: "'SETOPS_CHANGED' in serveur_keycloak_req_action.stdout" + no_log: true + when: + - serveur_keycloak_amorcage_uid | length > 0 + - "'pwdReset: TRUE' in (serveur_keycloak_pwdreset.stdout | default(''))" + - not ansible_check_mode