Set-OPS-Public/roles/serveur_ops/defaults/main.yml
Daniel Allaire 4188cf5b6f 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

310 lines
16 KiB
YAML

---
# 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 }}
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"
# 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 `ops-patient0`. Tout écosystème qui aurait déployé un
# poste sans déclarer ses propres dépôts aurait donc cloné le génome de **patient 0** —
# 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.
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: ""
# --- 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.
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, '') }}
serveur_ops_requirements_controleur: "{{ playbook_dir }}/../../requirements.yml"
# --- 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"
# --- Sonde de supervision -----------------------------------------------------------
serveur_ops_sonde_racine: /opt/setops
serveur_ops_sonde_venv: /opt/setops/venv
# 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.
serveur_ops_sonde_branche: main
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 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