une console pour le site, et un repli qui ouvrait vers l Internet
Icinga Web 2 gagne un troisieme mode, locale : nginx authentifie en HTTP Basic et pose REMOTE_USER, l application le croit. Un SITE n a ni annuaire ni Keycloak — ce sont des services d ecosysteme. Ni backend LDAP ni backend de groupes : un backend qui vise une ressource inexistante fait echouer chaque ouverture de session. L habilitation nomme alors une personne, entorse a D-66 ecrite plutot que contournee. Deux pieges en chemin : resoudre_annuaire etait appele sans condition et tombait sur NoneType has no len (default sans son second argument ne remplace pas None), et le flux du role ne nommait que l edge — le site n en a pas, donc personne ne pouvait entrer. serveur_grafana portait deja la reponse. LE DEFAUT DU DEVIS : une sortie vers un role ABSENT de l ecosysteme retombait sur !SETOPS_INTERNES, la forme de vers l Internet. La frontiere aurait autorise la console a parler LDAPS a n importe quelle machine du monde, pour joindre un annuaire qui n existe pas. C est une source vide ouvre le port, cote destination. Trois regles du meme defaut etaient DEJA posees pour postfix. La frontiere n est pas ecrite : elle porte la production, et le devis attend un mot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c308c9c5b0
commit
5ecce4282a
15 changed files with 335 additions and 13 deletions
85
CHANGELOG.md
85
CHANGELOG.md
|
|
@ -1,5 +1,90 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-14 (19) — Une console pour le site, et un repli qui ouvrait vers l'Internet
|
||||||
|
|
||||||
|
Le site calculait 88 verdicts que personne ne pouvait lire. Il a desormais sa console —
|
||||||
|
et la construire a revele un defaut du devis de frontiere qui, lui, etait deja applique.
|
||||||
|
|
||||||
|
### Un troisieme mode d'authentification : `locale`
|
||||||
|
|
||||||
|
Icinga Web 2 n'a pas d'OIDC natif. Ses deux modes existants supposent l'un un ANNUAIRE
|
||||||
|
(`ldap`), l'autre une PASSERELLE SSO (`external`). **Un SITE n'a ni l'un ni l'autre** :
|
||||||
|
ce sont des services d'ECOSYSTEME, et l'hebergeur n'en est pas un.
|
||||||
|
|
||||||
|
`locale` : nginx authentifie en HTTP Basic et pose `REMOTE_USER` ; l'application croit ce
|
||||||
|
que le serveur web lui dit — c'est exactement le contrat du backend `external`, avec un
|
||||||
|
vestibule plus simple. Ni backend d'annuaire, ni backend de groupes, ni ressource LDAP :
|
||||||
|
un backend `ldap` qui vise une ressource inexistante ne rend pas une liste vide, il fait
|
||||||
|
ECHOUER chaque ouverture de session.
|
||||||
|
|
||||||
|
C'est le meme choix que `serveur_grafana_connexion_locale` et le compte local de Forgejo
|
||||||
|
au site. **L'habilitation nomme alors une personne**, ce qui contredit D-66 (« le groupe,
|
||||||
|
jamais des personnes ») — la regle suppose un annuaire, et il n'y en a pas. C'est ecrit
|
||||||
|
dans `roles.ini.j2` plutot que contourne en silence.
|
||||||
|
|
||||||
|
`auth_basic` est pose au niveau du `server`, pas du seul bloc PHP : place la, il aurait
|
||||||
|
laisse passer tout ce que `try_files` sert directement. Une authentification qu'on
|
||||||
|
contourne par un chemin voisin n'en est pas une.
|
||||||
|
|
||||||
|
### Deux pieges rencontres en chemin
|
||||||
|
|
||||||
|
**`resoudre_annuaire` etait appele sans condition**, et tombait sur un plan sans annuaire :
|
||||||
|
|
||||||
|
object of type 'NoneType' has no len()
|
||||||
|
|
||||||
|
Un message qui ne nomme ni l'annuaire, ni le role qui le demandait, ni la raison. Deux
|
||||||
|
corrections : le consommateur ne resout plus d'annuaire quand son mode n'en a pas, et
|
||||||
|
`resoudre_annuaire` emploie `default('', true)` — le second argument dit « remplace aussi
|
||||||
|
ce qui est FAUX », c'est-a-dire `None`. Sans lui, `None` traverse et heurte `| length`.
|
||||||
|
|
||||||
|
**`serveur_icingaweb2` ne declarait son entree que pour l'`edge`.** Le site n'en a pas :
|
||||||
|
nginx ecoutait, php-fpm repondait, et le pare-feu ne laissait entrer personne.
|
||||||
|
`serveur_grafana` portait deja la reponse — *joignable depuis le plan d'administration
|
||||||
|
partout, sans quoi un deploiement sans edge n'a plus de console*. Les deux consoles de
|
||||||
|
l'observabilite avaient la meme contrainte et une seule des deux l'avait ecrite.
|
||||||
|
|
||||||
|
### LE DEFAUT DU DEVIS : un role absent traduit en « tout l'Internet »
|
||||||
|
|
||||||
|
`make frontiere-plan` proposait cinq regles. Quatre etaient attendues. La cinquieme :
|
||||||
|
|
||||||
|
+ regle opt9 tcp 636 SETOPS_SITE_SERVEUR_ICINGAWEB2 -> !SETOPS_INTERNES
|
||||||
|
|
||||||
|
`serveur_icingaweb2` declare une sortie LDAPS vers `serveur_openldap`. Le site n'en a pas.
|
||||||
|
Le devis cherchait la destination parmi les roles PRESENTS, n'en trouvait aucun, et
|
||||||
|
retombait sur `!SETOPS_INTERNES` — la forme de « vers l'Internet ». La frontiere aurait
|
||||||
|
autorise la console a parler LDAPS a **n'importe quelle machine du monde**, pour joindre
|
||||||
|
un annuaire qui n'existe pas.
|
||||||
|
|
||||||
|
C'est « une source vide ouvre le port », cote DESTINATION. Le repli est juste quand le
|
||||||
|
pair est lointain — un depot Debian, un serveur NTP. Il est faux des que le pair NOMME un
|
||||||
|
role : le flux ne parle alors pas de l'Internet, il parle d'une machine, et elle n'est pas
|
||||||
|
la. Le devis distingue desormais les deux, et le dit :
|
||||||
|
|
||||||
|
note : serveur_icingaweb2 declare une sortie vers serveur_openldap, absent de ce
|
||||||
|
site — aucune regle emise (le repli aurait ouvert le port vers l'Internet).
|
||||||
|
|
||||||
|
**ET TROIS REGLES DU MEME DEFAUT ETAIENT DEJA POSEES.** Le devis corrige les signale
|
||||||
|
perimees :
|
||||||
|
|
||||||
|
- opt9 TCP 24 SETOPS_SITE_SERVEUR_POSTFIX -> !SETOPS_INTERNES (vers serveur_dovecot)
|
||||||
|
- opt9 TCP 12345 SETOPS_SITE_SERVEUR_POSTFIX -> !SETOPS_INTERNES (vers serveur_dovecot)
|
||||||
|
- opt9 TCP 636 SETOPS_SITE_SERVEUR_POSTFIX -> !SETOPS_INTERNES (vers serveur_openldap)
|
||||||
|
|
||||||
|
Le relais de courriel du site n'a ni Dovecot ni annuaire a joindre. Ces trois regles ne
|
||||||
|
pouvaient porter aucun trafic utile — elles ne faisaient qu'elargir la frontiere.
|
||||||
|
Les retirer est un RETRECISSEMENT.
|
||||||
|
|
||||||
|
### Etat
|
||||||
|
|
||||||
|
Console deployee, verifiee sur la machine : nginx et php-fpm actifs, `401` sans
|
||||||
|
identifiants, `verify-full` vers PostgreSQL, **zero** reference LDAP dans les trois `.ini`.
|
||||||
|
`vault_icingaweb2_admin` pose dans la voute du site avec les memes gardes que la veille,
|
||||||
|
et ajoute aux quatre gabarits d'ecosysteme.
|
||||||
|
|
||||||
|
**LA FRONTIERE N'EST PAS ECRITE.** Elle porte la production, et `frontiere-appliquer`
|
||||||
|
exige `CONFIRMER=true` — ce garde-fou existe pour un humain. Le devis est lu, les quatre
|
||||||
|
ajouts et les trois retraits sont compris ; l'ecriture attend un mot.
|
||||||
|
|
||||||
## 2026-09-14 (18) — La supervision du SITE etait morte depuis 11:10, et rien ne le disait
|
## 2026-09-14 (18) — La supervision du SITE etait morte depuis 11:10, et rien ne le disait
|
||||||
|
|
||||||
L'exploitant : *« je ne vois pas de serveur icinga pour le site ? »*. Il y en a un. Il
|
L'exploitant : *« je ne vois pas de serveur icinga pour le site ? »*. Il y en a un. Il
|
||||||
|
|
|
||||||
|
|
@ -30,21 +30,21 @@
|
||||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 29 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 30 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
|
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
|
||||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. |
|
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. |
|
||||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 49 groupe(s), 88 regle(s). |
|
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 49 groupe(s), 90 regle(s). |
|
||||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 42 VM placee(s), aucun nom ni VMID en collision. |
|
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 42 VM placee(s), aucun nom ni VMID en collision. |
|
||||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 67 scripts expliques et atteignables, 123 cibles make documentees, 68 roles avec README. |
|
| 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 : 35 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). |
|
| 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 : 35 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
|
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 35 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 (40 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). |
|
| 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). |
|
||||||
|
|
@ -55,14 +55,14 @@
|
||||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 63 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 63 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
||||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 154 regle(s) du site. |
|
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. |
|
||||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 92 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 92 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (128 lignes). |
|
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (128 lignes). |
|
||||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 241 regles `pass`), tous non consignes et tous motives. |
|
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 240 regles `pass`), tous non consignes et tous motives. |
|
||||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 39 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 39 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
||||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 3 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (5 exposition(s)). |
|
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 3 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (5 exposition(s)). |
|
||||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 10 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 11 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
||||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
||||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
| `serveur_icinga` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base relationnelle du moteur Icinga (verify-full). |
|
| `serveur_icinga` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base relationnelle du moteur Icinga (verify-full). |
|
||||||
| `serveur_icinga` | egress | echo-request | icmp | serveur_debian | n-a | La supervision verifie que ses hotes repondent (hostalive) : sans ce flux, elle les tient tous pour morts et supprime leurs notifications. |
|
| `serveur_icinga` | egress | echo-request | icmp | serveur_debian | n-a | La supervision verifie que ses hotes repondent (hostalive) : sans ce flux, elle les tient tous pour morts et supprime leurs notifications. |
|
||||||
| `serveur_icinga` | egress | echo-request | icmp | fabric | n-a | La supervision verifie que la frontiere sert encore chaque zone. Aucun agent ne peut vivre sur un pare-feu : le controle ACTIF est le seul chemin, et il n'existait pas. |
|
| `serveur_icinga` | egress | echo-request | icmp | fabric | n-a | La supervision verifie que la frontiere sert encore chaque zone. Aucun agent ne peut vivre sur un pare-feu : le controle ACTIF est le seul chemin, et il n'existait pas. |
|
||||||
| `serveur_icingaweb2` | ingress | 8080 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via oauth2-proxy). |
|
| `serveur_icingaweb2` | ingress | 8080 | tcp | edge, admin | clair | Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via oauth2-proxy), et joignable depuis le plan d'administration là où il n'y a pas d'edge. |
|
||||||
| `serveur_icingaweb2` | egress | 636 | tcp | serveur_openldap | tls-requis | Authentification des utilisateurs sur l'annuaire (LDAPS). |
|
| `serveur_icingaweb2` | egress | 636 | tcp | serveur_openldap | tls-requis | Authentification des utilisateurs sur l'annuaire (LDAPS). |
|
||||||
| `serveur_icingaweb2` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Lecture d'IcingaDB (base relationnelle, verify-full). |
|
| `serveur_icingaweb2` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Lecture d'IcingaDB (base relationnelle, verify-full). |
|
||||||
| `serveur_keycloak` | ingress | 8080 | tcp | edge | clair | Console et endpoints OIDC servis au navigateur et aux applications via l'edge (TLS terminé à l'edge). |
|
| `serveur_keycloak` | ingress | 8080 | tcp | edge | clair | Console et endpoints OIDC servis au navigateur et aux applications via l'edge (TLS terminé à l'edge). |
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,13 @@ vault_forgejo_internal_token: ""
|
||||||
vault_grafana_admin: ""
|
vault_grafana_admin: ""
|
||||||
vault_redis: ""
|
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.
|
||||||
|
vault_icingaweb2_admin: ""
|
||||||
|
|
||||||
# LE COMPTE DE METRIQUES DE POSTGRESQL — lecture seule, role `pg_monitor`.
|
# LE COMPTE DE METRIQUES DE POSTGRESQL — lecture seule, role `pg_monitor`.
|
||||||
#
|
#
|
||||||
# Il ne lit que les vues de statistiques : pas une ligne de donnee applicative. Faire
|
# Il ne lit que les vues de statistiques : pas une ligne de donnee applicative. Faire
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,20 @@
|
||||||
file: "{{ setops_plan_dir }}/applications.yml"
|
file: "{{ setops_plan_dir }}/applications.yml"
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
|
# `default('', true)` ET PAS `default('')` — LA DIFFERENCE EST TOUT LE DEFAUT (2026-09-14).
|
||||||
|
#
|
||||||
|
# `.get('hote')` rend `None` quand l'application existe sans hote, et `default('')` ne
|
||||||
|
# remplace que l'INDEFINI : un `None` le traverse intact, puis `| length` le heurte :
|
||||||
|
#
|
||||||
|
# object of type 'NoneType' has no len()
|
||||||
|
#
|
||||||
|
# Un message qui ne nomme ni l'annuaire, ni le role qui le demandait, ni la raison. Le
|
||||||
|
# second argument de `default` dit « remplace aussi ce qui est faux » — c'est-a-dire
|
||||||
|
# `None`, la chaine vide et zero.
|
||||||
- name: Dériver l'hôte de l'annuaire depuis le plan
|
- name: Dériver l'hôte de l'annuaire depuis le plan
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
resoudre_annuaire_hote: "{{ (applications | default({})).get(resoudre_annuaire_app, {}).get('hote') }}"
|
resoudre_annuaire_hote: "{{ (applications | default({})).get(resoudre_annuaire_app, {}).get('hote') }}"
|
||||||
when: ((applications | default({})).get(resoudre_annuaire_app, {}).get('hote') | default('')) | length > 0
|
when: ((applications | default({})).get(resoudre_annuaire_app, {}).get('hote') | default('', true)) | length > 0
|
||||||
|
|
||||||
# set_fact (pas les defaults) pour que ces valeurs persistent dans le rôle consommateur
|
# set_fact (pas les defaults) pour que ces valeurs persistent dans le rôle consommateur
|
||||||
# après include_role (les defaults du rôle inclus ne fuient pas hors de son exécution).
|
# après include_role (les defaults du rôle inclus ne fuient pas hors de son exécution).
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,23 @@ serveur_icingaweb2_groupes_dn: "ou=groups,{{ resoudre_annuaire_base_dn | default
|
||||||
|
|
||||||
# Backend d'authentification : "ldap" (direct) ou "external" (utilisateur fourni par une
|
# Backend d'authentification : "ldap" (direct) ou "external" (utilisateur fourni par une
|
||||||
# passerelle SSO devant, ex. oauth2-proxy → Keycloak). En "external", nginx doit poser REMOTE_USER.
|
# passerelle SSO devant, ex. oauth2-proxy → Keycloak). En "external", nginx doit poser REMOTE_USER.
|
||||||
|
# TROIS MODES D'AUTHENTIFICATION, ET LE TROISIEME EST NE POUR LE SITE (2026-09-14).
|
||||||
|
#
|
||||||
|
# `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.
|
||||||
|
#
|
||||||
|
# POURQUOI `locale` 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.
|
||||||
serveur_icingaweb2_auth: "ldap"
|
serveur_icingaweb2_auth: "ldap"
|
||||||
# Variable nginx d'où vient REMOTE_USER. En SSO : l'en-tête d'oauth2-proxy.
|
# Variable nginx d'où vient REMOTE_USER. En SSO : l'en-tête d'oauth2-proxy.
|
||||||
serveur_icingaweb2_remote_user: "$remote_user"
|
serveur_icingaweb2_remote_user: "$remote_user"
|
||||||
|
|
@ -85,3 +102,21 @@ serveur_icingaweb2_bpm_processes: {}
|
||||||
# pile PHP est vivante. Ce qu'ils excluent, c'est le 502 et le 504 — php-fpm mort ou
|
# pile PHP est vivante. Ce qu'ils excluent, c'est le 502 et le 504 — php-fpm mort ou
|
||||||
# bloque — et l'absence de reponse, qui est le nginx local tombe.
|
# 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"
|
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 ---------------------------------
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
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"
|
||||||
|
|
||||||
|
# TLS VERS POSTGRESQL, DERIVE DE CE QUE SERT LE SERVEUR. Voir `resoudre_base` et P78.
|
||||||
|
#
|
||||||
|
# P78 nommait ce role comme « sans reglage TLS » — c'etait vrai et sans consequence tant
|
||||||
|
# qu'il ne tournait nulle part face a un `hostssl`. Au site, il en trouve un.
|
||||||
|
serveur_icingaweb2_db_sslmode: >-
|
||||||
|
{{ 'verify-full' if (resoudre_base_db_tls_force | default(false)) else '' }}
|
||||||
|
serveur_icingaweb2_db_sslrootcert: "/etc/step/certs/root_ca.crt"
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,26 @@
|
||||||
# Flux réseau d'Icinga Web 2 (frontal de supervision). Voir docs/flux-conception.md.
|
# Flux réseau d'Icinga Web 2 (frontal de supervision). Voir docs/flux-conception.md.
|
||||||
# Icinga 2 (API 5665) et IcingaDB (Redis 6380) sont co-localisés : consommés en localhost.
|
# Icinga 2 (API 5665) et IcingaDB (Redis 6380) sont co-localisés : consommés en localhost.
|
||||||
flux:
|
flux:
|
||||||
|
# `admin` EN PLUS DE `edge` — le meme raisonnement que `serveur_grafana`, qui le portait
|
||||||
|
# deja et dont la phrase vaut mot pour mot ici : *joignable depuis le plan
|
||||||
|
# d'administration partout, sans quoi un deploiement sans edge n'a plus de console*.
|
||||||
|
#
|
||||||
|
# CE QUE SON ABSENCE COUTAIT (2026-09-14) : le SITE n'a pas d'`infra-edge`. La console
|
||||||
|
# de supervision y a ete deployee, nginx ecoutait, php-fpm repondait — et le pare-feu
|
||||||
|
# ne laissait entrer PERSONNE. 88 verdicts lisibles par aucune machine au monde.
|
||||||
|
#
|
||||||
|
# Les deux consoles de l'observabilite avaient la meme contrainte et une seule des deux
|
||||||
|
# l'avait ecrite. En mode `locale`, nginx authentifie lui-meme : ce qui entre par le
|
||||||
|
# plan d'administration frappe un vestibule, pas l'application.
|
||||||
- sens: ingress
|
- sens: ingress
|
||||||
port: 8080
|
port: 8080
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
pair: edge
|
pair: [edge, admin]
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
raison: "Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via oauth2-proxy)."
|
raison: >-
|
||||||
|
Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via
|
||||||
|
oauth2-proxy), et joignable depuis le plan d'administration là où il n'y a pas
|
||||||
|
d'edge.
|
||||||
- sens: egress
|
- sens: egress
|
||||||
port: 5432
|
port: 5432
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,15 @@
|
||||||
vars:
|
vars:
|
||||||
resoudre_base_groupe: "{{ serveur_icingaweb2_icinga_groupe }}"
|
resoudre_base_groupe: "{{ serveur_icingaweb2_icinga_groupe }}"
|
||||||
|
|
||||||
|
# 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 —
|
||||||
|
# il tombait sur un plan sans application d'annuaire :
|
||||||
|
#
|
||||||
|
# object of type 'NoneType' has no len()
|
||||||
|
#
|
||||||
|
# Un message qui ne nomme ni l'annuaire, ni le role qui le demandait, ni la raison. Le
|
||||||
|
# mode `locale` existe precisement pour se passer d'annuaire : il n'a rien a resoudre.
|
||||||
- name: Résoudre l'annuaire (rôle partagé)
|
- name: Résoudre l'annuaire (rôle partagé)
|
||||||
ansible.builtin.include_role:
|
ansible.builtin.include_role:
|
||||||
name: resoudre_annuaire
|
name: resoudre_annuaire
|
||||||
|
|
@ -12,6 +21,7 @@
|
||||||
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
# SON compte, pas celui de l'administrateur de l'annuaire (2026-09-13).
|
||||||
resoudre_annuaire_bind_cn: "icingaweb2"
|
resoudre_annuaire_bind_cn: "icingaweb2"
|
||||||
resoudre_annuaire_secret: "vault_ldap_bind_icingaweb2"
|
resoudre_annuaire_secret: "vault_ldap_bind_icingaweb2"
|
||||||
|
when: serveur_icingaweb2_auth != 'locale'
|
||||||
|
|
||||||
- name: Adopter la connexion annuaire pour Icinga Web 2
|
- name: Adopter la connexion annuaire pour Icinga Web 2
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
@ -22,6 +32,7 @@
|
||||||
serveur_icingaweb2_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
serveur_icingaweb2_ldap_bind_dn: "{{ resoudre_annuaire_bind_dn }}"
|
||||||
serveur_icingaweb2_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}"
|
serveur_icingaweb2_ldap_bind_password: "{{ resoudre_annuaire_bind_password }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
when: serveur_icingaweb2_auth != 'locale'
|
||||||
|
|
||||||
# LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03).
|
# LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03).
|
||||||
#
|
#
|
||||||
|
|
@ -111,6 +122,75 @@
|
||||||
label: "{{ item.key }}"
|
label: "{{ item.key }}"
|
||||||
when: serveur_icingaweb2_bpm_processes | length > 0
|
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
|
- name: Déployer le vhost nginx local d'Icinga Web 2
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: nginx.conf.j2
|
src: nginx.conf.j2
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,16 @@
|
||||||
; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main.
|
; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main.
|
||||||
{% if serveur_icingaweb2_auth == 'external' %}
|
{% 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.
|
||||||
|
;
|
||||||
|
; 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.
|
||||||
|
[icingaweb2]
|
||||||
|
backend = "external"
|
||||||
|
{% elif serveur_icingaweb2_auth == 'external' %}
|
||||||
; SSO : l'utilisateur est fourni par la passerelle (oauth2-proxy → Keycloak) via REMOTE_USER.
|
; SSO : l'utilisateur est fourni par la passerelle (oauth2-proxy → Keycloak) via REMOTE_USER.
|
||||||
[icingaweb2]
|
[icingaweb2]
|
||||||
backend = "external"
|
backend = "external"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@
|
||||||
; Habilitation par GROUPE d'annuaire (D-66) : ajouter quelqu'un a
|
; Habilitation par GROUPE d'annuaire (D-66) : ajouter quelqu'un a
|
||||||
; `{{ serveur_icingaweb2_groupe_admin }}` dans LDAP lui ouvre la supervision, sans
|
; `{{ serveur_icingaweb2_groupe_admin }}` dans LDAP lui ouvre la supervision, sans
|
||||||
; deploiement et sans que personne ne soit nomme ici.
|
; 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.
|
||||||
|
{% else %}
|
||||||
[annuaire]
|
[annuaire]
|
||||||
backend = "ldap"
|
backend = "ldap"
|
||||||
resource = "icingaweb_ldap"
|
resource = "icingaweb_ldap"
|
||||||
|
|
@ -10,3 +15,4 @@ group_class = "{{ serveur_icingaweb2_groupe_class }}"
|
||||||
group_name_attribute = "cn"
|
group_name_attribute = "cn"
|
||||||
group_member_attribute = "{{ serveur_icingaweb2_groupe_membre_attr }}"
|
group_member_attribute = "{{ serveur_icingaweb2_groupe_membre_attr }}"
|
||||||
base_dn = "{{ serveur_icingaweb2_groupes_dn }}"
|
base_dn = "{{ serveur_icingaweb2_groupes_dn }}"
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,22 @@ server {
|
||||||
|
|
||||||
root {{ serveur_icingaweb2_docroot }};
|
root {{ serveur_icingaweb2_docroot }};
|
||||||
index index.php;
|
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 / {
|
location / {
|
||||||
try_files $uri $uri/ /index.php$is_args$args;
|
try_files $uri $uri/ /index.php$is_args$args;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ dbname = "{{ resoudre_base_entree.base }}"
|
||||||
username = "{{ resoudre_base_entree.proprietaire }}"
|
username = "{{ resoudre_base_entree.proprietaire }}"
|
||||||
password = "{{ resoudre_base_db_password }}"
|
password = "{{ resoudre_base_db_password }}"
|
||||||
charset = "UTF8"
|
charset = "UTF8"
|
||||||
|
{% if serveur_icingaweb2_db_sslmode | default('') %}
|
||||||
|
; LE SERVEUR IMPOSE `hostssl` : sans ces deux lignes, PostgreSQL refuse la connexion et
|
||||||
|
; l'erreur parle de `pg_hba`, pas de chiffrement. Derive de ce que sert le serveur — voir
|
||||||
|
; `resoudre_base` et P78.
|
||||||
|
ssl_mode = "{{ serveur_icingaweb2_db_sslmode }}"
|
||||||
|
ssl_ca = "{{ serveur_icingaweb2_db_sslrootcert }}"
|
||||||
|
{% endif %}
|
||||||
|
{% if serveur_icingaweb2_auth != 'locale' %}
|
||||||
|
|
||||||
[icingaweb_ldap]
|
[icingaweb_ldap]
|
||||||
type = "ldap"
|
type = "ldap"
|
||||||
|
|
@ -18,3 +26,4 @@ root_dn = "{{ serveur_icingaweb2_ldap_base }}"
|
||||||
bind_dn = "{{ serveur_icingaweb2_ldap_bind_dn }}"
|
bind_dn = "{{ serveur_icingaweb2_ldap_bind_dn }}"
|
||||||
bind_pw = "{{ serveur_icingaweb2_ldap_bind_password }}"
|
bind_pw = "{{ serveur_icingaweb2_ldap_bind_password }}"
|
||||||
timeout = "5"
|
timeout = "5"
|
||||||
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,22 @@
|
||||||
; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main.
|
; Géré par Set-OPS (rôle serveur_icingaweb2). Ne pas éditer à la main.
|
||||||
[Administrators]
|
[Administrators]
|
||||||
|
{% if serveur_icingaweb2_auth == 'locale' %}
|
||||||
|
; ICI ON NOMME QUELQU'UN, ET C'EST UNE ENTORSE ASSUMEE A D-66.
|
||||||
|
;
|
||||||
|
; « 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.
|
||||||
|
users = "{{ serveur_icingaweb2_admin_utilisateur }}"
|
||||||
|
{% else %}
|
||||||
; Le GROUPE, jamais des personnes (D-66) : revoquer quelqu'un ne demande alors
|
; Le GROUPE, jamais des personnes (D-66) : revoquer quelqu'un ne demande alors
|
||||||
; aucun deploiement. `serveur_icingaweb2_admins` reste un repli, vide par defaut.
|
; aucun deploiement. `serveur_icingaweb2_admins` reste un repli, vide par defaut.
|
||||||
groups = "{{ serveur_icingaweb2_groupe_admin }}"
|
groups = "{{ serveur_icingaweb2_groupe_admin }}"
|
||||||
{% if serveur_icingaweb2_admins | length > 0 %}
|
{% if serveur_icingaweb2_admins | length > 0 %}
|
||||||
users = "{{ serveur_icingaweb2_admins }}"
|
users = "{{ serveur_icingaweb2_admins }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
permissions = "*"
|
permissions = "*"
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
RACINE = Path(__file__).resolve().parents[1]
|
RACINE = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
# LES ROLES QUE LE DEPOT CONNAIT — pour distinguer un PAIR QUI NOMME UN ROLE d'un mot-cle
|
||||||
|
# de portee (`externe`, `flotte`, `admin`, `edge`, `voisins_site`, `frontiere`, `fabric`,
|
||||||
|
# `localhost`). La distinction decide de ce qu'on fait d'une destination introuvable :
|
||||||
|
# ignorer le flux, ou l'ouvrir vers l'Internet. Les deux n'ont rien a voir.
|
||||||
|
_ROLES_CONNUS = {d.name for d in (RACINE / "roles").iterdir() if d.is_dir()}
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
from inventory_rules import sous_reseau_de, supernet_de # noqa: E402
|
from inventory_rules import sous_reseau_de, supernet_de # noqa: E402
|
||||||
|
|
@ -971,8 +977,31 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
||||||
_dests = ["SETOPS_FABRIC"]
|
_dests = ["SETOPS_FABRIC"]
|
||||||
else:
|
else:
|
||||||
_internes = sorted(set(_pairs_fl) & set(_roles_site))
|
_internes = sorted(set(_pairs_fl) & set(_roles_site))
|
||||||
_dests = ([nom_alias(_x, "SITE") for _x in _internes]
|
if _internes:
|
||||||
if _internes else ["!SETOPS_INTERNES"])
|
_dests = [nom_alias(_x, "SITE") for _x in _internes]
|
||||||
|
elif set(_pairs_fl) & _ROLES_CONNUS:
|
||||||
|
# UN PAIR QUI NOMME UN ROLE ABSENT N'EST PAS « L'INTERNET ».
|
||||||
|
#
|
||||||
|
# Mesure du 2026-09-14, au devis et avant toute ecriture :
|
||||||
|
# `serveur_icingaweb2` declare une sortie LDAPS vers
|
||||||
|
# `serveur_openldap`. Un SITE n'a pas d'annuaire — ce sont des
|
||||||
|
# services d'ecosysteme. `_internes` etait donc vide, et le repli
|
||||||
|
# rendait `!SETOPS_INTERNES` : la frontiere aurait autorise la
|
||||||
|
# console a parler LDAPS a N'IMPORTE QUELLE machine du monde,
|
||||||
|
# pour joindre un annuaire qui n'existe pas.
|
||||||
|
#
|
||||||
|
# C'est « une source vide ouvre le port », cote DESTINATION. Le
|
||||||
|
# repli est juste quand le pair est un mot-cle lointain
|
||||||
|
# (`externe`, un depot Debian, un serveur NTP) ; il est faux des
|
||||||
|
# que le pair NOMME un role, parce qu'alors le flux ne parle pas
|
||||||
|
# de l'Internet — il parle d'une machine, et elle n'est pas la.
|
||||||
|
print(f"note : {_role} declare une sortie vers "
|
||||||
|
f"{'/'.join(sorted(set(_pairs_fl) & _ROLES_CONNUS))}, "
|
||||||
|
f"absent de ce site — aucune regle emise (le repli aurait "
|
||||||
|
f"ouvert le port vers l'Internet).", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
_dests = ["!SETOPS_INTERNES"]
|
||||||
for _src, _itf in _portees:
|
for _src, _itf in _portees:
|
||||||
for _dst in _dests:
|
for _dst in _dests:
|
||||||
regles.append({
|
regles.append({
|
||||||
|
|
|
||||||
|
|
@ -636,6 +636,13 @@ def inventaire() -> dict:
|
||||||
"serveur_postgresql_tls_actif": True,
|
"serveur_postgresql_tls_actif": True,
|
||||||
"serveur_grafana_oidc_actif": False,
|
"serveur_grafana_oidc_actif": False,
|
||||||
"serveur_grafana_connexion_locale": True,
|
"serveur_grafana_connexion_locale": True,
|
||||||
|
# LA CONSOLE DE SUPERVISION S'AUTHENTIFIE SEULE, AU SITE (2026-09-14).
|
||||||
|
#
|
||||||
|
# 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",
|
||||||
# Le depot du site, pour les machines du site qui y deposent. Vide s'il n'y
|
# Le depot du site, pour les machines du site qui y deposent. Vide s'il n'y
|
||||||
# en a pas — `client_backup` refuse alors, plutot que de viser un nom qui
|
# en a pas — `client_backup` refuse alors, plutot que de viser un nom qui
|
||||||
# ne repond pas : mieux vaut un deploiement qui s'arrete qu'une sauvegarde
|
# ne repond pas : mieux vaut un deploiement qui s'arrete qu'une sauvegarde
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue