serveur_ops : la difference entre une archive et une matrice
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Un ecosysteme pouvait DETENIR son genome sans savoir l'executer. Le poste d'exploitation porte Ansible epingle, le genome clone depuis SA PROPRE forge, et une cle SSH qui n'appartient qu'a lui. Il n'emporte ni la voute ni son mot de passe : la structure se reconstruit depuis la forge, les secrets depuis la sauvegarde. Deux sources qu'un meme incident n'atteint pas ensemble. Une machine neuve traverse tout le moteur sans rien heriter, et revele ce qu'un etat anterieur masquait. Deux defauts silencieux en sont sortis. setops_plan_dir pointait le lien `instance` en dur : neuf roles lisaient donc le plan d'une AUTRE instance. Le plancher de patient 0 portait les FQDN de Chezlepro, et son edge publiait server_name forge.chezlepro.internal. La variable suit desormais l'inventaire reellement charge. Trois instances sur quatre declaraient les SAN d'exposition de leur edge ; la quatrieme et le modele public ne les avaient pas. La forge de patient 0 etait donc publiee derriere le certificat auto-signe de Debian, sans que rien ne le signale -- `git clone` fut le premier a refuser, a juste titre. P42 le reclame maintenant pour tout ecosysteme qui declare un edge. Le harnais a ecrit la moitie de ce role : 5 echecs sur la piece neuve, aucun n'empechait le code de tourner, tous la rendaient invisible a la carte, au graphe et au lecteur. 42 preuves, 0 echec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8e125b71cb
commit
665b07ba82
16 changed files with 875 additions and 1 deletions
127
CHANGELOG.md
127
CHANGELOG.md
|
|
@ -1,5 +1,132 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-23 — `serveur_ops` : la différence entre une archive et une matrice
|
||||
|
||||
Un écosystème pouvait **détenir** son génome sans savoir l'exécuter. Les cinq dépôts
|
||||
vivaient sur sa forge — moteur, plans, modèles, étiquette signée — mais Set-OPS ne
|
||||
tournait que depuis le poste de son mainteneur. L'écosystème possédait le livre ;
|
||||
personne, chez lui, ne savait le lire à voix haute.
|
||||
|
||||
`serveur_ops` est le poste d'exploitation : Ansible épinglé sur la même famille que celle
|
||||
qui a servi à construire (core 2.18 — reconstruire avec une version différente, c'est
|
||||
changer la recette sans le dire), le génome cloné, et une clé SSH propre au poste.
|
||||
|
||||
### Le génome vient de SA PROPRE forge
|
||||
|
||||
Le poste clone `https://forge.<domaine>/genome/…`, pas la forge parente. Ces dépôts y sont
|
||||
des miroirs resynchronisés toutes les huit heures : l'écosystème se reconstruit donc
|
||||
depuis **lui-même**, et non depuis son ascendant. Le clone est anonyme — un secret de
|
||||
moins sur une machine qui en concentre déjà beaucoup.
|
||||
|
||||
### Les deux choses qu'il n'a pas, et qui ne sont pas des oublis
|
||||
|
||||
**Le mot de passe de la voûte** : saisi à l'exécution. Une machine détenant à la fois le
|
||||
plan, l'accès SSH à toute la flotte et la clé des secrets n'a plus aucune profondeur.
|
||||
|
||||
**Le fichier de voûte** : les dépôts d'instance excluent `vault.yml` de git. Le génome
|
||||
cloné porte donc le plan **sans les secrets**. D'où une conséquence qu'il vaut mieux
|
||||
énoncer que découvrir :
|
||||
|
||||
```text
|
||||
la STRUCTURE se reconstruit depuis la forge
|
||||
les SECRETS se restaurent depuis la sauvegarde (restic)
|
||||
```
|
||||
|
||||
Deux sources distinctes, qu'un même incident n'atteint pas ensemble.
|
||||
|
||||
### Sa clé doit être autorisée à la main
|
||||
|
||||
Le poste fabrique sa propre paire, distincte de celle du mainteneur : deux exploitants,
|
||||
deux révocations possibles. Tant que sa clé publique n'est pas portée aux intrants SSH du
|
||||
plan, il ne joint aucun hôte. C'est volontairement un geste humain — donner à une machine
|
||||
le droit d'entrer partout mérite une décision, pas un effet de bord.
|
||||
|
||||
### Le harnais a écrit la moitié de ce rôle
|
||||
|
||||
Le rôle écrit, `make prouver` a rendu **36 OK, 5 échecs**, tous sur la pièce neuve :
|
||||
|
||||
```
|
||||
P08 aucune couche de deploiement -> couches-deploiement.yml
|
||||
P09 pair de flux inconnu 'hyperviseur' -> vocabulaire : edge|flotte|externe|...
|
||||
P29 aucune declaration d'authentification -> meta/authentification.yml
|
||||
P31 aucun README -> roles/serveur_ops/README.md
|
||||
P38 le catalogue ne le nomme pas -> docs/catalogue-services.md
|
||||
```
|
||||
|
||||
Aucun de ces cinq oublis n'aurait empêché le rôle de fonctionner. Tous les cinq
|
||||
l'auraient rendu invisible à la carte, au graphe, à la politique de pare-feu et au
|
||||
lecteur. Le harnais ne vérifie pas que le code marche : il vérifie que le dépôt sait
|
||||
encore ce qu'il contient. Retour à **41 OK, 0 échec**.
|
||||
|
||||
Au passage, `pair: hyperviseur` a été refusé à juste titre : l'hyperviseur n'est pas dans
|
||||
l'écosystème, il est de l'autre côté de la frontière — donc `externe`. C'est le seul flux
|
||||
par lequel un écosystème peut en engendrer un autre.
|
||||
|
||||
### Ce que le poste a révélé en naissant
|
||||
|
||||
Une machine neuve est un révélateur : elle traverse tout le moteur sans rien hériter d'un
|
||||
état antérieur. `ops-01` a buté sur cinq obstacles, dont deux étaient des défauts réels et
|
||||
silencieux du dépôt.
|
||||
|
||||
**Le plan lu n'était pas celui déployé.** `setops_plan_dir` valait
|
||||
`{{ playbook_dir }}/../../instance/plan` — le lien `instance` du moteur, **en dur**. Neuf
|
||||
rôles lisent cette variable. En déployant patient 0 par `SETOPS_INSTANCE`, ils lisaient
|
||||
donc le plan de **Chezlepro**. Conséquences constatées sur les machines :
|
||||
|
||||
```
|
||||
/etc/hosts de ops-01 : auth.chezlepro.internal, forge.chezlepro.internal…
|
||||
nginx de l'edge : server_name forge.chezlepro.internal;
|
||||
```
|
||||
|
||||
Un écosystème publiait les noms d'un autre. La variable est désormais ancrée sur
|
||||
`{{ inventory_dir }}/../../plan` : le plan qui a **engendré** cet inventaire. Les deux ne
|
||||
peuvent plus se contredire, et l'expression reste juste par le symlink comme par
|
||||
`SETOPS_INSTANCE`. Corrigé dans les quatre instances et dans le modèle public.
|
||||
|
||||
**L'edge publiait derrière un certificat auto-signé.** Trois instances sur quatre
|
||||
portaient `group_vars/serveur_nginx.yml` — les SAN d'exposition, le chemin du certificat
|
||||
step-ca, le rechargement de nginx. La quatrième, plus récente, ne l'avait pas ; le modèle
|
||||
public non plus. Résultat : `ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem` devant
|
||||
la forge de patient 0. Le service répondait, la page s'affichait après un avertissement,
|
||||
`make prouver` était vert. Le premier à refuser fut `git clone` — et il avait raison.
|
||||
|
||||
C'est un oubli de recopie, pas un bug : un câblage reproduit à la main finit toujours par
|
||||
manquer quelque part. D'où **P42 — « L'edge porte les noms qu'il publie »**, qui le
|
||||
réclame désormais pour chaque écosystème déclarant un edge. Contrôle négatif fait : le
|
||||
fichier retiré, la preuve passe au rouge.
|
||||
|
||||
**Et trois obstacles d'exécution**, sans mystère mais instructifs :
|
||||
|
||||
- la frontière ne connaissait pas `ops-01` — normal, il est neuf : `+ alias
|
||||
SETOPS_PATI29_SERVEUR_OPS`, `+ SETOPS_PATI29_CLIENT_UNBOUND`, 0 retrait ;
|
||||
- `ansible-galaxy` ne joint pas `galaxy.ansible.com` depuis l'overlay, et **c'est très
|
||||
bien ainsi**. Ouvrir une règle vers un serveur étranger pour qu'un écosystème sache se
|
||||
reconstruire aurait été la mauvaise réponse. Le contrôleur télécharge dans son cache et
|
||||
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 chemin du contrôleur contient une espace (« Espace Chezlepro/… ») et `command: cmd:`
|
||||
la lit comme un séparateur. Le message d'erreur ne parlait pas du tout du vrai problème.
|
||||
Forme `argv` désormais.
|
||||
|
||||
### La preuve
|
||||
|
||||
Depuis `ops-01`, sous son propre compte :
|
||||
|
||||
```
|
||||
8e125b7 plancher : nommer ce qui est hors de l'ecosysteme le moteur
|
||||
8c4a39b amont : declarer par quelle adresse patient 0 … son plan
|
||||
etiquette v2026.08.21 — SSH SIGNATURE presente
|
||||
make aide -> « Set-OPS — moteur d ecosystemes numeriques souverains »
|
||||
```
|
||||
|
||||
L'écosystème lit son propre génome, depuis sa propre forge, avec son propre Ansible.
|
||||
|
||||
### Patient 0
|
||||
|
||||
Fonction `ops` (zone Services-infra), machine `ops-01` — adressage dérivé `10.29.19.41`,
|
||||
VMID 129404101. Pas de `client_backup` : le poste ne détient aucun état propre, tout ce
|
||||
qu'il porte se recompose depuis la forge.
|
||||
|
||||
## 2026-08-23 — Les miroirs du génome, et un nom qui ne résout pas pareil selon d'où on le demande
|
||||
|
||||
La copie du génome sur patient 0 était **figée au jour du poussage**. Elle ne l'est plus :
|
||||
|
|
|
|||
78
docs/audit/preuve-2026-08-23.md
Normal file
78
docs/audit/preuve-2026-08-23.md
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-08-23
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (42 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| 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). |
|
||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 31 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 30 rôles, 80 flux, schéma + matrice OK. |
|
||||
| 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 | playbook: playbooks/proxmox/cloner_vm_debian.yml |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 24 secret(s) exige(s), tous presents. Voute reelle : 25 cle(s), aucun manque. |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 7 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 51 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 47 groupe(s), 76 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 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. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 33 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 24 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 13, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 55 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 34 exigence(s) de role, toutes satisfaites (118 cle(s) declaree(s) par l'instance). |
|
||||
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 41 document(s) declarent leur lecteur (21 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 30 role(s) serveur/client tous nommes, 31 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. |
|
||||
| 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 : 43 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-08-23._
|
||||
|
|
@ -49,6 +49,7 @@ Ce que la reconstruction couvre, par capacité :
|
|||
| Collaboration | `serveur_nextcloud`, `serveur_collabora` | déployés par la reconstruction, base et client OIDC dérivés du plan — **usage** (dépôt de fichier, édition partagée) non consigné comme preuve |
|
||||
| Plateforme webapp | `serveur_web_frontal`, `serveur_web_dorsal` | sites statiques et webapps natives (venv + systemd + nginx), **zéro conteneur** |
|
||||
| Sauvegardes | `serveur_backup`, `client_backup` | restic hors-nœud, **restauration éprouvée** (2026-08-12 : la donnée revient) |
|
||||
| Exploitation | `serveur_ops` | le poste depuis lequel l'ecosysteme se reconstruit : Ansible epingle, genome clone depuis **sa propre forge**, cle SSH propre — **sans** la voute ni son mot de passe |
|
||||
| Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte |
|
||||
|
||||
*Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail`
|
||||
|
|
|
|||
|
|
@ -61,6 +61,9 @@ couches:
|
|||
- serveur_nextcloud
|
||||
- serveur_web_frontal
|
||||
- serveur_web_dorsal
|
||||
# Le poste d'exploitation vient APRES la forge : il clone le genome depuis
|
||||
# elle. Le placer plus tot le laisserait sans source.
|
||||
- serveur_ops
|
||||
|
||||
- nom: agents
|
||||
raison: "Les intégrations clientes qui expédient vers les services centraux (métriques, journaux, courriel, sauvegardes, résolution locale). Déployées en dernier, quand leurs cibles sont debout."
|
||||
|
|
|
|||
|
|
@ -81,6 +81,25 @@ groupes:
|
|||
raison: "Forgejo depend d'une base (serveur ou fichier) et d'une publication HTTP(S) ; le courriel est un agrement."
|
||||
surveillance: "Verifier HTTP(S), base, files Git et envoi courriel."
|
||||
|
||||
serveur_ops:
|
||||
requiert_groupes_actifs:
|
||||
- serveur_forgejo
|
||||
# LE POSTE LIT LE GENOME SUR LA FORGE DE SON PROPRE ECOSYSTEME — c'est ce qui le rend
|
||||
# autonome : il ne redemande rien a son parent. Sans forge, il n'a aucune source.
|
||||
#
|
||||
# Une forge EXTERNE reste possible (un ecosysteme peut lire le genome ailleurs) : il
|
||||
# suffit de surcharger `serveur_ops_forge_url`. L'exigence tombe alors, comme pour
|
||||
# toute exigence conditionnelle du registre.
|
||||
sauf_si:
|
||||
serveur_forgejo: { variable: serveur_ops_forge_externe, vaut: true }
|
||||
# UTILISE SI PRESENT : sans confiance PKI, `git clone` refuse le certificat de la
|
||||
# forge — et il a raison de refuser. Ce n'est pas une exigence du groupe : une forge
|
||||
# a certificat public se cloner sans client_pki.
|
||||
utilise_si_present:
|
||||
- serveur_step_ca
|
||||
raison: "Le poste d'exploitation clone le genome depuis la forge de l'ecosysteme ; sans elle, il n'a pas de source."
|
||||
surveillance: "Verifier que les depots clones suivent leur amont et qu'ansible repond dans le venv."
|
||||
|
||||
serveur_nextcloud:
|
||||
requiert_groupes_actifs:
|
||||
- serveur_postgresql
|
||||
|
|
|
|||
|
|
@ -16,4 +16,19 @@ domaine_interne: "exemple.internal"
|
|||
fuseau_horaire: "America/Toronto"
|
||||
organisation: "Exemple"
|
||||
identite_realm: "exemple"
|
||||
setops_plan_dir: "{{ playbook_dir }}/../../instance/plan"
|
||||
# LE PLAN SUIT L'INVENTAIRE, PAS LE SYMLINK (2026-08-23).
|
||||
#
|
||||
# La valeur precedente etait `{{ playbook_dir }}/../../instance/plan` : le lien
|
||||
# `instance` du moteur, en dur. Tout role lisant le plan lisait donc celui de
|
||||
# l'instance POINTEE PAR LE LIEN, et non celle qu'on deploie.
|
||||
#
|
||||
# CE QUE CA A DONNE. En deployant patient 0 avec `SETOPS_INSTANCE`, le plancher
|
||||
# /etc/hosts de `ops-01` a recu les FQDN de CHEZLEPRO -- auth.chezlepro.internal,
|
||||
# forge.chezlepro.internal... -- pointes sur l'edge de patient 0. Un ecosysteme
|
||||
# annoncait les noms d'un autre. Neuf roles lisent cette variable ; le plancher est
|
||||
# simplement celui qui l'a rendu visible.
|
||||
#
|
||||
# `inventory_dir` est le dossier de l'inventaire REELLEMENT charge. Le plan qui a
|
||||
# engendre cet inventaire est son voisin : les deux ne peuvent plus se contredire,
|
||||
# et l'expression reste juste qu'on passe par le symlink ou par SETOPS_INSTANCE.
|
||||
setops_plan_dir: "{{ inventory_dir }}/../../plan"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
# L'EDGE DOIT PORTER LES NOMS QU'IL PUBLIE (2026-08-23).
|
||||
#
|
||||
# Sans ce fichier, `client_pki` n'emet le certificat de l'edge qu'avec ses propres noms
|
||||
# (`infra-edge-01.genese.internal`), et nginx retombe sur le certificat auto-signe de
|
||||
# Debian pour tout FQDN expose. Le service repond, la page s'affiche apres un
|
||||
# avertissement — et rien ne signale la panne. C'est ce qui s'est passe ici : la forge de
|
||||
# patient 0 etait publiee derriere un `ssl-cert-snakeoil.pem`, et `git clone` a ete le
|
||||
# premier a refuser, a juste titre.
|
||||
#
|
||||
# Ce fichier appartient au MODELE parce que trois instances sur quatre le portaient
|
||||
# et que la quatrieme, plus recente, ne l'avait pas : un cablage recopie a la main
|
||||
# finit toujours par manquer quelque part. La preuve P42 le verifie desormais.
|
||||
#
|
||||
# `sans_exposition` est derive du plan (les `expose:` des applications).
|
||||
client_pki_sans: >-
|
||||
{{ ([ansible_fqdn | default(ansible_hostname), ansible_hostname, ansible_host]
|
||||
+ (sans_exposition | default([])))
|
||||
| select | unique | list }}
|
||||
serveur_nginx_certificat: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.crt"
|
||||
serveur_nginx_cle: "/etc/step/certs/{{ ansible_fqdn | default(ansible_hostname) }}.key"
|
||||
# Un cert renouvele sur disque reste PERIME en memoire tant que nginx n'a pas recharge.
|
||||
client_pki_reload_services:
|
||||
- nginx
|
||||
22
playbooks/groupes/serveur_ops.yml
Normal file
22
playbooks/groupes/serveur_ops.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
---
|
||||
- name: Appliquer le groupe serveur_ops
|
||||
hosts: serveur_ops
|
||||
become: true
|
||||
# Le verrou dpkg est tenu par les maj automatiques de Debian, par vagues, plusieurs
|
||||
# minutes apres le premier demarrage. Pose ICI plutot que dans chaque role : toute
|
||||
# tache apt du play en herite, y compris celles des roles inclus.
|
||||
module_defaults:
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: 300
|
||||
|
||||
gather_facts: true
|
||||
|
||||
pre_tasks:
|
||||
- name: Vérifier que la cible est Debian
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.distribution == "Debian"
|
||||
fail_msg: "Ce playbook est prévu pour Debian."
|
||||
|
||||
roles:
|
||||
- serveur_ops
|
||||
100
roles/serveur_ops/README.md
Normal file
100
roles/serveur_ops/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# serveur_ops
|
||||
|
||||
**Poste d'exploitation** : la machine depuis laquelle l'écosystème se reconstruit
|
||||
lui-même. Ansible épinglé, le génome cloné depuis **sa propre forge**, et une clé SSH
|
||||
qui n'appartient qu'à lui.
|
||||
|
||||
## Principe
|
||||
|
||||
Un écosystème pouvait jusqu'ici **détenir** son génome sans savoir l'exécuter. Les cinq
|
||||
dépôts vivaient sur sa forge — moteur, plans, modèles, étiquette signée — mais Set-OPS
|
||||
ne tournait que depuis le poste de son mainteneur. L'écosystème possédait le livre ;
|
||||
personne, chez lui, ne savait le lire à voix haute.
|
||||
|
||||
`serveur_ops` est la différence entre une **archive** et une **matrice**.
|
||||
|
||||
## Ce que le poste emporte
|
||||
|
||||
```text
|
||||
/opt/setops/
|
||||
Set-OPS-public/ le moteur ← cloné depuis genome/set-ops-public
|
||||
OPS-<instance>/ le plan ← cloné depuis genome/ops-<instance>
|
||||
venv/ ansible-core 2.18 (épinglé)
|
||||
.ssh/id_ed25519 la clé du poste
|
||||
LISEZ-MOI.md mode d'emploi, généré
|
||||
```
|
||||
|
||||
Le moteur et les instances sont des **dossiers frères**, comme sur le poste du
|
||||
mainteneur : c'est cette fraternité que `scripts/instances.py` découvre pour proposer
|
||||
les instances pilotables. Le lien `instance` désigne celle qu'on pilote.
|
||||
|
||||
## D'où vient le génome : de sa propre forge
|
||||
|
||||
Le poste clone depuis `https://forge.<domaine>/genome/…`, **pas** depuis la forge
|
||||
parente. Les dépôts y sont des miroirs, resynchronisés toutes les huit heures : la copie
|
||||
est vivante, et l'écosystème se reconstruit depuis **lui-même**.
|
||||
|
||||
Le clone est **anonyme** — les dépôts du génome sont lisibles sur la forge interne. Un
|
||||
secret de moins sur une machine qui en concentre déjà beaucoup.
|
||||
|
||||
La confiance TLS vient de `client_pki` (racine step-ca dans le magasin système). Sans
|
||||
elle, `git clone` refuse le certificat de la forge, et il a raison de refuser.
|
||||
|
||||
## Les deux choses qu'il n'a PAS
|
||||
|
||||
**Le mot de passe de la voûte.** Il se saisit à l'exécution. Une machine qui détiendrait
|
||||
à la fois le plan, l'accès SSH à toute la flotte et la clé des secrets n'aurait plus
|
||||
aucune profondeur : la compromettre serait compromettre l'écosystème entier.
|
||||
|
||||
**Le fichier de voûte lui-même.** Les dépôts d'instance excluent `vault.yml` de git —
|
||||
c'est la règle « aucun secret en clair ». Le génome cloné ici porte donc le plan **sans
|
||||
les secrets**.
|
||||
|
||||
Conséquence à connaître, et qui n'est pas un défaut mais une conception :
|
||||
|
||||
```text
|
||||
la STRUCTURE de l'écosystème se reconstruit depuis la forge
|
||||
les SECRETS se restaurent depuis la sauvegarde (restic)
|
||||
```
|
||||
|
||||
Deux sources distinctes, qu'un même incident n'atteint pas ensemble.
|
||||
|
||||
## Sa clé doit être autorisée
|
||||
|
||||
Le poste fabrique sa propre paire, distincte de celle du mainteneur : deux exploitants,
|
||||
deux révocations possibles, et l'accès du poste se lit dans les `authorized_keys` de la
|
||||
flotte au lieu de se confondre avec celui d'un humain.
|
||||
|
||||
Tant que cette clé publique n'est pas portée aux intrants SSH du plan, **le poste ne
|
||||
joint aucun hôte**. Le rôle l'affiche au déploiement ; elle se relit à tout moment :
|
||||
|
||||
```bash
|
||||
cat /opt/setops/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
C'est volontairement un geste humain : donner à une machine le droit d'entrer partout
|
||||
mérite une décision, pas un effet de bord.
|
||||
|
||||
## Variables principales
|
||||
|
||||
| Variable | Défaut | Rôle |
|
||||
|---|---|---|
|
||||
| `serveur_ops_utilisateur` | `setops` | compte d'exploitation |
|
||||
| `serveur_ops_racine` | `/opt/setops` | racine du poste |
|
||||
| `serveur_ops_ansible` | `ansible-core>=2.18,<2.19` | version **épinglée** |
|
||||
| `serveur_ops_forge_url` | `https://forge.<domaine>` | source du génome |
|
||||
| `serveur_ops_depots` | moteur + instance | quoi cloner, et où |
|
||||
| `serveur_ops_instance` | l'instance du plan | cible du lien `instance` |
|
||||
| `serveur_ops_cle_generer` | `true` | fabriquer la clé du poste |
|
||||
|
||||
## Dépendances
|
||||
|
||||
`serveur_forgejo` (la source du génome) — déclaré dans `docs/dependances-groupes.yml`,
|
||||
avec `sauf_si serveur_ops_forge_externe` pour l'écosystème qui lit son génome ailleurs.
|
||||
`serveur_step_ca` est **utilisé si présent** : sans confiance PKI, le clone échoue sur
|
||||
le certificat.
|
||||
|
||||
## Ce que ce rôle ne fait pas
|
||||
|
||||
Il n'ouvre aucun service, n'écoute sur aucun port, ne publie rien à l'edge. Il ne
|
||||
sauvegarde rien non plus : tout ce qu'il contient se recompose depuis la forge.
|
||||
81
roles/serveur_ops/defaults/main.yml
Normal file
81
roles/serveur_ops/defaults/main.yml
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
# 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: >-
|
||||
https://forge.{{ domaine_interne }}
|
||||
serveur_ops_forge_organisation: "genome"
|
||||
|
||||
# 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.
|
||||
serveur_ops_depots:
|
||||
- { depot: "set-ops-public", dest: "Set-OPS-public", role: "moteur" }
|
||||
- { depot: "ops-patient0", dest: "OPS-Patient0", role: "instance" }
|
||||
|
||||
# L'instance que le poste pilote par défaut (symlink `instance` du moteur).
|
||||
serveur_ops_instance: "OPS-Patient0"
|
||||
|
||||
# --- 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**.
|
||||
serveur_ops_cache_collections: >-
|
||||
{{ (setops_cache_artefacts | default(lookup('env', 'HOME') + '/.cache/setops')) }}/collections
|
||||
serveur_ops_requirements_controleur: "{{ playbook_dir }}/../../requirements.yml"
|
||||
13
roles/serveur_ops/meta/authentification.yml
Normal file
13
roles/serveur_ops/meta/authentification.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
# Position de ce role dans la directive d'authentification (D-38..D-41).
|
||||
# Voir docs/authentification.md. Gardee par la preuve P29.
|
||||
authentification:
|
||||
portee: sans-auth-humaine
|
||||
mecanisme: aucun
|
||||
formulaire_local: sans-objet
|
||||
secours: "Acces SSH a l'hote, puis `sudo -iu setops`"
|
||||
raison: >-
|
||||
Le poste d'exploitation n'expose aucune interface : il n'est ni serveur web ni
|
||||
service reseau, et rien ne s'y connecte. On y entre par SSH sur l'hote, comme sur
|
||||
n'importe quelle machine de la flotte, puis l'on devient l'utilisateur
|
||||
d'exploitation. Il n'y a donc rien a federer, et rien a fermer.
|
||||
8
roles/serveur_ops/meta/empreinte.yml
Normal file
8
roles/serveur_ops/meta/empreinte.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Empreinte ressources — un poste d'exploitation ne sert personne, il exécute. Ansible
|
||||
# est gourmand en processus courts (un fork par hôte × forks) mais pas en mémoire
|
||||
# résidente ; le disque porte le génome et les collections.
|
||||
setops_empreinte:
|
||||
coeurs: 2
|
||||
memoire_mo: 2048
|
||||
disque_go: 20
|
||||
27
roles/serveur_ops/meta/flux.yml
Normal file
27
roles/serveur_ops/meta/flux.yml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
# Flux réseau du poste d'exploitation. Voir docs/flux-conception.md.
|
||||
#
|
||||
# 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.
|
||||
flux:
|
||||
- sens: egress
|
||||
port: 443
|
||||
protocole: tcp
|
||||
pair: edge
|
||||
chiffrement: tls-requis
|
||||
raison: "Cloner et resynchroniser le génome depuis la forge de l'écosystème."
|
||||
- sens: egress
|
||||
port: 22
|
||||
protocole: tcp
|
||||
pair: flotte
|
||||
chiffrement: ssh
|
||||
raison: "Piloter la flotte — c'est la raison d'être du poste."
|
||||
# L'HYPERVISEUR N'EST PAS DANS L'ÉCOSYSTÈME : il appartient au monde physique, de
|
||||
# l'autre côté de la frontière. C'est donc un pair `externe`, et ce flux est le seul
|
||||
# par lequel un écosystème peut en engendrer un autre.
|
||||
- sens: egress
|
||||
port: 8006
|
||||
protocole: tcp
|
||||
pair: externe
|
||||
chiffrement: tls-requis
|
||||
raison: "API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant."
|
||||
227
roles/serveur_ops/tasks/main.yml
Normal file
227
roles/serveur_ops/tasks/main.yml
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
---
|
||||
- name: Exiger les intrants du poste d'exploitation
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- domaine_interne | default('') | length > 0
|
||||
- serveur_ops_depots | length > 0
|
||||
fail_msg: >-
|
||||
serveur_ops exige `domaine_interne` et au moins un dépôt dans
|
||||
`serveur_ops_depots` (le moteur au minimum).
|
||||
|
||||
- name: Installer l'outillage du poste
|
||||
ansible.builtin.apt:
|
||||
name: "{{ serveur_ops_paquets }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Créer l'utilisateur d'exploitation
|
||||
ansible.builtin.user:
|
||||
name: "{{ serveur_ops_utilisateur }}"
|
||||
home: "{{ serveur_ops_racine }}"
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
system: false
|
||||
|
||||
- name: Poser les droits de la racine d'exploitation
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0750"
|
||||
|
||||
# L'ENVIRONNEMENT PYTHON EST ISOLÉ, PAS SYSTÈME. Debian gère ansible en paquet, mais la
|
||||
# version qu'il propose suit son propre calendrier : reconstruire un écosystème avec un
|
||||
# ansible plus récent que celui qui l'a construit, c'est changer la recette sans le dire.
|
||||
# Le venv permet d'épingler exactement la famille utilisée par le mainteneur.
|
||||
- name: Créer l'environnement Python isolé
|
||||
ansible.builtin.command:
|
||||
cmd: "python3 -m venv {{ serveur_ops_venv }}"
|
||||
creates: "{{ serveur_ops_venv }}/bin/python"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
|
||||
- name: Installer Ansible dans l'environnement isolé
|
||||
ansible.builtin.pip:
|
||||
name:
|
||||
- "{{ serveur_ops_ansible }}"
|
||||
- "pyyaml"
|
||||
virtualenv: "{{ serveur_ops_venv }}"
|
||||
state: present
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_pip
|
||||
retries: 3
|
||||
delay: 6
|
||||
until: serveur_ops_pip is succeeded
|
||||
|
||||
# --- LE GÉNOME, DEPUIS SA PROPRE FORGE ---------------------------------------
|
||||
#
|
||||
# Cloné en anonyme : les dépôts du génome sont lisibles sur la forge de l'écosystème, et
|
||||
# le poste n'a donc AUCUN justificatif à détenir pour se reconstruire. C'est délibéré —
|
||||
# un secret de moins sur la machine qui en concentre déjà beaucoup.
|
||||
#
|
||||
# La confiance TLS vient de `client_pki` (racine step-ca dans le magasin système). Sans
|
||||
# lui, `git clone` refuse le certificat de la forge — et c'est bien qu'il refuse.
|
||||
- name: Cloner le génome depuis la forge de l'écosystème
|
||||
ansible.builtin.git:
|
||||
repo: "{{ serveur_ops_forge_url }}/{{ serveur_ops_forge_organisation }}/{{ item.depot }}.git"
|
||||
dest: "{{ serveur_ops_racine }}/{{ item.dest }}"
|
||||
version: "{{ item.branche | default('main') }}"
|
||||
update: true
|
||||
force: false
|
||||
loop: "{{ serveur_ops_depots }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }} → {{ item.dest }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
register: serveur_ops_clone
|
||||
retries: 3
|
||||
delay: 6
|
||||
until: serveur_ops_clone is succeeded
|
||||
|
||||
# --- LES COLLECTIONS, SANS DEPENDRE DE GALAXY ---------------------------------
|
||||
#
|
||||
# UN POSTE D'EXPLOITATION QUI APPELLE galaxy.ansible.com POUR SE CONSTRUIRE N'EST PAS
|
||||
# SOUVERAIN. Mesure du 2026-08-23, premier deploiement : `ansible-galaxy collection
|
||||
# install` a echoue depuis l'overlay de patient 0 —
|
||||
# `SSL: UNEXPECTED_EOF_WHILE_READING` — parce que la frontiere ne laisse pas passer ce
|
||||
# flux, et c'est tres bien ainsi. Ouvrir une regle vers un serveur americain pour que
|
||||
# l'ecosysteme sache se reconstruire aurait ete la mauvaise reponse.
|
||||
#
|
||||
# MEME IDIOME QUE serveur_forgejo DEVANT SON PROPRE TIERS : le CONTROLEUR telecharge une
|
||||
# fois, dans son cache, puis pousse par SSH. Aucun flux nouveau depuis la cible, et
|
||||
# l'artefact devient deployable hors ligne — un poste peut se reconstruire sans internet
|
||||
# du tout, ce qui est le sens meme d'une lignee autonome.
|
||||
- name: Preparer le cache des collections sur le controleur
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_cache_collections }}"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
|
||||
# `argv` ET NON `cmd` : le chemin du controleur peut contenir une ESPACE (« Espace
|
||||
# Chezlepro/… » chez le mainteneur), et `cmd` la lit comme un separateur d'arguments.
|
||||
# Constate le 2026-08-23 : ansible-galaxy recevait « /home/…/Espace » puis
|
||||
# « Chezlepro/… » comme deux arguments, et refusait — « positional collection_name arg
|
||||
# and --requirements-file are mutually exclusive ». Le message ne parlait pas du tout du
|
||||
# vrai probleme.
|
||||
- name: Telecharger les collections dans le cache du controleur (une seule fois)
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "ansible-galaxy"
|
||||
- "collection"
|
||||
- "download"
|
||||
- "-r"
|
||||
- "{{ serveur_ops_requirements_controleur }}"
|
||||
- "-p"
|
||||
- "{{ serveur_ops_cache_collections }}"
|
||||
creates: "{{ serveur_ops_cache_collections }}/requirements.yml"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Deposer les collections sur le poste
|
||||
ansible.builtin.copy:
|
||||
src: "{{ serveur_ops_cache_collections }}/"
|
||||
dest: "{{ serveur_ops_racine }}/.collections-hors-ligne/"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
directory_mode: "0755"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# `chdir` OBLIGATOIRE : le `requirements.yml` produit par `collection download` nomme les
|
||||
# archives en RELATIF (`community-postgresql-4.2.0.tar.gz`), et ansible-galaxy les cherche
|
||||
# depuis le repertoire COURANT, pas depuis celui du fichier. Sans chdir : « Could not find
|
||||
# community-postgresql-4.2.0.tar.gz » alors que l'archive est bien la, a cote.
|
||||
- name: Installer les collections depuis le depot local (hors ligne)
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
{{ serveur_ops_venv }}/bin/ansible-galaxy collection install
|
||||
-r requirements.yml
|
||||
-p {{ serveur_ops_racine }}/.ansible/collections
|
||||
chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne"
|
||||
creates: "{{ serveur_ops_racine }}/.ansible/collections/ansible_collections/community/general"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
# LES DEUX SYMLINKS (D-80). `instance` dit QUEL tenant on pilote. Le poste en pose un par
|
||||
# défaut ; l'exploitant le bascule comme sur le poste du mainteneur.
|
||||
- name: Désigner l'instance pilotée par défaut
|
||||
ansible.builtin.file:
|
||||
src: "{{ serveur_ops_racine }}/{{ serveur_ops_instance }}"
|
||||
dest: >-
|
||||
{{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}/instance
|
||||
state: link
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
force: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
# --- LA CLÉ DU POSTE ---------------------------------------------------------
|
||||
#
|
||||
# Le poste se fabrique sa propre paire, distincte de celle du mainteneur. Deux
|
||||
# exploitants, deux clés : on peut révoquer l'un sans couper l'autre — et l'accès du
|
||||
# poste se lit dans les `authorized_keys` de la flotte, au lieu de se confondre avec
|
||||
# celui d'un humain.
|
||||
#
|
||||
# `ssh-keygen` plutôt que `community.crypto.openssh_keypair` : le dépôt ne requiert que
|
||||
# `community.postgresql` et `community.general`, et fabriquer une paire de clés ne
|
||||
# justifie pas d'imposer une collection de plus à tout écosystème qui déploie un poste.
|
||||
- name: Assurer le répertoire SSH du poste
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_ops_racine }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0700"
|
||||
|
||||
- name: Fabriquer la clé SSH du poste
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
ssh-keygen -t {{ serveur_ops_cle_type }} -N ''
|
||||
-C "setops@{{ inventory_hostname }}.{{ domaine_interne }}"
|
||||
-f {{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}
|
||||
creates: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}"
|
||||
become: true
|
||||
become_user: "{{ serveur_ops_utilisateur }}"
|
||||
when:
|
||||
- serveur_ops_cle_generer | bool
|
||||
- not ansible_check_mode
|
||||
register: serveur_ops_cle
|
||||
|
||||
- name: Relire la clé publique du poste
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}.pub"
|
||||
register: serveur_ops_pub
|
||||
when:
|
||||
- serveur_ops_cle_generer | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
# ÉCRIRE, PUIS RELIRE (D-68) — et rendre le résultat EXPLOITABLE. Cette clé ne sert à
|
||||
# rien tant qu'elle n'est pas autorisée sur la flotte : on l'affiche pour que l'exploitant
|
||||
# la reprenne dans les intrants, au lieu de la laisser dormir sur le disque.
|
||||
- name: La clé publique à autoriser sur la flotte
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Clé publique du poste d'exploitation — à ajouter aux intrants SSH du plan :"
|
||||
- "{{ serveur_ops_pub.content | b64decode | trim }}"
|
||||
when:
|
||||
- serveur_ops_cle_generer | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Déposer le mode d'emploi sur le poste
|
||||
ansible.builtin.template:
|
||||
src: LISEZ-MOI.md.j2
|
||||
dest: "{{ serveur_ops_racine }}/LISEZ-MOI.md"
|
||||
owner: "{{ serveur_ops_utilisateur }}"
|
||||
group: "{{ serveur_ops_utilisateur }}"
|
||||
mode: "0644"
|
||||
58
roles/serveur_ops/templates/LISEZ-MOI.md.j2
Normal file
58
roles/serveur_ops/templates/LISEZ-MOI.md.j2
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Poste d'exploitation de {{ organisation | default('cet écosystème') }}
|
||||
|
||||
> Fichier **généré** par le rôle `serveur_ops`. Ne pas éditer à la main.
|
||||
|
||||
Cette machine sait reconstruire l'écosystème. Elle porte le moteur Set-OPS, le plan, et
|
||||
un Ansible épinglé sur la même famille que celle qui a servi à le construire.
|
||||
|
||||
## Ce qu'elle contient
|
||||
|
||||
```text
|
||||
{{ serveur_ops_racine }}/
|
||||
{% for d in serveur_ops_depots %}
|
||||
{{ "%-22s" | format(d.dest) }} {{ d.role }} — cloné depuis {{ serveur_ops_forge_url }}/{{ serveur_ops_forge_organisation }}/{{ d.depot }}.git
|
||||
{% endfor %}
|
||||
venv/ Ansible {{ serveur_ops_ansible }}
|
||||
.ssh/id_{{ serveur_ops_cle_type }} la clé propre au poste
|
||||
```
|
||||
|
||||
Le génome vient de **la forge de cet écosystème**, pas de son parent. Ces dépôts y sont
|
||||
des miroirs, resynchronisés automatiquement : le poste lit donc une copie vivante.
|
||||
|
||||
## S'en servir
|
||||
|
||||
```bash
|
||||
sudo -iu {{ serveur_ops_utilisateur }}
|
||||
cd {{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | first).dest }}
|
||||
export PATH={{ serveur_ops_venv }}/bin:$PATH
|
||||
export ANSIBLE_COLLECTIONS_PATH={{ serveur_ops_racine }}/.ansible/collections
|
||||
make aide
|
||||
```
|
||||
|
||||
L'instance pilotée est désignée par le lien `instance` → actuellement
|
||||
`{{ serveur_ops_instance }}`. On la bascule comme sur n'importe quel poste Set-OPS.
|
||||
|
||||
## Les deux choses qu'il n'a PAS, et pourquoi
|
||||
|
||||
**Le mot de passe de la voûte.** Il se saisit à l'exécution. Une machine qui détiendrait
|
||||
à la fois le plan, l'accès SSH à toute la flotte et la clé des secrets n'aurait plus
|
||||
aucune profondeur : la compromettre, ce serait compromettre l'écosystème entier. Le mot
|
||||
de passe reste dans une tête, ou dans un coffre hors de cette machine.
|
||||
|
||||
**Le fichier de voûte lui-même.** Les dépôts d'instance excluent `vault.yml` de git —
|
||||
c'est la règle « aucun secret en clair ». Le génome cloné ici porte donc le plan **sans
|
||||
les secrets**. Pour un déploiement réel, la voûte se restaure depuis la sauvegarde
|
||||
(`restic`), pas depuis la forge.
|
||||
|
||||
Autrement dit : ce poste reproduit la **structure** de l'écosystème de mémoire, et ses
|
||||
**secrets** depuis la sauvegarde. Deux sources distinctes, et c'est voulu.
|
||||
|
||||
## Sa clé doit être autorisée
|
||||
|
||||
Le poste s'est fabriqué sa propre paire SSH, distincte de celle du mainteneur — deux
|
||||
exploitants, deux révocations possibles. Tant que sa clé publique n'est pas dans les
|
||||
intrants SSH du plan, il ne joint aucun hôte. La clé publique :
|
||||
|
||||
```bash
|
||||
cat {{ serveur_ops_racine }}/.ssh/id_{{ serveur_ops_cle_type }}.pub
|
||||
```
|
||||
|
|
@ -829,6 +829,75 @@ def preuve_resolution_unique() -> tuple[bool, str]:
|
|||
f"`inventory_rules`, {len(exemptes) - 1} exemption(s) nommee(s).")
|
||||
|
||||
|
||||
def preuve_edge_porte_ses_noms() -> tuple[bool, str]:
|
||||
"""Tout ecosysteme qui PUBLIE des noms sert un certificat qui les porte.
|
||||
|
||||
POURQUOI CETTE PREUVE EXISTE (2026-08-23). L'edge de patient 0 publiait sa forge
|
||||
derriere `ssl-cert-snakeoil.pem` — le certificat auto-signe de Debian. Le service
|
||||
repondait, la page s'affichait apres un avertissement, `make prouver` etait vert :
|
||||
rien, nulle part, ne signalait que la publication n'etait pas de confiance. Le premier
|
||||
a refuser fut `git clone`, en voulant cloner le genome — et il avait raison.
|
||||
|
||||
LA CAUSE N'ETAIT PAS UN BUG, C'ETAIT UN OUBLI DE RECOPIE. Trois instances sur quatre
|
||||
portaient `group_vars/serveur_nginx.yml` (les SAN d'exposition, le chemin du cert
|
||||
step-ca, le rechargement de nginx). La quatrieme, plus recente, ne l'avait pas — et le
|
||||
modele public dont descendent les prochaines ne l'avait pas non plus. Un cablage
|
||||
recopie a la main finit toujours par manquer quelque part ; la seule parade est qu'une
|
||||
preuve le reclame.
|
||||
|
||||
CE QU'ELLE TESTE, hors ligne, pour CHAQUE instance qui declare un groupe
|
||||
`serveur_nginx` :
|
||||
- le fichier `group_vars/serveur_nginx.yml` existe ;
|
||||
- `client_pki_sans` y etend les SAN avec `sans_exposition` (les `expose:` du plan) ;
|
||||
- `serveur_nginx_certificat` pointe vers les certificats de l'AC, pas ailleurs.
|
||||
|
||||
CE QU'ELLE NE TESTE PAS : ce que l'edge sert REELLEMENT. Cela demande de joindre la
|
||||
machine — c'est le travail de `make certificats-plan`. Ici on verifie que l'ecosysteme
|
||||
a DEMANDE la bonne chose.
|
||||
"""
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
import instances as mod_instances
|
||||
|
||||
trouvees = mod_instances.decouvrir()
|
||||
if not trouvees:
|
||||
return True, "Aucune instance decouverte : rien a verifier."
|
||||
|
||||
fautes: list[str] = []
|
||||
verifiees: list[str] = []
|
||||
for i in trouvees:
|
||||
base = RACINE.parent / i["nom"]
|
||||
inventaires = sorted((base / "inventories").glob("*")) if (base / "inventories").is_dir() else []
|
||||
for inv in inventaires:
|
||||
if not inv.is_dir():
|
||||
continue
|
||||
hosts = inv / "hosts.yml"
|
||||
if not hosts.exists():
|
||||
continue
|
||||
# Un edge n'est requis que si l'ecosysteme en declare un.
|
||||
if "serveur_nginx" not in hosts.read_text(encoding="utf-8", errors="ignore"):
|
||||
continue
|
||||
f = inv / "group_vars" / "serveur_nginx.yml"
|
||||
nom = f"{i['nom']}/{inv.name}"
|
||||
if not f.exists():
|
||||
fautes.append(f"{nom} : aucun group_vars/serveur_nginx.yml — l'edge n'emettra "
|
||||
f"pas de cert pour les noms qu'il publie")
|
||||
continue
|
||||
texte = f.read_text(encoding="utf-8", errors="ignore")
|
||||
if "sans_exposition" not in texte:
|
||||
fautes.append(f"{nom} : client_pki_sans n'etend pas les SAN avec `sans_exposition`")
|
||||
if "/etc/step/certs/" not in texte:
|
||||
fautes.append(f"{nom} : serveur_nginx_certificat ne pointe pas vers l'AC interne")
|
||||
verifiees.append(nom)
|
||||
|
||||
if fautes:
|
||||
return False, (f"{len(fautes)} ecosysteme(s) publient sans porter leurs noms : "
|
||||
+ " | ".join(fautes[:4]) + ("…" if len(fautes) > 4 else ""))
|
||||
if not verifiees:
|
||||
return True, "Aucun ecosysteme ne declare d'edge : rien a publier."
|
||||
return True, (f"{len(verifiees)} edge(s) emettent un certificat portant les noms publies "
|
||||
f"({', '.join(verifiees)}).")
|
||||
|
||||
|
||||
def preuve_parente_inscrite() -> tuple[bool, str]:
|
||||
"""L'ecosysteme sait de QUOI il descend, et cette filiation tient encore.
|
||||
|
||||
|
|
@ -1195,6 +1264,8 @@ PREUVES: list[dict] = [
|
|||
"func": preuve_parente_inscrite},
|
||||
{"id": "P41", "titre": "Resolution d'instance : une seule, partagee", "refs": [],
|
||||
"func": preuve_resolution_unique},
|
||||
{"id": "P42", "titre": "L'edge porte les noms qu'il publie", "refs": [],
|
||||
"func": preuve_edge_porte_ses_noms},
|
||||
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
|
||||
"cmds": [[sys.executable, "scripts/verifier_ports.py"]]},
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in a new issue