patient 0 : le plan de l'ecosysteme dont les autres descendront

Aujourd'hui, tout ce qui fabrique Chezlepro vit sur `eregion` — une machine HORS FLOTTE,
montee a la main, que Set-OPS ne deploie pas, ne sauvegarde pas et ne prouve pas. Un
ecosysteme entier depend d'un point unique que le moteur ignore.

Patient 0 le remplace par le plus petit ecosysteme COMPLET (PKI, DNS, edge, courriel,
PostgreSQL, forge — six machines), bati par le moteur, sauvegarde par le moteur, verifie
par le harnais. On ne deplace pas le point unique de defaillance : on l'elimine.

Derive du modele `forge`, avec trois ecarts assumes :
- INDEX 29 (10.29.0.0/16, VLAN 1291-1296), choisi libre : 13 lab, 17 Chezlepro,
  23 Technolibre ; les index bas (1, 11) ont deja force deux renumerotages.
- `federe: false` — patient 0 est EXCLU des devis du site tant qu'il n'est pas
  materialise. Une flotte en cours de reconstruction n'a pas a se voir reserver des VLAN
  et des regles pour un ecosysteme qui n'existe pas (c'est exactement ce qui avait
  injecte les adresses d'un tenant perime dans le pare-feu partage, le 2026-08-12).
  A basculer a `true` AVANT `make sdn-appliquer`, le jour du deploiement.
- `forge-01` recoit 80G au lieu du defaut : elle hebergera les depots de TOUTE la
  lignee, pas seulement les siens.

Le modele `forge` dont il derive etait lui-meme perime sur deux points, corriges ici :
`client_pki` liste a la main alors que l'integration est devenue universelle, et un FQDN
expose reste en `exemple.internal`. Les modeles PRIVES ne sont pas couverts par le
harnais — P17 ne decouvre que le modele public.

VERIFIE : les quatre registres valident, le gabarit de voute couvre les 20 secrets exiges,
l'inventaire est genere et applique. Et sur l'instance de l'exploitant, en pleine
reconstruction : `make verifier` -> 38 OK, 0 echec, 0 saute ; les devis ne voient
toujours que Chezlepro et Technolibre.

CE QUI N'EST PAS ICI, ET NE LE SERA JAMAIS : le mot de passe de la voute. C'est le seul
objet que la reproduction exige d'un humain — le mettre dans la forge reviendrait a
enfermer la cle dans le coffre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-20 20:51:00 -04:00
commit 3cf00bbf3a
11 changed files with 438 additions and 0 deletions

33
.gitignore vendored Normal file
View file

@ -0,0 +1,33 @@
# Secrets (jamais en clair ; Vault chiffre OK via .example)
*.vault
*.vault.yml
vault.yml
.vault-pass
*.secret
*.pem
*.key
.env
.env.*
# Artefact genere
inventories/*/hosts.genere.yml
# Releve du devis d identite (etat lu sur la machine, regenere a chaque `make identite-plan`)
devis-identite.json
# Releves du devis des certificats (etat lu sur chaque hote, regeneres a chaque
# `make certificats-plan`)
devis-certificats.json.*
# Releve du devis des expositions + racine AC recuperee pour les sondes HTTPS
devis-expositions.json
.racine-ac.crt
# Releves des devis PostgreSQL et courriel
devis-postgresql.json.*
devis-courriel.json.*
# Releve du devis d'etancheite de la frontiere (`make frontiere-mesurer`).
devis-frontiere.json
# Releve du devis de MTU (`make mtu-mesurer`).
devis-mtu.json

64
README.md Normal file
View file

@ -0,0 +1,64 @@
# OPS-Patient0 — l'écosystème d'origine
> **Pour qui :** l'**exploitant** de la lignée. Ce dépôt est le plan d'un écosystème
> Set-OPS comme les autres — mais de celui dont les autres descendent.
## Ce qu'il est
Patient 0 est le **plus petit écosystème complet** : PKI, DNS, edge, courriel,
PostgreSQL, et une **forge**. Six machines. Sa raison d'être tient en une phrase :
> porter les dépôts qui fabriquent les écosystèmes, et les servir à ses enfants.
Aujourd'hui, ce rôle est tenu par `eregion.chezlepro.ca` — une machine **hors flotte**,
montée à la main, que Set-OPS ne déploie pas, ne sauvegarde pas et ne prouve pas. Tout ce
qui fabrique Chezlepro dépend d'elle. Patient 0 la remplace par un écosystème **bâti par
le moteur, sauvegardé par le moteur, vérifié par le harnais**. On ne déplace pas le point
unique de défaillance : on l'élimine.
## La boucle, et comment elle se casse
Pour rebâtir patient 0, il faut le dépôt du moteur — qui vit sur patient 0. On n'échappe
pas à ça par la ruse, mais par le **nombre**. Le génome doit exister en au moins trois
endroits vivants, sans coordination :
```
patient 0 (la forge mère) · le poste de l'exploitant · restic hors cluster
+ chaque écosystème enfant, qui en porte un miroir
```
N'importe quel survivant réamorce les autres. Une famille, pas un maître.
## Ce que ce dépôt contient, et ce qu'il ne contiendra jamais
| | |
|---|---|
| `plan/` | les six machines, leurs zones, leurs applications |
| `inventories/production/group_vars/` | les intrants, le placement Proxmox, le **gabarit** de voûte |
| `inventories/production/hosts.yml` | **généré** depuis le plan — ne jamais l'éditer à la main |
| le mot de passe de la voûte | **jamais**. Ni ici, ni dans aucun dépôt : c'est le seul objet que la reproduction exige d'un humain |
## Deux réglages à connaître
**`index: 29`** — tout l'adressage en dérive (`10.29.0.0/16`, VLAN 1291-1296). Choisi
libre : 13 est le lab, 17 Chezlepro, 23 Technolibre. Les index bas (1, 11) ont déjà forcé
deux renumérotages, ils tombaient dans des plages occupées par du matériel.
**`federe: false`** — patient 0 est **exclu des devis du site** tant qu'il n'est pas
matérialisé : ni la frontière, ni le SDN, ni le pare-feu de l'hyperviseur ne lui réservent
quoi que ce soit. C'est délibéré et temporaire. À basculer à `true` le jour où on le
déploie, **avant** `make sdn-appliquer` — sinon ses zones n'existeront nulle part.
## Ce qui reste à faire avant de le matérialiser
- [ ] **Décider où il vit.** Sur `asgard`, la perte du cluster emporte patient 0 *et*
Chezlepro d'un coup. Ailleurs, la famille survit à la perte du cluster.
Le déménagement tient en quatre valeurs (`group_vars/proxmox.yml`) et un symlink.
- [ ] **Créer la voûte réelle** :
`ansible-vault create inventories/production/group_vars/all/vault.yml`,
en couvrant les noms du gabarit (`vault.yml.example`).
- [ ] **Basculer `federe: true`**, puis `make instancier``make instancier-appliquer`.
- [ ] `make placement-plan` — confronter les quatre valeurs au cluster **avant** de
cloner quoi que ce soit : quarante minutes de déploiement ne se rattrapent pas.
- [ ] Déployer, puis y **pousser les quatre dépôts** du génome (moteur, instances,
hébergeur, modèles) et inscrire la parenté de chacun.

View file

@ -0,0 +1,28 @@
---
# Intrants d'IDENTITE de patient 0 — SOURCE UNIQUE, partagee par tous les envs.
# Edite par le panneau « Intrants de base » du GUI (make inventaire-ui).
# RESEAU(X) D'ADMINISTRATION — la seule source autorisee a ouvrir SSH sur la flotte.
# 10.0.0.0/24 est le plan de gestion d'asgard (VLAN 10 de l'underlay), celui d'ou l'on
# administre aujourd'hui. A revoir si patient 0 demenage sur un autre site : c'est alors
# le plan de gestion de CE site-la qu'il faut nommer, sinon le `default deny` ferme
# l'acces d'administration sur une flotte qu'on vient de batir.
nftables_admin_ssh:
- 10.0.0.0/24
# Resolveur d'AMORCAGE, pose par cloud-init a la creation d'une VM. Il ne sert qu'une
# fois : `client_unbound` bascule ensuite /etc/resolv.conf vers 127.0.0.1. Sans lui, la
# VM nait sans resolution et `apt` ne peut rien installer.
dns_amorcage: 9.9.9.9,149.112.112.112
domaine_interne: genese.internal
fuseau_horaire: America/Toronto
identite_realm: genese
organisation: Alliance Boréale
# Adresse du compte d'amorcage — la SEULE valeur du role `amorcage_acces` qui ne peut
# pas se deriver : elle designe une personne. Elle doit etre lisible SANS l'ecosysteme
# qu'on amorce — une adresse en @genese.internal serait un piege parfait.
amorcage_acces_courriel: sysadmin@chezlepro.ca
setops_plan_dir: "{{ playbook_dir }}/../../instance/plan"

View file

@ -0,0 +1,40 @@
---
# GABARIT de voute de patient 0 — des NOMS, jamais de valeurs.
#
# La voute reelle (group_vars/all/vault.yml, chiffree) se cree a la main :
# ansible-vault create inventories/production/group_vars/all/vault.yml
# Son mot de passe ne vit NI ici, NI dans aucun depot : c'est le seul objet que la
# reproduction exige d'un humain. Le mettre dans la forge reviendrait a enfermer la
# cle dans le coffre.
#
# P18 confronte ce gabarit aux secrets que le plan exige, puis la voute reelle a ce
# gabarit. Une cle qui manque ici passe inapercue jusqu'au deploiement.
# --- Exiges par des roles que patient 0 DEPLOIE ---
vault_backup_ssh_privkey: "" # role client_backup
vault_bd_forgejo: "" # base 'forgejo'
vault_forgejo_admin: "" # role serveur_forgejo
vault_forgejo_internal_token: "" # role serveur_forgejo
vault_forgejo_oidc: "" # role serveur_forgejo
vault_forgejo_secret_key: "" # role serveur_forgejo
vault_openldap_admin: "" # role amorcage_acces (playbook serveur_openldap.yml)
vault_postgresql_keycloak: "" # role serveur_postgresql
vault_redis: "" # role serveur_redis
vault_restic_password: "" # role client_backup
vault_step_ca_password: "" # role serveur_step_ca
vault_step_ca_provisioner_password: "" # role client_pki
# --- Exiges par le harnais, pour des services que patient 0 NE DEPLOIE PAS ---
# Ces references vivent dans des roles que patient 0 installe (ex. serveur_backup
# pousse ses resultats vers Icinga, serveur_postgresql connait les bases des
# autres). Le perimetre est donc plus large que la flotte : meme question de
# cadrage que celle corrigee sur P32 le 2026-08-20. Les laisser vides tant que le
# service correspondant n'existe pas.
vault_grafana_admin: "" # role serveur_grafana (playbook serveur_grafana.yml)
vault_grafana_oidc: "" # role serveur_grafana (playbook serveur_grafana.yml)
vault_icinga_api_depot: "" # role serveur_backup (playbook serveur_backup.yml)
vault_keycloak_admin: "" # role serveur_keycloak (playbook serveur_keycloak.yml)
vault_nextcloud_admin: "" # role serveur_nextcloud (playbook serveur_nextcloud.yml)
vault_nextcloud_oidc: "" # role serveur_nextcloud (playbook serveur_nextcloud.yml)
vault_oauth2_cookie: "" # role serveur_oauth2_proxy (playbook serveur_oauth2_proxy.yml)
vault_sysadmin_amorcage: "" # role amorcage_acces (playbook serveur_openldap.yml)

View file

@ -0,0 +1,16 @@
---
# Proxmox, cote TENANT : son gabarit dore et ses defauts de placement.
#
# L'API du cluster, ses noeuds, ses stockages et ses ponts appartiennent a l'HEBERGEUR
# et vivent dans SON depot (proxmox-hebergeur.yml, a cote d'underlay.yml). Recopies ici,
# ils avaient deja diverge entre tenants du meme cluster.
#
# Ce qui reste ici est un CHOIX de patient 0, fait a l'interieur de ce que l'hebergeur
# offre : quel modele cloner, sur quel noeud/stockage/pont poser ses VM par defaut.
# Ce sont les QUATRE valeurs qui rattachent un tenant a une fabric (D-80) : les seules
# a changer si patient 0 demenage. `make placement-plan` les confronte au cluster reel
# AVANT de deployer — quarante minutes de clone ne se rattrapent pas.
proxmox_clone_noeud: asgard
proxmox_clone_stockage: TrueNAS
proxmox_clone_pont: vmbr1
proxmox_clone_vmid_modele: 99998

View file

@ -0,0 +1,169 @@
# Inventaire GENERE depuis le plan (make instancier / instancier-appliquer).
# NE PAS editer a la main : edite instance/plan/serveurs.yml + instance/plan/applications.yml.
# Source : instance/plan/serveurs.yml + instance/plan/applications.yml + instance/plan/nomenclature.yml.
all:
children:
client_backup:
hosts:
data-sql-01: null
forge-01: null
infra-mail-01: null
infra-pki-01: null
client_journal:
hosts:
data-sql-01: null
forge-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
client_metrique:
hosts:
data-sql-01: null
forge-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
client_pki:
hosts:
data-sql-01: null
forge-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
client_smtp:
hosts:
data-sql-01: null
forge-01: null
client_unbound:
hosts:
data-sql-01: null
forge-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
hotes_actifs:
hosts: {}
hotes_planifies:
hosts:
data-sql-01:
ansible_host: 10.29.18.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 3
proxmox_disque_taille: 40G
proxmox_etiquette_vlan: ''
proxmox_memoire: 4096
proxmox_passerelle: 10.29.18.1
proxmox_pont: t29donn
proxmox_vlan: 1293
proxmox_vmid: 129301101
setops_supernet: 10.29.0.0/16
forge-01:
ansible_host: 10.29.21.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 1
proxmox_disque_taille: 80G
proxmox_etiquette_vlan: ''
proxmox_memoire: 2048
proxmox_passerelle: 10.29.21.1
proxmox_pont: t29appl
proxmox_vlan: 1296
proxmox_vmid: 129601101
setops_supernet: 10.29.0.0/16
infra-dns-01:
ansible_host: 10.29.19.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 1
proxmox_disque_taille: 40G
proxmox_etiquette_vlan: ''
proxmox_memoire: 1536
proxmox_passerelle: 10.29.19.1
proxmox_pont: t29serv
proxmox_vlan: 1294
proxmox_vmid: 129401101
setops_supernet: 10.29.0.0/16
infra-edge-01:
ansible_host: 10.29.16.11
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 1
proxmox_disque_taille: 15G
proxmox_etiquette_vlan: ''
proxmox_memoire: 1536
proxmox_passerelle: 10.29.16.1
proxmox_pont: t29fron
proxmox_vlan: 1291
proxmox_vmid: 129101101
sans_exposition:
- forge.genese.internal
setops_supernet: 10.29.0.0/16
infra-mail-01:
ansible_host: 10.29.19.31
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 1
proxmox_disque_taille: 20G
proxmox_etiquette_vlan: ''
proxmox_memoire: 1536
proxmox_passerelle: 10.29.19.1
proxmox_pont: t29serv
proxmox_vlan: 1294
proxmox_vmid: 129403101
setops_supernet: 10.29.0.0/16
infra-pki-01:
ansible_host: 10.29.19.21
ansible_user: ansible
proxmox_cidr: 24
proxmox_coeurs: 1
proxmox_disque_taille: 32G
proxmox_etiquette_vlan: ''
proxmox_memoire: 1024
proxmox_passerelle: 10.29.19.1
proxmox_pont: t29serv
proxmox_vlan: 1294
proxmox_vmid: 129402101
setops_supernet: 10.29.0.0/16
modeles_vm:
hosts: {}
serveur_debian:
hosts:
data-sql-01: null
forge-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
serveur_dovecot:
hosts:
infra-mail-01: null
serveur_durci:
hosts:
data-sql-01: null
forge-01: null
infra-dns-01: null
infra-edge-01: null
infra-mail-01: null
infra-pki-01: null
serveur_forgejo:
hosts:
forge-01: null
serveur_nginx:
hosts:
infra-edge-01: null
serveur_postgresql:
hosts:
data-sql-01: null
serveur_powerdns:
hosts:
infra-dns-01: null
serveur_redis:
hosts:
data-sql-01: null
serveur_step_ca:
hosts:
infra-pki-01: null

9
plan/applications.yml Normal file
View file

@ -0,0 +1,9 @@
---
applications:
step_ca: { groupe: serveur_step_ca, hote: infra-pki-01 }
nginx: { groupe: serveur_nginx, hote: infra-edge-01 }
powerdns: { groupe: serveur_powerdns, hote: infra-dns-01 }
dovecot: { groupe: serveur_dovecot, hote: infra-mail-01 }
postgresql: { groupe: serveur_postgresql, hote: data-sql-01 }
redis: { groupe: serveur_redis, hote: data-sql-01 }
forgejo: { groupe: serveur_forgejo, hote: forge-01, port: 3000, expose: [forge.genese.internal] }

12
plan/bases-donnees.yml Normal file
View file

@ -0,0 +1,12 @@
---
serveurs_bd:
sql-01: { type: postgres, hote: data-sql-01, port: 5432, groupe: serveur_postgresql }
bases_donnees:
forgejo:
serveur: sql-01
base: forgejo
proprietaire: forgejo
secret: vault_bd_forgejo
consommateur: serveur_forgejo
portee: groupe
usage: principale

12
plan/domaines.yml Normal file
View file

@ -0,0 +1,12 @@
---
# Patient 0 n'a AUCUNE presence publique, et c'est un choix.
#
# Il ne sert qu'une chose : porter les depots qui fabriquent les ecosystemes, et les
# servir a ses enfants. Rien la-dedans n'a de raison d'etre joignable depuis Internet.
# Moins il expose, moins il y a a defendre sur la machine dont tout le reste descend.
#
# Le jour ou il devrait publier quelque chose (un miroir lisible de l'exterieur, par
# exemple), ajouter ici le domaine public et son `edge` — le reste (vhost, SAN du
# certificat, plancher /etc/hosts) se derive tout seul du plan.
domaines_publics:
genese.internal: { autorite: auto-heberge, edge: serveur_nginx, secondaires: [], dnssec: false }

42
plan/nomenclature.yml Normal file
View file

@ -0,0 +1,42 @@
---
# Nomenclature = MODELE de l'ecosysteme (zones + placement des fonctions).
# L'ADRESSAGE (supernet, sous-reseaux, passerelles, VLAN, VMID) N'EST PAS ecrit ici :
# il se DERIVE du seul seed `index` (scripts/inventory_rules : supernet_de, base3_de,
# passerelle_de, vlan_de). Editer `index` via le panneau Intrants du GUI.
#
# INDEX 29 -> supernet 10.29.0.0/16, VLAN 1291 a 1296.
# Choisi libre le 2026-08-20 : 13 est le lab, 17 Chezlepro, 23 Technolibre. Les index
# bas (1, 11) ont deja force deux renumerotages — ils tombaient dans des plages que du
# materiel occupait deja. P21 garde la collision si un jour un autre s'en approche.
index: 29
cidr_hote: 24
reservations:
passerelle: 1
reserve_min: 2
reserve_max: 9
# PATIENT 0 NE PARTICIPE PAS ENCORE AUX DEVIS DU SITE.
#
# `federe: false` l'exclut de `devis_reseau.decouvrir()` : ni la frontiere, ni le SDN,
# ni le pare-feu de l'hyperviseur ne lui reservent quoi que ce soit. C'est deliberate et
# TEMPORAIRE — un ecosysteme qui n'est pas deploye n'a rien a faire dans la politique du
# site (c'est exactement ce qui avait injecte les adresses d'un tenant perime dans le
# pare-feu partage, le 2026-08-12).
#
# A BASCULER A `true` le jour ou on le materialise, avant `make sdn-appliquer`.
federe: false
# Zones de securite (un /24 + VLAN chacune) — seuls les libelles sont du modele.
categories:
1: { libelle: Frontiere }
3: { libelle: Donnees }
4: { libelle: Services-infra }
6: { libelle: Applications }
# Placement : quelle fonction dans quelle zone (categorie) et son rang (service).
fonctions:
data-sql: { categorie: 3, service: 1 }
forge: { categorie: 6, service: 1 }
infra-dns: { categorie: 4, service: 1 }
infra-edge: { categorie: 1, service: 1 }
infra-mail: { categorie: 4, service: 3 }
infra-pki: { categorie: 4, service: 2 }

13
plan/serveurs.yml Normal file
View file

@ -0,0 +1,13 @@
---
# `integrations:` ne porte que les integrations FACULTATIVES. Les universelles
# (PKI, metriques, journaux, resolution locale) viennent de la politique du role —
# voir roles/client_*/meta/integration.yml : tout hote les recoit sans etre listee ici.
serveurs:
infra-pki-01: { fonction: infra-pki, etat: planifie, disque: 32G, integrations: [client_backup] }
infra-edge-01: { fonction: infra-edge, etat: planifie }
infra-mail-01: { fonction: infra-mail, etat: planifie, integrations: [client_backup] }
infra-dns-01: { fonction: infra-dns, etat: planifie, disque: 40G }
data-sql-01: { fonction: data-sql, etat: planifie, integrations: [client_backup, client_smtp] }
# LA MACHINE QUI PORTE LE GENOME. Son disque n'est pas celui du modele : elle
# hebergera les depots de TOUS les ecosystemes de la lignee, pas seulement les siens.
forge-01: { fonction: forge, etat: planifie, disque: 80G, integrations: [client_backup, client_smtp] }