Set-OPS-Public/roles/serveur_ops/defaults/main.yml
Daniel Allaire e93e8f4c1d console chez les locataires, collision de noms, et un fichier ecrase
Les deux locataires declarent leur console derriere oauth2-proxy. En oidc le
vestibule n ecoute que la boucle locale : le gabarit annoncait cette protection
en s en remettant au pare-feu, qui ouvrait le port depuis l edge — la console
etait joignable sans passer par la passerelle.

COLLISION REELLE : serveur_oauth2_proxy est mono-instance par machine, et la
table des noms publics etait indexee par GROUPE. La passerelle de la vigie se
croyait la console ; son URL de retour OIDC aurait vise l autre machine. Une
application nomme un COUPLE (machine, role). P67 ne le voyait pas parce qu elle
indexait comme la derivation qu elle garde — une garde qui reproduit le
raisonnement qu elle verifie ne verifie rien.

ET J AI ECRASE group_vars/serveur_ops.yml sans le regarder : 68 lignes
detruites, deux preuves tombees, et une explication fausse construite dessus. Le
git diff qui m avait rassure portait un glob developpe par le shell parent : il
n a rien matche et n a rien dit. Une commande qui ne trouve rien et une commande
qui trouve que rien n a change rendent le meme silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-15 11:47:50 -04:00

304 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) }}
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