la console d exploitation devient un service, et elle n avait aucune serrure

Le GUI n a aucune authentification : GET / sert la page a qui la demande, jeton
ecrit dedans, et POST /api exige ce jeton que la page vient de donner. Le jeton
garde contre le CSRF, pas contre un visiteur — la seule serrure est
--hote 127.0.0.1. Ce qu il offre a qui entre : deployer, creer, raser, editer le
plan. La fabric entiere.

Le service reste donc sur la boucle locale. Ce qui est publie est un nginx local
qui authentifie d abord : oidc par defaut (oauth2-proxy, donc un groupe
d annuaire qu on revoque sans deploiement), locale en repli pour un ecosysteme
sans annuaire. L authentification est posee au niveau du server, pas d un
location.

La sonde console-ops mesure une SERRURE : une requete anonyme doit etre REFUSEE.
Un 200 y est la pire des reponses, et il ne fait echouer personne.

P54 a attrape une contrainte ratee : serveur_ops est insemine par le SITE, qui
ne detient pas la voute du locataire. Le role ne nomme donc aucune voute — il
declare un parametre, et la couche qui detient le secret le remplit.

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-14 19:27:28 -04:00
parent 5ecce4282a
commit 627ef00ec7
12 changed files with 542 additions and 10 deletions

View file

@ -1,5 +1,81 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-09-14 (20) — La console d'exploitation devient un service, et elle n'avait aucune serrure
Decision de l'exploitant : *« j'aimerais que chaque runner expose le GUI de Set-OPS a
travers ce edge »*. La construire a d'abord demande de mesurer ce qu'on allait publier.
### CE QUI A CHANGE LA FORME DE TOUT LE RESTE
**Le GUI n'a AUCUNE authentification.** Mesure du 2026-09-14, dans son propre code :
GET / sert la page A QUI LA DEMANDE, avec le jeton ECRIT DEDANS
POST /api/* exige ce jeton — que la page vient de donner a tout le monde
Le jeton est une garde **CSRF**, pas une serrure. La seule serrure est
`--hote 127.0.0.1` : le GUI est sur parce qu'il n'est joignable que de sa propre machine.
Ce qu'il offre a qui entre : `deployer`, `creer`, `instance-creer`, `instance-utiliser`,
`pousser`, et l'edition du plan. **La fabric entiere.** L'exposer tel quel aurait livre
un ecosysteme a quiconque trouve l'URL.
### Ce qui est construit
Le service reste sur la boucle locale, **toujours**. Ce qui est publie est un nginx local
qui authentifie D'ABORD et relaie ensuite.
| | |
|---|---|
| `setops-gui.service` | le GUI, `127.0.0.1:8765`, sous le compte `setops`, dans le venv du runner |
| nginx local | port declare au plan, `server_name` derive du plan (P67) |
| vestibule | `oidc` (oauth2-proxy -> Keycloak) **par defaut**, `locale` (HTTP Basic) en repli |
| flux | `ingress [edge, admin]` — la meme paire que Grafana et la vigie, pour la meme raison |
| sonde | `console-ops` |
`oidc` PAR DEFAUT, ET CE N'EST PAS UNE PREFERENCE. Cette console peut raser un
ecosysteme. Un mot de passe partage devant ce pouvoir est un accident qui attend ; un
groupe d'annuaire se revoque sans deploiement (D-66). `locale` existe pour un ecosysteme
qui n'a pas d'annuaire — un SITE — et c'est ecrit comme un repli, pas comme un choix.
L'AUTHENTIFICATION EST POSEE AU NIVEAU DU `server`, pas d'un `location` : placee dans le
seul bloc de relais, elle laisserait passer tout chemin qu'un `location` plus specifique
attraperait en premier. Meme geste que pour la vigie, et meme raison.
DEUX REGLAGES QUI NE SONT PAS DU CONFORT. `proxy_read_timeout` a 3600 s : un deploiement
dure, et le delai par defaut de nginx couperait la reponse au milieu — la page dirait
echec pendant que la fabric continue. `proxy_buffering off` : sans lui, la sortie d'un
deploiement arriverait d'un bloc a la fin, et la console resterait muette des minutes.
### La sonde mesure une SERRURE, ce qu'aucun greffon ne pense a faire
`console-ops` verifie deux choses, et la seconde est l'inverse d'une sonde ordinaire :
1. le GUI repond sur la boucle locale -> sinon plus personne ne pilote
2. une requete ANONYME au vestibule est REFUSEE (401/403, ou renvoi en `oidc`)
**Un 200 y est la pire des reponses.** Il veut dire que le vestibule laisse entrer. Ca ne
fait echouer personne — c'est exactement pourquoi il faut le mesurer.
### P54 a attrape une contrainte que j'avais ratee
`serveur_ops` est un role **d'insemination** : le SITE le pose sur le runner d'un
locataire qui vient de naitre, **sans detenir la voute de ce locataire**. Citer
`vault_setops_gui_admin` dans ce role rendait l'insemination impossible — le site
reclamait un secret qu'il n'a pas, et par construction ne doit pas avoir.
serveur_ops cite vault_setops_gui_admin dans roles/serveur_ops/defaults/main.yml :
le SITE ne detient pas cette voute
Le role declare donc un PARAMETRE vide, et la couche qui detient le secret est la seule a
le nommer. La garde a tenu deux fois : la seconde, le nom ne survivait plus que dans le
TEXTE d'un message d'erreur — ou il etait de toute facon faux, puisque la voute varie
selon qui publie.
### Ce qui reste
Le site n'a pas d'edge. L'exploitant vient d'en autoriser un : il publiera l'observatoire,
la vigie et la console du site sous leurs noms, en TLS. C'est la suite.
## 2026-09-14 (19) — Une console pour le site, et un repli qui ouvrait vers l'Internet ## 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 — Le site calculait 88 verdicts que personne ne pouvait lire. Il a desormais sa console —

View file

@ -13,7 +13,7 @@
| # | Preuve | Affirmations | Statut | Detail | | # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---| |---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | | P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK | | P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | | P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | | P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
@ -21,7 +21,7 @@
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. | | P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 110 flux, schéma + matrice OK. | | P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 111 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
@ -37,7 +37,7 @@
| 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), 90 regle(s). | | P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 52 groupe(s), 96 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. |
@ -45,7 +45,7 @@
| 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 : 36 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 : 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 (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). |
| 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). | | 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). |
@ -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, 153 regle(s) du site. | | 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. |
| 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 (129 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 240 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 241 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 |
@ -76,7 +76,7 @@
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | | P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | | P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). |
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | | P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
| 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 | 40 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 | 11 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. |
@ -88,7 +88,7 @@
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). | | P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. | | P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. | | P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 152 gabarits de role : tous se rendent. | | P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 155 gabarits de role : tous se rendent. |
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. | | P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). | | P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |

View file

@ -78,6 +78,7 @@
| `serveur_oauth2_proxy` | egress | 8080 | tcp | localhost | clair | Relais vers l'application co-localisée protégée (upstream en localhost). | | `serveur_oauth2_proxy` | egress | 8080 | tcp | localhost | clair | Relais vers l'application co-localisée protégée (upstream en localhost). |
| `serveur_openldap` | ingress | 389 | tcp | flotte | starttls | LDAP + STARTTLS pour les clients internes qui préfèrent la mise à niveau TLS sur 389. | | `serveur_openldap` | ingress | 389 | tcp | flotte | starttls | LDAP + STARTTLS pour les clients internes qui préfèrent la mise à niveau TLS sur 389. |
| `serveur_openldap` | ingress | 636 | tcp | serveur_keycloak, serveur_dovecot, serveur_icingaweb2, serveur_postfix | tls-requis | LDAPS : fédération (Keycloak), userdb courriel (Dovecot), auth web (Icinga Web 2), tables virtuelles (Postfix). | | `serveur_openldap` | ingress | 636 | tcp | serveur_keycloak, serveur_dovecot, serveur_icingaweb2, serveur_postfix | tls-requis | LDAPS : fédération (Keycloak), userdb courriel (Dovecot), auth web (Icinga Web 2), tables virtuelles (Postfix). |
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. | | `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. | | `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
| `serveur_ops` | egress | 443 | tcp | voisins_site | tls-requis | Cloner le genome depuis la forge du site, quand cet ecosysteme n'heberge pas la sienne. | | `serveur_ops` | egress | 443 | tcp | voisins_site | tls-requis | Cloner le genome depuis la forge du site, quand cet ecosysteme n'heberge pas la sienne. |
@ -119,7 +120,7 @@
## Synthèse chiffrement ## Synthèse chiffrement
- **clair** : 40 flux - **clair** : 41 flux
- **n-a** : 8 flux - **n-a** : 8 flux
- **ssh** : 8 flux - **ssh** : 8 flux
- **starttls** : 6 flux - **starttls** : 6 flux

View file

@ -49,6 +49,15 @@ vault_redis: ""
# Inutile en mode `ldap` ou `external` : le rôle ne l'exige que dans le mode qui s'en sert. # Inutile en mode `ldap` ou `external` : le rôle ne l'exige que dans le mode qui s'en sert.
vault_icingaweb2_admin: "" vault_icingaweb2_admin: ""
# LA CONSOLE D'EXPLOITATION, QUAND ELLE S'AUTHENTIFIE SEULE.
#
# `serveur_ops_gui_auth: locale` — le repli d'un ecosysteme SANS annuaire (un SITE).
# Un ecosysteme qui a Keycloak reste en `oidc` et laisse cette cle VIDE : la console
# lance des deploiements et peut raser, un mot de passe partage devant ce pouvoir est un
# accident qui attend.
vault_setops_gui_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

View file

@ -201,3 +201,78 @@ serveur_ops_sonde_branches: >-
{{ dict(serveur_ops_depots | map(attribute='dest') {{ dict(serveur_ops_depots | map(attribute='dest')
| zip(serveur_ops_depots | zip(serveur_ops_depots
| map(attribute='branche', default=serveur_ops_sonde_branche))) }} | map(attribute='branche', default=serveur_ops_sonde_branche))) }}
# --- LA CONSOLE D'EXPLOITATION, SERVIE PAR LE RUNNER (2026-09-14) ---------------------
#
# CE QU'ELLE ETAIT : une cible `make inventaire-ui` lancee a la main, qui ecoute sur la
# boucle locale du POSTE. Pour la voir, il fallait etre assis devant le controleur.
#
# CE QU'ELLE DEVIENT : un service du runner, publie par l'edge de son ecosysteme sous le
# nom que le plan lui donne. Chaque ecosysteme a sa console, chez lui, et c'est lui qui
# la gouverne.
#
# ═══ CE QUI NE PEUT PAS ETRE SAUTE ═══
#
# LE GUI N'A AUCUNE AUTHENTIFICATION. Mesure du 2026-09-14, dans son code :
#
# `GET /` sert la page A QUI LA DEMANDE, avec le jeton ECRIT DEDANS.
# `POST /api/*` exige ce jeton — que la page vient de donner a tout le monde.
#
# Le jeton est une garde CSRF, pas une serrure. La seule serrure est
# `--hote 127.0.0.1` : le GUI est sur parce qu'il n'est joignable que de sa propre
# machine. L'exposer sans vestibule livrerait `deployer`, `creer`, `instance-utiliser` et
# l'edition du plan — la fabric entiere — a quiconque atteint l'URL.
#
# LE SERVICE RESTE DONC SUR LA BOUCLE LOCALE, TOUJOURS. Ce qui est publie, c'est un
# nginx local qui authentifie D'ABORD et relaie ensuite. Aucun chemin ne contourne le
# vestibule : il est pose au niveau du `server`, pas d'un `location`.
# LE DOSSIER DU MOTEUR, DERIVE DE `serveur_ops_depots` ET PAS ECRIT DEUX FOIS. C'est la
# meme liste qui decide ou le genome est clone ; la console doit demarrer LA, pas dans un
# chemin qu'on aurait recopie et qui prendrait du retard le jour ou le dossier change.
serveur_ops_depot_moteur: >-
{{ (serveur_ops_depots | selectattr('role', 'equalto', 'moteur')
| map(attribute='dest') | first) | default('Set-OPS-public', true) }}
serveur_ops_gui_actif: false
serveur_ops_gui_ecoute: "127.0.0.1"
serveur_ops_gui_port: 8765
# LE PORT QUE L'EDGE VA CHERCHER. C'est nginx qui l'ouvre, jamais le GUI lui-meme.
serveur_ops_gui_port_public: 8090
# DEUX VESTIBULES, ET LE PREMIER EST LE BON.
#
# `oidc` — `serveur_oauth2_proxy` devant, donc Keycloak, donc un GROUPE d'annuaire.
# Revoquer quelqu'un ne demande alors aucun deploiement (D-66).
# `locale` — HTTP Basic, pour un ecosysteme SANS annuaire (un SITE). Un mot de passe
# vaut alors pour toute la fabric : c'est un repli, pas un choix.
#
# `oidc` PAR DEFAUT, et ce n'est pas une preference : la console peut RASER un
# ecosysteme. Un seul mot de passe partage devant ce pouvoir est un accident qui attend.
serveur_ops_gui_auth: "oidc"
serveur_ops_gui_admin_utilisateur: "setops-admin"
# LE MOT DE PASSE N'EST PAS NOMME ICI, ET P54 A RAISON DE L'EXIGER.
#
# `serveur_ops` est un role D'INSEMINATION : le SITE le pose sur le runner d'un locataire
# qui vient de naitre, SANS detenir la voute de ce locataire. Citer `vault_...` dans ce
# role rendrait l'insemination impossible — le site reclamerait un secret qu'il n'a pas,
# et par construction ne doit pas avoir.
#
# Le role declare donc un PARAMETRE VIDE. Qui le remplit est l'affaire de l'inventaire :
# `site_inventaire.py` y met la cle de la voute du SITE, un tenant la sienne dans ses
# `group_vars`. La couche qui detient le secret est la seule a le nommer.
#
# L'assertion en tete des taches refuse de deployer un vestibule `locale` sans mot de
# passe : le parametre peut rester vide, il ne peut pas rester vide ET actif.
serveur_ops_gui_admin_motdepasse: ""
serveur_ops_gui_htpasswd: "/etc/nginx/.setops-gui.htpasswd"
serveur_ops_gui_realm: "Console Set-OPS"
# LE NOM SOUS LEQUEL L'EDGE LE SERT — derive du plan par `instancier` et par
# `site_inventaire`, comme tout nom public (P67). Le defaut n'est qu'un repli.
serveur_ops_hostname: "console.{{ domaine_interne }}"
# Le vestibule et de quoi hacher son mot de passe.
serveur_ops_gui_paquets:
- nginx
- apache2-utils

View file

@ -0,0 +1,17 @@
---
- name: Redemarrer la console Set-OPS
ansible.builtin.systemd:
name: setops-gui.service
state: restarted
daemon_reload: true
when: not ansible_check_mode
# `reload`, PAS `restart` : le vestibule est un vhost, pas une ecoute nouvelle. Le jour ou
# `serveur_ops_gui_port_public` change, c'est une autre affaire — nginx ne reapplique pas
# un `listen` sur un `reload`, et le service resterait vert sur l'ancien port.
- name: Recharger le vestibule de la console
ansible.builtin.systemd:
name: nginx
state: reloaded
failed_when: false
when: not ansible_check_mode

View file

@ -4,6 +4,26 @@
# Le poste est presque tout en sortie : il va chercher son génome, puis il pilote. Rien # Le poste est presque tout en sortie : il va chercher son génome, puis il pilote. Rien
# ne s'y connecte, sinon l'administration elle-même. # ne s'y connecte, sinon l'administration elle-même.
flux: flux:
# LA CONSOLE D'EXPLOITATION, PUBLIEE PAR L'EDGE DE L'ECOSYSTEME (2026-09-14).
#
# `[edge, admin]` — la meme paire que `serveur_grafana` et `serveur_icingaweb2`, et
# pour la meme raison : sans edge, un ecosysteme n'aurait plus de console du tout.
#
# CE PORT N'EST PAS CELUI DU GUI. Le GUI ecoute `127.0.0.1:8765` et n'est joignable de
# nulle part ailleurs — c'est sa seule serrure, il sert sa page a qui la demande. Ce
# qui s'ouvre ici est le NGINX qui authentifie devant.
#
# LA REGLE N'EST EMISE QUE SI LE PLAN L'EXPOSE : un runner qui ne publie pas sa console
# n'a pas ce port ouvert, et `serveur_ops_gui_actif` reste faux.
- sens: ingress
port: 8090
protocole: tcp
pair: [edge, admin]
chiffrement: clair
raison: >-
Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable
depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste
sur la boucle locale : c'est nginx qui authentifie devant.
- sens: egress - sens: egress
port: 443 port: 443
protocole: tcp protocole: tcp

View file

@ -6,3 +6,19 @@ sondes:
raison: 'Le poste d''exploitation peut-il encore agir ? Deux choses : ses depots sont-ils PROPRES raison: 'Le poste d''exploitation peut-il encore agir ? Deux choses : ses depots sont-ils PROPRES
et sur leur branche (une modification locale fait diverger le runner du depot que tout le monde et sur leur branche (une modification locale fait diverger le runner du depot que tout le monde
lit), et Ansible repond-il dans son venv.' lit), et Ansible repond-il dans son venv.'
# LA CONSOLE, SURVEILLEE PAR CELUI QUI LA SERT (2026-09-14).
#
# DEUX PROCESSUS, UNE SEULE CAUSE D'ACTION : aller voir ce runner. Le GUI et son
# vestibule tombent separement, et le second cas est le plus retors — nginx repond
# parfaitement, et relaie vers un service mort.
#
# CE QUI COMPTE VRAIMENT : que le vestibule TIENNE. Un nginx qui laisserait passer sans
# authentifier ne fait echouer personne — il ouvre la fabric. La sonde exige donc un
# REFUS sur une requete anonyme : c'est la seule facon de mesurer une serrure.
- nom: console-ops
ttl: 5400
raison: 'La console d''exploitation repond-elle, et son vestibule tient-il ? Un GUI
mort derriere un nginx vivant rend une page d''erreur a qui sait deja entrer ; un
vestibule tombe rend la fabric entiere a qui trouve l''URL. Le second ne fait
echouer personne — c''est pourquoi il faut le mesurer.'

View file

@ -549,3 +549,166 @@
owner: root owner: root
group: root group: root
mode: "0750" mode: "0750"
# --- LA CONSOLE D'EXPLOITATION, SERVIE PAR CE RUNNER (2026-09-14) ---------------------
#
# CE QU'ELLE ETAIT : `make inventaire-ui`, lancee a la main, ecoutant la boucle locale du
# POSTE. Pour la voir, il fallait etre assis devant le controleur.
#
# CE QU'ELLE DEVIENT : un service du runner, publie par l'edge de son ecosysteme sous le
# nom que le plan lui donne. Chaque ecosysteme a sa console, chez lui.
#
# ELLE RESTE SUR LA BOUCLE LOCALE. Voir `serveur_ops_gui_actif` dans les defauts : le GUI
# sert sa page a qui la demande, jeton compris — ce jeton garde contre le CSRF, pas
# contre un visiteur. Ce qui est publie, c'est le nginx qui authentifie devant.
- name: Exiger un vestibule devant la console
ansible.builtin.assert:
that:
- (serveur_ops_gui_auth != 'locale') or (serveur_ops_gui_admin_motdepasse | length > 0)
fail_msg: >-
`serveur_ops_gui_auth: locale` mais `serveur_ops_gui_admin_motdepasse` est vide.
Cette console lance des deploiements et peut raser un ecosysteme : elle ne s'ouvre
pas avec un mot de passe devine. Le renseigner depuis la voute de l'ECOSYSTEME QUI
LA PUBLIE — ce role n'en nomme aucune, il est insemine par un site qui ne la
detient pas — ou passer en `oidc`.
when: serveur_ops_gui_actif | bool
- name: Installer le vestibule de la console
ansible.builtin.apt:
name: "{{ serveur_ops_gui_paquets }}"
state: present
when: serveur_ops_gui_actif | bool
- name: Déployer le service de la console
ansible.builtin.template:
src: setops-gui.service.j2
dest: /etc/systemd/system/setops-gui.service
owner: root
group: root
mode: "0644"
when: serveur_ops_gui_actif | bool
notify: Redemarrer la console Set-OPS
# LE MOT DE PASSE DU VESTIBULE, QUAND IL N'Y A PAS D'ANNUAIRE.
#
# `htpasswd -v` VERIFIE sans rien reecrire : c'est lui qui rend la pose idempotente. Une
# comparaison de fichiers ne le pourrait pas — bcrypt sale a chaque hachage, et deux
# fichiers justes different toujours.
- name: Le mot de passe deja pose est-il le bon ?
ansible.builtin.command:
argv:
- htpasswd
- -vb
- "{{ serveur_ops_gui_htpasswd }}"
- "{{ serveur_ops_gui_admin_utilisateur }}"
- "{{ serveur_ops_gui_admin_motdepasse }}"
register: serveur_ops_gui_verif
changed_when: false
failed_when: false
no_log: true
when:
- serveur_ops_gui_actif | bool
- serveur_ops_gui_auth == 'locale'
- name: Poser le mot de passe du vestibule
ansible.builtin.command:
argv:
- htpasswd
- -cbB
- "{{ serveur_ops_gui_htpasswd }}"
- "{{ serveur_ops_gui_admin_utilisateur }}"
- "{{ serveur_ops_gui_admin_motdepasse }}"
no_log: true
# Elle ne tourne que si la verification a echoue : quand elle tourne, elle change
# vraiment quelque chose. L'idempotence vit dans le `when`, pas dans un test de sortie.
changed_when: true
when:
- serveur_ops_gui_actif | bool
- serveur_ops_gui_auth == 'locale'
- (serveur_ops_gui_verif.rc | default(1)) != 0
notify: Recharger le vestibule de la console
- name: Refermer le fichier de mots de passe sur nginx
ansible.builtin.file:
path: "{{ serveur_ops_gui_htpasswd }}"
owner: root
group: www-data
mode: "0640"
when:
- serveur_ops_gui_actif | bool
- serveur_ops_gui_auth == 'locale'
- name: Déployer le vestibule de la console
ansible.builtin.template:
src: nginx-gui.conf.j2
dest: /etc/nginx/sites-available/setops-gui.conf
owner: root
group: root
mode: "0644"
when: serveur_ops_gui_actif | bool
notify: Recharger le vestibule de la console
- name: Activer le vestibule de la console
ansible.builtin.file:
src: /etc/nginx/sites-available/setops-gui.conf
dest: /etc/nginx/sites-enabled/setops-gui.conf
state: link
when: serveur_ops_gui_actif | bool
notify: Recharger le vestibule de la console
# LE SITE PAR DEFAUT SERT TOUT CE QU'ON NE LUI A PAS DEMANDE. Le laisser en place
# donnerait une seconde porte, sans vestibule, sur le meme port.
- name: Retirer le site nginx par defaut
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
when: serveur_ops_gui_actif | bool
notify: Recharger le vestibule de la console
- name: Activer et demarrer la console
ansible.builtin.systemd:
name: setops-gui.service
enabled: true
state: started
daemon_reload: true
when:
- serveur_ops_gui_actif | bool
- not ansible_check_mode
# UN RUNNER QUI CESSE DE PUBLIER SA CONSOLE DOIT L'ETEINDRE. Sans ce retrait, retirer
# l'exposition du plan laisserait le service tourner et le vestibule ouvert — la console
# resterait joignable alors que plus rien ne la declare.
- name: Éteindre la console quand ce runner ne la publie plus
ansible.builtin.systemd:
name: setops-gui.service
enabled: false
state: stopped
failed_when: false
when:
- not (serveur_ops_gui_actif | bool)
- not ansible_check_mode
- name: Retirer le vestibule quand ce runner ne publie plus
ansible.builtin.file:
path: /etc/nginx/sites-enabled/setops-gui.conf
state: absent
when: not (serveur_ops_gui_actif | bool)
notify: Recharger le vestibule de la console
- name: Deposer la sonde « console-ops »
ansible.builtin.template:
src: sonde-console-ops.sh.j2
dest: /usr/local/lib/setops/sondes/console-ops.sh
owner: root
group: root
mode: "0750"
when: serveur_ops_gui_actif | bool
# LA SONDE SUIT LA DECLARATION, DANS LES DEUX SENS. Un runner qui cesse de publier sa
# console garderait sinon une sonde orpheline : `client_sante` continuerait de la jouer
# et de pousser un resultat pour un service qu'Icinga ne definit plus.
- name: Retirer la sonde « console-ops » quand ce runner ne publie plus
ansible.builtin.file:
path: /usr/local/lib/setops/sondes/console-ops.sh
state: absent
when: not (serveur_ops_gui_actif | bool)

View file

@ -0,0 +1,48 @@
# GENERE par Set-OPS (role serveur_ops). Ne pas editer a la main.
#
# LE VESTIBULE DE LA CONSOLE. Le GUI, lui, n'ecoute que la boucle locale.
#
# TOUT CE QUI EST PUBLIE PASSE PAR ICI, et l'authentification est posee au niveau du
# `server` — pas d'un `location`. Placee dans le seul bloc de relais, elle laisserait
# passer tout chemin qu'un `location` plus specifique attraperait en premier. Une
# serrure qu'on contourne par la porte d'a cote n'est pas une serrure.
server {
listen {{ serveur_ops_gui_port_public }};
listen [::]:{{ serveur_ops_gui_port_public }};
server_name {{ serveur_ops_hostname }};
{% if serveur_ops_gui_auth == 'locale' %}
# SANS ANNUAIRE : un mot de passe, et il vaut pour toute la fabric. C'est un repli
# assume, pas un choix — voir `serveur_ops_gui_auth` dans les defauts du role.
auth_basic "{{ serveur_ops_gui_realm }}";
auth_basic_user_file {{ serveur_ops_gui_htpasswd }};
{% else %}
# AVEC ANNUAIRE : c'est `serveur_oauth2_proxy` qui se tient devant, et lui seul doit
# pouvoir frapper ce port. Le pare-feu de l'hote le dit deja (`meta/flux.yml`), et on
# ne le redit pas ici : deux couches qui declarent la meme politique finissent par
# en declarer deux differentes.
{% endif %}
# LA CONSOLE DEPLOIE, ET UN DEPLOIEMENT EST LONG. Les delais par defaut de nginx
# (60 s) couperaient la reponse au milieu d'un `deployer`, et la page afficherait une
# erreur reseau pour une operation qui, elle, continue sur la machine. La pire forme
# de mensonge : l'ecran dit echec, la fabric dit succes.
proxy_read_timeout {{ serveur_ops_gui_delai | default(3600) }}s;
proxy_send_timeout {{ serveur_ops_gui_delai | default(3600) }}s;
# LA SORTIE D'UN DEPLOIEMENT ARRIVE AU FIL DE L'EAU. Sans ces deux lignes, nginx la
# retiendrait pour l'envoyer d'un bloc : la console resterait muette pendant des
# minutes, puis tout apparaitrait d'un coup.
proxy_buffering off;
gzip off;
location / {
proxy_pass http://{{ serveur_ops_gui_ecoute }}:{{ serveur_ops_gui_port }};
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View file

@ -0,0 +1,47 @@
# GENERE par Set-OPS (role serveur_ops). Ne pas editer a la main.
#
# LA CONSOLE D'EXPLOITATION DE CET ECOSYSTEME.
#
# ELLE N'ECOUTE QUE SA PROPRE MACHINE, et ce n'est pas un reglage : c'est SA SEULE
# SERRURE. Le GUI sert sa page a qui la demande, avec son jeton ecrit dedans — le jeton
# garde contre le CSRF, pas contre un visiteur. Ce qui protege la fabric, c'est que
# personne d'autre que `127.0.0.1` ne peut frapper.
#
# Ce qui est publie est le nginx local, qui authentifie d'abord.
[Unit]
Description=Console d'exploitation Set-OPS ({{ setops_instance | default('instance') }})
Documentation=file://{{ serveur_ops_racine }}/{{ serveur_ops_depot_moteur }}/docs/carte-set-ops.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ serveur_ops_utilisateur }}
Group={{ serveur_ops_utilisateur }}
WorkingDirectory={{ serveur_ops_racine }}/{{ serveur_ops_depot_moteur }}
# LE PYTHON DE L'ENVIRONNEMENT ISOLE, pas celui du systeme : le GUI importe `yaml` et
# les modules du moteur, et c'est cet environnement-la qui les porte.
ExecStart={{ serveur_ops_venv }}/bin/python3 scripts/inventory_gui.py \
--hote {{ serveur_ops_gui_ecoute }} --port {{ serveur_ops_gui_port }}
Restart=on-failure
RestartSec=5
# CE QU'ELLE PEUT FAIRE, ET CE QU'ON LUI RETIRE. La console LANCE des deploiements : on
# ne peut donc pas la priver d'executables ni de reseau. Ce qu'on peut lui retirer sans
# la casser, on le retire — un service qui deploie merite plus de soin que les autres,
# pas moins.
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=read-only
ProtectSystem=full
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
LockPersonality=yes
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,60 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_ops). Ne pas editer a la main.
#
# SONDE « console-ops » — la console repond-elle, et son vestibule tient-il ?
#
# PAS UNE ENVELOPPE DE GREFFON, ET C'EST ASSUME. `check_http` sait dire « ce service
# repond ». La verite d'ici est l'inverse : « ce service REFUSE-t-il encore ceux qui
# n'ont pas de quoi entrer ». Aucun greffon standard ne pense a mesurer une serrure.
set -uo pipefail
GUI={{ serveur_ops_gui_ecoute }}:{{ serveur_ops_gui_port }}
VESTIBULE=127.0.0.1:{{ serveur_ops_gui_port_public }}
NOM='{{ serveur_ops_hostname }}'
MODE='{{ serveur_ops_gui_auth }}'
# 1. LE GUI VIT-IL ? On frappe la boucle locale, la ou il est le seul a pouvoir repondre.
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "http://${GUI}/" 2>/dev/null)
if [[ "${code}" != "200" ]]; then
echo "La console ne repond pas sur ${GUI} (HTTP ${code:-aucune reponse}) — plus personne ne pilote cet ecosysteme depuis le navigateur."
exit 2
fi
# 2. LE VESTIBULE REFUSE-T-IL ? On frappe SANS justificatif, avec le bon `Host` : c'est
# ainsi que nginx choisit son vhost, et sans cet en-tete on mesurerait le serveur par
# defaut — c'est-a-dire autre chose.
#
# UN 200 ICI EST LA PIRE DES REPONSES. Il veut dire que le vestibule laisse entrer : la
# console est alors ouverte a quiconque atteint l'adresse, avec `deployer`, `creer` et
# l'edition du plan. On le traite comme une panne, parce que c'en est une.
vu=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
-H "Host: ${NOM}" "http://${VESTIBULE}/" 2>/dev/null)
case "${vu}" in
401|403)
echo "Console vivante, vestibule (${MODE}) en place : une requete anonyme rend HTTP ${vu}."
exit 0
;;
30[12478])
# Une passerelle SSO renvoie vers son fournisseur d'identite : c'est un refus, dit
# autrement. On l'accepte seulement quand c'est le mode declare — en `locale`, une
# redirection signifierait que le `auth_basic` n'est plus applique.
if [[ "${MODE}" != "locale" ]]; then
echo "Console vivante, vestibule (${MODE}) en place : une requete anonyme est renvoyee (HTTP ${vu})."
exit 0
fi
echo "LE VESTIBULE NE DEMANDE PLUS RIEN : en mode locale, une requete anonyme devrait rendre 401, elle rend ${vu}."
exit 2
;;
200)
echo "LE VESTIBULE EST OUVERT : une requete anonyme obtient la console (HTTP 200). Deployer, creer et raser sont a la portee de qui trouve l'adresse."
exit 2
;;
"" | 000)
echo "Le vestibule ne repond pas sur ${VESTIBULE} — la console vit, mais l'edge ne peut plus l'atteindre."
exit 2
;;
*)
echo "Reponse inattendue du vestibule : HTTP ${vu}."
exit 1
;;
esac