L'epreuve de portabilite est passee. Un second ecosysteme souverain complet, monte depuis zero par le meme moteur : 14 hotes, 2583 taches ok, 331 changed, 0 failed. Plan distinct, voute separee, realm technolibre, sa propre AC — et une topologie differente : LDAP et SSO sur des machines separees la ou Chezlepro les co-localise. Cinq devis CONFORME (identite, certificats, PostgreSQL, courriel, frontiere — 55 lignes 0 ecart). Le sixieme dit exactement la bonne chose : les 6 services repondent depuis l'edge, aucun depuis le poste, qui ne resout pas encore technolibre.internal (6 entrees /etc/hosts absentes — le plancher). SIXIEME DEFAUT MOTEUR. Le devis d'identite interrogeait LDAP en `ldapi:///` — un socket UNIX LOCAL — depuis l'hote serveur_keycloak. Cela ne marchait que par CO-LOCATION ACCIDENTELLE. Un tenant qui separe l'annuaire du SSO echouait sur « Failed to import python-ldap » : l'hote SSO n'a pas de client LDAP. Les deux lectures sont deleguees a l'hote DERIVE par resoudre_annuaire. P35 (D-75) : toute application dont le role exige une base en a une au plan. La garde de resoudre_base existait deja, mais s'est declenchee a la 92e tache de collab-01, apres quarante minutes, pour un ecart entierement lisible dans le plan. Rien n'y est code en dur : les roles concernes sont ceux qui INCLUENT resoudre_base, et le groupe reclame est lu dans le DEFAUT de la variable passee — jamais deduit du nom. serveur_icingaweb2 reclame la base de serveur_icinga ; une preuve supposant « role = groupe » aurait crie sur un cas sain. Eprouvee dans les deux sens ET sur les deux tenants, dont les registres n'ont pas la meme portee : base retiree -> ECHEC la nommant ; restauree -> OK. Verifie : prouver.py 35 OK sur les deux instances, ansible-lint production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
7.7 KiB
YAML
175 lines
7.7 KiB
YAML
---
|
|
# Devis d'identite — RELEVE seul. Ne modifie rien (D-23 : un devis se lit d'abord).
|
|
#
|
|
# Le playbook collecte deux dictionnaires — ce que le depot DECLARE, ce que la machine
|
|
# PORTE — et les depose en JSON sur le controleur. La comparaison est faite par
|
|
# `scripts/devis_identite.py` : elle n'a rien a faire en Jinja, et le depot a deja
|
|
# cette forme (devis_opnsense, devis_sdn : Python raisonne, Ansible releve).
|
|
#
|
|
# Pourquoi ce devis existe. Les 30 preuves de `prouver.py` sont STATIQUES : elles
|
|
# montrent que le depot est coherent avec lui-meme. Aucune ne demande au systeme
|
|
# deploye s'il ressemble a ce que le depot annonce. Les quatre defauts du 2026-08-08
|
|
# etaient tous de ce second type — chacun trouve en relisant apres avoir ecrit,
|
|
# aucun signale par un test.
|
|
#
|
|
# make identite-plan
|
|
|
|
- name: Devis d'identité — relever le déclaré et le réel
|
|
hosts: serveur_keycloak
|
|
become: true
|
|
gather_facts: false
|
|
|
|
vars:
|
|
kc: "http://localhost:8080"
|
|
devis_identite_sortie: "{{ playbook_dir }}/../../instance/devis-identite.json"
|
|
|
|
tasks:
|
|
# PRECEDENCE — `include_vars` au niveau du play PRIME sur les `group_vars`.
|
|
# Charger les defauts d'un role tels quels ferait donc mentir le devis : il
|
|
# comparerait au defaut du depot au lieu de la valeur reellement declaree par
|
|
# l'inventaire. Mesure du 2026-08-08 : le premier jet rapportait
|
|
# `client_pki_reload_services: []` sur les 14 hotes alors que quatre groupes le
|
|
# declarent. Le devis portait exactement le defaut qu'il est cense traquer.
|
|
#
|
|
# On retient donc ce que l'inventaire declare AVANT de charger les defauts, puis
|
|
# on le reimpose. (Charger sous un `name:` ne marche pas : les defauts se citent
|
|
# entre eux — `serveur_keycloak_cert` reference `serveur_keycloak_steppath` — et ces
|
|
# references ne se resolvent plus une fois enfermees dans un dictionnaire.)
|
|
- name: Retenir ce que l'inventaire déclare pour serveur_keycloak
|
|
ansible.builtin.set_fact:
|
|
devis_inventaire: >-
|
|
{{ hostvars[inventory_hostname] | dict2items
|
|
| selectattr('key', 'match', '^serveur_keycloak_') | items2dict }}
|
|
|
|
- name: Charger les défauts du rôle serveur_keycloak
|
|
ansible.builtin.include_vars:
|
|
dir: "{{ playbook_dir }}/../../roles/serveur_keycloak/defaults"
|
|
|
|
# Cle par cle : `set_fact` ne prend pas un dictionnaire entier en argument libre —
|
|
# il l'accepte sans erreur et n'en fait rien. C'est ainsi que le premier correctif
|
|
# a paru fonctionner tout en laissant le devis mentir.
|
|
- name: Rendre le dernier mot à l'inventaire
|
|
ansible.builtin.set_fact:
|
|
"{{ item.key }}": "{{ item.value }}"
|
|
loop: "{{ devis_inventaire | dict2items }}"
|
|
loop_control:
|
|
label: "{{ item.key }}"
|
|
|
|
- name: Résoudre la politique de mot de passe (déclaration unique)
|
|
ansible.builtin.include_role:
|
|
name: resoudre_politique_mdp
|
|
|
|
# Meme raison : les DN de l'annuaire ne se recopient pas, ils se resolvent.
|
|
- name: Résoudre la connexion à l'annuaire
|
|
ansible.builtin.include_role:
|
|
name: resoudre_annuaire
|
|
|
|
- name: Charger le registre des applications
|
|
ansible.builtin.include_vars:
|
|
file: "{{ setops_plan_dir }}/applications.yml"
|
|
|
|
- name: Obtenir un jeton d'administration
|
|
ansible.builtin.uri:
|
|
url: "{{ kc }}/realms/master/protocol/openid-connect/token"
|
|
method: POST
|
|
body_format: form-urlencoded
|
|
body:
|
|
grant_type: password
|
|
client_id: admin-cli
|
|
username: "{{ serveur_keycloak_admin_user }}"
|
|
password: "{{ serveur_keycloak_admin_password }}"
|
|
register: devis_jeton
|
|
no_log: true
|
|
|
|
- name: Lire le realm
|
|
ansible.builtin.uri:
|
|
url: "{{ kc }}/admin/realms/{{ serveur_keycloak_realm }}"
|
|
headers:
|
|
Authorization: "Bearer {{ devis_jeton.json.access_token }}"
|
|
register: devis_realm
|
|
no_log: true
|
|
|
|
- name: Lire la fédération LDAP
|
|
ansible.builtin.uri:
|
|
url: "{{ kc }}/admin/realms/{{ serveur_keycloak_realm }}/components?type=org.keycloak.storage.UserStorageProvider"
|
|
headers:
|
|
Authorization: "Bearer {{ devis_jeton.json.access_token }}"
|
|
register: devis_fed
|
|
no_log: true
|
|
|
|
- name: Lire les mappeurs de la fédération
|
|
ansible.builtin.uri:
|
|
url: >-
|
|
{{ kc }}/admin/realms/{{ serveur_keycloak_realm }}/components?parent={{
|
|
devis_fed.json[0].id }}&type=org.keycloak.storage.ldap.mappers.LDAPStorageMapper
|
|
headers:
|
|
Authorization: "Bearer {{ devis_jeton.json.access_token }}"
|
|
register: devis_mappeurs
|
|
no_log: true
|
|
|
|
# `ldapi:///` est un socket UNIX LOCAL : ces deux lectures ne peuvent avoir lieu que
|
|
# SUR l'annuaire. Le play tourne sur `serveur_keycloak`, et cela fonctionnait par
|
|
# ACCIDENT — chez l'instance d'origine, l'annuaire et le SSO sont co-localises sur le
|
|
# meme hote. Un tenant qui les separe (`id-ldap-01` / `id-sso-01`) faisait echouer le
|
|
# devis sur « Failed to import the required Python library (python-ldap) » : l'hote
|
|
# SSO n'a evidemment pas de client LDAP. Mesure du 2026-08-10 sur Technolibre.
|
|
#
|
|
# On delegue donc a l'hote DERIVE par `resoudre_annuaire`, qui sait deja ou vit
|
|
# l'annuaire. Co-localises, la delegation est un aller-retour sans effet ; separes,
|
|
# elle est la seule facon que ca marche.
|
|
- name: Lire la politique de mot de passe de l'annuaire
|
|
community.general.ldap_search:
|
|
dn: "ou=policies,{{ resoudre_annuaire_base_dn }}"
|
|
scope: onelevel
|
|
filter: "(objectClass=pwdPolicy)"
|
|
server_uri: "ldapi:///"
|
|
delegate_to: "{{ resoudre_annuaire_hote }}"
|
|
register: devis_ppolicy
|
|
changed_when: false
|
|
|
|
# Un compte sans adresse ne peut pas recevoir de lien de reinitialisation : pour
|
|
# lui, « mot de passe oublie » est un ecran qui ne mene nulle part, et le seul
|
|
# recours redevient l'exploitant manipulant le mot de passe d'autrui.
|
|
- name: Lire les comptes de l'annuaire
|
|
community.general.ldap_search:
|
|
dn: "{{ resoudre_annuaire_users_dn }}"
|
|
scope: onelevel
|
|
filter: "(objectClass=inetOrgPerson)"
|
|
attrs: ["uid", "mail", "givenName"]
|
|
server_uri: "ldapi:///"
|
|
delegate_to: "{{ resoudre_annuaire_hote }}"
|
|
register: devis_comptes
|
|
changed_when: false
|
|
|
|
- name: Déposer le relevé sur le contrôleur
|
|
ansible.builtin.copy:
|
|
dest: "{{ devis_identite_sortie }}"
|
|
mode: "0600"
|
|
content: "{{ {
|
|
'hote': inventory_hostname,
|
|
'realm': serveur_keycloak_realm,
|
|
'declare': {
|
|
'passwordPolicy': resoudre_politique_mdp_keycloak,
|
|
'brute': resoudre_politique_mdp_brute,
|
|
'ppolicy': resoudre_politique_mdp_ldap,
|
|
'durcissement': serveur_keycloak_ldap_durcissement,
|
|
'mappeurs': serveur_keycloak_ldap_attributs,
|
|
'editMode': serveur_keycloak_ldap_edit_mode,
|
|
'resetPasswordAllowed': serveur_keycloak_reset_mot_de_passe | bool,
|
|
'smtpHost': applications[serveur_keycloak_smtp_app].hote ~ '.' ~ domaine_interne,
|
|
},
|
|
'reel': {
|
|
'realm': devis_realm.json,
|
|
'federation': devis_fed.json[0].config,
|
|
'mappeurs': dict(devis_mappeurs.json | map(attribute='name') | list
|
|
| zip(devis_mappeurs.json | map(attribute='config') | list)),
|
|
'ppolicy': (devis_ppolicy.results | first) | default({}),
|
|
'comptes': devis_comptes.results,
|
|
},
|
|
} | to_nice_json }}"
|
|
delegate_to: localhost
|
|
become: false
|
|
|
|
- name: Indiquer le relevé
|
|
ansible.builtin.debug:
|
|
msg: "Relevé déposé : {{ devis_identite_sortie }} — comparaison par scripts/devis_identite.py"
|