Set-OPS-Public/roles/serveur_ops/defaults/main.yml

311 lines
16 KiB
YAML
Raw Normal View History

2026-08-23 11:58:10 -04:00
---
# LE POSTE D'EXPLOITATION — la machine depuis laquelle l'écosystème se reconstruit.
#
# Jusqu'ici, Set-OPS ne s'exécutait que depuis le poste de son mainteneur. L'écosystème
# détenait donc la recette (le génome, sur sa forge) sans que personne, chez lui, ne
# sache la lire à voix haute. `serveur_ops` est la cuisine : ansible, le génome cloné
# depuis SA PROPRE forge, et de quoi rejouer le moteur.
#
# CE QU'IL N'EMPORTE PAS, ET NE DOIT PAS EMPORTER : le mot de passe de la voûte. Il est
# saisi à l'exécution. Une machine qui détient à la fois le plan, l'accès SSH à la flotte
# et la clé des secrets n'a plus aucune profondeur : sa compromission est celle de tout
# l'écosystème. Voir README.md du rôle.
serveur_ops_utilisateur: "setops"
serveur_ops_racine: "/opt/setops"
serveur_ops_venv: "{{ serveur_ops_racine }}/venv"
# Paquets du poste. `make` parce que l'interface opérateur de Set-OPS est un Makefile ;
# `git` parce que le génome vit dans des dépôts ; `rsync` pour les transferts de fichiers
# d'Ansible sur les gros arbres.
serveur_ops_paquets:
- git
- make
- python3-venv
- python3-pip
- openssh-client
- rsync
- ca-certificates
# Ansible est ÉPINGLÉ sur la même famille que le poste du mainteneur (core 2.18) : un
# écosystème qui se reconstruit avec une version différente de celle qui l'a construit ne
# reproduit pas la même chose, et l'écart ne se voit qu'au premier échec.
serveur_ops_ansible: "ansible-core>=2.18,<2.19"
# --- D'OÙ VIENT LE GÉNOME ----------------------------------------------------
#
# DE SA PROPRE FORGE, pas de l'amont. C'est tout l'objet des miroirs : la forge de
# l'écosystème détient une copie vivante du génome, resynchronisée toutes les huit
# heures. Le poste d'exploitation lit CETTE copie — l'écosystème se reconstruit donc
# depuis lui-même, et non depuis son parent.
#
# Dérivé du groupe `serveur_forgejo` de l'inventaire. Surchargeable pour une forge
# externe au plan.
serveur_ops_forge_hote: >-
{{ (groups['serveur_forgejo'] | default([]) | first | default('')) }}
serveur_ops_forge_url: >-
{{ serveur_ops_forge_amont if (serveur_ops_forge_externe | bool)
else 'https://forge.' ~ domaine_interne }}
2026-08-23 11:58:10 -04:00
serveur_ops_forge_organisation: "genome"
# --- FILIATION, MUTUALISATION, ÉMANCIPATION ----------------------------------
#
# Un écosystème naît FILS : il n'héberge pas encore tout ce dont il a besoin, et emprunte
# à son hôte. Il peut rester ainsi — la MUTUALISATION est un état durable et choisi, pas
# une infirmité — ou devenir AUTOPORTANT le jour où il le décide.
#
# La forge est le premier service concerné. `false` : le génome est lu sur la forge de
# CET écosystème (état émancipé). `true` : il est lu chez l'hôte (état natal, ou
# mutualisation assumée) — et l'écosystème n'a alors pas besoin d'héberger de forge.
#
# CE N'EST PAS UN RABAIS. Un modèle sans forge n'est pas un modèle amputé : c'est un
# écosystème au premier âge, dont le runner lit le génome chez son parent. L'émancipation
# consiste à lever ce drapeau, déployer sa propre forge, et PROUVER que le lien est coupé.
#
# Cette variable était citée par `docs/dependances-groupes.yml` et par le README sans
# exister nulle part (constaté le 2026-08-24) : une exemption qui ne pouvait jamais
# s'appliquer, donc un écosystème sans forge que le registre refusait quand même.
serveur_ops_forge_externe: false
serveur_ops_forge_amont: ""
# --- LA CONFIANCE ENVERS UNE FORGE D'UN AUTRE TENANT -------------------------
#
# Lire le genome chez un voisin suppose de faire confiance a SON autorite — or chaque
# ecosysteme a la sienne, et `client_pki` n'etablit la confiance que dans celle du tenant.
#
# ON NE POSE PAS CETTE RACINE DANS LE MAGASIN SYSTEME. L'y mettre ferait confiance a cette
# AC pour TOUT ce que la machine contacte, alors qu'on ne lui demande qu'une chose : servir
# un depot git. `git config http.<url>.sslCAInfo` limite la confiance a CETTE url — une
# porte, pas un trousseau.
#
# Chemin d'un certificat racine PUBLIC, sur le controleur. Vide = rien a poser (forge du
# meme ecosysteme, ou AC publiquement reconnue).
serveur_ops_forge_amont_ac: ""
serveur_ops_forge_amont_ac_depot: "{{ serveur_ops_racine }}/.ac-amont.crt"
2026-08-23 11:58:10 -04:00
# Les dépôts du génome, et où ils atterrissent. Les noms sont ceux que la forge porte
# (minuscules) ; les destinations reprennent la disposition du poste du mainteneur, où
# le moteur et les instances sont des dossiers frères — c'est cette fraternité que
# `scripts/instances.py` découvre.
#
# AUCUN DÉFAUT NE NOMME UN ÉCOSYSTÈME PARTICULIER (2026-08-24).
#
# La première version listait ici le dépôt d'un écosystème précis. Tout écosystème qui aurait
# déployé un poste sans déclarer ses propres dépôts aurait donc cloné le génome d'un **autre** —
# silencieusement, et en croyant piloter le sien. C'est la faute que ce dépôt combat sous
# tous ses déguisements : un défaut plausible qui rend un résultat faux sans rien dire.
#
# Le moteur est le seul défaut légitime : il est le même pour tous. Le dépôt d'instance,
# lui, est propre à l'écosystème et DOIT être déclaré — la tâche d'exigence refuse de
# poursuivre sans lui.
2026-08-23 11:58:10 -04:00
serveur_ops_depots:
- { depot: "set-ops-public", dest: "Set-OPS-public", role: "moteur" }
# --- LES DEUX SYMLINKS (D-80) ------------------------------------------------
#
# `instance` dit QUEL tenant on pilote.
# `underlay.yml` dit SUR QUELLE FABRIC il repose.
#
# Les deux sont independants, et le second manquait. Un poste qui ne connaît que son
# plan sait CONFIGURER des machines existantes ; il ne sait pas en CREER. Placer une VM
# demande de savoir sur quel nœud, quel stockage, quel pont — c'est-à-dire l'underlay.
# Sans lui, le poste est un exploitant, pas un géniteur.
#
# CE QUE LE POSTE N'AURA PAS POUR AUTANT : `underlay.vault.yml`, qui porte les secrets du
# monde physique. Il est hors dépôt (gitignore), donc absent du génome. Le poste lit la
# CARTE de la fabric, pas ses clés.
# Le dossier de l'instance pilotée, cible du lien `instance`. Sans valeur, le rôle
# refuse : un poste qui ne sait pas quel tenant il pilote n'a rien à piloter.
serveur_ops_instance: ""
# Vide = le poste ne pose pas de lien `underlay.yml` (il configure, il n'engendre pas).
serveur_ops_underlay: ""
2026-08-23 11:58:10 -04:00
# --- CLÉ SSH DU POSTE --------------------------------------------------------
#
# Le poste a besoin d'atteindre toute la flotte en SSH. Il se fabrique donc sa PROPRE
# paire, distincte de celle du mainteneur : deux exploitants, deux clés, deux révocations
# possibles. La clé publique produite doit être autorisée sur la flotte — le rôle
# l'affiche et l'écrit dans un fichier prévu pour être repris au plan (voir README).
serveur_ops_cle_type: "ed25519"
serveur_ops_cle_generer: true
# --- CACHE DES COLLECTIONS (côté CONTRÔLEUR) ---------------------------------
#
# Le poste ne va PAS chercher ses collections sur galaxy.ansible.com : la frontière ne
# laisse pas passer ce flux, et ouvrir une règle vers un serveur étranger pour qu'un
# écosystème sache se reconstruire serait la mauvaise réponse. Le contrôleur télécharge
# une fois dans son cache, puis pousse par SSH — même idiome que `serveur_forgejo`
# devant son propre tiers.
#
# Effet de bord recherché : le poste devient déployable **hors ligne**.
# LE CACHE SUIT LE CONTENU DE `requirements.yml`, PAS SON EXISTENCE (2026-08-24).
#
# La premiere version gardait le telechargement par `creates: <cache>/requirements.yml` :
# une collection AJOUTEE au fichier n'aurait jamais ete recuperee, puisque le temoin
# existait deja. Le runner serait reste en retard sur le moteur, en silence -- exactement
# le defaut qui a fait qu'`ansible.posix` manquait sans que rien ne le dise.
#
# L'empreinte du fichier entre dans le chemin : changer une ligne de `requirements.yml`
# cree un cache neuf, donc un telechargement. Les anciens caches restent (ils ne genent
# pas) et documentent ce que les versions precedentes exigeaient.
2026-08-23 11:58:10 -04:00
serveur_ops_cache_collections: >-
{{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/collections-{{
lookup('file', serveur_ops_requirements_controleur) | hash('sha1') | truncate(12, true, '') }}
2026-08-23 11:58:10 -04:00
serveur_ops_requirements_controleur: "{{ playbook_dir }}/../../requirements.yml"
roues : pip par le meme chemin que les collections — le controleur telecharge, la cible n installe rien du dehors LA DERNIERE DEPENDANCE QUI SORTAIT PAR ELLE-MEME. Les collections Ansible suivent depuis longtemps l idiome de ce depot : le CONTROLEUR telecharge une fois puis pousse par SSH, aucun flux nouveau depuis la cible, et l artefact devient deployable hors ligne. pip sortait encore vers PyPI, en HTTPS et par nom. Un tenant n a NI DNS SORTANT NI 443 vers l Internet, et c est voulu. Mesure sur ops-01, premiere machine de la reconstruction de Chezlepro : ERROR: Could not find a version that satisfies the requirement ansible-core Echec temporaire dans la resolution du nom PAS LE PAQUET DEBIAN : trixie propose ansible-core 2.19.4, hors de la plage epinglee. Y aller demanderait de deplacer l epinglage vers une version majeure aux changements de gabarits connus, trois jours apres l avoir pose pour cause de portabilite. --no-index EST LE POINT, PAS UNE OPTIMISATION. Sans lui, pip resterait capable de sortir vers PyPI le jour ou le depot local serait incomplet : la reussite dependrait d un flux que ce tenant n a pas le droit d avoir, et une lignee autonome deviendrait une lignee qui a l air autonome. LE CONTROLEUR ET LA CIBLE DOIVENT PARTAGER LEUR PYTHON, et ca ne se devine pas. pyyaml porte du C compile ; une roue cp313 posee sur un python 3.12 ne s installe pas, et l echec accuserait le depot local au lieu de l ecart entre deux machines. Mesure et dit ici, ou on peut encore le nommer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 15:37:09 -04:00
# --- LA ROUE HORS-LIGNE : PYTHON PAR LE MEME CHEMIN QUE LES COLLECTIONS -------
#
# LA DERNIERE DEPENDANCE QUI SORTAIT PAR ELLE-MEME (2026-08-28). Les collections
# Ansible suivent depuis longtemps l'idiome de ce depot — « le CONTROLEUR telecharge une
# fois, puis pousse par SSH ; aucun flux nouveau depuis la cible, et l'artefact devient
# deployable hors ligne, ce qui est le sens meme d'une lignee autonome ». `pip`, lui,
# sortait encore vers PyPI, en HTTPS et par nom de domaine.
#
# Un tenant n'a NI DNS SORTANT NI 443 vers l'Internet, et c'est voulu. Mesure sur ops-01,
# premiere machine de la reconstruction de Chezlepro :
#
# ERROR: Could not find a version that satisfies the requirement ansible-core<2.19
# Echec temporaire dans la resolution du nom
#
# POURQUOI PAS LE PAQUET DEBIAN : trixie propose `ansible-core 2.19.4`, hors de la plage
# epinglee. Y aller demanderait de deplacer l'epinglage vers une version majeure aux
# changements de gabarits connus, trois jours apres l'avoir pose pour cause de
# portabilite. Ce n'est pas le moment, et ce n'est pas ce probleme-ci.
#
# L'EMPREINTE DES SOURCES ENTRE DANS LE CHEMIN, comme pour les collections : changer une
# ligne de `requirements-python.txt` ou l'epinglage d'ansible-core cree un cache neuf,
# donc un telechargement. Les anciens restent et documentent ce qu'exigeaient les
# versions precedentes.
serveur_ops_roues_source: "{{ playbook_dir }}/../../requirements-python.txt"
serveur_ops_cache_roues: >-
{{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/roues-{{
(lookup('file', serveur_ops_roues_source) ~ serveur_ops_ansible) | hash('sha1') | truncate(12, true, '') }}
serveur_ops_roues_depot: "{{ serveur_ops_racine }}/.roues-hors-ligne"
sondes : quatorze, une par role, derivees en objets Icinga boites (dovecot) · cache-apt (artefacts) · certificat (client_pki, 14/14) collaboration (nextcloud) · collecte (prometheus) · file-courriel (postfix) forge (forgejo) · identite (keycloak) · ingestion (loki) · moteur (icinga) resolution (resolveur) · runner (serveur_ops) · tableaux (grafana) voute (ops_tenant) — toutes vertes, sans une ligne ecrite dans Icinga. DEUX PRINCIPES QUE LA PREMIERE SONDE A IMPOSES. Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE : cible et seuils sont des variables du role, on prouve le rouge avec un port ferme ou un seuil impossible, sans rien casser. Et la sonde vit LA OU VIT LA VERITE : « ce noeud est-il collecte ? » appartient a prometheus, pas au client — une seule y voit les N noeuds, et surtout elle voit le cas SILENCIEUX. ON DEMANDE AU SERVICE CE QU IL PENSE DE LUI-MEME quand il sait le dire (healthz, /ready, status.php, decouverte OIDC). Quand il ne sait pas, on va chercher la verite de terrain : « moteur » ne regarde ni le service ni le port, il demande a la base depuis combien de temps elle n a pas ete rafraichie — la lecon des sauvegardes appliquee a la supervision. QUATRE FOIS J AI ECRIT LA SONDE AVANT DE MESURER, QUATRE FOIS ELLE A EU TORT. La forge : port et chemin des depots inventes, elle ecoute en 3000 derriere l edge et n a legitimement aucun depot. Loki : « panne persistante » conclue sur deux lectures a quelques secondes d intervalle juste apres un redemarrage — deux mesures rapprochees ne distinguent pas un etat d un instant. Keycloak : vise en 8443, il ecoute en 8080. Le runner : git en root refuse un depot d un autre proprietaire. A chaque fois le remede est le meme — lire la verite du role, ne pas la supposer. Et le meme piege Jinja qu avec client_sante : ${#tableau[@]} contient {#. Le remede etait deja au depot ; je l ai reecrit au lieu de le chercher. RESTE : client_smtp, client_artefacts, client_journal, icingaweb2 et ops_site. Ce sont des chemins de report, dont la panne se voit deja par le silence des sondes qu ils portent. make prouver : CONFORME, 64 OK, 0 echec, 0 saute (P64 : 14 sondes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 03:00:21 -04:00
# --- Sonde de supervision -----------------------------------------------------------
serveur_ops_sonde_racine: /opt/setops
serveur_ops_sonde_venv: /opt/setops/venv
frontiere posee, et cinq sondes qui disaient faux Application de ce que l entree precedente avait prepare : 49 regles creees, 5 retirees. Cibles Prometheus 2/8 -> 8/8, hotes dans Loki 1/7 -> 7/7. Puis correction de ce que l application a revele. Cinq defauts, tous de la meme famille : un reglage qui ne suit pas l interrupteur dont il depend. - la sonde de Loki interrogeait en HTTPS un Loki servant en clair ; - la sonde de la forge visait http:// sur un port 443 chiffre, puis validait un certificat emis pour le FQDN en interrogeant 127.0.0.1 ; - la sonde du runner comparait six depots a UNE branche quand le plan en declare une par depot. Elle contredisait la declaration qu elle etait censee verifier : elle n accusait pas la machine, elle s accusait elle-meme. Le tenant restait juste dans les cinq cas. C est le site, qui deploie ces roles autrement, qui les a tous reveles d un coup. La sonde des journaux, elle, avait deux defauts a la naissance. Elle criait une perte deja reparee, faute de dire sur QUELLE fenetre - un ecart sans sa fenetre n est pas une mesure, c est un nombre. Et elle alertait sur la cicatrice plutot que sur la plaie : le compteur d Alloy est cumulatif, donc six machines restaient orange pour toujours. Ce qui alerte est desormais la CROISSANCE. En echange, un cas muet leve maintenant : zero envoi et zero rejet n est pas la sante, c est un agent qui ne fait rien. Enfin, le runner ne pouvait plus cloner le genome. Defaut PREEXISTANT, revele par le redeploiement et non cause par lui - le diff des regles generees le montre. generer_nftables(site=True) lisait le plan du TENANT : derive se resolvait donc a 3000, le port que Forgejo ecoute derriere un edge, alors que le plan du site dit 443 et explique pourquoi. La forge ouvrait un port que personne n ecoute et laissait 443 ferme a toute la flotte. Mesure finale : Icinga 51/51, Prometheus 8/8, Loki 7/7, prouver 64 OK, lint 0 defaut sur 82 fichiers. Controles negatifs sur des COPIES des sondes : les quatre rendent CRITICAL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 10:27:12 -04:00
# LA BRANCHE ATTENDUE SE DECLARE PAR DEPOT, PAS UNE FOIS POUR TOUS (2026-09-10).
#
# `serveur_ops_depots` porte deja un `branche:` par entree — le plan du site declare
# `master` pour `set-ops-modeles` et `ops-technolibre`. La sonde comparait pourtant les six
# depots a une valeur unique, et rapportait donc une divergence CRITIQUE sur deux depots
# posos EXACTEMENT la ou le plan les veut.
#
# Une sonde qui contredit la declaration qu'elle est censee verifier n'accuse pas la
# machine : elle s'accuse elle-meme. On derive la table de ce que le plan dit, et
# `serveur_ops_sonde_branche` ne sert plus que de defaut aux entrees qui se taisent.
sondes : quatorze, une par role, derivees en objets Icinga boites (dovecot) · cache-apt (artefacts) · certificat (client_pki, 14/14) collaboration (nextcloud) · collecte (prometheus) · file-courriel (postfix) forge (forgejo) · identite (keycloak) · ingestion (loki) · moteur (icinga) resolution (resolveur) · runner (serveur_ops) · tableaux (grafana) voute (ops_tenant) — toutes vertes, sans une ligne ecrite dans Icinga. DEUX PRINCIPES QUE LA PREMIERE SONDE A IMPOSES. Une sonde doit pouvoir etre mise en defaut PAR PARAMETRE : cible et seuils sont des variables du role, on prouve le rouge avec un port ferme ou un seuil impossible, sans rien casser. Et la sonde vit LA OU VIT LA VERITE : « ce noeud est-il collecte ? » appartient a prometheus, pas au client — une seule y voit les N noeuds, et surtout elle voit le cas SILENCIEUX. ON DEMANDE AU SERVICE CE QU IL PENSE DE LUI-MEME quand il sait le dire (healthz, /ready, status.php, decouverte OIDC). Quand il ne sait pas, on va chercher la verite de terrain : « moteur » ne regarde ni le service ni le port, il demande a la base depuis combien de temps elle n a pas ete rafraichie — la lecon des sauvegardes appliquee a la supervision. QUATRE FOIS J AI ECRIT LA SONDE AVANT DE MESURER, QUATRE FOIS ELLE A EU TORT. La forge : port et chemin des depots inventes, elle ecoute en 3000 derriere l edge et n a legitimement aucun depot. Loki : « panne persistante » conclue sur deux lectures a quelques secondes d intervalle juste apres un redemarrage — deux mesures rapprochees ne distinguent pas un etat d un instant. Keycloak : vise en 8443, il ecoute en 8080. Le runner : git en root refuse un depot d un autre proprietaire. A chaque fois le remede est le meme — lire la verite du role, ne pas la supposer. Et le meme piege Jinja qu avec client_sante : ${#tableau[@]} contient {#. Le remede etait deja au depot ; je l ai reecrit au lieu de le chercher. RESTE : client_smtp, client_artefacts, client_journal, icingaweb2 et ops_site. Ce sont des chemins de report, dont la panne se voit deja par le silence des sondes qu ils portent. make prouver : CONFORME, 64 OK, 0 echec, 0 saute (P64 : 14 sondes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 03:00:21 -04:00
serveur_ops_sonde_branche: main
frontiere posee, et cinq sondes qui disaient faux Application de ce que l entree precedente avait prepare : 49 regles creees, 5 retirees. Cibles Prometheus 2/8 -> 8/8, hotes dans Loki 1/7 -> 7/7. Puis correction de ce que l application a revele. Cinq defauts, tous de la meme famille : un reglage qui ne suit pas l interrupteur dont il depend. - la sonde de Loki interrogeait en HTTPS un Loki servant en clair ; - la sonde de la forge visait http:// sur un port 443 chiffre, puis validait un certificat emis pour le FQDN en interrogeant 127.0.0.1 ; - la sonde du runner comparait six depots a UNE branche quand le plan en declare une par depot. Elle contredisait la declaration qu elle etait censee verifier : elle n accusait pas la machine, elle s accusait elle-meme. Le tenant restait juste dans les cinq cas. C est le site, qui deploie ces roles autrement, qui les a tous reveles d un coup. La sonde des journaux, elle, avait deux defauts a la naissance. Elle criait une perte deja reparee, faute de dire sur QUELLE fenetre - un ecart sans sa fenetre n est pas une mesure, c est un nombre. Et elle alertait sur la cicatrice plutot que sur la plaie : le compteur d Alloy est cumulatif, donc six machines restaient orange pour toujours. Ce qui alerte est desormais la CROISSANCE. En echange, un cas muet leve maintenant : zero envoi et zero rejet n est pas la sante, c est un agent qui ne fait rien. Enfin, le runner ne pouvait plus cloner le genome. Defaut PREEXISTANT, revele par le redeploiement et non cause par lui - le diff des regles generees le montre. generer_nftables(site=True) lisait le plan du TENANT : derive se resolvait donc a 3000, le port que Forgejo ecoute derriere un edge, alors que le plan du site dit 443 et explique pourquoi. La forge ouvrait un port que personne n ecoute et laissait 443 ferme a toute la flotte. Mesure finale : Icinga 51/51, Prometheus 8/8, Loki 7/7, prouver 64 OK, lint 0 defaut sur 82 fichiers. Controles negatifs sur des COPIES des sondes : les quatre rendent CRITICAL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-10 10:27:12 -04:00
serveur_ops_sonde_branches: >-
{{ dict(serveur_ops_depots | map(attribute='dest')
| zip(serveur_ops_depots
| 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) }}
la console mesure le pouvoir sur la voute, pas sur la carte Le document des responsabilites attache chaque pouvoir a une voute : calculer n'en demande aucune, configurer demande celle du tenant, materialiser celle du site. Le code lisait les symlinks, c'est-a-dire les cartes. Le runner de Chezlepro-locataire montait la carte du site sans en avoir jamais eu la voute : sa console se declarait poste et offrait 126 etapes sur 126. Ces gestes seraient partis puis tombes sur un secret vide — un echec au milieu du chemin, la ou un refus net aurait dit la verite avant de commencer. contexte() derive desormais les pouvoirs des voutes presentes, et la portee decoule des pouvoirs au lieu de les preceder. On ne prouve pas qu'une voute s'ouvre, le mot de passe se tape a l'execution ; mais son absence est decisive et se mesure sans rien ouvrir. Lire une carte reste permis : le pouvoir fabric suit toujours le symlink, consulter un miroir n'est pas engendrer. serveur_ops retire aussi le lien quand la fabric n'est plus declaree — il ne retirait rien, et un runner gardait le pouvoir que son plan ne lui donnait plus. Il ne retire qu'un lien, jamais un fichier : une vraie carte a cette place n'a pas ete ecrite par ce role, et il le dit plutot que de detruire ce qui n'est pas le sien. Valide : syntax-check et ansible-lint sur le role (profil production, 0/0), make test a 0 echec, P81 et P83 vertes. Six tests montent quatre faux disques et exigent la portee qui leur revient, dont le defaut lui-meme : carte presente, voute absente, portee tenant. Limite : P02 reste en echec pour la raison anterieure deja consignee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 20:12:18 -04:00
# LA PLACE DE LA FABRIC, derivee du dossier du moteur et ecrite UNE fois. Trois taches
# la visent — poser le lien, le retirer, dire qu'un fichier l'occupe — et trois copies
# d'un meme chemin finissent par ne plus designer le meme endroit.
serveur_ops_chemin_underlay: >-
{{ serveur_ops_racine }}/{{ serveur_ops_depot_moteur }}/underlay.yml
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
# SUR QUOI CE PORT S'OUVRE — ET CA DEPEND DU VESTIBULE (2026-09-15).
#
# En `locale`, nginx EST le vestibule : il doit etre joignable par l'edge, donc il ecoute
# sur toutes les interfaces et `meta/flux.yml` ouvre le 8090 depuis `[edge, admin]`.
#
# En `oidc`, la passerelle SSO se tient devant et c'est ELLE qui est publiee. Si nginx
# restait ouvert, l'edge pourrait frapper le 8090 directement et CONTOURNER la passerelle
# — la console serait alors accessible sans authentification a qui atteint l'hote. Le
# commentaire du gabarit annoncait cette protection ; rien ne la tenait.
#
# On la tient ici, a l'ecoute, et pas par une regle : un port qui n'est pas ouvert ne se
# contourne pas, alors qu'une regle se deplace. La regle du 8090 reste declaree pour le
# mode `locale`, et devient simplement sans objet en `oidc`.
serveur_ops_gui_nginx_bind: >-
{{ '127.0.0.1:' if serveur_ops_gui_auth == 'oidc' else '' }}
# 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 LA CONSOLE EST SERVIE.
#
# EN `oidc`, LE PLAN LE DONNE A LA PASSERELLE, PAS AU RUNNER. C'est elle qui est publiee ;
# `serveur_ops_hostname` sert alors au `server_name` du vestibule local, qui doit
# reconnaitre l'en-tete `Host` que la passerelle relaie telle quelle. On le derive donc du
# nom de la passerelle POSEE SUR CETTE MACHINE — une seule source, pas deux conventions.
#
# EN `locale`, le plan expose le runner lui-meme et `instancier` pose directement
# `serveur_ops_hostname` : ce `default` ne s'applique alors pas.
#
# Le dernier repli garde la convention du depot, pour un ecosysteme qui n'expose rien.
serveur_ops_hostname: >-
{{ serveur_oauth2_proxy_hostname | default('console.' ~ domaine_interne, true) }}
# Le vestibule et de quoi hacher son mot de passe.
serveur_ops_gui_paquets:
- nginx
- apache2-utils