icingaweb2 : le backend natif remplace un vestibule de trop

L exploitant : 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.

Le mode locale 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 ; le GUI de Set-OPS est dans ce cas, Icinga Web 2 non.

Mode db : backend natif, groupes natifs, base a elle (les tables du moteur sont
reecrites par ses migrations). Le moteur pose UN compte d amorcage et ne
l ecrase jamais. D-66 redevient applicable sans annuaire, les groupes vivant en
base.

Deux pieges du renommage : une garde ecrite en negation a cesse de garder, et le
bloc de la base pose apres le rendu des .ini a produit un echec CENSURE par
no_log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-15 08:49:32 -04:00
parent 95dc225037
commit 51dae506fb
12 changed files with 285 additions and 131 deletions

View file

@ -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)

View file

@ -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. |

View file

@ -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 |

View file

@ -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.

View file

@ -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.
#

View file

@ -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

View file

@ -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]

View file

@ -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"

View file

@ -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;

View file

@ -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"

View file

@ -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

View file

@ -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