diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5280a..82c7f5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # CHANGELOG — Set-OPS +## 2026-09-15 (2) — Un vestibule devant une application qui savait deja s'authentifier + +L'exploitant, en voyant la boite du navigateur sur `vigie` : *« Dans le contexte d'un +site, cela complexifie inutilement les choses puisque Icinga Web 2 permet nativement de +gerer des comptes. Ce genre d'intervention n'est pertinente que pour integrer Icinga a +Keycloak chez les tenants. »* + +Il a raison, et la correction retire du code au lieu d'en ajouter. + +### Ce que le mode `locale` faisait, et pourquoi c'etait de trop + +Il posait un `auth_basic` nginx devant le backend `external` : nginx demandait le mot de +passe, Icinga Web 2 croyait le `REMOTE_USER` qu'il recevait. Ca marchait. Ca reinventait +une page de connexion **devant une application qui en a une**, et ca privait l'exploitant +de ce que l'application sait faire seule. + +UN VESTIBULE N'A DE SENS QUE DEVANT UNE APPLICATION QUI NE SAIT PAS S'AUTHENTIFIER. Celle +de Set-OPS — le GUI — est dans ce cas, et son vestibule reste. Icinga Web 2, non. + +### Le mode `db` : le backend natif + + authentication.ini backend = "db" resource = "icingaweb_db" + groups.ini backend = "db" — les groupes aussi sont natifs + nginx plus aucun auth_basic + +CE QUE CA REND, ET QUI MANQUAIT. La gestion des comptes DANS l'interface : creer, +desactiver, changer un mot de passe ne demande plus un deploiement ni un passage par la +voute. Le moteur ne pose qu'UN compte — celui qui permet d'entrer la premiere fois — et +il ne l'ecrase jamais : un mot de passe change dans l'interface appartient a celui qui +l'a change. + +ET D-66 REDEVIENT APPLICABLE SANS ANNUAIRE. Les groupes d'Icinga Web 2 vivent en base : +`roles.ini` habilite donc `sysadmin`, un GROUPE, et non plus une personne nommee en dur. +Revoquer quelqu'un redevient un clic. + +UNE BASE A ELLE, PAS UN COIN D'`icingadb`. Les tables de comptes appartiennent a +l'application web ; celles du moteur sont reecrites par ses migrations. Les meler ferait +disparaitre les comptes le jour d'une mise a jour, sans que personne n'ait touche aux +comptes. + +LE HACHAGE EST CELUI QUE L'APPLICATION VERIFIE : `password_hash()` de PHP, la fonction +exacte qu'Icinga Web 2 appelle a la connexion. Un hachage d'un autre outil donnerait une +base valide et une connexion impossible. + +### Deux pieges du renommage + +**`when: serveur_icingaweb2_auth != 'locale'`** gardait l'appel a `resoudre_annuaire`. +Juste tant que `locale` etait le seul mode sans annuaire ; renomme en `db`, la garde a +cesse de garder et le role a reclame un secret de liaison LDAP qui n'existe pas. Les +conditions nomment desormais les modes qui VEULENT un annuaire (`ldap`, `external`) : +une condition qui dit ce qu'elle veut survit a un renommage, une qui dit ce qu'elle +refuse, non. + +**L'ordre des taches.** Le bloc de la base avait pris la place de l'ancien vestibule — +c'est-a-dire APRES le rendu des `.ini`, qui nomment cette base. Le gabarit lisait des +variables inexistantes, et l'echec etait **censure par `no_log`** : `changed: true` et +rien d'autre. Une tache qui manipule des secrets ne peut pas dire ce qui lui manque ; +c'est a l'ordre de ne pas la mettre dans cette situation. + +### Eprouve + + auth_basic dans nginx 0 + backend declare db / icingaweb_db + compte en base icinga-admin, actif + depuis le poste 302 -> /authentication/login -> 200 + la page « Icinga Web 2 Login », champs username et password + ## 2026-09-15 (1) — L'edge publie : trois noms, en TLS verifie de bout en bout observatoire.genese.internal http=302 tls=0 (Grafana, vers sa connexion) diff --git a/docs/audit/preuve-2026-09-15.md b/docs/audit/preuve-2026-09-15.md index 2a9fb38..d4e0c12 100644 --- a/docs/audit/preuve-2026-09-15.md +++ b/docs/audit/preuve-2026-09-15.md @@ -46,7 +46,7 @@ | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 67 scripts expliques et atteignables, 123 cibles make documentees, 68 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 36 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 36 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). | -| P34 | Chaque document declare son lecteur | — | ✅ OK | 46 document(s) declarent leur lecteur (40 genere(s) exempte(s)). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 46 document(s) declarent leur lecteur (41 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 8 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index dcac619..17f00c4 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous. | rôles | 68 | `roles/*/` | | README de rôles | 68 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | documents | 42 | `docs/*.md` | -| pièces d'audit | 45 | `docs/audit/*` | +| pièces d'audit | 46 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | | décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` | | décisions renversées | 3 | lignes `\| **D-nn** —` du même document | diff --git a/exemples/vault.exemple.yml b/exemples/vault.exemple.yml index 5754e7b..1ef9206 100644 --- a/exemples/vault.exemple.yml +++ b/exemples/vault.exemple.yml @@ -32,6 +32,8 @@ vault_postgresql_keycloak: "" vault_bd_keycloak: "" vault_bd_forgejo: "" vault_bd_icingadb: "" +# La base des COMPTES de la console (mode `db`) — distincte de celle du moteur. +vault_bd_icingaweb2: "" # --- Forge (Forgejo) --- vault_forgejo_admin: "" @@ -44,9 +46,10 @@ vault_redis: "" # LA CONSOLE DE SUPERVISION, QUAND ELLE S'AUTHENTIFIE SEULE. # -# `serveur_icingaweb2_auth: locale` — le mode d'un SITE, qui n'a ni annuaire ni Keycloak. -# nginx demande ce mot de passe à l'entrée, Icinga Web 2 croit le nom que nginx lui passe. -# Inutile en mode `ldap` ou `external` : le rôle ne l'exige que dans le mode qui s'en sert. +# `serveur_icingaweb2_auth: db` — le mode d'un SITE, qui n'a ni annuaire ni Keycloak. +# Icinga Web 2 gère ses comptes nativement ; ce mot de passe est celui du compte +# D'AMORÇAGE, celui qui permet d'entrer la première fois pour créer les autres dans +# l'interface. Inutile en mode `ldap` ou `external`. vault_icingaweb2_admin: "" # LA CONSOLE D'EXPLOITATION, QUAND ELLE S'AUTHENTIFIE SEULE. diff --git a/roles/serveur_icingaweb2/defaults/main.yml b/roles/serveur_icingaweb2/defaults/main.yml index fa95b72..bfaf546 100644 --- a/roles/serveur_icingaweb2/defaults/main.yml +++ b/roles/serveur_icingaweb2/defaults/main.yml @@ -68,18 +68,28 @@ serveur_icingaweb2_groupes_dn: "ou=groups,{{ resoudre_annuaire_base_dn | default # `ldap` — connexion directe a l'annuaire. Icinga Web 2 n'a pas d'OIDC natif. # `external` — une passerelle SSO devant (oauth2-proxy -> Keycloak) pose `REMOTE_USER`, # et un backend LDAP sert a resoudre les GROUPES. -# `locale` — ni annuaire ni passerelle : nginx authentifie lui-meme, et -# l'habilitation nomme une personne. +# `db` — ni annuaire ni passerelle : Icinga Web 2 gere ses comptes LUI-MEME, +# dans sa propre base, avec sa propre page de connexion. # -# POURQUOI `locale` EXISTE. Un SITE n'a ni LDAP ni Keycloak — ce sont des services +# POURQUOI `db` EXISTE. Un SITE n'a ni LDAP ni Keycloak — ce sont des services # d'ecosysteme, et l'hebergeur n'en est pas un. Sans ce mode, la console de supervision # du site etait impossible : le plan declarait `serveur_icinga` sans `serveur_icingaweb2`, # et 88 verdicts se calculaient sans que personne puisse les LIRE. # -# C'EST LE MEME CHOIX QUE POUR GRAFANA ET FORGEJO au site — `connexion_locale`, un compte -# d'administration, un mot de passe en voute. Nommer une personne contredit D-66 (« le -# GROUPE, jamais des personnes ») ; la regle suppose un annuaire, et il n'y en a pas ici. -# On l'ecrit plutot que de la contourner en silence. +# PREMIERE ECRITURE, ET ELLE ETAIT DE TROP (corrigee le 2026-09-15). Ce mode s'appelait +# `locale` et posait un `auth_basic` nginx devant le backend `external` : nginx demandait +# le mot de passe et l'application croyait le nom qu'il lui passait. Ca marchait, et ca +# reinventait une page de connexion devant une application qui en a une. +# +# L'exploitant l'a dit en une phrase : *« Icinga Web 2 permet nativement de gerer des +# comptes ; ce genre d'intervention n'est pertinente que pour integrer Icinga a Keycloak +# chez les tenants. »* Un vestibule n'a de sens que devant une application qui ne sait +# pas s'authentifier — et celle-ci sait. +# +# CE QUE LE MODE NATIF DONNE EN PLUS : la gestion des comptes DANS l'interface. Creer, +# desactiver, changer un mot de passe ne demande plus un deploiement ni un passage par la +# voute. Et l'habilitation cesse de nommer une personne en dur dans `roles.ini` : les +# GROUPES d'Icinga Web 2 existent aussi en base. serveur_icingaweb2_auth: "ldap" # Variable nginx d'où vient REMOTE_USER. En SSO : l'en-tête d'oauth2-proxy. serveur_icingaweb2_remote_user: "$remote_user" @@ -103,15 +113,22 @@ serveur_icingaweb2_bpm_processes: {} # bloque — et l'absence de reponse, qui est le nginx local tombe. serveur_icingaweb2_sonde_codes: "HTTP/1.1 200,HTTP/1.1 302,HTTP/1.1 401,HTTP/1.1 403" -# --- MODE `locale` : ce que nginx demande a l'entree --------------------------------- +# --- MODE `db` : le compte d'amorcage de la console ------------------------------------ # -# LE MOT DE PASSE VIENT DE LA VOUTE, comme partout. Vide, le role REFUSE de deployer en -# mode `locale` — degrader, jamais deviner, et surtout jamais un mot de passe par defaut -# sur une console qui montre l'etat de toute une fabric. +# UN SEUL COMPTE EST POSE PAR LE MOTEUR, et c'est voulu : celui qui permet d'entrer la +# premiere fois. Les suivants se creent DANS l'interface, par quelqu'un qui a une tete et +# un contexte — pas par un deploiement. +# +# LE MOT DE PASSE VIENT DE LA VOUTE. Vide, le role REFUSE de deployer en mode `db` : +# degrader, jamais deviner, et surtout jamais un mot de passe par defaut sur une console +# qui montre l'etat de toute une fabric. serveur_icingaweb2_admin_utilisateur: "icinga-admin" serveur_icingaweb2_admin_motdepasse: "{{ vault_icingaweb2_admin | default('') }}" -serveur_icingaweb2_htpasswd: "/etc/nginx/.icingaweb2.htpasswd" -serveur_icingaweb2_realm: "Supervision" + +# LE SCHEMA DES TABLES DE COMPTES, livre par le paquet. On le charge UNE FOIS — la +# presence de `icingaweb_user` sert de temoin, parce que rejouer ce fichier sur une base +# deja peuplee echouerait sur les objets existants. +serveur_icingaweb2_schema: "/usr/share/icingaweb2/schema/pgsql.schema.sql" # TLS VERS POSTGRESQL, DERIVE DE CE QUE SERT LE SERVEUR. Voir `resoudre_base` et P78. # diff --git a/roles/serveur_icingaweb2/tasks/main.yml b/roles/serveur_icingaweb2/tasks/main.yml index 0458e37..d9da5c7 100644 --- a/roles/serveur_icingaweb2/tasks/main.yml +++ b/roles/serveur_icingaweb2/tasks/main.yml @@ -5,6 +5,120 @@ vars: resoudre_base_groupe: "{{ serveur_icingaweb2_icinga_groupe }}" +# --- MODE `db` : LA BASE DES COMPTES, ET LE COMPTE D'AMORCAGE (2026-09-15) ------------ +# +# TOUT EN HAUT, ET C'EST L'ORDRE QUI L'EXIGE. `resources.ini` nomme cette base ; il est +# rendu quelques taches plus bas. Pose apres lui — la ou vivait l'ancien vestibule — ce +# bloc laissait le gabarit lire des variables qui n'existaient pas encore, et l'echec +# etait CENSURE par `no_log` : « changed: true » et rien d'autre. Une tache qui rend des +# secrets ne peut pas dire ce qui lui manque ; c'est a l'ordre de ne pas la mettre dans +# cette situation. +# +# Icinga Web 2 gere ses comptes NATIVEMENT. La premiere ecriture posait un `auth_basic` +# nginx devant le backend `external` — ca marchait, et ca reinventait une page de +# connexion devant une application qui en a une, en privant l'exploitant de la gestion +# des comptes dans l'interface. Un vestibule n'a de sens que devant une application qui +# ne sait pas s'authentifier. +- name: Résoudre la base des comptes de la console (rôle partagé) + ansible.builtin.include_role: + name: resoudre_base + vars: + resoudre_base_groupe: "icingaweb2" + when: serveur_icingaweb2_auth == 'db' + +- name: Adopter la base des comptes pour Icinga Web 2 + ansible.builtin.set_fact: + serveur_icingaweb2_db_host: "{{ resoudre_base_db_host }}" + serveur_icingaweb2_db_port: "{{ resoudre_base_db_port }}" + serveur_icingaweb2_db_base: "{{ resoudre_base_entree.base }}" + serveur_icingaweb2_db_utilisateur: "{{ resoudre_base_entree.proprietaire }}" + serveur_icingaweb2_db_motdepasse: "{{ resoudre_base_db_password }}" + no_log: true + when: serveur_icingaweb2_auth == 'db' + +- name: Exiger un mot de passe pour le compte d'amorçage + ansible.builtin.assert: + that: + - serveur_icingaweb2_admin_motdepasse | length > 0 + fail_msg: >- + `serveur_icingaweb2_auth: db` mais `serveur_icingaweb2_admin_motdepasse` est vide. + Une console qui montre l'état de toute une fabric ne s'ouvre pas avec un mot de + passe deviné : renseigner la clé dans la voûte de cet écosystème. + when: serveur_icingaweb2_auth == 'db' + +# LE SCHEMA NE SE CHARGE QU'UNE FOIS. Le rejouer sur une base deja peuplee echouerait sur +# les objets existants — on lit donc d'abord si la table des comptes est la. +- name: La base des comptes est-elle déjà en place ? + community.postgresql.postgresql_query: + login_host: "{{ serveur_icingaweb2_db_host }}" + login_user: "{{ serveur_icingaweb2_db_utilisateur }}" + login_password: "{{ serveur_icingaweb2_db_motdepasse }}" + login_db: "{{ serveur_icingaweb2_db_base }}" + ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" + ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" + query: "SELECT to_regclass('public.icingaweb_user') IS NOT NULL AS presente" + register: serveur_icingaweb2_schema_etat + changed_when: false + no_log: true + when: + - serveur_icingaweb2_auth == 'db' + - not ansible_check_mode + +- name: Charger le schéma des comptes + community.postgresql.postgresql_script: + login_host: "{{ serveur_icingaweb2_db_host }}" + login_user: "{{ serveur_icingaweb2_db_utilisateur }}" + login_password: "{{ serveur_icingaweb2_db_motdepasse }}" + login_db: "{{ serveur_icingaweb2_db_base }}" + ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" + ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" + path: "{{ serveur_icingaweb2_schema }}" + no_log: true + when: + - serveur_icingaweb2_auth == 'db' + - not ansible_check_mode + - not (serveur_icingaweb2_schema_etat.query_result[0].presente | default(false)) + +# LE HACHAGE EST CELUI QUE L'APPLICATION VERIFIE, PAS UN QU'ON CHOISIT. `password_hash()` +# de PHP est exactement la fonction qu'Icinga Web 2 appelle a la connexion ; produire un +# hachage d'un autre outil donnerait une base valide et une connexion impossible. +- name: Hacher le mot de passe du compte d'amorçage + ansible.builtin.command: + argv: + - php + - -r + - 'echo password_hash($argv[1], PASSWORD_DEFAULT);' + - "{{ serveur_icingaweb2_admin_motdepasse }}" + register: serveur_icingaweb2_hachage + changed_when: false + no_log: true + when: + - serveur_icingaweb2_auth == 'db' + - not ansible_check_mode + +# CREE S'IL MANQUE, JAMAIS ECRASE. Un mot de passe change dans l'interface appartient a +# celui qui l'a change : le moteur ne le reprend pas au deploiement suivant. Il ne pose +# que le compte qui permet d'entrer la premiere fois. +- name: Poser le compte d'amorçage s'il n'existe pas + community.postgresql.postgresql_query: + login_host: "{{ serveur_icingaweb2_db_host }}" + login_user: "{{ serveur_icingaweb2_db_utilisateur }}" + login_password: "{{ serveur_icingaweb2_db_motdepasse }}" + login_db: "{{ serveur_icingaweb2_db_base }}" + ssl_mode: "{{ serveur_icingaweb2_db_sslmode | default('prefer', true) or 'prefer' }}" + ca_cert: "{{ serveur_icingaweb2_db_sslrootcert if serveur_icingaweb2_db_sslmode | default('') else omit }}" + query: >- + INSERT INTO icingaweb_user (name, active, password_hash) + VALUES (%s, 1, %s) ON CONFLICT (name) DO NOTHING + positional_args: + - "{{ serveur_icingaweb2_admin_utilisateur }}" + - "{{ serveur_icingaweb2_hachage.stdout }}" + no_log: true + when: + - serveur_icingaweb2_auth == 'db' + - not ansible_check_mode + + # ON NE RESOUT PAS UN ANNUAIRE QUAND ON N'EN A PAS (2026-09-14). # # `resoudre_annuaire` etait appele SANS CONDITION. Au site — qui n'a ni LDAP ni Keycloak — @@ -21,7 +135,12 @@ # SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13). resoudre_annuaire_bind_cn: "icingaweb2" resoudre_annuaire_secret: "vault_ldap_bind_icingaweb2" - when: serveur_icingaweb2_auth != 'locale' + # LES MODES NOMMES, PAS UNE NEGATION. `!= 'locale'` etait juste tant que `locale` + # etait le seul mode sans annuaire ; renomme en `db`, la garde a cesse de garder et + # `resoudre_annuaire` s'est remis a exiger un secret de liaison qui n'existe pas. + # Une condition qui dit ce qu'elle VEUT survit a un renommage ; une qui dit ce qu'elle + # refuse, non. + when: serveur_icingaweb2_auth in ('ldap', 'external') - name: Adopter la connexion annuaire pour Icinga Web 2 ansible.builtin.set_fact: @@ -32,7 +151,12 @@ serveur_icingaweb2_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}" serveur_icingaweb2_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}" no_log: true - when: serveur_icingaweb2_auth != 'locale' + # LES MODES NOMMES, PAS UNE NEGATION. `!= 'locale'` etait juste tant que `locale` + # etait le seul mode sans annuaire ; renomme en `db`, la garde a cesse de garder et + # `resoudre_annuaire` s'est remis a exiger un secret de liaison qui n'existe pas. + # Une condition qui dit ce qu'elle VEUT survit a un renommage ; une qui dit ce qu'elle + # refuse, non. + when: serveur_icingaweb2_auth in ('ldap', 'external') # LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03). # @@ -122,75 +246,6 @@ label: "{{ item.key }}" when: serveur_icingaweb2_bpm_processes | length > 0 -# --- MODE `locale` : LE VESTIBULE, AVANT LA PORTE (2026-09-14) ------------------------ -# -# Un SITE n'a ni annuaire ni passerelle SSO — ce sont des services d'ecosysteme. La -# console de supervision etait donc impossible chez lui, et 88 verdicts se calculaient -# sans que personne puisse les lire. -# -# `htpasswd` A LA MAIN PLUTOT QUE LE MODULE `community.general.htpasswd` : ce module -# reecrit le fichier a chaque passage quand le hachage porte un sel aleatoire, et le role -# se declarerait `changed` pour toujours. On hache une fois, on compare, on n'ecrit que -# si ca differe. -- name: Exiger un mot de passe pour la console locale - ansible.builtin.assert: - that: - - serveur_icingaweb2_admin_motdepasse | length > 0 - fail_msg: >- - `serveur_icingaweb2_auth: locale` mais `vault_icingaweb2_admin` est vide. - Une console qui montre l'etat de toute une fabric ne s'ouvre pas avec un mot de - passe devine : ajouter la cle a la voute de cet ecosysteme. - when: serveur_icingaweb2_auth == 'locale' - -- name: Installer de quoi hacher un mot de passe pour nginx - ansible.builtin.apt: - name: apache2-utils - state: present - when: serveur_icingaweb2_auth == 'locale' - -# CE QUI EST DEJA POSE EST-IL LE BON MOT DE PASSE ? `htpasswd -v` le VERIFIE sans rien -# reecrire — c'est lui qui rend la tache idempotente, pas une comparaison de fichiers -# (le sel change a chaque hachage, deux fichiers justes different toujours). -- name: Le mot de passe deja pose est-il le bon ? - ansible.builtin.command: - argv: - - htpasswd - - -vb - - "{{ serveur_icingaweb2_htpasswd }}" - - "{{ serveur_icingaweb2_admin_utilisateur }}" - - "{{ serveur_icingaweb2_admin_motdepasse }}" - register: serveur_icingaweb2_htpasswd_verif - changed_when: false - failed_when: false - no_log: true - when: serveur_icingaweb2_auth == 'locale' - -- name: Poser le mot de passe de la console locale - ansible.builtin.command: - argv: - - htpasswd - - -cbB - - "{{ serveur_icingaweb2_htpasswd }}" - - "{{ serveur_icingaweb2_admin_utilisateur }}" - - "{{ serveur_icingaweb2_admin_motdepasse }}" - no_log: true - # ELLE NE S'EXECUTE QUE SI LA VERIFICATION A ECHOUE — donc quand elle tourne, elle - # change vraiment quelque chose. `changed_when: true` n'est pas une complaisance ici : - # c'est l'idempotence qui vit dans le `when`, pas dans un test de sortie. - changed_when: true - when: - - serveur_icingaweb2_auth == 'locale' - - (serveur_icingaweb2_htpasswd_verif.rc | default(1)) != 0 - notify: Recharger nginx - -- name: Refermer le fichier de mots de passe sur nginx - ansible.builtin.file: - path: "{{ serveur_icingaweb2_htpasswd }}" - owner: root - group: www-data - mode: "0640" - when: serveur_icingaweb2_auth == 'locale' - - name: Déployer le vhost nginx local d'Icinga Web 2 ansible.builtin.template: src: nginx.conf.j2 diff --git a/roles/serveur_icingaweb2/templates/authentication.ini.j2 b/roles/serveur_icingaweb2/templates/authentication.ini.j2 index 9bb9e7a..8c48a66 100644 --- a/roles/serveur_icingaweb2/templates/authentication.ini.j2 +++ b/roles/serveur_icingaweb2/templates/authentication.ini.j2 @@ -1,15 +1,14 @@ ; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main. -{% if serveur_icingaweb2_auth == 'locale' %} -; MODE `locale` : nginx a DEJA authentifie, et pose `REMOTE_USER`. Icinga Web 2 fait -; confiance a ce que le serveur web lui dit — c'est exactement le contrat du backend -; `external`, celui qu'emploie aussi le SSO. La difference n'est pas ici, elle est devant : -; une authentification HTTP Basic au lieu d'une passerelle OIDC. +{% if serveur_icingaweb2_auth == 'db' %} +; MODE `db` : Icinga Web 2 gere ses comptes LUI-MEME, dans sa propre base, avec sa propre +; page de connexion. C'est la reponse pour un ecosysteme sans annuaire — un SITE. ; -; AUCUN BACKEND D'ANNUAIRE, parce qu'il n'y en a pas. Un site n'a ni LDAP ni Keycloak : -; ce sont des services d'ecosysteme. L'habilitation se fait donc par NOM dans `roles.ini`, -; et c'est ecrit la-bas pourquoi. +; AUCUN VESTIBULE DEVANT. La premiere ecriture posait un `auth_basic` nginx et faisait +; confiance a `REMOTE_USER` : ca reinventait une page de connexion devant une application +; qui en a une, et ca privait l'exploitant de la gestion des comptes dans l'interface. [icingaweb2] -backend = "external" +backend = "db" +resource = "icingaweb_db" {% elif serveur_icingaweb2_auth == 'external' %} ; SSO : l'utilisateur est fourni par la passerelle (oauth2-proxy → Keycloak) via REMOTE_USER. [icingaweb2] diff --git a/roles/serveur_icingaweb2/templates/groups.ini.j2 b/roles/serveur_icingaweb2/templates/groups.ini.j2 index 21b32ce..a0c42d2 100644 --- a/roles/serveur_icingaweb2/templates/groups.ini.j2 +++ b/roles/serveur_icingaweb2/templates/groups.ini.j2 @@ -2,10 +2,16 @@ ; Habilitation par GROUPE d'annuaire (D-66) : ajouter quelqu'un a ; `{{ serveur_icingaweb2_groupe_admin }}` dans LDAP lui ouvre la supervision, sans ; deploiement et sans que personne ne soit nomme ici. -{% if serveur_icingaweb2_auth == 'locale' %} -; MODE `locale` : AUCUN BACKEND DE GROUPES. Un backend `ldap` qui vise une ressource -; inexistante ne rend pas une liste vide — il fait ECHOUER chaque ouverture de session, -; et l'erreur parle d'annuaire a qui n'en a jamais declare. +{% if serveur_icingaweb2_auth == 'db' %} +; MODE `db` : LES GROUPES AUSSI SONT NATIFS. `icingaweb_group` et +; `icingaweb_group_membership` viennent avec le meme schema que les comptes. +; +; CE QUE CA REND A D-66. « Le groupe, jamais des personnes » supposait un annuaire ; ici +; le groupe existe en base, se peuple dans l'interface, et `roles.ini` peut donc nommer +; un GROUPE meme sans LDAP. Revoquer quelqu'un redevient un clic, pas un deploiement. +[db] +backend = "db" +resource = "icingaweb_db" {% else %} [annuaire] backend = "ldap" diff --git a/roles/serveur_icingaweb2/templates/nginx.conf.j2 b/roles/serveur_icingaweb2/templates/nginx.conf.j2 index b7f7c40..020a0de 100644 --- a/roles/serveur_icingaweb2/templates/nginx.conf.j2 +++ b/roles/serveur_icingaweb2/templates/nginx.conf.j2 @@ -9,22 +9,6 @@ server { root {{ serveur_icingaweb2_docroot }}; index index.php; -{% if serveur_icingaweb2_auth == 'locale' %} - - # MODE `locale` : C'EST NGINX QUI AUTHENTIFIE. - # - # Icinga Web 2 n'a pas d'OIDC natif et un SITE n'a ni annuaire ni passerelle SSO. - # HTTP Basic devant `external` est le contrat que l'application attend deja — le - # serveur web dit QUI, l'application le croit. C'est exactement ce que fait - # oauth2-proxy chez un locataire, avec un vestibule plus simple. - # - # AU NIVEAU DU `server`, PAS DU `location` : place dans le seul bloc PHP, la - # protection sauterait tout ce que `try_files` sert directement — feuilles de style, - # icones, et surtout les fichiers que `location ~ /\.ht` croit couvrir. Une - # authentification qu'on peut contourner par un chemin voisin n'en est pas une. - auth_basic "{{ serveur_icingaweb2_realm }}"; - auth_basic_user_file {{ serveur_icingaweb2_htpasswd }}; -{% endif %} location / { try_files $uri $uri/ /index.php$is_args$args; diff --git a/roles/serveur_icingaweb2/templates/resources.ini.j2 b/roles/serveur_icingaweb2/templates/resources.ini.j2 index 8648177..b23e8d4 100644 --- a/roles/serveur_icingaweb2/templates/resources.ini.j2 +++ b/roles/serveur_icingaweb2/templates/resources.ini.j2 @@ -15,7 +15,26 @@ charset = "UTF8" ssl_mode = "{{ serveur_icingaweb2_db_sslmode }}" ssl_ca = "{{ serveur_icingaweb2_db_sslrootcert }}" {% endif %} -{% if serveur_icingaweb2_auth != 'locale' %} +{% if serveur_icingaweb2_auth == 'db' %} + +; LA BASE DES COMPTES DE LA CONSOLE — distincte d'`icingadb`, qui est celle du MOTEUR. +; Meler les deux ferait disparaitre les comptes au premier passage des migrations du +; moteur, sans que personne n'ait touche aux comptes. +[icingaweb_db] +type = "db" +db = "pgsql" +host = "{{ serveur_icingaweb2_db_host }}" +port = "{{ serveur_icingaweb2_db_port }}" +dbname = "{{ serveur_icingaweb2_db_base }}" +username = "{{ serveur_icingaweb2_db_utilisateur }}" +password = "{{ serveur_icingaweb2_db_motdepasse }}" +charset = "UTF8" +{% if serveur_icingaweb2_db_sslmode | default('') %} +ssl_mode = "{{ serveur_icingaweb2_db_sslmode }}" +ssl_ca = "{{ serveur_icingaweb2_db_sslrootcert }}" +{% endif %} +{% endif %} +{% if serveur_icingaweb2_auth not in ('db',) %} [icingaweb_ldap] type = "ldap" diff --git a/roles/serveur_icingaweb2/templates/roles.ini.j2 b/roles/serveur_icingaweb2/templates/roles.ini.j2 index a9b2e71..eca826c 100644 --- a/roles/serveur_icingaweb2/templates/roles.ini.j2 +++ b/roles/serveur_icingaweb2/templates/roles.ini.j2 @@ -1,15 +1,14 @@ ; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main. [Administrators] -{% if serveur_icingaweb2_auth == 'locale' %} -; ICI ON NOMME QUELQU'UN, ET C'EST UNE ENTORSE ASSUMEE A D-66. +{% if serveur_icingaweb2_auth == 'db' %} +; LE GROUPE, MEME SANS ANNUAIRE. Les groupes d'Icinga Web 2 vivent en base : D-66 +; s'applique donc ici comme ailleurs — on habilite `{{ serveur_icingaweb2_groupe_admin }}`, +; pas une personne, et y ajouter quelqu'un se fait dans l'interface. ; -; « Le groupe, jamais des personnes » suppose un annuaire ou le groupe existe. Un SITE -; n'en a pas : ni LDAP, ni Keycloak — ce sont des services d'ecosysteme, et l'hebergeur -; n'en est pas un. Nommer un groupe absent ne protegerait personne, il empecherait -; seulement d'entrer. -; -; Le jour ou un site porte un annuaire, ce role bascule en `ldap` ou `external` et cette -; branche cesse de s'appliquer — sans rien reecrire. +; LE COMPTE D'AMORCAGE EST NOMME EN PLUS, et lui seul : il faut bien que quelqu'un puisse +; entrer la premiere fois pour peupler le groupe. C'est le meme role que le compte de +; secours de Grafana ou de Forgejo. +groups = "{{ serveur_icingaweb2_groupe_admin }}" users = "{{ serveur_icingaweb2_admin_utilisateur }}" {% else %} ; Le GROUPE, jamais des personnes (D-66) : revoquer quelqu'un ne demande alors diff --git a/scripts/site_inventaire.py b/scripts/site_inventaire.py index 4cda723..f702d08 100755 --- a/scripts/site_inventaire.py +++ b/scripts/site_inventaire.py @@ -673,13 +673,18 @@ def inventaire() -> dict: "serveur_postgresql_tls_actif": True, "serveur_grafana_oidc_actif": False, "serveur_grafana_connexion_locale": True, - # LA CONSOLE DE SUPERVISION S'AUTHENTIFIE SEULE, AU SITE (2026-09-14). + # LA CONSOLE DE SUPERVISION GERE SES COMPTES ELLE-MEME, AU SITE. # # Meme raison que pour Grafana juste au-dessus : un SITE n'a ni annuaire ni # Keycloak — ce sont des services d'ECOSYSTEME, et l'hebergeur n'en est pas # un. Les deux autres modes d'Icinga Web 2 (`ldap`, `external`) supposent - # l'un ou l'autre ; ici, nginx authentifie et l'application le croit. - "serveur_icingaweb2_auth": "locale", + # l'un ou l'autre, et n'ont de sens que chez un locataire. + # + # `db` EST LE MODE NATIF : sa propre base, sa propre page de connexion, et la + # gestion des comptes DANS l'interface. La premiere ecriture posait un + # `auth_basic` nginx devant — un vestibule devant une application qui sait + # deja s'authentifier (corrige le 2026-09-15). + "serveur_icingaweb2_auth": "db", # L'EDGE SERT LE CERTIFICAT DE L'AC INTERNE, PAS LE BOUCHON DE DEBIAN. # # `serveur_nginx` a pour DEFAUT `ssl-cert-snakeoil.pem`, avec un commentaire