documentation : la tournee des 74 documents, parce qu un balayage ne lit pas
Some checks failed
verifier / verifier (push) Has been cancelled

La revision a commence par un balayage par motifs — chemins morts, cibles make
absentes, comptes derives. Il a trouve une trentaine d ecarts et rate presque
tout le reste : un motif ne voit que ce qui s exprime en motif.

make hote-planifier en est l exemple. La cible EXISTE, donc le controle passait
au vert. C est une cible depreciee qui refuse et sort en 2, recommandee par
AGENTS.md, et qui contredit la REGLE D OR du meme fichier trois ecrans plus
haut. Il fallait lire pour la voir.

74 documents lus un par un. 66 corriges, 8 exacts.

CE QUI ETAIT FRANCHEMENT FAUX

AGENTS.md, la source d autorite, annoncait la flotte pas encore executee contre
des VM reelles. Elle a ete rasee et remontee depuis zero trois fois.
ecosysteme-chezlepro.md, le document montre a un client, portait la meme
phrase : il se sous-vendait gravement.

courriel-conception.md s ouvrait sur aucun role n est encore ecrit, au-dessus de
son propre paragraphe 1 qui les nomme. autorisation.md se terminait sur rien n
est construit alors qu il rapporte des mesures datees du role en fonctionnement.
hebergeur-exploitation.md disait rien n est fait d un depot qui existe.
filiation-emancipation.md se contredisait a deux ecrans de distance.

DES MODELES DECRITS D APRES UN MONDE ANTERIEUR

Le resolveur : cinq documents decrivaient un Unbound par VM en opt-in, trois le
donnaient en exemple d integration FACULTATIVE — il est universel depuis le
2026-08-24. L adressage de nomenclature-vm.md : reseau unique, VLAN 11-15, VMID
a cinq chiffres. Le nommage SDN de sdn-evpn.md contre le code : c est le wiki
qui avait raison.

CE QUI CASSE AU PREMIER ESSAI

Le nom du gabarit dore etait faux a quatre endroits, dont la procedure qui le
FABRIQUE et le critere R2 de l epreuve d operateur independant.
preparer-un-site-hebergeur.md avertissait qu une VM faite a la main serait
detruite : raser derive du plan, il ne la detruira jamais — le risque est l
inverse. Un mot de passe d essai en clair dans un depot public.

DEUX PREUVES ETENDUES, ET UNE QUI SE TROMPAIT ELLE-MEME

P57 couvre les groupes : elle a signale aussitot 29 groupes annonces au-dessus d
un tableau qui en cite 40. P29 confronte le tableau de authentification.md aux
declarations reelles : 12 annonces, 21 reels.

Et P57 imposait un chiffre faux — 56 preuves alors que le depot en porte 57, la
conditionnelle vivant hors de tout comptage. Un garde-fou qui fait respecter une
erreur ajoute l assurance a l erreur.

CE QUI RESTE, ET QU AUCUNE PREUVE NE TIENT

Deux comptes trouves a la main. Et une lacune reelle : rien ne garde les
meta/acces.yml — ni qu un service web-sso en porte un, ni que le groupe qu il
nomme existe. P29 tient les positions d authentification, personne ne tient les
habilitations.

make prouver : CONFORME, 56 OK, 0 echec, 1 saute. 0 lien mort.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-06 16:18:23 -04:00
parent 00cee67f02
commit 5bc3bceac1
73 changed files with 1761 additions and 654 deletions

108
AGENTS.md
View file

@ -20,7 +20,7 @@ Piliers de l’écosystème :
- confiance — autorité de certification interne (ACME) ;
- nommage et adressage — DNS interne et nomenclature dérivable ;
- données — bases relationnelles et cache, avec registre des connexions ;
- communication — relais courriel interne ;
- communication — service de courriel souverain (boîtes LDAP, SMTP, IMAP, antispam, DKIM) ; le relais des notifications système en est distinct (`client_smtp`) ;
- observabilité et supervision — métriques, journaux, tableaux de bord, supervision active ;
- applicatif — services internes (forge, etc.) et couche web.
@ -29,7 +29,7 @@ Propriétés visées, avec leurs nuances honnêtes :
- **Souverain / auto-suffisant** : c’est l’objectif. Aucune dépendance à un service externe pour la confiance, l’identité, le nom ou la communication. Cela justifie le choix de construire plutôt qu’assembler des SaaS.
- **Déclaratif et convergent** : l’état voulu est décrit dans des registres machine-lisibles (`instance/plan/nomenclature.yml`, `docs/dependances-groupes.yml`, `instance/plan/bases-donnees.yml`) et appliqué par les groupes Ansible. Ces registres sont la **source unique de vérité**.
- **Pas (encore) auto-réparé** : la convergence est pilotée par l’opérateur (GUI / CLI / `make`), pas une boucle fermée d’auto-remédiation.
- **Définition avant déploiement** : une grande partie est planifiée et validée (`--syntax-check`, `ansible-lint`) mais pas encore exécutée contre des VM réelles. Ne jamais présenter un rôle non déployé comme « en production ».
- **Éprouvé sur VM réelles — mais la nuance tient toujours.** Cette ligne a dit, jusqu’au 2026-09-06, que « une grande partie est planifiée et validée mais pas encore exécutée contre des VM réelles ». C’est faux depuis longtemps : la flotte a été **rasée et remontée depuis zéro** le 2026-08-13, puis **deux fois le 2026-09-02** (15/15 puis 14/14 hôtes, 0 échec, `make valider` à 0 échec sur 13 hôtes). Ce qui reste vrai, et qu’il faut garder : **un `--syntax-check` vert ne prouve rien de l’exécution**, et le tableau de maturité de `docs/catalogue-services.md` — pas ce fichier — dit ce qui est éprouvé et ce qui ne l’est pas. Ne jamais présenter comme « en production » un rôle que ce tableau ne donne pas pour tel.
Conséquence pour le travail : préserver la discipline qui tient l’ensemble — registres comme source unique, dépendances explicites, validation de chaque pièce, secrets hors dépôt. C’est ce qui empêche l’écosystème de devenir un objet ingérable. Le template Debian 13 reste la fondation (le moule des VM), pas la finalité.
@ -40,7 +40,9 @@ Conséquence pour le travail : préserver la discipline qui tient l’ensemble
`Set-OPS` se pilote par un **plan**, pas par l’édition directe de l’inventaire.
L’inventaire Ansible est **généré** depuis le plan.
**RÈGLE D’OR : `instance/inventories/production/hosts.yml` est un artefact GÉNÉRÉ. Ne jamais l’éditer à la main.** On édite le *plan*, puis on régénère.
**RÈGLE D’OR : `instance/inventories/<inventaire>/hosts.yml` est un artefact GÉNÉRÉ. Ne jamais l’éditer à la main.** On édite le *plan*, puis on régénère.
`<inventaire>` est une **place, pas un nom** : le moteur le résout (`principal`, sinon `production` — `scripts/inventory_rules.py`, `ORDRE_INVENTAIRE`). La flotte dit `principal`, le modèle public dit `production`. Ne coder ni l’un ni l’autre en dur dans un document.
- L’**application** est l’entité pivot ; le **groupe** Ansible n’est qu’une capacité (le rôle appliqué), plus une cible de liaison.
- Le plan vit dans des registres machine-lisibles : `instance/plan/serveurs.yml` (les VM), `instance/plan/applications.yml` (les services et leurs liens `requiert`/`utilise`/`expose`), `instance/plan/bases-donnees.yml`, `instance/plan/domaines.yml`, `instance/plan/nomenclature.yml`.
@ -160,9 +162,11 @@ Avant de proposer un changement comme terminé, vérifier au minimum la syntaxe
Exemple pour le template Debian 13 Proxmox :
```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_preparer.yml --syntax-check
ansible-playbook -i "$SETOPS_INVENTAIRE" playbooks/modeles_vm/debian13_proxmox_preparer.yml --syntax-check
```
`SETOPS_INVENTAIRE` est **exporté par le `Makefile`**, qui résout le nom de l’inventaire au lieu de le coder en dur (cf. la règle d’or ci-dessus) — la variable est donc déjà là dans toute recette `make`. Pour couvrir tous les playbooks d’un coup : `make syntaxe`. Depuis un shell nu, la variable n’existe pas : passer le chemin réel de l’inventaire de l’instance montée.
Pour un autre playbook, remplacer le chemin par le playbook concerné.
Ne jamais déclarer un playbook prêt si `--syntax-check` échoue.
@ -180,7 +184,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
## Écrire, puis relire (D-68)
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
C'est aussi ce que font les 35 preuves de `make prouver` : elles lisent le dépôt, sans le
C'est aussi ce que font les 57 preuves de `make prouver` : elles lisent le dépôt, sans le
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
dépôt annonce.**
@ -207,6 +211,9 @@ make identite-plan make certificats-plan make expositions-plan
make postgresql-plan make courriel-plan
```
Le même patron existe **sous** les services, pour le monde physique — `make frontiere-plan`,
`proxmox-fw-plan`, `sdn-plan`, `underlay-plan`, `placement-plan`.
Ils ne modifient rien et sortent en code 1 s'il y a un écart. Après un changement qui
touche l'identité, les certificats, une exposition, la base ou le courriel, **lancer le
devis correspondant** : une tâche verte ne prouve pas que le service rend son service.
@ -276,36 +283,32 @@ Lorsqu’une commande shell est nécessaire, elle doit être encadrée avec les
## Structure générale du dépôt
Le dépôt peut contenir progressivement :
Ce que la racine porte réellement (mesuré le 2026-09-06) :
```text
inventories/
playbooks/
roles/
templates/
files/
scripts/
docs/
roles/ les rôles Ansible
playbooks/ les playbooks, classés par domaine
scripts/ le moteur de plan, la GUI, les devis, les preuves
filter_plugins/ les filtres Jinja du dépôt
docs/ wiki/ la documentation
exemples/ les modèles d'instance prêts à copier
instance/ SYMLINK vers le dépôt de l'instance active — jamais un vrai dossier ici
```
Les playbooks peuvent être classés par domaine :
**Il n'y a ni `inventories/`, ni `templates/`, ni `files/` à la racine**, et il ne doit pas y en avoir : les inventaires appartiennent à l'instance (derrière le symlink), les gabarits et fichiers appartiennent à leur rôle.
Les playbooks sont classés par domaine :
```text
playbooks/
├── groupes/
├── maintenance/
├── monitoring/
├── networking/
├── proxmox/
├── modeles_vm/
├── web/
├── database/
├── identity/
├── backup/
└── applications/
├── groupes/ un playbook par groupe opérationnel — la conformité passe par là
├── maintenance/ les devis et les manœuvres ponctuelles
├── modeles_vm/ la fabrication du gabarit doré
├── proxmox/ le clonage et le cycle de vie des VM
└── applications/ backup/ database/ monitoring/ web/ — vides, un README d'espace réservé
```
À ce jour, seuls `groupes/`, `maintenance/`, `modeles_vm/` et `proxmox/` sont réellement peuplés. Les autres domaines de cette liste sont **prospectifs** : ils n'apparaissent que lorsqu'un besoin réel les justifie (cf. la règle « ne pas créer de structure inutile » ci-dessous).
Seuls les quatre premiers sont peuplés. Les cinq autres ne contiennent qu'un README : ce sont des **espaces réservés**, et leur existence contredit à demi la règle « ne pas créer de structure inutile » ci-dessous — les laisser vides est un choix assumé, en créer d'autres ne l'est pas.
Les rôles peuvent être ajoutés progressivement selon les besoins.
@ -338,11 +341,12 @@ Ne pas multiplier les cibles `make` secondaires si elles ne correspondent pas à
Les cibles d'exploitation des VM doivent privilégier les groupes :
```text
make deployer HOTE=web-01
make deployer HOTE=web-frontal-01
make deployer-groupe GROUPE=serveur_debian
make hote-planifier HOTE=obs-01 VMID=94101 GROUPES="serveur_debian serveur_durci serveur_prometheus"
```
**`make hote-planifier`, `hote-ajouter` et `hote-groupes` sont DÉPRÉCIÉES** — elles refusent et sortent en 2. Elles éditaient l'inventaire à la main, ce que la RÈGLE D'OR interdit. Pour ajouter un hôte : le déclarer dans `instance/plan/serveurs.yml` (ou la vue **Serveurs** du GUI), puis `make instancier-appliquer`. Le VMID n'est plus saisi : il est **dérivé** (neuf chiffres, miroir de l'IP — `117602101`, et non le format à cinq chiffres d'avant).
Éviter les cibles parallèles qui réappliquent les mêmes rôles par couche, par exemple `make socle`, `make durcissement`, `make converger` ou `make deployer-vm`.
Toute cible `make` qui lance une action destructive ou risquée doit exiger une confirmation explicite.
@ -437,15 +441,20 @@ intégration cliente : raccorde les VM au service central
Exemples :
```text
PowerDNS serveur → clients DNS / resolver / enregistrements
step-ca serveur → confiance CA / ACME client
LDAP serveur → SSSD / NSS / PAM client
Keycloak serveur → intégrations OIDC applicatives
Prometheus → node_exporter sur les VM
Icinga2 → agent ou checks distants
PowerDNS serveur → hosts_statiques (plancher) + client_resolveur (opt-in)
step-ca serveur → client_pki (certificat + renouvellement + rechargement du service)
LDAP serveur → resoudre_annuaire, et les applications qui s'y lient (Postfix, Dovecot, Keycloak)
Keycloak serveur → intégrations OIDC applicatives, ou serveur_oauth2_proxy si l'app n'a pas d'OIDC
Prometheus → client_metrique (node_exporter) sur les VM
Icinga2 → SANS AGENT : contrôles actifs depuis le cœur, résultats passifs poussés par l'API
Grafana → datasources et dashboards côté plateforme
```
Deux précisions qui ont manqué ici longtemps, et qui changent la conception d'un nouveau service :
- **Pas de login LDAP au niveau du système.** `SSSD` / `NSS` / `PAM` ne sont pas la couche cliente de l'annuaire : le rôle `client_ldap` a été **retiré (2026-07-04), hors conception**. L'annuaire sert les *applications*, pas l'ouverture de session Unix.
- **Pas d'agent de supervision.** Icinga ne pose rien sur les hôtes. Un nœud qui doit rapporter le fait **lui-même**, en poussant son résultat à l'API — c'est ainsi que `client_backup` rapporte l'état de son propre dépôt. Ne pas prévoir de rôle « agent ».
Quand un nouveau service central est ajouté, prévoir aussi le ou les rôles clients nécessaires pour intégrer les VM existantes et futures.
Les dépendances entre groupes doivent être déclarées dans :
@ -475,11 +484,13 @@ Les variables de rôle doivent utiliser un préfixe correspondant au rôle.
Exemples :
```yaml
ssh_durcissement_port: 22
nftables_socle_enabled: false
fail2ban_ssh_enabled: true
ssh_hardening_port: 22 # rôle ssh_hardening
nftables_baseline_enabled: false # rôle nftables_baseline
fail2ban_ssh_enabled: true # rôle fail2ban_ssh
```
Le préfixe est **le nom du rôle, tel qu'il est**, pas sa traduction : ces trois lignes sont copiées des `defaults/main.yml` réels. (Elles disaient `ssh_durcissement_port` et `nftables_socle_enabled` jusqu'au 2026-09-06 — deux préfixes qui ne correspondaient à aucun rôle, dans l'exemple censé illustrer la règle.)
Séparer clairement :
```text
@ -596,7 +607,7 @@ Ne pas activer un pare-feu générique dans un template sans confirmation explic
Pour le template Debian 13, `nftables` peut être installé et préparé, mais rester désactivé par défaut.
L’activation du pare-feu doit être faite sur un clone ou sur un serveur final, avec des règles adaptées à son rôle.
L’activation du pare-feu doit être faite sur un clone ou sur un serveur final. **Ses règles ne s’écrivent pas à la main** : elles sont *dérivées* du registre des flux — chaque rôle déclare ce qu’il reçoit dans son `meta/flux.yml`, et `scripts/resoudre_flux.py` en produit le jeu de règles. Le même registre alimente le pare-feu de l’hyperviseur et la frontière OPNsense, ce qui leur interdit de se contredire. Voir `docs/flux-conception.md` et `docs/registre-flux.md`.
---
@ -630,7 +641,7 @@ Le nettoyage final avant conversion en template doit être protégé par une con
Exemple :
```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_nettoyer.yml -e template_cleanup_confirm=true
ansible-playbook -i "$SETOPS_INVENTAIRE" playbooks/modeles_vm/debian13_proxmox_nettoyer.yml -e template_cleanup_confirm=true
```
Le nettoyage peut inclure :
@ -650,21 +661,22 @@ Ne jamais lancer ce nettoyage sur un serveur de production sans confirmation exp
Chaque modification significative doit être inscrite dans `CHANGELOG.md`.
Format recommandé :
**Le format a changé, et ce fichier disait encore l'ancien.** Les entrées d'avant le
2026-08-03 suivaient trois sections fixes (`### Ajouté` / `### Modifié` / `### Corrigé`).
Depuis, chaque entrée est un **récit** : ce qui était cassé, pourquoi personne ne le voyait,
ce que ça coûte de le réapprendre. Suivre la pratique en vigueur :
```markdown
## YYYY-MM-DD
## AAAA-MM-JJ — un titre qui dit CE QUI A ÉTÉ APPRIS, pas ce qui a été touché
### Ajouté
- ...
**N preuves.** Une ou deux phrases : l'état du harnais, et l'enjeu.
### Modifié
- ...
### Corrigé
- ...
### Un sous-titre par piège payé
Ce que le dépôt croyait, ce que la machine faisait, et la mesure qui a tranché.
```
Deux entrées le même jour se distinguent par un rang : `## 2026-09-02 (6) — …`.
Les corrections de rôles, handlers, playbooks et templates doivent être consignées.
---

View file

@ -1,5 +1,99 @@
# CHANGELOG — Set-OPS
## 2026-09-06 — Tournee des 74 documents : ce que le depot disait de lui-meme avait vieilli
**57 preuves (P01-P57, dont une conditionnelle). `make prouver` : CONFORME, 56 OK, 0 echec,
1 saute.** Aucun comportement ne change. Ce qui change, c'est que les documents cessent de
decrire un depot qui n'existe plus.
### La lecon de methode, d'abord
La revision a commence par un BALAYAGE PAR MOTIFS — chemins morts, cibles `make` absentes,
comptes derives. Il a trouve une trentaine d'ecarts, et il a rate presque tout le reste. Un
motif ne voit que ce qui s'exprime en motif.
`make hote-planifier` en est l'exemple exact : la cible EXISTE, donc le controle passait au
vert. C'est une cible DEPRECIEE qui refuse et sort en 2 — recommandee par `AGENTS.md`, et
contredisant la REGLE D'OR du meme fichier trois ecrans plus haut. Il a fallu lire pour la
voir. D'ou la tournee : les 74 documents, un par un.
### Les affirmations franchement fausses
`AGENTS.md` — la source d'autorite — annoncait « pas encore execute contre des VM reelles ».
La flotte a ete rasee et remontee depuis zero le 2026-08-13, puis DEUX FOIS le 2026-09-02.
`docs/ecosysteme-chezlepro.md`, qui est le document montre a un client, portait la meme
phrase : il se sous-vendait gravement.
`docs/courriel-conception.md` s'ouvrait sur « Statut : CONCEPTION. Aucun role n'est encore
ecrit » — au-dessus de son propre §1 qui nomme les trois roles, deployes et prouves.
`docs/autorisation.md` se terminait sur « Rien n'est construit », alors que le meme document
rapporte des mesures DATEES prises sur le role en fonctionnement.
`docs/hebergeur-exploitation.md` disait « Rien n'est fait » d'un depot qui existe :
`SITE-Chezlepro`, avec son plan, ses sept VM et le symlink en place.
`docs/filiation-emancipation.md` se contredisait a deux ecrans de distance : une section
decrivait `make emancipation-prouver`, une autre affirmait que cet instrument n'existait pas.
### Les modeles decrits d'apres un monde anterieur
- **Le resolveur.** `dns-interne.md`, `integrations-vm.md`, deux unites du wiki et
`intrants-communs.md` decrivaient un Unbound par VM, en opt-in. Depuis le 2026-08-24,
`client_resolveur` N'INSTALLE PLUS RIEN et son integration est UNIVERSELLE. Trois
documents le donnaient meme en exemple d'integration *facultative* — l'inverse exact.
- **L'adressage.** `nomenclature-vm.md` decrivait un reseau unique `10.0.0.0/16`, des VLAN
11 a 15 et des VMID a cinq chiffres : le modele d'avant le multi-instance.
- **Le nommage SDN.** `sdn-evpn.md` annoncait `CHEZ17` / `chez174` ; le code produit `t17` /
`t17serv`. C'est le wiki qui avait raison.
- **La bascule D-77.** Trois documents la donnaient « en cours » ; `10.0.0.0/24` n'existe
plus depuis le 2026-08-22.
- **Le mecanisme de voute.** Cinq documents — dont le runbook de REPRISE — designaient un
`ANSIBLE_VAULT_PASSWORD_FILE` unique. Une voute, une cle depuis le 2026-08-28.
### Ce qui casse au premier essai
Le nom du gabarit dore etait faux a **quatre** endroits, dont la procedure qui le FABRIQUE
(`debian13-template`) et le critere de reussite R2 de l'epreuve de l'operateur independant.
Le defaut du code est `modeleSetOPS`, et le clonage cherche sa source PAR CE NOM.
`preparer-un-site-hebergeur.md` avertissait qu'une VM creee a la main serait detruite par
l'outil. C'est l'inverse : `raser` derive sa liste du plan, il ne la detruira JAMAIS — elle
survit sans DNS, sans certificat, sans sauvegarde, et son VMID n'est garde par aucune preuve.
Un mot de passe d'essai en clair dans `wiki/Courriel.md`, dans un depot public.
### Le nom de l'inventaire n'en est pas un
Douze chemins ecrivaient `instance/inventories/production/hosts.yml`, la REGLE D'OR
d'`AGENTS.md` comprise. Cet inventaire n'existe pas ici : la flotte dit `principal`. Mais le
modele public dit bien `production`, et le moteur CHERCHE le nom au lieu de l'imposer :
ecrire l'un des deux en dur etait faux pour la moitie des lecteurs.
### Deux preuves etendues, et une qui se trompait elle-meme
**P57** (comptes en prose) couvre desormais les GROUPES. Elle a signale dans la seconde qui
a suivi que `catalogue-services.md` annoncait « 30 groupes classes » la ou il y en a 40, et
« les 29 groupes » au-dessus d'un tableau qui en cite 40 — une contradiction A UNE LIGNE DE
DISTANCE.
**P29** confronte desormais le tableau de `docs/authentification.md` aux declarations
reelles. La ligne `sans-auth-humaine` annoncait 12 roles ; il y en a 21. La preuve lisait ces
declarations depuis le debut sans jamais regarder ce que le document en disait.
**Et P57 imposait un chiffre faux.** Elle mesurait `len(PREUVES)` = 56, mais le depot porte
**57** preuves : P16 est conditionnelle et vivait DANS `main()`, hors de tout comptage. Un
garde-fou qui fait respecter une erreur est pire qu'aucun garde-fou — il ajoute l'assurance
a l'erreur.
### Ce que la tournee laisse en place, et qu'aucune preuve ne tient
Deux comptes trouves a la main : le README annoncait cinq portes et en ouvrait six ;
`implanter-un-tenant-sur-un-site.md` renvoyait aux « huit lignes » d'une fiche qui en compte
dix. Et une lacune reelle, nommee dans `autorisation.md` : **rien ne garde les
`meta/acces.yml`** — ni qu'un service `web-sso` en porte un, ni que le groupe qu'il nomme
existe. P29 garde les POSITIONS d'authentification ; personne ne garde les HABILITATIONS.
## 2026-09-05 (2) — Rouvrir les cles : la cle USB doit se suffire a elle-meme
**56 preuves.** Les cles sont sorties du poste. Restait la moitie qui compte : savoir les

View file

@ -231,7 +231,7 @@ aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' ' Graphe de production:'
@printf '%s\n' ' make inventaire'
@printf '%s\n' ' Graphe explicite:'
@printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml'
@printf '%s\n' ' make inventaire-graphe FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE)'
@printf '%s\n' ' Verifier les inventaires:'
@printf '%s\n' ' make inventaire-verifier'
@printf '%s\n' ' Lister les donnees brutes:'
@ -254,7 +254,7 @@ aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste)
@printf '%s\n' 'Variables frequentes'
@printf '%s\n' ' HOTE=web-frontal-01 GROUPE=serveur_debian GROUPES="serveur_debian serveur_durci"'
@printf '%s\n' ' VMID=95301 VLAN=15 ADRESSE_IP=10.0.2.31 PASSERELLE=10.0.2.1'
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
@printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INVENTAIRE) FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint
lint: ansible-runtime ## Passe ansible-lint sur tout le depot
@ -275,7 +275,7 @@ syntaxe-nettoyage: ansible-runtime ## Verifie la syntaxe du playbook de nettoyag
syntaxe-verification-hote: ansible-runtime ## Verifie la syntaxe du playbook de verification d'hote
ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) --syntax-check
syntaxe-groupes: ansible-runtime ## Verifie la syntaxe des 30 playbooks de groupe
syntaxe-groupes: ansible-runtime ## Verifie la syntaxe de TOUS les playbooks de groupe (playbooks/groupes/*.yml)
@for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \
done
@ -1311,7 +1311,7 @@ serveurs: ## Liste les serveurs declares au plan
serveurs-verifier: ## Valide le registre des serveurs
python3 scripts/serveurs.py verifier
serveurs-bootstrap: ## Amorce l'acces SSH aux serveurs neufs
serveurs-bootstrap: ## REPRISE : (re)constitue plan/serveurs.yml depuis un inventaire existant
python3 scripts/serveurs.py bootstrap
.PHONY: instancier instancier-appliquer

View file

@ -22,8 +22,10 @@ git clone <url-de-Set-OPS> Set-OPS && cd Set-OPS
```
## 2. Choisir un modèle et créer ton instance
Le dépôt public fournit **un modèle générique : `socle`** — le socle souverain minimal
(DNS interne, AC/PKI, edge TLS, relais courriel) sur lequel on ajoute des modules. Les
Le dépôt public fournit **un modèle générique : `socle`** — quatre VM, le minimum
souverain : PKI (`step-ca`), DNS interne (PowerDNS), edge TLS (nginx) et **magasin
courriel** (Dovecot). C'est un magasin de boîtes, pas un relais : le MTA (`serveur_postfix`)
s'ajoute ensuite, comme les autres modules. Les
modèles **assemblés** par offre (`identite`, `observabilite`, `forge`, `collaboration`,
`presence-web`, `integral`) sont un actif à part, dans le dépôt privé `Set-OPS-modeles`
(cf. [`exemples/modeles/README.md`](exemples/modeles/README.md)).
@ -35,6 +37,11 @@ ln -s ../mon-instance instance # le moteur la trouve via ce lien
*(Alternative au symlink : `export SETOPS_INSTANCE=../mon-instance`.)*
## 3. Renseigner ton instance (« tes couleurs »)
> Les chemins ci-dessous disent `production/` parce que **c'est le nom que porte le modèle
> que tu viens de copier**. Ce n'est pas un nom imposé : le moteur cherche `principal`
> d'abord, `production` ensuite. Si tu lis ailleurs `<inventaire>`, c'est cette place-là.
- `instance/inventories/production/group_vars/all/10-intrants.yml` → **`domaine_interne`** (ex. `monorg.internal`) ;
- `instance/plan/nomenclature.yml` → ton **supernet** (ex. `10.20.0.0/16`) ;
- `instance/plan/domaines.yml` → ton **domaine public** ;
@ -47,14 +54,26 @@ Tu peux aussi le faire dans le GUI plus tard (`make inventaire-ui`).
make config # renseigne API host/user/port, nœud, stockage, VMID du template...
```
Chaque paramètre demandé est expliqué dans [`docs/config-proxmox.md`](docs/config-proxmox.md).
Tous tes secrets (token API + `vault_*`) vont dans **une voûte unique** :
Tous tes secrets (token API + `vault_*`) vont dans **une voûte unique par instance** :
`instance/inventories/production/group_vars/all/vault.yml`, à partir du
gabarit [`exemples/vault.exemple.yml`](exemples/vault.exemple.yml), chiffrée avec
`ansible-vault`. Exporte ton mot de passe Vault, ex. :
`ansible-vault`.
**Une voûte, une clé** (depuis le 2026-08-28). Le mot de passe de ta voûte se dépose dans un
fichier dont le nom est **dérivé du dossier de ton instance**, en minuscules :
```bash
export ANSIBLE_VAULT_PASSWORD_FILE=~/.config/setops-vault-pass
# instance dans ../mon-instance -> clé dans ~/.config/setops-vault-mon-instance
install -m 600 /dev/null ~/.config/setops-vault-mon-instance
$EDITOR ~/.config/setops-vault-mon-instance # ta phrase de passe, une seule ligne
python3 scripts/voutes.py etat # vérifie que le moteur la trouve
```
Il n'y a **rien à exporter** : le `Makefile` construit `ANSIBLE_VAULT_IDENTITY_LIST` en
appelant `scripts/voutes.py`. Un seul `ANSIBLE_VAULT_PASSWORD_FILE` ne suffirait plus de
toute façon — créer une VM ouvre **deux** voûtes dans la même exécution : la tienne, et
celle de l'hébergeur qui détient le jeton Proxmox.
## 5. Construire le golden template Debian 13 (UNE seule fois)
Une grappe vierge n'a aucun template. Crée une VM **Debian 13 vanille**, rends-la
joignable par Ansible, puis :
@ -63,8 +82,10 @@ make preparer-modele # socle + durcissement + cloud-init + qemu-guest-age
make verifier-modele
make nettoyer-modele CONFIRMER=true
```
Convertis ensuite la VM en **template Proxmox** nommé `modele-debian13` (le nom de
clone source par défaut). Détails et procédure : `docs/vm-lifecycle.md` et
Convertis ensuite la VM en **template Proxmox**. Son nom doit être celui que `make config`
a enregistré comme *nom logique du modèle* — **`modeleSetOPS`** par défaut
(`proxmox_clone_source_nom`). Un nom qui ne correspond pas se solde par un clonage qui ne
trouve pas sa source. Détails et procédure : `docs/vm-lifecycle.md` et
`docs/procedure-template-debian13-proxmox.md`.
## 6. Générer ton inventaire depuis le plan
@ -102,8 +123,8 @@ Les déploiements de groupe ne ciblent **que** les hôtes actifs.
make inventaire-verifier # registres + inventaire + garde-fous
make verifier # + ansible-lint + --syntax-check
```
> Ces cibles chargent l'inventaire complet : exporte d'abord ton mot de passe Vault
> (étape 4, `ANSIBLE_VAULT_PASSWORD_FILE`), sinon Ansible s'arrête sur
> Ces cibles chargent l'inventaire complet : pose d'abord la clé de ta voûte
> (étape 4), sinon Ansible s'arrête sur
> « Attempting to decrypt but no vault secrets found ».
---

View file

@ -6,7 +6,7 @@ Le dépôt est le **moteur** (générique, partageable). Chaque déploiement ré
## Par où entrer — selon ce que tu viens faire
On n'arrive pas avec un *sujet*, on arrive avec une **situation**. Il y en a cinq :
On n'arrive pas avec un *sujet*, on arrive avec une **situation**. Il y en a six :
| Ta situation | Ta porte |
|---|---|
@ -36,18 +36,18 @@ Piliers de l'écosystème :
- **confiance** — autorité de certification interne (ACME) ;
- **nommage et adressage** — DNS interne et nomenclature dérivable ;
- **données** — bases relationnelles et cache, avec registre des connexions ;
- **communication** — relais courriel interne ;
- **communication** — service de courriel souverain (boîtes LDAP, SMTP, IMAP, antispam, DKIM), et le relais des notifications système ;
- **observabilité et supervision** — métriques, journaux, tableaux de bord, supervision active ;
- **applicatif** — services internes (forge, etc.) et couche web.
L'état voulu est **déclaratif et convergent** (appliqué par les groupes Ansible), **souverain** par conception, mais **piloté par l'opérateur** (pas d'auto-remédiation : la boucle n'est pas fermée). Une grande partie est aujourd'hui *définie et validée* avant d'être déployée sur des VM réelles ; le template Debian 13 reste la fondation, pas la finalité.
L'état voulu est **déclaratif et convergent** (appliqué par les groupes Ansible), **souverain** par conception, mais **piloté par l'opérateur** (pas d'auto-remédiation : la boucle n'est pas fermée). Ce n'est pas resté sur le papier : la flotte a été **rasée et remontée depuis zéro** le 2026-08-13, puis deux fois le 2026-09-02, sans échec. Le degré de maturité service par service est dans `docs/catalogue-services.md`. Le template Debian 13 reste la fondation, pas la finalité.
Cadre et règles d'autorité : voir `AGENTS.md` (section « Mission et identité »).
## Le plan : on édite, l'inventaire se génère
`Set-OPS` se pilote par un **plan**, pas par l'édition directe de l'inventaire.
`instance/inventories/production/hosts.yml` est **généré** depuis le plan — **ne pas l'éditer à la main**.
`instance/inventories/<inventaire>/hosts.yml` est **généré** depuis le plan — **ne pas l'éditer à la main**.
```
éditer le PLAN → make instancier (revoir le diff) → make instancier-appliquer → make deployer
@ -80,16 +80,18 @@ playbooks/modeles_vm/debian13_proxmox_nettoyer.yml
## Principe
Le template contient seulement le socle commun.
Le template contient seulement le socle commun. Les services spécialisés sont installés
ensuite sur les clones, par les playbooks de groupes : edge nginx, PostgreSQL et Redis,
identité (OpenLDAP, Keycloak), courriel (Postfix, Dovecot, rspamd), observabilité
(Prometheus, Loki, Grafana), supervision (Icinga), forge (Forgejo), collaboration
(Nextcloud, Collabora), plateforme web. La liste qui fait foi est
[`docs/catalogue-services.md`](docs/catalogue-services.md).
Les services spécialisés seront installés ensuite sur les clones :
- NGINX ;
- PostgreSQL ;
- MariaDB ;
- Docker/Podman ;
- monitoring complet ;
- applications métier.
> **Ni MariaDB, ni Docker, ni Podman.** Cette section les a listés jusqu'au 2026-09-06,
> par recopie de la liste de ce que le *template* ne doit pas contenir. Le dépôt n'a pas de
> rôle MariaDB, et **plus aucun rôle n'a besoin de Docker** depuis la réécriture native de
> `serveur_collabora` — qui en était la dernière exception. Le conteneur n'est pas un
> détail d'implémentation ici : c'est un choix fondateur (voir `roles/serveur_collabora/README.md`).
## Exploitation courante
@ -135,7 +137,7 @@ Planifier une VM passe désormais par le **plan**, pas par l'édition de l'inven
```bash
make instancier # génère + diff sémantique (que va-t-il changer ?)
make instancier-appliquer # régénère instance/inventories/production/hosts.yml
make instancier-appliquer # régénère instance/inventories/<inventaire>/hosts.yml
```
Les anciennes commandes `make hote-planifier` / `hote-ajouter` / `hote-groupes` éditaient l'inventaire **directement** ; elles sont **supplantées** par le plan (l'inventaire est généré, ne pas l'éditer à la main).

View file

@ -46,15 +46,19 @@ après avoir vérifié l'accès SSH par clé et les privilèges sudo du compte `
## Vérification
```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_verifier.yml
make verifier-modele
```
## Nettoyage final avant conversion
```bash
ansible-playbook -i instance/inventories/lab/hosts.yml playbooks/modeles_vm/debian13_proxmox_nettoyer.yml -e template_cleanup_confirm=true
make nettoyer-modele CONFIRMER=true
```
*(Ces deux blocs appelaient `ansible-playbook -i instance/inventories/lab/hosts.yml …`
jusqu'au 2026-09-06. Cet inventaire n'existe pas : le nom est résolu par le `Makefile`,
c'est précisément pourquoi les cibles `make` supplantent les commandes brutes.)*
Ensuite :
```bash

View file

@ -2,7 +2,28 @@
> **Pour qui :** l'**agent IA** qui reprend le dépôt — et le mainteneur qui relit ce qu'on lui dit.
## État du dépôt
> ## ⚠️ Document HISTORIQUE — relu le 2026-09-06
>
> Cette note était une **passation datée de juillet 2026**, écrite quand le chantier en
> cours était le gabarit Debian 13. Elle a continué d'être listée comme lecture de
> gouvernance longtemps après avoir cessé d'être vraie, et elle envoyait l'agent réparer un
> incident réglé, dans un rôle qui n'existe pas (`roles/ssh_durcissement/` — le rôle
> s'appelle `ssh_hardening`).
>
> **Ce qui tient toujours** : les fichiers de gouvernance à lire (ci-dessous), la règle de
> conduite finale (« lire l'existant, corriger petit, valider »), et la discipline des
> handlers — qui est désormais **prouvée** par **P04** et le harnais, plus seulement
> recommandée.
>
> **Ce qui ne tient plus** : « le chantier en cours est le template Debian 13 »,
> l'« incident récent » et son correctif. Les deux `handlers/main.yml` existent
> (`ssh_baseline`, `ssh_hardening`) depuis longtemps.
>
> **Où aller à la place** : `AGENTS.md` (autorité), `docs/carte-set-ops.md` (par où entrer
> pour modifier le moteur), `docs/catalogue-services.md` (ce qui est éprouvé et ce qui ne
> l'est pas).
## État du dépôt *(juillet 2026)*
`Set-OPS` est le moteur Ansible global d’exploitation d’écosystèmes numériques souverains.
@ -67,7 +88,12 @@ Il ne doit pas contenir :
---
## Incident récent à corriger
## Incident de juillet 2026 — RÉGLÉ, conservé pour la leçon
> Les deux `handlers/main.yml` existent. `roles/ssh_durcissement/` cité plus bas **n'a
> jamais existé sous ce nom** : le rôle s'appelle `ssh_hardening`. Ce qui reste utile ici,
> c'est la *classe* d'erreur — un `notify` sans handler local — et le fait qu'elle est
> maintenant tenue par une preuve plutôt que par la vigilance.
Le playbook :

View file

@ -3,7 +3,7 @@
> **Pour qui :** le **mainteneur** — le survol du modèle. À lire avant `plan-et-generation.md`.
Set-OPS définit et construit l'écosystème numérique souverain. Il se
pilote par un **plan** : l'inventaire Ansible (`instance/inventories/production/hosts.yml`)
pilote par un **plan** : l'inventaire Ansible (`instance/inventories/<inventaire>/hosts.yml`)
est **généré** depuis le plan, pas édité à la main.
- **Par où commencer + catalogue des mécanismes transverses : `docs/carte-set-ops.md`.**
@ -36,7 +36,7 @@ Les valeurs communes aux rôles doivent rester dans les defaults des rôles quan
Les groupes opérationnels de l'inventaire doivent correspondre à un playbook homonyme :
```text
instance/inventories/production/hosts.yml
instance/inventories/<inventaire>/hosts.yml
serveur_debian
playbooks/groupes/serveur_debian.yml
@ -60,9 +60,11 @@ Le cycle de vie des VM est documenté dans :
docs/vm-lifecycle.md
```
## Intégrations futures
## Intégrations transversales
Les intégrations transversales des VM sont documentées dans :
Elles ne sont plus « futures » : `client_pki`, `client_backup`, `client_metrique`,
`client_journal`, `client_smtp`, `client_resolveur` et `client_artefacts` sont déployés sur
la flotte. Elles sont documentées dans :
```text
docs/integrations-vm.md

View file

@ -26,18 +26,37 @@ pré-commit). Une preuve **SAUTÉE** (⚪) n'est pas un échec.
### Prérequis Vault
La preuve `P15` (inventaire Ansible complet, `ansible-inventory --list`) déchiffre le
`group_vars` de l'instance. Sans `ANSIBLE_VAULT_PASSWORD_FILE`, elle est **automatiquement
sautée** (⚪) avec la mention du prérequis — le reste du harnais reste vert, car les
validateurs Python lisent le plan et l'inventaire directement, sans secret. Pour l'inclure :
La preuve **`P16`** (inventaire Ansible complet, `ansible-inventory --list`) déchiffre le
`group_vars` de l'instance. Sans la clé de la voûte, elle est **automatiquement sautée** (⚪)
avec la mention du prérequis — le reste du harnais reste vert, car les validateurs Python
lisent le plan et l'inventaire directement, sans secret.
Pour l'inclure, il suffit que la clé de l'instance soit en place ; il n'y a **rien à
exporter** (une voûte, une clé — `scripts/voutes.py` la trouve par convention de nommage) :
```bash
export ANSIBLE_VAULT_PASSWORD_FILE=~/.config/setops-vault-pass
python3 scripts/voutes.py etat # la clé de cette instance est-elle là ?
make prouver
```
*(Ce paragraphe désignait `P15` et un `ANSIBLE_VAULT_PASSWORD_FILE` unique jusqu'au
2026-09-06 : ni l'un ni l'autre n'était juste.)*
## Ce que couvre `make prouver`
> **Ce tableau est un EXTRAIT, pas l'inventaire.** Il s'arrête à `P23` et le dépôt porte
> **57 preuves** (`P01`–`P57`, sans trou). La liste complète et à jour est produite par le
> harnais lui-même, jamais recopiée :
>
> ```bash
> make prouver # écrit docs/audit/preuve-<date>.md : chaque preuve, son verdict
> grep -oE '"id": "P[0-9]+", "titre": "[^"]+"' scripts/prouver.py # la source
> ```
>
> On garde l'extrait parce qu'il **explique** les premières preuves, celles qui fondent le
> reste. On ne le complète pas : un tableau de 57 lignes recopié à la main aurait dérivé
> avant d'être fini — c'est exactement ce qui est arrivé à celui-ci.
| # | Preuve | Ce qu'elle établit |
|---|---|---|
| P01 | Lint (`ansible-lint`) | 0 violation, profil `production`. |

View file

@ -10,7 +10,7 @@ Ce plan est le **pendant manuel** de `make prouver` : là où le harnais prouve
le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.
`protocole-operateur-independant.md`).
**89 gestes** sur **22 unités** · **19** en « casse-répare »
**90 gestes** sur **22 unités** · **19** en « casse-répare »
· **5** doublés d'un garde-fou machine (colonne *Preuve auto*).
> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une
@ -27,7 +27,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Observe le claim | Dans Keycloak (console admin) → Clients → grafana → *Client scopes* → *Evaluate* pour testmail : le jeton contient "roles": ["grafana-editor"]. C'est ② en vrai. | 👁 observe | — |
| 4 | Casse & répare | Retire grafana-editor de testmail (ou renomme-le en grafana-viewer), reconnecte : Explore disparaît. Remets-le : il revient. Tu *sens* que c'est le rôle, pas l'identité, qui ouvre la porte. | 🔨 casse-répare | — |
*Source : [Autorisation & RBAC](Autorisation-et-RBAC) · § À toi de jouer.*
*Source : [Autorisation & RBAC](../../wiki/Autorisation-et-RBAC.md) · § À toi de jouer.*
## Bases de données
@ -38,7 +38,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Suis une liaison | Ouvre plan/bases-donnees.yml : l'entrée keycloak (serveur, base, propriétaire, secret) est *exactement* ce que le rôle serveur_keycloak va lire pour se connecter. | 👁 observe | — |
| 4 | Casse & répare | Change le mot de passe d'un compte dans PostgreSQL (garde l'ancien !) sans mettre à jour la voûte : l'app ne se connecte plus. Restaure : ça repart. Tu *sens* que la connexion = *identité + secret cohérents des deux côtés*. | 🔨 casse-répare | — |
*Source : [Bases de données](Bases-de-données) · § À toi de jouer.*
*Source : [Bases de données](../../wiki/Bases-de-donn%C3%A9es.md) · § À toi de jouer.*
## Cache
@ -49,17 +49,17 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Vois la borne | : redis-cli -a … CONFIG GET maxmemory et … maxmemory-policy (LRU). | 👁 observe | — |
| 4 | Casse & répare | Interroge sans mot de passe : redis-cli GET essai:1 → NOAUTH (refusé). Puis vide le cache (FLUSHALL) : rien ne casse dans l'écosystème — la vraie donnée est en base. Tu *sens* qu'un cache est jetable. | 🔨 casse-répare | — |
*Source : [Cache](Cache) · § À toi de jouer.*
*Source : [Cache](../../wiki/Cache.md) · § À toi de jouer.*
## Courriel (SMTP / IMAP)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Envoie via la soumission `:587` | (client authentifié) : « commande » 235 Authentication successful puis 250 queued = ① en action. | 👁 observe | — |
| 1 | Envoie via la soumission `:587` | (client authentifié) : « commande » *(MDP_ESSAI se saisit à la main — read -rs MDP_ESSAI. Un mot de passe écrit ici partirait dans l'historique du shell et dans le dépôt public : ce document en portait un en clair jusqu'au 2026-09-06.)*… | 👁 observe | — |
| 2 | Lis la boîte | (le MDA) : doveadm search -u testmail mailbox INBOX all \| wc -l sur infra-mail-01. | 👁 observe | — |
| 3 | Casse & répare | Coupe l'annuaire (arrête OpenLDAP), renvoie un courriel : Postfix **rejette le destinataire** (il ne peut plus valider en LDAP). Rallume OpenLDAP : ça repart. Tu *sens* la dépendance requise MTA → annuaire. | 🔨 casse-répare | — |
*Source : [Courriel (SMTP / IMAP)](Courriel) · § À toi de jouer.*
*Source : [Courriel (SMTP / IMAP)](../../wiki/Courriel.md) · § À toi de jouer.*
## DNS & résolution de noms
@ -68,9 +68,10 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 1 | Le plancher, sans DNS | Sur un nœud : « commande » | 👁 observe | — |
| 2 | Interroge l'autoritatif | Demande à PowerDNS directement : « commande » | 👁 observe | — |
| 3 | Vois les couches | Compare getent hosts (plancher) et dig (DNS) : deux chemins, même IP. | 👁 observe | — |
| 4 | Casse & répare | Sur un nœud sans client_resolveur, vide /etc/hosts de ses entrées chezlepro (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne échoue. Restaure /etc/hosts : ça remarche sans DNS. Tu viens de *sentir* pourquoi le pla… | 🔨 casse-répare | — |
| 4 | Casse & répare | Vide /etc/hosts de ses entrées chezlepro (garde une sauvegarde !) et pointe /etc/resolv.conf ailleurs : la résolution interne échoue. Restaure /etc/hosts seul : ça remarche sans DNS. Tu viens de *sentir* pourquoi le plancher est le filet… | 🔨 casse-répare | — |
| 5 | Le piège du récursif | Demande un nom qui n'existe pas sous internal., puis redemande un nom qui existe. Si le résolveur répond NXDOMAIN aux deux, tu viens de reproduire la panne de deux jours du 2026-09-02 : la racine étant signée, elle *prouve* que internal.… | 👁 observe | — |
*Source : [DNS & résolution de noms](DNS-et-résolution) · § À toi de jouer.*
*Source : [DNS & résolution de noms](../../wiki/DNS-et-r%C3%A9solution.md) · § À toi de jouer.*
## Filiation, signatures et témoins
@ -81,18 +82,18 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | — | make genome-inscrire, puis ouvre parente.yml. Dans un an, qu'est-ce que ce fichier te dira que ta mémoire ne dira plus ? | 👁 observe | — |
| 4 | — | Demande-toi où sont les témoins aujourd'hui. Combien de copies vivantes du moteur existent, sur combien de machines distinctes ? C'est la vraie mesure de la résistance de la lignée — pas la longueur des clés. Voir aussi : Multi-instance… | 👁 observe | — |
*Source : [Filiation, signatures et témoins](Filiation-signatures-et-témoins) · § À toi de jouer.*
*Source : [Filiation, signatures et témoins](../../wiki/Filiation-signatures-et-t%C3%A9moins.md) · § À toi de jouer.*
## Identité & SSO
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Vis le SSO | Ouvre https://grafana.lab.chezlepro.internal → « *Se connecter avec Chezlepro* » → testmail. Puis ouvre Forgejo, puis Icinga : tu n'es reconnecté nulle part. | 👁 observe | — |
| 1 | Vis le SSO | Ouvre https://grafana.chezlepro.internal → « *Se connecter avec Chezlepro* » → testmail. Puis ouvre Forgejo, puis Icinga : tu n'es reconnecté nulle part. | 👁 observe | — |
| 2 | Observe le flux | Rouvre Grafana en navigation privée, ouvre les outils dév (F12 → Réseau) : repère la redirection vers Keycloak, puis le retour avec un code=. C'est ① en action. | 👁 observe | — |
| 3 | Interroge l'annuaire (la source de vérité) | Sur un nœud avec ldap-utils : « commande » Tu vois l'entrée que Keycloak fédère — il ne l'a pas recopiée. | 👁 observe | — |
| 4 | Casse & répare (la fédération) | Dans la console admin Keycloak → *User Federation* → désactive le fournisseur LDAP. Reconnecte-toi : échec (l'IdP ne voit plus l'annuaire). Réactive : ça remarche. Tu viens de *sentir* la dépendance requise entre l'IdP et l'annuaire. | 🔨 casse-répare | — |
*Source : [Identité & SSO](Identité-et-SSO) · § À toi de jouer.*
*Source : [Identité & SSO](../../wiki/Identit%C3%A9-et-SSO.md) · § À toi de jouer.*
## Infra as Code & idempotence
@ -103,7 +104,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | La réversibilité | git diff / git checkout sur le plan : l'état est du code, donc annulable. | 👁 observe | — |
| 4 | Casse & répare | Modifie à la main un fichier géré par un rôle (ex. un .conf), puis redéploie : Ansible rétablit l'état voulu (le code gagne sur la dérive manuelle). Tu *sens* que la source de vérité, c'est le code. | 🔨 casse-répare | — |
*Source : [Infra as Code & idempotence](Infra-as-Code-et-idempotence) · § À toi de jouer.*
*Source : [Infra as Code & idempotence](../../wiki/Infra-as-Code-et-idempotence.md) · § À toi de jouer.*
## La preuve — prouver, pas affirmer
@ -115,7 +116,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 4 | Lis le registre | Ouvre docs/audit/affirmations.md : trouve une affirmation ⚪ (*non prouvable localement*) — vois comment elle est assumée comme intention, jamais présentée comme prouvée. | 👁 observe | — |
| 5 | Comprends la valeur | Demande-toi : *quelle promesse est-ce que je fais sans preuve ?* C'est exactement ce que ce registre force à regarder en face. | 👁 observe | — |
*Source : [La preuve — prouver, pas affirmer](La-preuve) · § À toi de jouer.*
*Source : [La preuve — prouver, pas affirmer](../../wiki/La-preuve.md) · § À toi de jouer.*
## Le GUI (console d'exploitation)
@ -128,7 +129,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 5 | Sens le garde-fou | Essaie de sauvegarder un plan incohérent (ex. une base dont le consommateur n'existe pas) : la console refuse avec une raison. L'invalide ne passe pas. | 👁 observe | — |
| 6 | Casse & répare | Édite hosts.yml à la main, reviens dans la GUI, « Appliquer le plan » : ta modification est écrasée par le plan. La source de vérité, c'est le plan — pas l'inventaire. | 🔨 casse-répare | — |
*Source : [Le GUI (console d'exploitation)](Le-GUI-console-d-exploitation) · § À toi de jouer.*
*Source : [Le GUI (console d'exploitation)](../../wiki/Le-GUI-console-d-exploitation.md) · § À toi de jouer.*
## Le plan & l'adressage dérivé
@ -140,7 +141,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 4 | Casse & répare | Édite hosts.yml à la main (change une IP). Relance make instancier : il signale l'écart. Ré-applique : le plan écrase ta modification. Tu *sens* que hosts.yml n'est pas la vérité — le plan l'est. | 🔨 casse-répare | — |
| 5 | Éprouve le garde-fou | Ajoute une ligne supernet: 10.99.0.0/16 dans une nomenclature, puis make prouver : P20 échoue (« adressage stocké »). Retire-la : vert. La règle se *prouve*. | 👁 observe | **P20** |
*Source : [Le plan & l'adressage dérivé](Le-plan-et-l-adressage-dérivé) · § À toi de jouer.*
*Source : [Le plan & l'adressage dérivé](../../wiki/Le-plan-et-l-adressage-d%C3%A9riv%C3%A9.md) · § À toi de jouer.*
## Le réseau des tenants — du câble au VRF
@ -151,7 +152,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Trouve la sortie d'un tenant | : dans make devis-sdn, repère la strophe FRR et l'adresse du prochain saut. À quel équipement appartient-elle ? | 👁 observe | — |
| 4 | Change `index` dans un modèle | (jamais en production) et régénère : combien de valeurs ont bougé ? C'est la mesure exacte de ce que la dérivation t'épargne. Pour aller plus loin : docs/sdn-evpn.md (référence technique), Le plan & l'adressage dérivé, Multi-instance & f… | 👁 observe | — |
*Source : [Le réseau des tenants — du câble au VRF](Le-réseau-des-tenants) · § À toi de jouer.*
*Source : [Le réseau des tenants — du câble au VRF](../../wiki/Le-r%C3%A9seau-des-tenants.md) · § À toi de jouer.*
## Liaisons (bindings)
@ -159,10 +160,10 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
|---|---|---|---|---|
| 1 | Lis une liaison | Ouvre instance/plan/applications.yml : une app avec expose: (liaison *app→domaine*), et serveurs.yml : un nœud avec integrations: (liaison *nœud→service*). | 👁 observe | — |
| 2 | Vois-la se résoudre | Après make instancier, regarde l'inventaire généré : la cible est devenue une valeur concrète (FQDN, groupe) — le moteur a câblé. | 👁 observe | — |
| 3 | Requise vs optionnelle | Compare : retirer client_journal d'un nœud → aucun problème (optionnelle). Déclarer une base sans serveur → make instancier/la validation échoue (requise). Tu *sens* la différence de modalité. | 👁 observe | — |
| 3 | Requise, optionnelle, universelle | Compare les trois : retirer client_backup d'un nœud sans état → aucun problème (optionnelle). Déclarer une base sans serveur → make instancier échoue (requise). Essayer de recopier client_metrique dans serveurs.yml → le plan refuse (univ… | 👁 observe | — |
| 4 | Casse & répare | Casse une liaison requise (ex. réfère une base à un serveur inexistant), relance l'instanciation : échec clair *avant* tout déploiement. Corrige : ça passe. Le moteur attrape le câblage manquant à ta place. | 🔨 casse-répare | — |
*Source : [Liaisons (bindings)](Liaisons-bindings) · § À toi de jouer.*
*Source : [Liaisons (bindings)](../../wiki/Liaisons-bindings.md) · § À toi de jouer.*
## Multi-instance & fédération
@ -175,7 +176,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 5 | Casse & répare | Donne à deux instances fédérées le même index (édite une nomenclature), make instances : la bannière de collision s'allume ; make prouver : P21 échoue. Corrige l'index : tout redevient vert. | 🔨 casse-répare | **P21** |
| 6 | (Avancé) Promeus un produit | Une instance qui *tourne et se prouve* peut devenir un modèle vendable : make model-creer MODE=instance SOURCE=OPS-… NOM=… — elle est généralisée (identité → exemple.*, secrets retirés) et validée. | 👁 observe | — |
*Source : [Multi-instance & fédération](Multi-instance-et-fédération) · § À toi de jouer.*
*Source : [Multi-instance & fédération](../../wiki/Multi-instance-et-f%C3%A9d%C3%A9ration.md) · § À toi de jouer.*
## Métriques & journaux
@ -183,10 +184,10 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
|---|---|---|---|---|
| 1 | Interroge les métriques | (sur obs-01) — combien de nœuds scrapés, tous UP ? « commande » | 👁 observe | — |
| 2 | Vois les journaux | : curl -s http://localhost:3100/loki/api/v1/label/host/values → les nœuds qui expédient leurs logs. | 👁 observe | — |
| 3 | Ouvre Grafana | (https://grafana.lab.chezlepro.internal) — métriques *et* logs au même endroit. | 👁 observe | — |
| 3 | Ouvre Grafana | (https://grafana.chezlepro.internal) — métriques *et* logs au même endroit. | 👁 observe | — |
| 4 | Casse & répare | Arrête prometheus-node-exporter sur un nœud : dans Prometheus, sa cible passe up=0 (DOWN). Redémarre : elle repasse UP. Tu *sens* que c'est l'agent qui nourrit le serveur (modèle pull). | 🔨 casse-répare | — |
*Source : [Métriques & journaux](Métriques-et-journaux) · § À toi de jouer.*
*Source : [Métriques & journaux](../../wiki/M%C3%A9triques-et-journaux.md) · § À toi de jouer.*
## PKI & confiance
@ -197,38 +198,38 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Vois-le servir en vrai | Le LDAPS d'OpenLDAP utilise ce certificat : « commande » 0 (ok) = confiance vérifiée. | 👁 observe | — |
| 4 | Casse & répare | Retire la racine du magasin système, refais un curl HTTPS interne : avertissement de certificat (plus de confiance). Réinstalle la racine : ça remarche. Tu viens de *sentir* pourquoi « faire confiance à la racine » est la clé de voûte. | 🔨 casse-répare | — |
*Source : [PKI & confiance](PKI-et-confiance) · § À toi de jouer.*
*Source : [PKI & confiance](../../wiki/PKI-et-confiance.md) · § À toi de jouer.*
## Reverse-proxy & TLS
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Route par nom | Deux noms, un seul edge (192.168.15.21) : « commande » Change grafana en forge : même IP, backend différent. C'est le routage par SNI. | 👁 observe | — |
| 2 | Vois la terminaison TLS | Le certificat présenté est celui de l'edge (avec les SAN des exposés) : openssl s_client -connect 192.168.15.21:443 -servername grafana.lab… \| openssl x509 -noout -text \| grep -A1 'Subject Alternative'. | 👁 observe | — |
| 1 | Route par nom | Deux noms, un seul edge (10.17.16.11) : « commande » Change grafana en forge : même IP, backend différent. C'est le routage par SNI. | 👁 observe | — |
| 2 | Vois la terminaison TLS | Le certificat présenté est celui de l'edge (avec les SAN des exposés) : openssl s_client -connect 10.17.16.11:443 -servername grafana.chezlepro.internal \| openssl x509 -noout -text \| grep -A1 'Subject Alternative'. | 👁 observe | — |
| 3 | Casse & répare | Arrête le backend (ex. systemctl stop grafana-server sur obs-01) et rouvre Grafana : l'edge répond 502 Bad Gateway (le proxy est là, le service non). Redémarre : ça remarche. Tu distingues le proxy de ce qu'il sert. | 🔨 casse-répare | — |
*Source : [Reverse-proxy & TLS](Reverse-proxy-et-TLS) · § À toi de jouer.*
*Source : [Reverse-proxy & TLS](../../wiki/Reverse-proxy-et-TLS.md) · § À toi de jouer.*
## Sauvegardes (3-2-1)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Lance une sauvegarde | Sur un nœud avec client_backup : « commande » | 👁 observe | — |
| 2 | Liste les instantanés | (le dépôt vit hors-nœud) : « commande » | 👁 observe | — |
| 2 | Liste les instantanés | (le dépôt vit hors-nœud, et hors de l'écosystème) : « commande » *Le même dépôt est interrogé chaque nuit par setops-verifier-mon-depot.sh, qui rapporte à Icinga : c'est le nœud, seul détenteur de la clé, qui juge.* | 👁 observe | — |
| 3 | Restaure — le vrai test | Restaure dans un dossier temporaire et compare : « commande » *(sur infra-pki-01 ; ailleurs, compare le dump correspondant.)* | 👁 observe | — |
| 4 | Casse & répare | Supprime un fichier de donnée (une copie de test !), restaure-le depuis l'instantané, vérifie qu'il est identique. Tu viens de *sentir* que la valeur d'une sauvegarde est la restauration, pas la sauvegarde. | 🔨 casse-répare | — |
*Source : [Sauvegardes (3-2-1)](Sauvegardes) · § À toi de jouer.*
*Source : [Sauvegardes (3-2-1)](../../wiki/Sauvegardes.md) · § À toi de jouer.*
## Supervision & impact
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Ouvre Icinga Web 2 | (https://icinga.lab.chezlepro.internal, via le SSO) : la liste des hôtes et services supervisés, avec leur état (vert/jaune/rouge). | 👁 observe | — |
| 1 | Ouvre Icinga Web 2 | (https://icinga.chezlepro.internal, via le SSO) : la liste des hôtes et services supervisés, avec leur état (vert/jaune/rouge). | 👁 observe | — |
| 2 | Vois l'impact | Menu *Business Processes* → « Supervision Chezlepro » : un processus qui agrège des checks (load, procs, ping…) en un état roulé. C'est l'impact, pas une case. | 👁 observe | — |
| 3 | Casse & répare | Provoque l'échec d'un check (ex. arrête un service surveillé) : l'état passe CRITICAL, et le processus BPM qui en dépend rougit (l'impact remonte). Répare : tout reverdit. Tu *sens* la différence entre *mesurer* et *superviser/alerter*. | 🔨 casse-répare | — |
*Source : [Supervision & impact](Supervision-et-impact) · § À toi de jouer.*
*Source : [Supervision & impact](../../wiki/Supervision-et-impact.md) · § À toi de jouer.*
## Sécurité & durcissement
@ -238,7 +239,7 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 2 | Moindre privilège | : PermitRootLogin est à no, l'accès se fait par le compte ansible + clé SSH. Vérifie : sshd -T \| grep -E 'permitrootlogin\|passwordauthentication'. | 👁 observe | — |
| 3 | Casse & répare (avec prudence, en lab) | Assouplis un réglage sysctl, observe, puis remets-le. Tu *sens* que chaque ligne de durcissement ferme une porte précise. | 🔨 casse-répare | — |
*Source : [Sécurité & durcissement](Sécurité-et-durcissement) · § À toi de jouer.*
*Source : [Sécurité & durcissement](../../wiki/S%C3%A9curit%C3%A9-et-durcissement.md) · § À toi de jouer.*
## Virtualisation & clonage
@ -249,14 +250,14 @@ le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (c
| 3 | Sépare les deux couches | cloud-init a posé *l'identité* ; tout le reste (paquets, services, durcissement) vient d'Ansible. Le template, lui, ne contient aucune donnée de clone. | 👁 observe | — |
| 4 | Casse & répare (mentalement + lab) | Supprime un nœud non critique et reclone-le depuis le template, puis redéploie : il revient à l'identique. Tu *sens* que la machine est reconstructible. | 🔨 casse-répare | — |
*Source : [Virtualisation & clonage](Virtualisation-et-clonage) · § À toi de jouer.*
*Source : [Virtualisation & clonage](../../wiki/Virtualisation-et-clonage.md) · § À toi de jouer.*
## Vérifier le déployé — quand la preuve statique ne suffit plus
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | — | Lance les cinq devis sur ta flotte. Note le temps que ça prend : quelques minutes pour ce qui demandait une journée d'enquête à la main. | 👁 observe | — |
| 1 | — | Lance les dix devis sur ta flotte — les cinq de service, puis les cinq d'infrastructure. Note le temps que ça prend : quelques minutes pour ce qui demandait une journée d'enquête à la main. | 👁 observe | — |
| 2 | Casse quelque chose exprès | — arrête un service publié, change un port — et relance le devis concerné. S'il ne dit rien, c'est *lui* qu'il faut réparer, pas le service. | 👁 observe | — |
| 3 | — | Cherche, dans ton propre outillage, une vérification qui n'a jamais échoué. Demande-toi si c'est parce que tout va bien, ou parce qu'elle ne regarde rien. \| Terme \| Ce que tu retiens \| \| \| \| \| preuve statique \| lit le code ; rapide, univ… | 👁 observe | — |
*Source : [Vérifier le déployé — quand la preuve statique ne suffit plus](Vérifier-le-déployé) · § À toi de jouer.*
*Source : [Vérifier le déployé — quand la preuve statique ne suffit plus](../../wiki/V%C3%A9rifier-le-d%C3%A9ploy%C3%A9.md) · § À toi de jouer.*

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ❌ NON CONFORME (55 OK · 1 echec · 0 saute)
- **Verdict** : ❌ NON CONFORME (56 OK · 1 echec · 0 saute)
## Preuves
@ -43,7 +43,7 @@
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, 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 | 56 scripts expliques et atteignables, 111 cibles make documentees, 65 roles avec README. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 57 scripts expliques et atteignables, 114 cibles make documentees, 65 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (32 genere(s) exempte(s)). |
@ -53,15 +53,14 @@
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 39 role(s) serveur/client tous nommes, 40 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 : 52 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 53 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 |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ❌ ECHEC | La carte d'orientation ne dit plus vrai :
- « pieces d'audit » : la carte annonce 34, le depot en compte 35 |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (117 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 169 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
@ -70,6 +69,9 @@
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ❌ ECHEC | Des comptes ecrits en prose ne disent plus vrai :
- docs/autorisation.md:13 annonce 29 roles, le depot en compte 65
- wiki/Vérifier-le-déployé.md:30 annonce |
## Couverture des affirmations ✅ du registre

View file

@ -0,0 +1,93 @@
# Preuve de conformite — Set-OPS — 2026-09-06
> 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** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (56 OK · 0 echec · 1 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| 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 : 40 groupes classes, aucun cycle, aucune arete en arriere. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 99 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 | serveur_resolveur_site |
| 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 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
| 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 : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
| 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 : 13 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 80 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 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, 34 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, 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 | 57 scripts expliques et atteignables, 114 cibles make documentees, 65 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (131 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 43 document(s) declarent leur lecteur (33 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) de l'ecosysteme et 3 du site 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 : 39 role(s) serveur/client tous nommes, 40 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 : 53 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 |
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 104 regle(s) du site. |
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 87 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (117 lignes). |
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 169 regles `pass`), tous non consignes et tous motives. |
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 14. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (57 preuves, 65 roles, 40 groupes). |
## 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-09-06._

View file

@ -94,7 +94,7 @@ Binaires, vérifiables, définis **avant** le début. On ne les renégocie pas e
| # | Critère | Vérification |
|---|---|---|
| **R1** | Instance créée depuis `socle`, plan renseigné à ses valeurs | `make inventaire-verifier` → rc=0 |
| **R2** | Golden template Debian 13 construit et converti en template Proxmox | `make verifier-modele` OK ; template `modele-debian13` visible dans Proxmox |
| **R2** | Golden template Debian 13 construit et converti en template Proxmox | `make verifier-modele` OK ; template visible dans Proxmox **sous le nom que `make config` a enregistré** (`proxmox_clone_source_nom`, `modeleSetOPS` par défaut) |
| **R3** | Au moins une VM créée depuis le plan | `make creer-vm HOTE=…` OK ; VM jointe par Ansible (`ansible -m ping`) |
| **R4** | Cet hôte déployé selon ses groupes | `make deployer HOTE=…` → rc=0 |
| **R5** | Validation globale du dépôt passée par l'opérateur | `make verifier` → rc=0 |

View file

@ -98,13 +98,19 @@ Une directive qu'aucune garde ne vérifie finit par ne plus être vraie. Chaque
| `socle-identite` | **est** la chaîne d'identité, ne peut pas se déléguer à elle-même | keycloak, openldap |
| `ldap-direct` | protocole non-OIDC lié à LDAP | dovecot, postfix |
| `interne-sans-auth` | interface joignable **dans** le tenant sans authentification, non exposée | prometheus, loki |
| `sans-auth-humaine` | aucun point d'authentification humaine | 12 |
| `sans-auth-humaine` | aucun point d'authentification humaine | 21 |
La preuve refuse **l'oubli et le mensonge** : un rôle sans déclaration, une portée
inventée, un `web-sso` sans accès de secours ou sans posture de formulaire, un `web-sso
natif` sans réglage `<rôle>_connexion_locale` **défini dans `defaults`**, et une
déclaration que le code contredit.
**Depuis le 2026-09-06, elle refuse aussi que ce tableau mente.** Les chiffres et les listes
de la troisième colonne sont confrontés aux déclarations réelles. Ils avaient cessé d'être
vrais sans que rien ne le signale : la ligne `sans-auth-humaine` annonçait 12 rôles, il y en
avait 21 — la preuve lisait les déclarations depuis le début, mais ne regardait pas ce que
le document en disait.
> **Les indices doivent être nommés.** Une première version cherchait les mots « ldap » et
> « oidc » dans le rôle. Le mot *LDAP*, présent dans un commentaire de `serveur_grafana`,
> suffisait alors à valider une déclaration `ldap-direct` mensongère. La preuve exige

View file

@ -16,6 +16,12 @@ Toute personne qui s'authentifie obtient donc le défaut du service — tout, ou
aucun humain n'a de compte : `ou=people` est vide aussi. L'écosystème est déployé et
personne ne peut y entrer autrement que par les comptes de secours en voûte.
> **Révisé le 2026-09-05 — le constat ci-dessus est daté, et il a bougé.** `ou=groups`
> n'est plus un conteneur que personne ne lit : `amorcage_acces`, `serveur_keycloak` et
> `serveur_icingaweb2` s'en servent. La mesure du 2026-08-07 est conservée telle quelle
> parce qu'elle explique *pourquoi* ce document existe — mais elle ne décrit plus l'état
> du dépôt. Le §2 et la suite, eux, restent la doctrine en vigueur.
## 2. Deux régimes, et la frontière entre eux
C'est la décision principale, et elle **contredit délibérément la doctrine du dépôt**.
@ -170,10 +176,22 @@ ansible-vault view instance/inventories/principal/group_vars/all/vault.yml \
opération sauf le changement lui-même (§3). C'est le premier geste de la reprise, et il
n'est pas optionnel.
> Le mot de passe de la voûte est lu depuis `ANSIBLE_VAULT_PASSWORD_FILE`
> (`~/.config/setops-vault-pass` par défaut). **Sans ce fichier, rien de ce qui suit n'est
> possible** — c'est la clé de voûte au sens propre, et la première chose à sauvegarder
> hors de la machine.
> **La clé de la voûte — une voûte, une clé (depuis le 2026-08-28).** Ce paragraphe a
> longtemps désigné un `ANSIBLE_VAULT_PASSWORD_FILE` unique, `~/.config/setops-vault-pass`.
> Ce n'est plus le mécanisme : un seul mot de passe ouvrait alors *toutes* les voûtes de la
> flotte, celle de l'hébergeur comprise — compromettre le plus petit locataire, c'était
> obtenir les secrets de tous. Chaque dépôt a désormais **sa** clé, nommée d'après lui :
> `~/.config/setops-vault-<dépôt-en-minuscules>`. Le `Makefile` les rassemble tout seul dans
> `ANSIBLE_VAULT_IDENTITY_LIST` (`scripts/voutes.py`), et Ansible les essaie toutes — il n'y
> a **rien à exporter**. Pour voir ce que cette machine peut ouvrir :
>
> ```
> python3 scripts/voutes.py etat
> ```
>
> **Sans la clé de ton écosystème, rien de ce qui suit n'est possible** — c'est la clé de
> voûte au sens propre, et la première chose à sortir de la machine
> (`make cles-exporter`, cf. [`sortir-les-cles-du-poste.md`](sortir-les-cles-du-poste.md)).
### 6.2 Deux consoles Keycloak, et la racine mène à la mauvaise
@ -375,5 +393,21 @@ quel dossier Nextcloud : ça se règle dans le service, à partir du groupe qu'i
Set-OPS accorde l'entrée et le niveau ; il ne réimplémente pas le modèle de chaque
application.
**Rien n'est construit.** Ce document fixe la direction ; le rôle d'amorçage, les
`meta/acces.yml` et la preuve restent à écrire.
**Ce qui est construit, et ce qui ne l'est pas — mesuré le 2026-09-06.** Ce document s'est
terminé jusqu'à cette date sur « **Rien n'est construit** [...] le rôle d'amorçage, les
`meta/acces.yml` et la preuve restent à écrire ». C'était devenu faux au point de contredire
le §3 du même document, qui rapporte des mesures **datées du 2026-08-11** prises sur le rôle
en fonctionnement. L'état réel :
| | État |
|---|---|
| Le rôle d'amorçage | **`roles/amorcage_acces/`** — écrit, déployé, et c'est lui qui crée l'unique compte `sysadmin` du §3 |
| Les `meta/acces.yml` | **écrits pour les cinq services `web-sso`** : `serveur_forgejo`, `serveur_grafana`, `serveur_icingaweb2`, `serveur_keycloak`, `serveur_nextcloud` |
| La preuve | **toujours à écrire** — c'est la seule ligne d'origine qui tient |
La lacune restante mérite d'être nommée pour ce qu'elle est. Le §5 de
[`authentification.md`](authentification.md) le formule ainsi : *une directive qu'aucune
garde ne vérifie finit par ne plus être vraie*. `meta/acces.yml` est exactement dans ce cas
— rien ne vérifie qu'un service `web-sso` en porte un, ni que le groupe qu'il nomme existe
dans l'annuaire. **P29** garde les *positions* d'authentification ; personne ne garde encore
les *habilitations*.

View file

@ -2,8 +2,14 @@
> **Pour qui :** le **mainteneur** qui ajoute une relation service → service.
> Note de conception, 2026-07-02. Décision d'architecture à valider avant implémentation.
> Direction retenue : **liens déclarés côté application (le consommateur déclare ses besoins)**.
> Note de conception, 2026-07-02. Direction retenue : **liens déclarés côté application (le
> consommateur déclare ses besoins)**.
>
> **Statut, revu le 2026-09-06 : ce n'est plus « à valider avant implémentation ».** Le
> mécanisme est construit et déployé — le §9 en donne le phasage, et les §1 et §2 ci-dessous
> décrivent l'**état de départ de juillet**, conservé parce qu'il explique *pourquoi* le
> modèle est ce qu'il est. Ils ne décrivent pas le dépôt d'aujourd'hui : voir l'encadré au
> §2. Ce qui reste ouvert est nommé au §7 (le graphe des liens) et au §10.
## 1. Problème
@ -19,9 +25,9 @@ Conséquence : **la topologie n'est pas déclarative**. Déplacer Dovecot sur un
éditer des variables à la main. **Cela échoue à l'épreuve de la portabilité multi-tenant** —
pourtant au cœur de la mission.
## 2. Ce qui existe déjà (partiel)
## 2. Ce qui existait déjà, en juillet 2026 (état de départ)
Le concept est à moitié né, mais éclaté et non câblé :
Le concept était à moitié né, éclaté et non câblé :
- **Bases** (`plan/bases-donnees.yml`) : `consommateur` + `portee` (groupe|hote|application) +
`secret` (Vault) + `usage` + `proprietaire`. Un vrai binding base → consommateur, mais vide et
@ -31,6 +37,12 @@ Le concept est à moitié né, mais éclaté et non câblé :
- **Applications** (`plan/applications.yml`) : seulement `groupe` + `hote`. Aucun lien app → app.
- **Rôles** : portent déjà une méta auto-descriptive (`meta/empreinte.yml`).
> **Aucune de ces quatre lignes n'est encore vraie (mesuré le 2026-09-06).** Le registre des
> bases porte **4 bases** et un serveur, tous consommés ; **6 applications** déclarent un
> `expose` ; `postfix` déclare **3 liens** app→app. La section est gardée au passé parce
> qu'elle est le *problème* que le reste du document résout — la lire au présent donnerait
> l'impression que rien n'a bougé.
## 3. Modèle proposé
**Un binding est une arête typée et dirigée**, d'un **consommateur** (une application) vers une
@ -132,7 +144,7 @@ Pour chaque application portant des `liens`, pour chaque lien `{vers, role}` :
applications:
forgejo:
groupe: serveur_forgejo
hote: git-01
hote: forge-01
liens:
- vers: git.chezlepro.ca # domaine public (domaines.yml)
role: exposition
@ -149,9 +161,12 @@ le lien app ↔ domaine relie enfin app + domaine + edge, aujourd'hui séparés.
> app ». Mais le binding app→base **existe déjà et fonctionne**, par un mécanisme *différent* de
> celui des liens app→app. Il **ne faut pas le dupliquer** dans `instancier`.
Réalité : quatre rôles (`serveur_postgresql`, `serveur_forgejo`, `serveur_keycloak`,
`serveur_icinga`) résolvent leur base **au déploiement, dans le rôle**, depuis le registre
`plan/bases-donnees.yml` :
Réalité : les rôles **consommateurs** résolvent leur base **au déploiement, dans le rôle**,
depuis le registre `plan/bases-donnees.yml`. Ils sont **cinq** aujourd'hui — `serveur_forgejo`,
`serveur_icinga`, `serveur_icingaweb2`, `serveur_keycloak`, `serveur_nextcloud` — et passent
tous par `roles/resoudre_base` (cf. le paragraphe qui suit). *`serveur_postgresql` figurait
dans cette liste jusqu'au 2026-09-06 : il n'y a pas sa place. Il lit bien le registre, mais
pour **créer** les bases et leurs comptes — il est le serveur, pas un consommateur.* :
```yaml
- include_vars: bases-donnees.yml # charge le registre
@ -175,9 +190,12 @@ annuaire, milter…) résolus par instancier. Les liens **app→base** restent *
dans le rôle. Deux directions, **assumées**, chacune selon la nature de la cible et la sensibilité
du secret. La §3.1 est donc raffinée, pas contredite.
Reste comme valeur réelle (non bloquant) : **factoriser** le bloc de résolution copié-collé dans
les 4 rôles en un include partagé (ex. `roles/_resoudre_base/`). À faire **avec l'épreuve de
Keycloak** (qui utilise ce mécanisme), pour valider le DRY en le déployant. Cf. §9.
~~Reste comme valeur réelle (non bloquant) : **factoriser** le bloc de résolution copié-collé
dans les 4 rôles en un include partagé.~~ **Fait.** Le rôle utilitaire
[`roles/resoudre_base/`](../roles/resoudre_base/README.md) porte la résolution, et **cinq**
rôles consommateurs l'incluent (`serveur_forgejo`, `serveur_icinga`, `serveur_icingaweb2`,
`serveur_keycloak`, `serveur_nextcloud`). Le DRY a bien été validé **en le déployant**, comme
prévu — l'épreuve de Keycloak. Cf. §9.
## 6. Règles de validation
@ -200,27 +218,39 @@ Keycloak** (qui utilise ce mécanisme), pour valider le DRY en le déployant. Cf
d'inventaire), très parlante pour la **démo de portabilité** (déplacer un nœud, voir les
arêtes suivre).
## 8. Preuve de migration (les 3 liens mail)
## 8. Preuve de migration (les liens mail) — faite, mais pas comme prévu
Premier cas concret, à faire en régression (les mêmes variables doivent être générées) :
Premier cas concret. **Deux des trois lignes sont passées par les `liens`, la troisième
non** — et l'écart est instructif, il fixe la frontière entre les deux mécanismes.
| Avant (codé en dur) | Après (déclaratif) |
|---|---|
| `serveur_postfix_mailstore_hote` (group_var) | `postfix.liens: [mailstore → dovecot]` |
| `serveur_postfix_rspamd_milter` (group_var) | `postfix.liens: [milter → rspamd]` |
| `id-ldap-01` (defaults) | `postfix.liens: [annuaire → openldap]`, `dovecot.liens: [annuaire → openldap]` |
| Avant (codé en dur) | Après | Par quel mécanisme |
|---|---|---|
| `serveur_postfix_mailstore_hote` (group_var) | `postfix.liens: [mailstore → dovecot]` | **lien** ✅ (2026-07-03) |
| `serveur_postfix_rspamd_milter` (group_var) | `postfix.liens: [milter → rspamd]` | **lien** ✅ (2026-07-03) |
| `id-ldap-01` (defaults) | connexion LDAP dérivée du `domaine_interne` | **rôle utilitaire** `resoudre_annuaire` |
**Pourquoi l'annuaire n'est pas un lien.** Ce document a annoncé jusqu'au 2026-09-06
`postfix.liens: [annuaire → openldap]` et l'équivalent pour Dovecot. Ni l'un ni l'autre
n'existe : `roles/serveur_postfix/meta/liens.yml` n'accepte que `mailstore` et `milter`.
L'annuaire est traité comme les bases (§5) — par un **rôle utilitaire** que quatre
consommateurs incluent (`serveur_dovecot`, `serveur_postfix`, `serveur_keycloak`,
`serveur_icingaweb2`, plus `amorcage_acces`), parce qu'il n'y a **qu'un** annuaire par
écosystème et que sa connexion se dérive entièrement du `domaine_interne` : il n'y a pas de
choix de topologie à déclarer, donc pas d'arête à porter dans le plan. Un lien exprime un
choix ; ici il n'y en a pas.
## 9. Phasage
- **Phase 0** : cette note + décision. ✅ (direction : liens côté app pour app→app)
- **Phase 1** : résolveur de liens dans `instancier.py` + `meta/liens.yml` des rôles mail ;
migrer les liens mail ; régression DIFF VIDE. ✅ (2026-07-03 : `mailstore` + `milter` migrés)
- **Phase 2** : bases. ✅ **déjà en place** — binding **côté base** (registre + résolution en
rôle, 4 rôles), cf. §5. Ne PAS dupliquer dans instancier. **Reste (reporté à la session
Keycloak)** : factoriser le bloc de résolution copié-collé en un include partagé (DRY),
validé en déployant Keycloak.
- **Phase 3** : exposition / domaines (vhosts nginx dérivés des liens ; machinerie `expose` /
`expositions_des_applications` déjà présente).
- **Phase 2** : bases. ✅ **complète** — binding **côté base** (registre + résolution en
rôle), cf. §5. Ne PAS dupliquer dans instancier. La factorisation qui restait est faite :
`roles/resoudre_base/`, inclus par cinq rôles consommateurs.
- **Phase 3** : exposition / domaines. ✅ — **6 applications** déclarent un `expose`
(keycloak, forgejo, grafana, oauth2_proxy, nextcloud, collabora), et `serveur_nginx` en
dérive vhost, SAN et enregistrement A (`expositions_des_applications`). `make expositions-plan`
vérifie ensuite que chacune répond réellement.
- **Phase 4** : vue GUI des liens + graphe. 🟡 **éditeur fait** (2026-07-22, cf. §7) ;
**graphe des liens reste à faire**.

View file

@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
| rôles | 65 | `roles/*/` |
| README de rôles | 65 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 39 | `docs/*.md` |
| pièces d'audit | 35 | `docs/audit/*` |
| pièces d'audit | 36 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 79 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
@ -42,11 +42,11 @@ README de rôles). Cette page comble ces deux trous.
| **Conceptions de domaine** | `docs/identite-sso.md`, `docs/courriel-conception.md`, `docs/bindings-conception.md`, `docs/dns-interne.md`, `docs/dimensionnement-ressources.md`, `docs/integrations-vm.md` |
| **Réseau / pare-feu** | `docs/flux-conception.md` (le modèle) → `docs/registre-flux.md` (**généré**, matrice d'audit) → `docs/frontiere-opnsense.md` (la bordure nord/sud) ; underlay : `underlay.yml.example` + `make underlay` |
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`) **et les cinq devis d'infrastructure** (`frontiere-plan`, `proxmox-fw-plan`, `sdn-plan`, `underlay-plan`, `placement-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver` → `docs/audit/preuve-<date>.md` — **statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
| **Décisions d'architecture** | `docs/decisions-architecture.md` — les décisions en vigueur (comptées ci-dessus), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **En service** depuis le 2026-08-03 (`routage_tenants: sdn` dans l'`underlay.yml` du site) : les trunks ne portent plus que deux VLAN d'underlay au lieu de quinze, les passerelles `.1` sont anycast sur chaque hyperviseur. Écart mesuré par `make sdn-plan` |
| **Migration de tenant** | `docs/migration-tenant.md` — recette en **neuf étapes (0 à 8)**, machine à états, gardes ; le receveur se construit **avant** tout gel |
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |
| **Pédagogie (le wiki)** | `wiki/` — les unités (comptées ci-dessus) publiées par `make wiki-publier` ; entrer par `wiki/Home.md` |
| **Vision / positionnement** | `docs/ecosysteme-chezlepro.md`, `docs/positionnement.md`, `docs/pouvoirs-set-ops.md` |
@ -62,18 +62,18 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
| Dimensionnement | RAM/CPU/disque sommés par logiciel | `roles/*/meta/empreinte.yml` → `deriver_ressources` | `dimensionnement-ressources.md` |
| **Bindings app→app** | lien **côté app** (`liens`) résolu en host_vars | `plan/applications.yml` `liens:` + `roles/*/meta/liens.yml` + `instancier.resoudre_liens` | `bindings-conception.md` |
| **Bindings app→base** | lien **côté base** (`consommateur`/`portee`) résolu **dans le rôle** | `plan/bases-donnees.yml` + rôle utilitaire `resoudre_base` (`lookup('vars', secret)`, `no_log`) inclus par le consommateur | `bindings-conception.md` §5 |
| Résolution d'annuaire | connexion LDAP (uri/base DN/bind) **dérivée**, jamais recopiée | rôle utilitaire `resoudre_annuaire` (inclus par dovecot/postfix/keycloak/icingaweb2) | `identite-sso.md` |
| Résolution d'annuaire | connexion LDAP (uri/base DN/bind) **dérivée**, jamais recopiée | rôle utilitaire `resoudre_annuaire` (inclus par dovecot/postfix/keycloak/icingaweb2 et `amorcage_acces`) | `identite-sso.md` |
| Plancher de résolution | `/etc/hosts` généré depuis l'inventaire + alias d'`expose` → l'écosystème se résout **DNS éteint** | rôle `hosts_statiques` (appliqué dans la couche socle) | `dns-interne.md` |
| Pont de certificat | cert step_ca → service, resync au renouvellement | script `*-cert-sync` + unité `.path`, dans chaque rôle serveur ; cert déposé par `client_pki` | — |
| Ordonnancement socle-first | socle/durci avant les `client_*` | `serveur_debian`/`serveur_durci` d'abord (posent `/etc/hosts` via `hosts_statiques`) | — |
| Sûreté check-mode | dry-run fiable | `when: not ansible_check_mode` sur les tâches de service + handlers | — |
| Voûte au déploiement | secret jamais en clair | `ANSIBLE_VAULT_PASSWORD_FILE` / `~/.config/setops-vault-pass` ; déréférencé par `lookup('vars', <nom>)` | — |
| Voûte au déploiement | secret jamais en clair ; **une voûte, une clé** depuis le 2026-08-28 | `ANSIBLE_VAULT_IDENTITY_LIST` construit par `scripts/voutes.py` (clé nommée `~/.config/setops-vault-<dépôt>`) ; déréférencé par `lookup('vars', <nom>)` | `autorisation.md` §6.1 |
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
| Exploitation de l'hébergeur | ses **opérations** (supervision de la fabric, sauvegarde des configs, DNS d'underlay) n'appartiennent à aucun tenant et restent **hors overlay** | décidé, **non construit** : aucun équipement d'hébergeur n'est encore dans un inventaire | `hebergeur-exploitation.md` |
| Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* — hyperviseurs, commutateurs, frontière — n'ont toujours ni inventaire, ni sauvegarde de configuration, ni supervision | `hebergeur-exploitation.md` |
| Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` |
| Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) |
| SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du tenant (`CHEZ17`, `chez174`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 |
| SDN EVPN | ajouter un tenant implique **1 zone + 6 VNets + 6 sous-réseaux**, tous dérivés du seed | `scripts/devis_sdn.py` (`make devis-sdn`) ; nommage dérivé du seed (`t17`, `t17serv`), ≤ 8 caractères ; garde **P30** | `sdn-evpn.md` §2 |
| Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 |
| Routage | **aucun commutateur ne route** : la frontière est le seul équipement L3 ; les switches commutent | `passerelle` dit qui porte la passerelle, le SVI se dérive du rôle du porteur | `decisions-architecture.md` D-49/50 |
| **Devis de service** | LIT le système en marche et le compare à ce que le plan dérive ; n'écrit rien (D-23/D-24 portés du réseau aux services). Le playbook **relève**, Python **compare** | `playbooks/maintenance/devis-*.yml` + `scripts/devis_*.py` ; cible `make <sujet>-plan` | `devis-services.md` |

View file

@ -10,10 +10,10 @@ groupe opérationnel -> playbooks/groupes/<groupe>.yml (P04 le prouve)
```
Le **rôle porte le nom du groupe** : `serveur_keycloak`, `client_pki`. Il n'y a pas de nom
court séparé. Seule exception, `serveur_durci` : un groupe dont le playbook **compose**
onze rôles de durcissement (`hardening_packages`, `sysctl_hardening`, `apparmor`,
`auditd`, `fail2ban_ssh`, `ssh_hardening`, `nftables_baseline`…) plutôt qu'un rôle
homonyme.
court séparé. Seule exception, `serveur_durci` : un groupe dont le playbook **compose dix
rôles** de durcissement, et dans cet ordre — `hardening_packages`, `sysctl_hardening`,
`core_dumps`, `unattended_upgrades`, `apparmor`, `auditd`, `fail2ban_ssh`, `journald`,
`ssh_hardening`, `nftables_baseline` — plutôt qu'un rôle homonyme.
La nomenclature des VM et des VMID est documentée dans `docs/nomenclature-vm.md`.
@ -23,19 +23,26 @@ Un service central peut partager un hôte avec d'autres services de la même fon
## État d'implémentation des rôles
> **Mise à jour (2026-08-18), vérifiée rôle par rôle contre `roles/`.** Les 29 groupes
> `serveur_*` / `client_*` de ce catalogue ont **tous** leur rôle et leur playbook. Aucune
> capacité annoncée ici n'est un point d'ancrage vide.
> **Mise à jour (2026-09-06), vérifiée groupe par groupe contre `roles/` et
> `playbooks/groupes/`.** Les **40 groupes** `serveur_*` / `client_*` du tableau ci-dessous
> ont **tous** leur rôle et leur playbook — 40 fichiers dans `playbooks/groupes/`, 40 noms
> distincts cités ici. Aucune capacité annoncée n'est un point d'ancrage vide.
>
> *(Le chiffre lu ici jusqu'au 2026-09-06 était 29, mesuré le 2026-08-18 : le catalogue a
> grandi de onze groupes — site, runners, résolveur, artefacts — sans que la phrase suive.
> C'est ce genre d'écart que la preuve **P57** garde désormais.)*
**Éprouvés sur VM réelles.** Le 2026-08-13, la flotte a été **reconstruite depuis zéro**
— 43 groupes, 0 échec, 37 minutes — puis remontée d'un seul trait. Ce n'est donc plus
« du code validé » : chaque rôle a repris une machine nue et l'a menée à l'état voulu.
**Éprouvés sur VM réelles.** Le 2026-08-13, la flotte a été **reconstruite depuis zéro** —
43 groupes, 0 échec, 37 minutes. L'épreuve a été **rejouée deux fois le 2026-09-02**, sur
un dépôt qui avait beaucoup bougé depuis : 15/15 hôtes puis 14/14, 0 échec, `make valider`
à 0 échec sur 13 hôtes. Ce n'est donc plus « du code validé » : chaque rôle a repris une
machine nue et l'a menée à l'état voulu — et il l'a refait après coup.
Ce que la reconstruction couvre, par capacité :
| Capacité | Rôles | Ce qui est éprouvé |
|---|---|---|
| Socle et durcissement | `serveur_debian`, `serveur_durci` | clone du gabarit doré → machine conforme |
| Socle et durcissement | `serveur_debian`, `serveur_durci` (dix rôles composés) | clone du gabarit doré → machine conforme |
| Confiance | `serveur_step_ca`, `client_pki` | mTLS avec SAN dérivés du plan, renouvellement |
| Noms | `serveur_powerdns`, `client_resolveur` | autoritaire interne + résolveur local |
| Identité | `serveur_openldap`, `serveur_keycloak` | LDAPS, SSO OIDC, **fédération LDAP automatisée** (`tasks/federation-ldap.yml`), exposé par le plan (`auth.<domaine>`) |
@ -145,9 +152,14 @@ table décrit une répartition éprouvée, pas un minimum requis.
| `mon-01` | Icinga 2, Icinga Web 2, oauth2-proxy |
| `forge-01` | Forgejo |
| `collab-01` | Nextcloud, Collabora |
| `backup-01` | dépôt restic |
| `web-frontal-01` | site statique |
| `web-dorsal-01` | webapp native |
| `ops-01` | runner de l'écosystème (`serveur_ops`, `serveur_ops_tenant`) |
> `ops-01` manquait de cette table jusqu'au 2026-09-06, alors que le compte annoncé
> ci-dessus le comptait : quatorze hôtes, treize lignes. C'est le nœud depuis lequel
> l'écosystème se reconstruit **sans le poste de l'exploitant** — la pièce la moins visible
> et la plus structurante de la reconstruction autonome.
Le courriel occupe **deux** hôtes, et ce n'est pas un détail de taille : `edge-mta-01`
porte ce qui parle à l'extérieur (Postfix, rspamd), `infra-mail-01` ce qui détient les
@ -161,6 +173,7 @@ boîtes (Dovecot). La coupure suit l'exposition, pas le logiciel.
| Métriques Prometheus | `client_metrique` | **tout hôte** (universelle) |
| Journaux vers Loki | `client_journal` | **tout hôte** (universelle) |
| Résolution locale (Unbound) | `client_resolveur` | **tout hôte** (universelle) |
| Source d'artefacts (cache apt) | `client_artefacts` | **tout hôte** (universelle) |
| Relais SMTP | `client_smtp` | déclaré par hôte, dans le plan |
| Sauvegarde restic | `client_backup` | déclaré par hôte — **obligatoire pour tout détenteur d'état** (P36) |
@ -172,14 +185,21 @@ dans le plan.
> **`client_supervision` n'existe pas.** Ce catalogue l'a longtemps annoncé ; il n'a
> jamais eu ni rôle ni playbook, et rien ne l'attend. La supervision s'exerce **sans agent
> sur les hôtes** : contrôles actifs depuis le cœur (`hostalive`) et résultats **passifs
> poussés par l'API** par celui qui détient la vérité de terrain — ainsi l'état des
> sauvegardes est-il rapporté par `backup-01`, seul à pouvoir lire ses dépôts. Le nom est
> retiré plutôt que réservé : une case vide dans un catalogue se lit comme une promesse.
> poussés par l'API** par celui qui détient la vérité de terrain. Le nom est retiré plutôt
> que réservé : une case vide dans un catalogue se lit comme une promesse.
> **Qui rapporte l'état des sauvegardes a changé le 2026-09-02.** Tant que le dépôt vivait
> dans l'écosystème, il était le seul à voir ce qui était réellement arrivé, et il
> rapportait pour tout le monde. Depuis que les écosystèmes déposent chez leur **hébergeur**
> — qui héberge des octets chiffrés côté client et ne peut pas les juger — **chaque nœud
> vérifie son propre dépôt distant** et le rapporte lui-même. La vérification suit la clé,
> pas le stockage. `serveur_icinga` se branche sur les deux modèles ; `backup-01` a été
> retiré du plan de Chezlepro, sa VM détruite.
## Ordre de déploiement — le raisonnement
> **L'ordre exécutable n'est pas ici.** Il vit dans `docs/couches-deploiement.yml` et
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (30 groupes classés, aucun
> `docs/dependances-groupes.yml`, que P08 prouve cohérents (40 groupes classés, aucun
> cycle, aucune arête en arrière) et que `make reconstruire` suit. Ce qui suit en est le
> **raisonnement**, utile pour comprendre pourquoi cet ordre-là — et pour placer un
> service nouveau. Les phases sont franchies : les « intégrations à prévoir » ci-dessous
@ -190,7 +210,11 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
### Phase 1 - Fondations transversales
1. `serveur_powerdns`
- Service central : DNS interne autoritaire et/ou résolution interne selon le design retenu.
- Service central : DNS interne **autoritaire** de la zone souveraine. La *résolution*
est une couche distincte (`serveur_resolveur` / `client_resolveur`, Unbound), et le
**plancher `/etc/hosts`** posé par `hosts_statiques` précède les deux — c'est lui qui
permet à l'écosystème de se résoudre DNS éteint. Trois couches, pas un choix de design
(`docs/dns-interne.md`).
- Raison : les autres intégrations auront besoin de noms stables plutôt que d'adresses IP.
2. `serveur_step_ca`
@ -277,7 +301,7 @@ L'ordre ci-dessous privilégie les dépendances structurantes avant les applicat
- Tout service exposé en HTTP(S) doit prévoir son intégration avec `serveur_nginx`.
- Tout service avec authentification humaine doit prévoir son intégration avec `serveur_keycloak`, sauf justification contraire.
- Tout service générant des alertes ou notifications doit prévoir `client_smtp`.
- Toute VM de service rejoint `client_pki`, `client_metrique`, `client_journal` et `client_resolveur` — **par dérivation, sans rien écrire** (intégrations universelles, P26). La supervision, elle, ne pose rien sur l'hôte.
- Toute VM de service rejoint `client_pki`, `client_metrique`, `client_journal`, `client_resolveur` et `client_artefacts` — **par dérivation, sans rien écrire** (les cinq intégrations marquées `universelle: true`, P26). La supervision, elle, ne pose rien sur l'hôte.
- Tout hôte qui **détient de l'état** doit porter `client_backup` (P36 le refuse sinon).
- Tout service utilisant un certificat interne doit dépendre de `client_pki`.
- Tout rôle serveur doit documenter ses ports, secrets, sauvegardes, dépendances et groupes clients associés.

View file

@ -13,7 +13,7 @@ la connexion au cluster Proxmox et les valeurs de clonage par défaut. Il pose
Le chemin se **dérive** du symlink qui désigne déjà l'hébergeur — rien de nouveau
n'est déclaré. Sans underlay monté, tout retombe dans le fichier du tenant et
`make config` fonctionne comme avant.
- Secrets → **voûte unique** `instance/inventories/production/group_vars/all/vault.yml`
- Secrets → **voûte unique** `instance/inventories/<inventaire>/group_vars/all/vault.yml`
(chiffrée par `ansible-vault`), qui contient **tous** les secrets de l'instance
(token Proxmox + `vault_*`). Voir [§4](#4-secrets-de-linstance-).
@ -42,7 +42,7 @@ sans tout retaper.
| Invite | Variable | Défaut | Sens / quoi saisir |
| --- | --- | --- | --- |
| VMID du modèle Debian 13 | `proxmox_clone_vmid_modele` | `9000` | VMID de la VM-modèle existante à cloner pour chaque nouvelle VM. |
| Nom logique du modèle | `proxmox_clone_source_nom` | `modele-debian13` | Nom de référence du template (lisibilité ; doit correspondre au modèle). |
| Nom logique du modèle | `proxmox_clone_source_nom` | `modeleSetOPS` | Nom de référence du template. **Il doit correspondre au nom réel du template Proxmox** : sinon le clonage ne trouve pas sa source. *(Ce tableau a annoncé `modele-debian13` jusqu'au 2026-09-06 — un défaut qui n'a jamais été celui du code.)* |
> Le golden template est l'**actif central** : il est cloné pour chaque VM, jamais
> jeté ni reconstruit à la légère.
@ -68,10 +68,17 @@ Ces valeurs s'appliquent à toute VM clonée, **sauf** si l'hôte les surcharge
## 4. Secrets de l'instance 🔒 *(voûte unique)*
Tous les secrets de l'instance vivent dans **une seule voûte chiffrée par
environnement** : `instance/inventories/<env>/group_vars/all/vault.yml`. Un seul
fichier, un seul mot de passe — fini les voûtes éparpillées. Gabarit committé :
[`exemples/vault.exemple.yml`](../exemples/vault.exemple.yml) (token Proxmox +
17 clés `vault_*` pour PKI, LDAP/SSO, bases, forge, observabilité).
instance** : `instance/inventories/<inventaire>/group_vars/all/vault.yml`. Un seul fichier
par écosystème — fini les voûtes éparpillées.
> **Une voûte, une clé (2026-08-28).** « Un seul mot de passe » a été vrai, et c'était le
> défaut : le même ouvrait *toutes* les voûtes de la flotte, celle de l'hébergeur comprise.
> Chaque dépôt a maintenant **sa** clé — `~/.config/setops-vault-<dépôt-en-minuscules>` —
> et le `Makefile` les rassemble dans `ANSIBLE_VAULT_IDENTITY_LIST` via
> `scripts/voutes.py`. Créer une VM ouvre d'ailleurs **deux** voûtes dans la même
> exécution : celle du tenant, et celle de l'hébergeur qui détient le jeton Proxmox. Gabarit committé :
[`exemples/vault.exemple.yml`](../exemples/vault.exemple.yml) (les deux clés du token
Proxmox + **15 clés `vault_*`** pour PKI, LDAP/SSO, bases, forge, observabilité).
L'assistant demande « Configurer la voûte de secrets maintenant ». Si `oui` :
@ -96,10 +103,16 @@ L'assistant demande « Configurer la voûte de secrets maintenant ». Si `oui` :
| **Saisir** | un **tiers** — le secret existe déjà ailleurs et ne s'invente pas (clé d'API OPNsense, jeton Proxmox) | `python3 scripts/voute.py saisir <clés>` |
```bash
ANSIBLE_VAULT_PASSWORD_FILE=~/.config/setops-vault-pass \
python3 scripts/voute.py saisir vault_opnsense_api_key vault_opnsense_api_secret
python3 scripts/voute.py saisir vault_opnsense_api_key vault_opnsense_api_secret
```
Il n'y a **rien à exporter** : `voute.py` trouve la clé de la voûte par la convention de
nommage (`scripts/voutes.py etat` la montre). Il n'y a pas non plus de cible `make` pour ce
geste — c'est délibéré : saisir un secret est une manœuvre rare et attentive.
*(`voute.py` au singulier manipule **le contenu** d'une voûte ; `voutes.py` au pluriel dit
**où sont les clés**. Les deux existent, et ce n'est pas une faute de frappe.)*
Saisie **sans écho**, double confirmation, rien sur la ligne de commande — donc ni
dans l'historique du shell, ni dans la liste des processus. Rien n'est écrit en clair
sur disque : la voûte est déchiffrée en mémoire, complétée, reparsée et re-déchiffrée

View file

@ -2,8 +2,15 @@
> **Pour qui :** le **mainteneur** du service de courriel.
> **Statut : CONCEPTION (cadrage).** Aucun rôle n'est encore écrit. Ce document fixe
> les décisions, les prérequis et la topologie avant toute implémentation.
> **Statut, revu le 2026-09-06 — l'Étape A est LIVRÉE, l'Étape B reste du cadrage.**
> Ce document annonçait « aucun rôle n'est encore écrit » : les trois rôles
> (`serveur_postfix`, `serveur_dovecot`, `serveur_rspamd`) existent, sont déployés, et le
> flux interne est **prouvé de bout en bout** — SMTP → validation LDAP → LMTP chiffré →
> boîte → **lecture IMAP**, avec antispam et signature DKIM. Ce qui n'est **pas** livré,
> c'est l'**Étape B** (§11) : la face publique — Let's Encrypt, reprise du MX `.53`,
> enregistrements chez Namespro, tests de délivrabilité. Lire ce document ainsi : les
> décisions du §1 sont **arrêtées et appliquées** ; la feuille de route du §11 est
> **ouverte**.
## 1. Décisions arrêtées
@ -194,13 +201,16 @@ _dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@chezlepro.
But : prouver **toute la pile en interne**, en **code de prod**, dans le bac à sable.
Aucune dépendance au public.
1. **Pilier identité** : `serveur_openldap` (TLS **step_ca**) — ✅ **déployé et prouvé** en bac à sable.
2. **`serveur_postfix` + `serveur_dovecot` + `serveur_rspamd`** sur `mail-01` : TLS **step_ca**,
annuaire/auth **LDAP**, DKIM interne, nftables mail.
1. **Pilier identité** : `serveur_openldap` (TLS **step_ca**) — ✅ **déployé et prouvé**.
2. **`serveur_postfix` + `serveur_rspamd`** sur `edge-mta-01`, **`serveur_dovecot`** sur
`infra-mail-01` : TLS **step_ca**, annuaire/auth **LDAP**, DKIM, nftables mail — ✅ **déployés**.
3. **Prouver** : réception → boîte → accès **IMAP** → envoi **intra-écosystème**, le tout en
TLS interne, auth LDAP.
TLS interne, auth LDAP — ✅ **prouvé de bout en bout**, et rejoué à la demande par
`make courriel-plan`, file d'attente comprise.
*(Étape actuelle : identité OK ; on démarre `serveur_postfix`.)*
*(Ce paragraphe disait « Étape actuelle : identité OK ; on démarre `serveur_postfix` » et
plaçait toute la pile sur un `mail-01` unique. La topologie retenue est celle du §3 révisé —
le MTA en périphérie, les boîtes à l'intérieur — et l'Étape A est close.)*
### Étape B — Fonctionnement EXTERNE (transition prod, plus tard)
@ -216,4 +226,5 @@ But : brancher sur le monde **en reprenant l'existant** (voir §2).
---
*Ce document est un cadrage vivant : il évolue à mesure que les décisions ouvertes se
tranchent. Il ne décrit pas encore de code livré.*
tranchent. **L'Étape A qu'il décrit est livrée et prouvée** ; la séquence ci-dessus, qui est
l'Étape B, ne l'est pas.*

View file

@ -71,9 +71,9 @@ sont les seules vérifiables.
| **D-48** | Les **hyperviseurs** sont gérables par Ansible ; « hors flotte » ne vaut que pour les **commutateurs** et la **frontière** | ce sont des Debian joignables en SSH ; c'est la seule façon d'y poser un exportateur de métriques | `hebergeur-exploitation.md` §5 | — |
| **D-55** | Le dépôt réseau porte une **interface normalisée** vers les tenants de l'Alliance, et abstrait le matériel en les encapsulant dans des zones EVPN | un tenant qui ne nomme aucun équipement se déplace d'un hébergeur à l'autre sans rien changer ; le VRF borne ce qu'il a le droit de connaître | `hebergeur-exploitation.md` §7 | — |
| **D-56** | Le **VNet d'une VM est dérivé** (`index` + zone), jamais déclaré ; l'étiquette VLAN est **vide** en SDN | déclaré, il faisait naître les VM sur `vmbr1` avec un tag — l'ancien monde, à rebrancher une par une | `instancier.py` | P02, P03 |
| **D-53** | Le **réseau et l'underlay** de l'hébergeur méritent leur **propre dépôt**, séparé de son tenant | `underlay.yml` et le cluster décrivent une infrastructure ; le dépôt de tenant décrit une organisation. Les mêler oblige à trancher qui possède quoi à chaque commit | `hebergeur-exploitation.md` §7 | — |
| **D-54** | `10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB** — accès sysadmin | aucune VM, aucun trafic tenant ; c'est la raison d'être des VLAN 11 et 40 | `underlay.yml` | — |
| **D-57** | L'interface **sysadmin** d'un hyperviseur (`vmbr0`) n'a **pas de route par défaut** ; celle-ci vit sur `vlan40`, vers la frontière | on n'atteint l'administration que depuis son propre domaine de diffusion — un accès distant doit être ouvert explicitement, il ne peut pas exister par accident. Et le trafic tenant ne touche plus la carte d'administration | `underlay.yml` | — |
| **D-53** | Le **réseau et l'underlay** de l'hébergeur ont leur **propre dépôt**, séparé de son tenant — **appliqué** : `SITE-Chezlepro` | `underlay.yml` et le cluster décrivent une infrastructure ; le dépôt de tenant décrit une organisation. Les mêler obligeait à trancher qui possède quoi à chaque commit. Le dépôt porte aussi, depuis, le **plan des VM du site** : l'hébergeur n'est pas qu'un porteur de fabric, c'est un exploitant | `hebergeur-exploitation.md` §8 | — |
| **D-54** | Le **plan d'administration** est réservé à l'**IPAM, la gestion des équipements et l'OOB** — accès sysadmin | aucune VM, aucun trafic tenant ; c'est la raison d'être des VLAN de transport et de transit, qui l'en sortent. **Adresses révisées le 2026-09-06** : la décision citait `10.0.0.0/24` et « les VLAN 11 et 40 ». D-77 a déplacé ce plan en `10.<index>.0.0/24` (`10.17.0.0/24` chez l'hébergeur de référence, **sans VLAN** — segment physique, aucun pont ne le touche), et D-78 a fait passer le transport VXLAN au **VLAN 50**. Le principe est intact ; seules les adresses ont bougé | `underlay.yml` | P23 |
| **D-57** | ~~La route par défaut d'un hyperviseur vit sur `vlan40`, vers la frontière~~ → **NON APPLIQUÉE, et gelée** | L'intention tient : le trafic tenant ne doit pas toucher la carte d'administration. Mais la bascule elle-même **n'a pas été faite et ne doit pas être proposée** : la route par défaut des hyperviseurs reste sur `vmbr0`, vers le routeur du site (`192.168.11.254`), et c'est un état **gelé**. Conséquence assumée, écrite noir sur blanc dans l'`underlay.yml` : ce que ce plan envoie dehors **ne passe pas par la frontière**. Déplacer la route par défaut d'un hyperviseur en service, c'est risquer de perdre l'hyperviseur *et* le chemin pour le réparer | `underlay.yml` | — |
| **D-58** | Un hôte déclare **par quelle interface** (`via`) chaque réseau lui arrive ; le devis en dérive un **port par interface** et son **type** | un hyperviseur a plusieurs pattes ; les grouper remettait la gestion sur le trunk du transport | `devis_reseau.py` | P23 |
| **D-59** | Un VLAN qui ne porte que des **adresses d'hôte** n'a **pas besoin de pont** | un pont sert à brancher des invités ; vide, il coûte une table MAC et un saut de plus sur le lien qui porte tout le trafic tenant | `underlay.yml` | — |
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |

View file

@ -16,9 +16,21 @@ make mtu-mesurer # l'invité porte-t-il le MTU de sa zone SDN ?
make versions-mesurer # de combien nos épinglages ont-ils vieilli ?
```
**Le patron a été porté sous les services, au monde physique** — mêmes pièces (un playbook
qui relève, un script qui compare), même refus d'écrire. Ce document ne traite que la
moitié haute ; ces cinq-là existent aussi :
```
make frontiere-plan # les règles de la frontière OPNsense contre leur devis
make proxmox-fw-plan # le pare-feu est-ouest de l'hyperviseur contre le registre des flux
make sdn-plan # la zone EVPN, ses VNets, et la sortie des VRF
make underlay-plan # l'underlay déclaré contre ce que le cluster porte vraiment
make placement-plan # chaque VM est-elle là où le plan la met
```
## Le trou qu'il comble
`scripts/prouver.py` porte 35 preuves. Elles sont toutes **statiques** : elles lisent le
`scripts/prouver.py` porte 57 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
propriétaire, que rien n'est codé en dur.
@ -215,7 +227,7 @@ Une API est souvent préférable, mais pour une raison précise : elle rend la r
l'attendu dans le réel ; si rien ne change, c'est conforme*. Une interface qui n'accepte
que des écritures ne peut pas le soutenir.
**Les cinq devis sont cette relecture**, faite après coup et par une autre main que celle
**Ces devis sont cette relecture**, faite après coup et par une autre main que celle
qui a écrit. C'est ce qui les distingue d'un déploiement : `make deployer` réconcilie, les
devis constatent.
@ -242,5 +254,12 @@ La forme correcte, reprise dans les deux devis :
Ils ne corrigent pas — c'est `make deployer` qui réconcilie. Ils répondent à l'autre
question, et sortent en code 1 s'il y a un écart.
Ils couvrent l'identité et les certificats. Le courriel, la base de données et les
expositions web attendent le même traitement ; le patron est là pour être repris.
**Ce paragraphe disait, en dernière ligne du document, que le courriel, la base de données
et les expositions web « attendent le même traitement ».** Ils l'ont reçu — ce document
décrit leurs trois devis quelques écrans plus haut, et le patron a même été porté sous les
services, au monde physique (frontière, pare-feu de l'hyperviseur, SDN, underlay, placement).
Ce qui reste vraiment hors de leur portée, et qu'aucun ne mesure : la **tenue sous charge**
et le **comportement dans la durée**. Un devis dit que le service rend son service à
l'instant où on le lui demande, pas qu'il le rendra encore à mille utilisateurs, ni dans six
mois.

View file

@ -28,7 +28,7 @@ que recalculer la cible ; aucune dérive silencieuse.
| Donnée | Emplacement | Remarque |
| --- | --- | --- |
| Empreinte d'un logiciel | `roles/<groupe>/meta/empreinte.yml` | Propriété du logiciel, voyage avec le rôle. Fichier *pur données* (parsable sans Jinja). |
| Groupes sans rôle dédié | `EMPREINTES_SANS_ROLE` dans `scripts/inventory_rules.py` | p. ex. `serveur_web_frontal`, `serveur_web_dorsal`. |
| Groupes sans rôle dédié | `EMPREINTES_SANS_ROLE` dans `scripts/inventory_rules.py` | **repli aujourd'hui vide d'effet** : tous les groupes de service ont leur rôle. Voir l'encadré plus bas. |
| Repli ultime | `EMPREINTE_DEFAUT` | groupe inconnu : empreinte minimale. |
| Socle SE | `SOCLE_SE` dans `inventory_rules.py` | coût de base Debian durci. |
| Override par hôte | `serveurs.yml` (`coeurs`/`memoire`/`disque`) | déjà supporté par le générateur (`PLACEMENT`). |
@ -65,24 +65,35 @@ la somme. Le socle (`serveur_debian`/`serveur_durci`) et les groupes d'état son
5. `playbooks/proxmox/cloner_vm_debian.yml` — passe `cores`/`memory` à `proxmox_kvm`
(avec `omit` si absent : aucune régression, on garde alors les specs du template).
## Empreintes actuelles
## Les empreintes : ne pas les recopier, les mesurer
| Rôle | cœurs | RAM (Mo) | disque (Go) |
| --- | --- | --- | --- |
| serveur_step_ca | 1 | 256 | 2 |
| serveur_powerdns | 1 | 512 | 2 |
| serveur_nginx | 1 | 512 | 3 |
| serveur_openldap | 1 | 512 | 3 |
| serveur_keycloak | 2 | 1536 | 5 |
| serveur_postgresql | 2 | 2048 | 20 |
| serveur_redis | 1 | 512 | 2 |
| serveur_forgejo | 1 | 1024 | 20 |
| serveur_prometheus | 1 | 1024 | 20 |
| serveur_loki | 1 | 1024 | 20 |
| serveur_grafana | 1 | 512 | 2 |
| serveur_icinga | 2 | 1024 | 10 |
| serveur_web_frontal (repli) | 1 | 512 | 5 |
| serveur_web_dorsal (repli) | 1 | 1024 | 10 |
Ce document a porté jusqu'au 2026-09-06 un tableau de quatorze empreintes recopiées à la
main. Il y en a **32** aujourd'hui, et l'une des quatorze avait cessé d'être vraie. Recopier
une valeur qui vit ailleurs, c'est s'engager à la suivre — cette page ne s'y engage plus :
```bash
# l'empreinte de chaque logiciel, telle qu'elle est déclarée
python3 - <<'EOF'
import yaml, pathlib
for f in sorted(pathlib.Path('roles').glob('*/meta/empreinte.yml')):
e = (yaml.safe_load(f.read_text()) or {}).get('setops_empreinte') or {}
print(f"{f.parts[1]:26} {e.get('coeurs')} coeur(s) {e.get('memoire_mo')} Mo {e.get('disque_go')} Go")
EOF
# ce que ça donne pour un hôte donné, une fois sommé et arrondi
make hote-afficher HOTE=obs-01
```
Ce qui mérite d'être écrit ici, c'est ce qui **façonne** le calcul et ne se lit pas dans un
rôle — le socle, les marges, les paliers, les bornes. Ils sont au §« Règles d'agrégation »
ci-dessus, et vivent en tête de `scripts/inventory_rules.py`.
> **Un repli devenu inutile.** `EMPREINTES_SANS_ROLE` couvrait `serveur_web_frontal` et
> `serveur_web_dorsal` du temps où ces groupes n'avaient pas de rôle. Ils en ont un depuis,
> avec leur propre `meta/empreinte.yml` — et comme la précédence est *rôle > repli > défaut*,
> ces deux entrées ne sont plus jamais lues. Elles disent d'ailleurs autre chose que les
> rôles (5 Go contre 10 pour le frontal) : c'est sans effet, mais c'est le genre d'écart
> qu'on croit lire comme une vérité.
## Ajuster

View file

@ -8,18 +8,42 @@ Le service DNS interne est la premiere capacite de plateforme.
> `serveur_debian`) genere `/etc/hosts` sur **chaque** VM depuis l'inventaire : tout
> l'ecosysteme se resout par nom **meme serveur DNS eteint** (et au bootstrap, avant
> que PowerDNS ne soit la). PowerDNS devient une **commodite** (zone, externe,
> dynamique), plus un point de defaillance. Le resolveur local `client_resolveur` est
> **optionnel** (opt-in, avec bascule validee) : sans lui, le plancher `/etc/hosts` suffit.
> dynamique), plus un point de defaillance.
## Trois couches, et une seule est optionnelle
> **Ce paragraphe decrivait le modele d'avant le 2026-08-24** — un Unbound *sur chaque VM*,
> en opt-in. Ce n'est plus le cas : `client_resolveur` **n'installe plus rien**, et son
> integration est **universelle**, pas elective.
```text
1. hosts_statiques le PLANCHER : /etc/hosts genere sur chaque VM depuis l'inventaire
-> l'ecosysteme se resout DNS eteint. Jamais optionnel.
2. serveur_powerdns l'AUTORITATIF de la zone souveraine (un par ecosysteme)
3. serveur_resolveur le RECURSIF : UN seul Unbound pour tout le tenant, qui recurse
depuis la racine et delegue la zone souveraine a PowerDNS
client_resolveur l'integration : ecrit /etc/resolv.conf pour designer ce resolveur
```
**`client_resolveur` n'installe plus de demon** (2026-08-24). Il en posait un par VM — N
demons identiques de ~21 Mo pour quelques centaines de requetes. Il ne fait plus qu'une
chose : **ecrire `/etc/resolv.conf`**. Son integration est marquee `universelle: true`, et
elle n'a **aucune exemption, pas meme l'hote qui porte le resolveur** : il se sert
lui-meme. L'ancien nom (`client_unbound`) mentait des lors qu'il n'installait plus Unbound.
**L'integration suit l'existence du service, elle ne se declare pas.** Aucun
`serveur_resolveur` au plan rend `client_resolveur_actif` faux et le role ne touche a rien :
l'ecosysteme garde la resolution d'amorcage de cloud-init. C'est un choix valide, pas une
panne.
## Groupes
```text
serveur_powerdns -> service DNS central PowerDNS Authoritative
client_resolveur -> resolveur local optionnel (opt-in, bascule validee)
serveur_powerdns -> autoritatif interne (PowerDNS Authoritative)
serveur_resolveur -> LE recursif du tenant (Unbound), un seul
client_resolveur -> integration universelle : designe ce resolveur dans /etc/resolv.conf
```
`client_resolveur` (résolveur local optionnel) peut viser PowerDNS en stub-zone + récursion.
## Zone initiale
La zone initiale est :
@ -31,7 +55,7 @@ exemple.internal
Elle est definie dans :
```text
instance/inventories/production/group_vars/serveur_powerdns.yml
instance/inventories/<inventaire>/group_vars/serveur_powerdns.yml
```
## Enregistrement automatique
@ -67,13 +91,10 @@ Raison :
Quand `serveur_postgresql` sera stable, il sera possible de migrer vers un backend SQL si le besoin operationnel le justifie.
## Résolveur local (optionnel)
## La bascule de `/etc/resolv.conf` est protegee
Le role `client_resolveur` (opt-in) installe un résolveur récursif local qui, en stub-zone,
délègue les noms internes à PowerDNS et récurse le reste.
La bascule du resolver local est **protegee** (le role valide qu'Unbound répond AVANT de
basculer `/etc/resolv.conf`) :
Le role `client_resolveur` ne bascule `/etc/resolv.conf` qu'apres avoir verifie que le
resolveur repond **deja** — pour l'interne *et* pour l'Internet :
```yaml
client_resolveur_apply: true
@ -82,7 +103,30 @@ client_resolveur_confirm: true
Sans ces deux variables, le role prépare Unbound mais ne modifie pas `/etc/resolv.conf`.
Cette protection est volontaire : une mauvaise configuration DNS peut couper la resolution de noms.
Cette protection est volontaire : une mauvaise configuration DNS peut couper la resolution
de noms. C'est la dependance la plus dangereuse du lot — basculer un hote sur un resolveur
qui n'est pas encore pret le rend **muet**, et le runner qui devrait reparer tombe avec les
autres. Le playbook de groupe applique donc l'hote qui *porte* le resolveur avant ceux qui
s'y adressent, et la preuve **P44** refuse tout ecart entre cette declaration et lui.
## Le piege qui a coute deux jours : la racine signee nie notre TLD
`internal.` n'est **pas delegue dans la racine**, qui est signee : elle rend donc une preuve
NXDOMAIN *validee* pour ce TLD. Or `harden-below-nxdomain` — **actif par defaut** dans
Unbound — tient ce « non » pour prouve et repond NXDOMAIN pour **tout** nom sous
`internal.` depuis son cache, **sans jamais interroger la `stub-zone`** declaree plus bas.
La delegation etait correcte. L'autoritatif repondait juste. Pas une requete ne lui
parvenait.
Ce qui declenche l'empoisonnement : n'importe quelle question sur un nom inexistant sous
`internal.` — y compris la zone d'**un autre ecosysteme**, que ce resolveur ne sert pas et
va donc chercher a la racine. Sur un resolveur partage, ca arrive en permanence.
Et la panne parait **intermittente** : au redemarrage le cache est vide, tout fonctionne, on
conclut que c'est regle. Le remede mesure (2026-09-02) est `harden-below-nxdomain: no` dans
`roles/serveur_resolveur/templates/setops.conf.j2` — **pas** `aggressive-nsec`, qui traite
un autre symptome.
## Surveillance a prevoir

View file

@ -35,13 +35,17 @@ cohérent.
| **Confiance** | Autorité de certification interne (PKI/ACME) : les serveurs se reconnaissent par certificats émis localement, sans acheter de confiance à l'extérieur. |
| **Nommage** | DNS interne : des noms de machines stables et dérivables, plutôt que des adresses IP fragiles. |
| **Données** | Bases relationnelles et cache, avec un registre des connexions applicatives. |
| **Communication** | Relais courriel interne pour les alertes, notifications et réinitialisations. |
| **Communication** | Service de courriel souverain : boîtes adossées à l'annuaire, réception SMTP, lecture IMAP, antispam et signature DKIM. Le relais des alertes système en est distinct. |
| **Observabilité** | Métriques, journaux centralisés, tableaux de bord et supervision active. |
| **Applicatif** | Services internes (forge logicielle, collaboration documentaire) derrière une couche web sécurisée. |
Tous ces services sont **libres** (OpenLDAP, Keycloak, step-ca, PowerDNS, PostgreSQL,
Redis, NGINX, Prometheus, Loki, Grafana, Icinga, Forgejo, Sendmail…). Aucun verrou
propriétaire, aucune licence captive.
Tous ces services sont **libres** : OpenLDAP, Keycloak, step-ca, PowerDNS, Unbound,
PostgreSQL, Redis, NGINX, Postfix, Dovecot, rspamd, Prometheus, Loki, Grafana, Icinga,
Forgejo, Nextcloud, Collabora, restic. Aucun verrou propriétaire, aucune licence captive,
**et aucun conteneur** — tout est installé nativement, en paquets et unités systemd.
*(Cette liste nommait Sendmail jusqu'au 2026-09-06 : ce rôle a été retiré le 2026-07-04,
supersédé par Postfix.)*
---
@ -60,12 +64,18 @@ plan :
reconstruit à l'identique depuis le plan et le code. L'infrastructure n'est pas un
objet fragile patiemment bricolé — c'est un artefact reproductible.
> **Honnêteté sur le statut.** Une grande partie de l'écosystème est aujourd'hui
> *définie, codée et validée* (vérification de syntaxe, analyse statique) mais n'a pas
> encore été éprouvée sur des machines de production réelles. Le socle Debian durci et
> son modèle sont la fondation établie ; les services applicatifs sont prêts en tant
> que code et se déploient progressivement. Nous ne présentons jamais un service non
> déployé comme « en production ».
> **Honnêteté sur le statut — mise à jour le 2026-09-06.** Ce paragraphe disait, bien après
> que ce fut faux, que l'écosystème n'avait « pas encore été éprouvé sur des machines
> réelles ». Il l'a été : la flotte entière a été **rasée et remontée depuis zéro** le
> 2026-08-13, puis **deux fois le 2026-09-02** — 15/15 puis 14/14 machines, **zéro échec**,
> et la validation complète à zéro échec sur treize hôtes. La reconstruction n'est donc pas
> une promesse de conception : c'est une manœuvre exécutée, chronométrée et rejouée.
>
> Ce qui reste honnête à dire : la reconstruction prouve qu'un plan mène des machines nues à
> l'état voulu. Elle ne dit rien de la tenue d'un service **sous charge**, ni de sa mise à
> jour dans la durée — deux questions distinctes, et non traitées ici. Le degré de maturité,
> service par service, est tenu à jour dans `docs/catalogue-services.md`, et nous ne
> présentons jamais comme « en production » un service que ce tableau ne donne pas pour tel.
---
@ -100,7 +110,8 @@ Une trentaine de paramètres noyau resserrent le comportement du système et du
### 4. Pare-feu prêt, activé au bon moment
- **nftables** est installé et préparé sur le socle, mais **volontairement désactivé dans le modèle**. Il est activé sur les serveurs finaux avec des règles adaptées à leur rôle (politique par défaut « tout refuser » en entrée et en transit). Principe : on n'active jamais un pare-feu générique sans connaître la fonction réelle de la machine, pour ne pas couper l'accès par accident.
- **nftables** est **volontairement désactivé dans le modèle** — on n'active jamais un pare-feu générique sans connaître la fonction réelle de la machine — et **armé sur chaque serveur de la flotte**, en politique « tout refuser » par défaut.
- **Ses règles ne sont pas écrites à la main.** Chaque rôle déclare ce qu'il écoute et ce à quoi il se connecte ; le jeu de règles en est **dérivé**. Le même registre alimente le pare-feu de l'hyperviseur et la frontière du site : trois couches de filtrage qui ne *peuvent pas* se contredire, parce qu'elles descendent d'une seule décision. C'est aussi ce qui produit la **matrice d'accès réseau** exigée par un audit de sécurité — source, destination, port, chiffrement et *raison*, pour chaque flux autorisé.
### 5. Confinement et intégrité
@ -134,6 +145,8 @@ Au-delà des réglages machine, l'architecture elle-même est une mesure de séc
- **Certificats internes** : les services se font confiance via la PKI interne, sans exposer de secrets à des autorités externes.
- **Dépendances déclarées** : un service ne se déploie pas si ses prérequis (DNS, PKI…) ne sont pas réellement actifs — ce qui évite les états incohérents.
- **Source unique de vérité** : l'état voulu vit dans des registres lisibles, versionnés par Git, qui sert de filet en cas d'erreur.
- **Les sauvegardes sortent du site.** L'état non régénérable (clés de l'autorité, annuaire, bases, boîtes, forge) est sauvegardé **chiffré côté client** vers un dépôt hors-machine, puis emporté **hors du bâtiment**. Chaque nœud vérifie lui-même son propre dépôt distant : celui qui héberge les octets ne peut pas les lire, donc ne peut pas les juger.
- **Ce qu'on affirme, on le prouve.** Un harnais de **57 vérifications** rejoue à la demande ce que le dépôt promet et produit une pièce justificative datée ; dix « devis » interrogent en plus le système *déployé* pour confirmer qu'il ressemble à ce qui est déclaré.
---

View file

@ -204,21 +204,20 @@ la plus élégante, mais parce que les deux autres avaient cessé d'être dispon
`eregion`, hors flotte — n'a pas disparu : il alimente maintenant l'autorité. La dette a
changé de propriétaire, pas de nature. Elle appartient au SITE.
## État au 2026-08-28
## État — revu le 2026-09-06
Seule la forge porte cette déclaration complète (`serveur_ops`). Les autres services
mutualisés le sont par des mécanismes antérieurs, cohérents mais non uniformes. Les
aligner sur la forme ci-dessus reste à faire.
**Les deux pièces manquantes**, l'une et l'autre nommées plus haut :
Des trois manques que cette section listait au 2026-08-28, **deux sont comblés** :
- le **lien d'insémination** n'est pas déclaré — il existe pourtant, par le symlink
`instance` du runner du SITE, sans borne ni visibilité ;
- l'**instrument de preuve** de la dernière ligne — « le lien est coupé » — n'existe pas.
Tant qu'il manque, aucune émancipation ne peut être déclarée faite.
| Manque du 2026-08-28 | Aujourd'hui |
|---|---|
| l'**instrument de preuve** de la dernière ligne — « le lien est coupé » — n'existe pas | **`make emancipation-prouver`**, depuis le 2026-09-01 (§ ci-dessus). Il *coupe* au lieu de sonder, et rend les deux verdicts. |
| la séparation des voûtes est **organisationnelle, pas cryptographique** — un seul mot de passe ouvre celle du site et celles des tenants | **Séparé le 2026-08-28 même** : une voûte, **une clé** (`~/.config/setops-vault-<dépôt>`, `scripts/voutes.py`). L'ancien mot de passe unique n'ouvre plus rien. La séparation précédait nécessairement la distribution des clés aux runners : sinon, poser « la » clé sur le plus petit locataire lui donnait les secrets de l'hébergeur. |
| le **lien d'insémination** n'est pas déclaré | **Toujours vrai.** Il existe, par le symlink `instance` du runner du SITE, sans borne ni visibilité. C'est le manque qui reste. |
**Une limite mesurée le 2026-08-28, à connaître avant de s'appuyer sur cette frontière :**
la séparation des voûtes est **organisationnelle, pas cryptographique**. Les fichiers sont
bien séparés — le runner du SITE ne détient que `underlay.vault.yml`, vérifié sur la
machine — mais **un seul mot de passe ouvre la voûte du site et celles des tenants**. « Deux
pouvoirs, aucun omnipotent » décrit donc la répartition des fichiers, pas celle des clés.
*(Le paragraphe sur les voûtes se contredisait avec `scripts/voutes.py` — daté du même jour —
et celui sur l'instrument avec la section qui le décrit, deux écrans plus haut. Un document
qui se contredit lui-même à cette distance n'est plus lu comme une référence.)*

View file

@ -38,9 +38,26 @@ flux:
| `externe` | hors flotte (frontière publique — géré à l'OPNsense, pas dans le nœud) |
| `localhost` | boucle locale — aucune règle inter-nœud (nftables autorise `lo`) |
| `expositions` | dérivé des `expose:` des applications (cas de l'edge → backends) |
| `admin` | les **réseaux** d'administration (intrant `nftables_admin_ssh`) — l'exploitant n'est ni la flotte, ni l'Internet |
| `voisins_site` | les **autres tenants fédérés** de la même fabric (leurs supernets) — le voisinage, quatrième chemin d'arrivée |
| `fabric` | le **matériel de l'hébergeur** : hyperviseurs, frontière, commutateurs |
| `runner_site` | le **runner du site**, seul autorisé à matérialiser des VM |
| `derive` | résolu ailleurs que par le nœud (hors périmètre de sa règle) |
La résolution `pair → IP` réutilise le **plan** (registre IP/FQDN/zones) déjà en place.
> **Quatre de ces mots sont nés d'un piège, et pas d'un besoin de vocabulaire.**
> `voisins_site` (2026-08-24) : sans lui, le chaînage des caches d'artefacts n'aurait pu se
> déclarer qu'en `ingress` + `externe` — ce qui aurait **publié le cache à l'Internet
> entier**. `fabric` (2026-08-25) : le runner de site déclarait son API Proxmox en
> `externe`, or « externe » se rend par « tout sauf les espaces privés » et les hyperviseurs
> *sont* en RFC 1918 — la règle avait l'air d'ouvrir le flux et l'excluait. Un flux qui a
> l'air ouvert et qui ne l'est pas est pire qu'un flux fermé : il ne se cherche pas.
>
> Les cinq derniers ne rendent **pas des hôtes** : `admin`, `voisins_site`, `fabric` et
> `runner_site` rendent des CIDR ou des sources extérieures à l'écosystème, `derive` ne rend
> rien. Ils sont donc traités à part dans `scripts/resoudre_flux.py`.
## Génération
Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml` de tous ses rôles
(services + intégrations), résout les `pair`, et produit :
@ -50,14 +67,26 @@ Un **résolveur** (miroir de `instancier`) agrège, par serveur, les `flux.yml`
## Activation prudente
Activer nftables = **action destructive** (peut couper l'accès) → confirmation explicite +
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud). nftables reste *préparé mais
non activé* tant que le registre n'est pas complet et validé.
déploiement graduel (garder l'accès SSH/Ansible, tester par nœud).
## Séquence
> **Le pare-feu est armé sur la flotte depuis.** Ce paragraphe disait « nftables reste
> *préparé mais non activé* tant que le registre n'est pas complet et validé ». Le registre
> **est** complet — **P49** vérifie qu'il reproduit exactement ce que les `meta/flux.yml`
> déclarent — et `nftables_baseline_enabled` vaut `true` pour `hotes_actifs` dans le plan.
> Le rôle, lui, garde `false` par défaut : le pare-feu n'est pas une propriété du rôle,
> c'est une décision de l'instance. Le gabarit doré reste à `false`, et c'est voulu.
## Séquence — franchie
1. ✅ figer le schéma (ce doc) + **piloter** sur postgresql / client_metrique / nginx ;
2. le **résolveur** (agrégation → règles + registre) ;
3. **remplir** tous les rôles (large transcription du travail zéro-confiance déjà fait) ;
4. **générer** + registre d'audit ; puis **activer** nftables nœud par nœud.
2. ✅ le **résolveur** (`scripts/resoudre_flux.py`) — agrégation → règles + registre ;
3. ✅ **remplir** tous les rôles (transcription du travail zéro-confiance) ;
4. ✅ **générer** + registre d'audit (`docs/registre-flux.md`, gardé par **P49**) ; puis
**activer** nftables nœud par nœud — fait.
Ce qui a suivi la séquence n'y était pas prévu : le même registre alimente désormais aussi
le **pare-feu est-ouest de l'hyperviseur** (`make proxmox-fw-plan`, garde **P45**) et la
**frontière nord/sud OPNsense** (`make frontiere-plan`). Trois couches, une seule décision —
elles ne peuvent pas se contredire parce qu'elles dérivent de la même source.
## `poste: false` — un service publié qui ne s'adresse pas à un humain
@ -93,8 +122,9 @@ Le registre confondait deux situations :
| le rôle **ouvre** l'écoute | `serveur_dovecot` lie 12345 (SASL réseau) | absent |
| le rôle **décrit** celle d'un autre | `serveur_backup` emprunte le sshd de `serveur_debian` | `true` |
Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur
`backup-01` — serait signalée à tort. Une preuve qui crie sur un cas sain finit par être
Sans cette distinction, la seule co-location légitime de la flotte — `tcp/22` sur le
dépôt de sauvegarde, `site-backup-01` depuis que les écosystèmes déposent chez leur
hébergeur — serait signalée à tort. Une preuve qui crie sur un cas sain finit par être
ignorée, ce qui est pire que de ne pas l'avoir.
**Corollaire à retenir** : un port qu'on **subit** (le défaut amont d'un logiciel) doit être

View file

@ -65,7 +65,7 @@ mais elle décide de l'emplacement de chaque chose :
|---|---|---|
| plan des services, inventaire | le **tenant** | `OPS-<tenant>` |
| `underlay.yml` (fabric physique) | l'**hébergeur** | `OPS-<hébergeur>`, monté par symlink |
| `group_vars/opnsense.yml` (frontière) | l'**hébergeur** | idem — **une** frontière pour tous ses tenants |
| `opnsense.yml` (frontière) | l'**hébergeur** | idem — **une** frontière pour tous ses tenants |
Conséquence pratique : `make instance-utiliser` bascule le **tenant** actif, jamais
l'hébergeur. Le devis lit donc la fabric et les intrants de frontière chez l'hébergeur, quel
@ -84,7 +84,7 @@ silence : chemin présent, politique absente, exactement le mode de panne du 202
Les alias d'hôtes sont préfixés du tenant (`SETOPS_CHEZ17_SERVEUR_NGINX`), et surtout
**chaque tenant a son propre alias d'administration** : `SETOPS_ADMIN_CHEZ17` n'ouvre que
`10.27.0.0/16`. Une union aurait laissé le plan de gestion d'un tenant entrer chez le voisin
`10.17.0.0/16`. Une union aurait laissé le plan de gestion d'un tenant entrer chez le voisin
— ce que les ACL de switch interdisent par ailleurs. La bordure ne doit pas rouvrir ce que
l'isolation inter-tenant ferme.
@ -126,7 +126,9 @@ règle) et un tenant dont `nftables_admin_ssh` est vide (règle SSH omise — l'
exposerait le SSH à Internet).
Le registre des flux distingue les pairs par **mot-clé**. Or `resoudre_flux.py` **saute
volontairement** le pair `externe` (`scripts/resoudre_flux.py:184`) : ces flux-là ne
volontairement** le pair `externe` (fonction de résolution des pairs, aux côtés de
`localhost`, `expositions` et `derive` — chercher le nom, pas un numéro de ligne : celui
qui figurait ici avait vieilli de quarante-sept lignes) : ces flux-là ne
concernent pas le pare-feu d'hôte, ils relèvent de la bordure. Plusieurs `raison` le disent
déjà noir sur blanc — « Frontière publique gérée à l'OPNsense ».
@ -165,9 +167,9 @@ alors **deux règles et deux alias**, chacun ne portant que les sources qui peuv
emprunter ce chemin.
```
SETOPS_ADMIN_CHEZ17_GESTION 10.0.0.0/24 → pass in on lan
SETOPS_ADMIN_TECH11_GESTION 10.0.0.0/24 → pass in on lan
SETOPS_ADMIN_TECH11_WAN 192.168.255.2/32, 192.168.254.2/32 → pass in on wan
SETOPS_ADMIN_CHEZ17_GESTION 10.17.0.0/24 → pass in on lan
SETOPS_ADMIN_TECH23_GESTION 10.17.0.0/24 → pass in on lan
SETOPS_ADMIN_TECH23_WAN 192.168.255.2/32, 192.168.254.2/32 → pass in on wan
```
**Le piège de la case à cocher.** Une source **RFC1918** qui arrive par le WAN se heurte à
@ -179,13 +181,13 @@ entre par la gestion affaiblirait l'interface publique sans rien ouvrir du tout.
**Invariant du dernier octet.** Un point de routage porte **le même dernier octet sur tous
les sous-réseaux où il participe** — on retient une adresse, pas treize. `le commutateur` est
donc `.1` partout : `10.0.0.1`, `10.27.16.1`, `10.27.21.1`… Le chiffre n'est pas codé en dur,
donc `.1` partout : `10.17.16.1`, `10.17.21.1`… Le chiffre n'est pas codé en dur,
il vient de `reservations.passerelle` dans la nomenclature, et `make underlay` (**preuve
P23**) refuse une passerelle qui s'en écarte.
Seule exception, assumée : les liens plus étroits qu'un `/24`. Sur le `/29` de transit,
l'adressage est dicté par les participants du lien — les deux frontières occupent `.1` et
`.2`, le switch prend `.6`.
Seule exception, assumée : le **lien de transit**, dont l'adressage est dicté par ses
participants — les deux frontières occupent `.1` et `.2`, les hyperviseurs `.41`, `.43` et
`.47`.
## 5. La garde anti-lockout
@ -210,7 +212,7 @@ peut dériver d'aucun `index`. Sa place est l'underlay, cluster-global, au même
management, l'iSCSI et Ceph.
**Une route par sous-réseau attribué, jamais une par supernet** (2026-08-09). Router
`10.27.0.0/16` faisait porter à la frontière des destinations qui n'existent nulle part :
`10.17.0.0/16` faisait porter à la frontière des destinations qui n'existent nulle part :
elles atteignaient le nœud de sortie, y arrivaient dans la table *principale* — le VRF n'est
atteint que par les `/24` annoncés en BGP — et repartaient vers la passerelle
d'administration. Les alias `SETOPS_TENANT_*` énumèrent exactement les mêmes `/24`, et
@ -228,20 +230,24 @@ sur ce lien :
```yaml
- nom: transit-frontiere
vlan: 40
sous_reseau: 10.0.4.0/29
passerelle: 10.0.4.6 # SVI du switch L3 (le commutateur)
sous_reseau: 10.0.4.0/24
passerelle_sortie: 10.0.4.1 # bifrost-1 = sortie par défaut de la flotte
```
**Plan du `/29`.** Les frontières occupent le bas de la plage, le SVI du switch le haut :
**Le lien est passé d'un `/29` à un `/24`, et ce n'est pas cosmétique.** En SDN, les
**hyperviseurs** sont eux-mêmes sur ce lien — ce sont eux les nœuds de sortie des VRF, ce
n'était plus le SVI d'un commutateur. Huit adresses n'y suffisaient plus.
| Adresse | Qui |
|---|---|
| `10.0.4.1` | `bifrost-1` — frontière active, sortie par défaut de la flotte |
| `10.0.4.2` | `bifrost-2` — seconde frontière |
| `10.0.4.3` | libre, réservée à une IP virtuelle CARP si les deux passent en HA |
| `10.0.4.4-.5` | libres |
| `10.0.4.6` | SVI du switch routeur (`le commutateur`) |
| `10.0.4.41 / .43 / .47` | `asgard`, `gandalf`, `vishnu` — les hyperviseurs, via `bond3` |
*(Ce bloc annonçait `10.0.4.0/29` et une `passerelle: 10.0.4.6` — le SVI du commutateur —
jusqu'au 2026-09-06. Le commutateur ne route plus les tenants : il transporte du VXLAN
qu'il ne lit pas.)*
Le jour où les deux OPNsense passent en haute disponibilité, `passerelle_sortie` devra
pointer sur l'**IP virtuelle CARP** et non sur un boîtier nommé — c'est le seul changement
@ -318,7 +324,7 @@ Même partage que Proxmox — l'anodin en clair, le secret dans la voûte :
| Quoi | Où | Réglable |
|---|---|---|
| URL de gestion, interfaces, prochain saut | `group_vars/opnsense.yml` (en clair) | **panneau « Intrants de base » du GUI**, section *Frontière* |
| URL de gestion, interfaces, prochain saut | `opnsense.yml` (en clair) | **panneau « Intrants de base » du GUI**, section *Frontière* |
| Clé et secret d'API | voûte unique de l'instance, sous `vault_opnsense_api_key` / `vault_opnsense_api_secret` | `ansible-vault edit` |
Les valeurs non sensibles sont de **vrais intrants** : `opnsense_api_url`,

View file

@ -32,14 +32,25 @@ qu'on touche à son plan — c'est ce qui rend la portabilité possible.
> **Un tenant ne détient jamais un secret du monde physique.**
Aujourd'hui, chaque tenant porte dans sa voûte le jeton d'API du cluster. Patient 0 a dû le
recopier pour exister. C'est exactement la faute des **neuf copies** de la résolution
d'instance, appliquée aux secrets : une valeur qui vit à N endroits finit par diverger, et
on ne peut plus révoquer l'une sans révoquer les autres.
Chaque tenant a porté dans sa voûte le jeton d'API du cluster — patient 0 a dû le recopier
pour exister. C'était exactement la faute des **neuf copies** de la résolution d'instance,
appliquée aux secrets : une valeur qui vit à N endroits finit par diverger, et on ne peut
plus révoquer l'une sans révoquer les autres.
Conséquence : **l'underlay a sa propre voûte**, chez l'hébergeur, à côté d'`underlay.yml`.
Les opérations qui parlent au matériel — cloner une VM, poser une zone SDN, écrire sur la
frontière — l'y lisent. Les tenants n'y ont pas accès et n'en ont pas besoin.
**C'est réglé depuis le 2026-08-22** : l'underlay a **sa propre voûte**
(`underlay.vault.yml`), chez l'hébergeur, à côté d'`underlay.yml`. Les opérations qui parlent
au matériel — cloner une VM, poser une zone SDN, écrire sur la frontière — l'y lisent : le
chemin se **dérive** du symlink `underlay.yml`, sans rien redéclarer
(`playbooks/proxmox/cloner_vm_debian.yml`). Les tenants n'y ont pas accès et n'en ont pas
besoin.
Deux compléments arrivés depuis, et qui achèvent la séparation :
- les **clés** aussi sont séparées (2026-08-28) : une voûte, **une clé**. Tant qu'un seul mot
de passe les ouvrait toutes, la séparation était organisationnelle, pas cryptographique ;
- **P25** refuse qu'une clé de l'hébergeur — nœuds, stockages, ponts, API — réapparaisse dans
un `group_vars` de tenant. La garde attrape la rechute : un `make config` lancé d'un autre
poste, une reprise à la main.
---
@ -101,12 +112,20 @@ qui répond. Deux précautions y sont inscrites, toutes deux apprises en l'écri
---
## Ce qui reste à faire, dans l'ordre
## La séquence — faite, dans cet ordre
- [ ] **Reconnaître** : `make underlay-plan`, puis écrire dans `underlay.yml` ce qui est —
pas ce qui était prévu. Chaque réseau porté et non déclaré est un réseau que le
moteur ne sait pas classer.
- [ ] **Séparer les voûtes** : créer celle de l'underlay, y déplacer le jeton Proxmox et la
clé de la frontière, les retirer des voûtes de tenants.
- [ ] **Alors seulement**, appliquer la frontière — ses règles dépendent des deux points
ci-dessus.
Cette section était une liste de tâches ouvertes. **Les trois sont faites** ; on la garde
sous forme d'ordre parce que c'est l'ordre lui-même qui est la leçon — un site neuf le
rejouera tel quel.
- [x] **Reconnaître** — `make underlay-plan` confronte l'underlay *déclaré* au réel (API du
cluster + sondes). `underlay.yml` décrit ce qui **est**, pas ce qui était prévu :
chaque réseau porté et non déclaré est un réseau que le moteur ne sait pas classer.
- [x] **Séparer les voûtes** — fait le **2026-08-28**. Une voûte, une clé
(`scripts/voutes.py`, `ANSIBLE_VAULT_IDENTITY_LIST`) : le jeton Proxmox et la clé de
la frontière vivent dans `underlay.vault.yml`, hors de toute voûte de tenant. La
séparation devait précéder la distribution des clés aux runners — sans elle, poser
« la » clé sur le plus petit locataire lui donnait les secrets de l'hébergeur.
- [x] **Appliquer la frontière** — ses règles dépendent des deux points ci-dessus, et
elles en dérivent : P43 mesure que le devis de la frontière retrouve les machines du
plan (7 machines, 104 règles du site).

View file

@ -34,15 +34,23 @@ poste de l'opérateur et sa forge. Rien à changer.
## 3. Ce qui n'a pas de maison aujourd'hui
Constaté le 2026-08-04 : **aucun équipement de l'hébergeur n'est dans un inventaire
Ansible**, et rien ne sauvegarde leurs configurations.
Constaté le 2026-08-04, **révisé le 2026-09-05**. La moitié du constat a été levée
entre-temps ; l'autre tient toujours, et il faut distinguer les deux.
**Ce qui a une maison désormais** : les **VM du site** ont leur inventaire Ansible
(`scripts/site_inventaire.py` — 7 machines, 23 groupes, dérivé d'`underlay.yml` sans
fichier intermédiaire), leur socle, leur durcissement, leurs sauvegardes et leur
supervision (`site-mon-01`, depuis le 2026-09-02).
**Ce qui n'en a toujours pas** : les **équipements** eux-mêmes — hyperviseurs,
commutateurs, frontière. C'est là que le constat d'origine reste entier.
| Besoin | État |
|---|---|
| Supervision des hyperviseurs, commutateurs, frontière | personne |
| Supervision des hyperviseurs, commutateurs, frontière | personne — `site-mon-01` ne voit que les VM du site |
| Journaux de ces équipements | personne |
| Sauvegarde de leurs configs (`running-config`, `config.xml`, `/etc/pve`) | personne |
| Résolution des noms d'underlay (`asgard`, `bifrost-3`, `bifrost-1`) | personne |
| Sauvegarde de leurs configs (`running-config`, `config.xml`, `/etc/pve`) | personne — **vérifié le 2026-09-05, aucun rôle ne les touche** |
| Résolution des noms d'underlay (`asgard`, `bifrost-3`, `bifrost-1`) | personne — **vérifié : `site-dns-01` ne les résout pas** |
| Certificats pour leurs interfaces web | personne |
Ce n'est pas un oubli de conception : ces besoins tombaient entre les chaises. Ils ne sont
@ -54,7 +62,9 @@ d'aucun tenant, et `underlay.yml` ne décrit que du matériel, sans service.
`proxmox-hebergeur.yml`. Ses services d'exploitation lui appartiennent au même titre que
sa fabric ; les loger ailleurs recréerait la confusion qu'on vient de défaire.
Ce dépôt n'a pas d'inventaire Ansible aujourd'hui : c'est ce que le chantier ajoutera.
Ce dépôt **a désormais son inventaire Ansible** — `scripts/site_inventaire.py`, dynamique
plutôt que généré, parce qu'un site ne dérive de rien : sa déclaration *est* déjà sa forme
finale. Ce qui reste à faire, c'est d'y loger les **équipements**, qui n'y sont pas.
**Rattachement réseau : un pont VLAN ordinaire, jamais un VNet du SDN.** C'est la
contrainte qui découle du §1, et la seule qui distingue ces VM de celles d'un tenant.
@ -143,30 +153,48 @@ emporter — mais il ne devrait pas non plus les nommer. L'interface normalisée
`proxmox_noeuds` et `proxmox_stockages` sont déjà chez lui ; il manque la classe, pas le
catalogue.
## 8. Un dépôt à part pour le réseau (décidé, non fait)
## 8. Un dépôt à part pour l'hébergeur — FAIT
`underlay.yml` et `proxmox-hebergeur.yml` vivent aujourd'hui dans `OPS-Chezlepro`, qui est
aussi le dépôt du **tenant** Chezlepro. C'est ce qui a permis de démarrer, et c'est ce qui
oblige, à chaque commit, à trancher si l'on touche à l'infrastructure ou à l'organisation.
> **Cette section disait « Rien n'est fait » jusqu'au 2026-09-06.** Le dépôt existe :
> **`SITE-Chezlepro`**, frère de `OPS-Chezlepro`, et le symlink `underlay.yml` du moteur y
> pointe (`../SITE-Chezlepro/underlay.yml`).
Ils décrivent des objets différents : l'un une **infrastructure** — des câbles, des VLAN,
un cluster —, l'autre une **organisation** — ses serveurs, ses applications, ses comptes.
Un tenant peut déménager ; une fabric ne déménage pas.
Le raisonnement qui l'a motivé, et qui vaut pour tout hébergeur : `underlay.yml` et
`proxmox-hebergeur.yml` décrivent une **infrastructure** — des câbles, des VLAN, un
cluster ; le plan d'un tenant décrit une **organisation** — ses serveurs, ses applications,
ses comptes. Un tenant peut déménager ; une fabric ne déménage pas. Les garder dans le même
dépôt obligeait, à chaque commit, à trancher lequel des deux on touchait.
Le dépôt réseau de l'hébergeur porterait donc `underlay.yml`, `proxmox-hebergeur.yml`, et
plus tard l'inventaire des services d'exploitation (§4). Le symlink qui désigne
l'hébergeur pointerait vers lui plutôt que vers son tenant — ce qui rendrait enfin la
distinction visible dans les chemins eux-mêmes.
Ce que le dépôt de l'hébergeur porte aujourd'hui :
**Rien n'est fait.** La bascule demande de déplacer deux fichiers, de refaire le symlink,
et de vérifier que les trois générateurs qui les lisent suivent.
| Fichier | Ce qu'il décrit |
|---|---|
| `underlay.yml` | la fabric physique — réseaux, VLAN, MTU, commutateurs, hyperviseurs |
| `proxmox-hebergeur.yml` | l'API du cluster, ses nœuds, ses stockages, ses ponts |
| `opnsense.yml` | la frontière nord/sud (paramètres non sensibles) |
| `underlay.vault.yml` | ses secrets à lui — **voûte séparée**, clé séparée (2026-08-28) |
| `plan/` | ses **propres VM** : sept machines de service (pilotage, autorité, génome, cache, sauvegarde, supervision, DNS) |
## 9. Le VLAN de gestion, et ce qu'il n'est pas
C'est cette dernière ligne qui a le plus changé : l'hébergeur n'est plus seulement un
porteur de fabric, **c'est un exploitant** — avec son inventaire (`scripts/site_inventaire.py`),
son socle, son durcissement, ses sauvegardes et sa supervision.
`10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB/IPMI**. Accès
sysadmin uniquement.
## 9. Le plan d'administration, et ce qu'il n'est pas
Aucun hyperviseur n'y a d'adresse, aucune VM n'y est branchée, aucun trafic tenant ne le
traverse — ni encapsulé, ni décapsulé. C'est précisément la raison d'être des VLAN 11
(transport VXLAN) et 40 (sortie tenant) : les avoir sortis de ce domaine de diffusion.
> **Les adresses de cette section ont toutes changé** avec la bascule D-77/D-78, terminée le
> 2026-08-22. Elle désignait `10.0.0.0/24` et les « VLAN 11 (transport VXLAN) et 40 (sortie
> tenant) » ; `10.0.0.0/24` n'existe plus, et le transport est passé au VLAN 50.
Le plan d'**administration** — `10.17.0.0/24`, dans la bande basse du supernet du tenant
Chezlepro (D-77) — est réservé aux **équipements** et à l'exploitant. Il n'a **aucun VLAN** :
c'est un segment physique, et **aucun pont d'hyperviseur ne le touche**, donc aucune VM ne
peut y naître. Le validateur refuse d'ailleurs qu'on y déclare une machine.
Distinct de lui, le **contrôle de la grappe** (`192.168.11.0/24`, `vmbr0`) porte l'interface
web de Proxmox et le dialogue entre nœuds.
Aucun trafic tenant ne traverse ni l'un ni l'autre — ni encapsulé, ni décapsulé. C'est
précisément la raison d'être du **VLAN 50** (transport VXLAN) et du **VLAN 40** (transit vers
la frontière) : les avoir sortis de ces domaines de diffusion. *(Le transport a porté le
numéro 11 jusqu'à la bascule ; `192.168.11.0/24` étant pris par le contrôle de la grappe, il
est passé au 50.)*

View file

@ -32,9 +32,15 @@
|---|---|---|
| **Apps web** (Forgejo, Grafana, Nextcloud…) | **Keycloak OIDC** (SSO) | un seul login, MFA, jetons |
| **Mail** (Dovecot, Postfix) | **LDAP direct** (bind) | IMAP/SMTP ne parlent pas OIDC ; username + mot de passe |
| **Système / services** (SSSD, etc.) | **LDAP direct** | annuaire standard |
| **App web SANS OIDC natif** | **`oauth2-proxy` devant**, lui-même client OIDC | met au SSO ce qui ne sait pas y aller seul |
| Tout | → **même OpenLDAP** | identité unifiée, aucun compte en double |
> **L'ouverture de session Unix n'est PAS dans ce tableau, et c'est délibéré.** Cette ligne
> annonçait « Système / services (SSSD, etc.) → LDAP direct » jusqu'au 2026-09-06. Le dépôt
> ne fait pas ça : le rôle `client_ldap` a été **retiré le 2026-07-04, hors conception**, et
> aucun rôle n'installe SSSD. L'annuaire sert les **applications** ; l'accès à un hôte se
> fait par **clé SSH**, et l'élévation par `sudo`. Voir `docs/authentification.md` §2.
## Sens de provisionnement
Les identités se créent et se gèrent dans **OpenLDAP**. Keycloak **fédère** (lit LDAP en
@ -54,7 +60,10 @@ Keycloak : c'est LDAP la référence. Le mail bind directement sur LDAP.
## Conséquences pour Set-OPS
- `serveur_openldap` (déployé) = le pilier annuaire, source de vérité.
- `serveur_keycloak` = pilier SSO, à **fédérer sur OpenLDAP** (User Federation → LDAP).
- `serveur_keycloak` = pilier SSO, **fédéré sur OpenLDAP** — et la fédération est
*automatisée*, pas un geste manuel : `roles/serveur_keycloak/tasks/federation-ldap.yml`
(avec ses mappeurs d'attributs, ses groupes et sa politique de mot de passe dans les
tâches voisines). `make identite-plan` relit ensuite ce qui est réellement en place.
- `serveur_dovecot` / `serveur_postfix` = auth **LDAP direct** (cohérent avec ce modèle).
- Les apps web déclarées dans le plan → clients **OIDC** de Keycloak.

View file

@ -27,7 +27,7 @@ par une **commande qui interroge le système**, jamais par une conviction.
## Phase 0 — au bureau, avant de partir
- [ ] **Recevoir la fiche de l'hébergeur** — les huit lignes du §7 de
- [ ] **Recevoir la fiche de l'hébergeur** — les dix lignes du §7 de
[`preparer-un-site-hebergeur.md`](preparer-un-site-hebergeur.md). Sans le nom exact
du nœud et des stockages, la journée s'arrête à la phase 1.
- [ ] **Fixer l'`index` du site = l'`index` de son tenant.** Il n'y a pas de second
@ -44,11 +44,13 @@ par une **commande qui interroge le système**, jamais par une conviction.
moteur de pare-feu.
> **Un site neuf se construit d'emblée dans l'adressage cible** — gestion en
> `10.<index>.0.0/24`, chemins en `192.168.<vlan>.0/24` (D-77, D-78). Le site historique
> est encore en `10.0.x` et migrera par [`runbooks-exploitation.md`](runbooks-exploitation.md) §6.
> Sur un site vierge, la cible ne coûte rien — et deux sites en `10.0.0.0/24` rendraient
> la **reprise mutuelle impossible** : deux plans de gestion identiques ne peuvent pas
> s'atteindre.
> `10.<index>.0.0/24`, chemins en `192.168.<vlan>.0/24` (D-77, D-78). Sur un site vierge, la
> cible ne coûte rien — et deux sites qui porteraient le **même** plan de gestion rendraient
> la **reprise mutuelle impossible** : deux réseaux identiques ne peuvent pas s'atteindre.
>
> *(État du site historique, mesuré le 2026-09-06 : sa gestion est **déjà** en `10.17.0.0/24`
> et son transport VXLAN en `192.168.50.0/24` ; le transit et le stockage restent dans
> l'ancien espace. Ce paragraphe le donnait entièrement « encore en `10.0.x` ».)*
---
@ -203,17 +205,17 @@ make frontiere-plan make sdn-plan make placement-plan
```yaml
---
underlay:
# LE SEED DU SITE — le même que celui de son tenant, et la clé qu'on oublie.
# C'est elle qui dit au validateur que 10.<index>.0.0/16 est SON supernet, donc que
# la bande basse lui appartient. Sans elle, `make underlay` refuse le réseau de
# gestion en le prenant pour celui d'un AUTRE site — message déroutant, cause triviale.
index: <index>
# LES TENANTS QUE CE SITE PORTE — noms de dossier, pas de fantaisie. Les trois devis
# d'équipement (frontière, commutateur, SDN) découvrent TOUTE la fédération : sans
# cette clé, le second site se voit proposer les règles, les VLAN et les zones du
# premier. Le matériel les accepte, aucune ne correspond jamais à un paquet, et rien
# ne le signale. Un site UNIQUE n'a rien à déclarer ; c'est le second qui se nomme.
tenants: [OPS-<tenant>]
# UN SITE N'A PAS D'INDEX (depuis le 2026-08-25). Il en portait un ; c'était un vestige.
# Un site ne dérive AUCUN adressage — ses machines vivent sur des réseaux de fabric.
# Cette valeur ne disait qu'une chose : quel supernet de tenant est le sien. Et elle le
# disait pour le site ENTIER alors qu'UN SEUL réseau est concerné.
#
# LES TENANTS QUE CE SITE PORTE — nom de dossier -> index. Les trois devis d'équipement
# (frontière, commutateur, SDN) découvrent TOUTE la fédération : sans cette clé, le
# second site se voit proposer les règles, les VLAN et les zones du premier. Le matériel
# les accepte, aucune ne correspond jamais à un paquet, et rien ne le signale.
tenants:
OPS-<tenant>: <index>
routeur: <nom-du-commutateur> # racine du spanning-tree, pas un routeur
# `stp` exige `routeur` : ne pas le déclarer tant qu'aucun commutateur ne l'est.
routage_tenants: sdn # le routage inter-zone vit sur l'hyperviseur
@ -222,7 +224,12 @@ underlay:
dialecte: cisco # cisco | binardat — propriété du MATÉRIEL
stp: { mode: mstp, topologie: etoile }
reseaux:
- { nom: management, vlan: 10, sous_reseau: 10.<index>.0.0/24, passerelle: 10.<index>.0.1, mtu: 1500 }
# `bande_basse_de` DÉCLARE le chevauchement VOULU : ce /24 vit dans le /16 du tenant
# nommé, dans la bande 0-15 que ses zones (3e octet >= 16) n'allouent jamais. Sans
# cette clé, `make underlay` refuse le réseau — et il a raison : il ne peut pas
# distinguer un chevauchement voulu d'un accident. Le nom est vérifié contre `tenants:`,
# donc une faute de frappe est refusée.
- { nom: management, vlan: 10, bande_basse_de: OPS-<tenant>, sous_reseau: 10.<index>.0.0/24, passerelle: 10.<index>.0.1, mtu: 1500 }
- { nom: transit-frontiere, vlan: 40, sous_reseau: 192.168.40.0/24, passerelle_sortie: 192.168.40.1, mtu: 1500 }
- { nom: underlay-vxlan, vlan: 50, sous_reseau: 192.168.50.0/24, mtu: 1500 }
# Stockage : uniquement les réseaux réellement câblés (fabric: stockage, mtu 9000).

View file

@ -7,9 +7,11 @@
Une intégration est de l'un des deux genres, et ils ne se déclarent pas au même endroit.
**Universelle** — supervision, journaux, PKI. Il n'y a aucun choix de cible : un seul
Prometheus, un seul Loki, une seule AC. Elle est déclarée **une fois, par le rôle**, dans
`roles/<role>/meta/integration.yml`, et tout hôte la reçoit :
**Universelle** — métriques, journaux, PKI, **résolution** et **source d'artefacts**
(les cinq qui portent `universelle: true`). Il n'y a aucun choix de cible : un seul
Prometheus, un seul Loki, une seule AC, un seul résolveur, un seul cache. Elle est déclarée
**une fois, par le rôle**, dans `roles/<role>/meta/integration.yml`, et tout hôte la
reçoit :
```yaml
integration:
@ -18,8 +20,16 @@ integration:
sauf_role: serveur_step_ca # facultatif — voir « exemptions »
```
**Facultative** — `client_backup`, `client_smtp`, `client_resolveur`. Là il y a un vrai choix,
et il se déclare par serveur, dans `plan/serveurs.yml : integrations`.
**Facultative** — `client_backup` et `client_smtp`. Là il y a un vrai choix, et il se
déclare par serveur, dans `plan/serveurs.yml : integrations`.
> **`client_resolveur` a changé de camp le 2026-08-24, et ce document l'annonçait encore
> facultatif.** Il posait alors un Unbound sur chaque VM — un vrai coût, donc un vrai choix.
> Il n'installe plus rien : il écrit `/etc/resolv.conf` pour désigner le résolveur du
> tenant. Son intégration est devenue **universelle, sans aucune exemption — pas même
> l'hôte qui porte le résolveur** : il se sert lui-même, et l'exempter reviendrait à dire
> que le résolveur ne se fait pas confiance. Une machine restée sur la résolution
> d'amorçage envoie chacune de ses questions dehors, sans que rien ne le signale.
**Pourquoi cette inversion.** Le plan portait 57 lignes d'intégration écrites à la main. 28
d'entre elles disaient oui à quelque chose de vrai pour tous les hôtes — elles n'existaient

View file

@ -9,14 +9,34 @@ de l'écosystème, en distinguant **constantes** et **défauts surchargeables**.
> au même titre que le [dimensionnement](dimensionnement-ressources.md). Décidée le
> 2026-06-26.
> **Statut, revu le 2026-09-06 : le panneau est construit.** Ce document reste la note de
> *conception* — il explique les arbitrages, pas l'état. Trois écarts entre ce qui était
> proposé et ce qui a été fait, et ils comptent :
>
> 1. **La migration en répertoires n'a eu lieu que pour `all/`.** `group_vars/all/` porte
> bien `00-instance.yml` (tenu à la main) et `10-intrants.yml` (écrit par le GUI) ;
> `proxmox.yml` et `modeles_vm.yml` sont restés des **fichiers plats**. Le §3 les
> présente encore comme des répertoires.
> 2. **Les constantes Proxmox ont déménagé chez l'hébergeur.** L'accès au cluster
> (`proxmox_api_*`) n'est plus un intrant du tenant : il vit dans
> `proxmox-hebergeur.yml`, à côté d'`underlay.yml`, parce qu'un cluster appartient à
> qui possède le matériel. Cf. `config-proxmox.md`.
> 3. **Les « points ouverts » du §8 sont tranchés** par ce qui a été bâti : la liste de
> rappel des secrets attendus est bien dans le panneau, en lecture seule, et elle se
> *recense* (`scripts/voute.py lister`) au lieu d'être recopiée — trois copies manuelles
> avaient existé, toutes avaient divergé.
## 1. Décisions cadre (validées)
1. **Secrets : hors périmètre.** Le GUI n'affiche ni ne stocke aucun secret. Les
`vault_*` et tokens Proxmox restent édités via Ansible Vault en ligne de commande.
Le panneau peut, au plus, afficher une **liste de rappel en lecture seule** des
secrets attendus (sans valeur).
2. **Nomenclature : lecture seule** dans le panneau. L'édition de `supernet`/VLAN/
catégories reste dans `plan/nomenclature.yml` (autorité unique du plan réseau).
2. **Nomenclature : lecture seule** dans le panneau — à une exception près, l'**`index`**,
qui est le seul champ d'adressage saisissable (panneau *Réseau*, écrit chirurgicalement).
Tout le reste — supernet, sous-réseaux, passerelles, VLAN, VMID — se **dérive** et **P20
refuse qu'on l'écrive**. *(Ce point disait « l'édition de `supernet`/VLAN/catégories reste
dans `plan/nomenclature.yml` » : ces valeurs n'y sont plus du tout.)*
3. **Conception avant code** (cette note).
## 2. Modèle : constante vs défaut surchargeable
@ -88,9 +108,10 @@ supporté) ; par groupe, dans le `group_vars/<groupe>/` correspondant.
- **Suite** : politiques de durcissement (défauts par groupe), DNS internes, relais
SMTP, endpoints services centraux.
## 8. Points ouverts à confirmer
- OK pour la **migration `group_vars/*.yml` → répertoires** (§3) ? (alternative :
réécrire les fichiers existants en bloc, au prix des commentaires).
- Le panneau affiche-t-il la **liste de rappel des secrets attendus** (lecture seule),
ou on n'en parle pas du tout dans le GUI ?
- Périmètre MVP (§7) suffisant pour une première itération ?
## 8. Points ouverts — tranchés par ce qui a été construit
| Question de juin | Réponse, telle que le code la donne |
|---|---|
| Migrer `group_vars/*.yml` → répertoires ? | **Pour `all/` seulement.** `proxmox.yml` et `modeles_vm.yml` sont restés plats — la migration n'a payé que là où le GUI écrivait vraiment. |
| Afficher la liste de rappel des secrets ? | **Oui, en lecture seule** — et *recensée*, jamais recopiée (`scripts/voute.py lister`, gardée par **P18**). |
| Périmètre MVP suffisant ? | Oui, et il a été dépassé : la couverture du GUI est désormais **prouvée** par **P19**, qui refuse un champ du plan que la console ne saurait pas éditer. |

View file

@ -18,14 +18,22 @@ fois**, depuis un endroit unique, puis les laisser se **dériver** ou se **propa
- `setops_plan_dir` — chemin du plan.
### B. Réseau & nomenclature — `plan/nomenclature.yml`
- `supernet`, `cidr_hote`, `reservations`, `categories` (VLAN/sous-réseau/passerelle),
`fonctions` (catégorie + service).
- **`index`** — le **seed**, et le seul champ d'adressage. Supernet, sous-réseaux,
passerelles, VLAN et VMID en **dérivent** ; la preuve **P20** refuse qu'on les y écrive.
- `cidr_hote`, `reservations`, `categories` (libellés de zones), `fonctions`
(catégorie + service).
> Ce paragraphe listait `supernet` comme un intrant de la nomenclature jusqu'au
> 2026-09-06. C'est exactement ce que **P20 interdit** : un adressage stocké est un
> adressage qui peut contredire celui qu'on dérive.
### C. Hyperviseur Proxmox — `group_vars/proxmox.yml`
- `proxmox_api_host`, `proxmox_api_user`, `proxmox_api_port`, `proxmox_validate_certs`.
- 🔒 `proxmox_api_token_id`, `proxmox_api_token_secret` — dans la **voûte unique** de
l'instance, `group_vars/all/vault.yml`. (L'ancienne `proxmox.vault.yml` reste lue en
compatibilité si elle existe encore ; cf. `docs/config-proxmox.md`.)
l'instance, `group_vars/all/vault.yml`. **`proxmox.vault.yml` n'est plus lue** (retirée le
2026-08-03) : tolérée « en compatibilité », elle était restée le *seul* porteur du jeton
chez un tenant — et comme `*.vault.yml` est gitignoré, ce jeton ne voyageait avec aucun
dépôt. Une voûte unique qui ne l'était pas. Cf. `docs/config-proxmox.md`.
- Golden template : `proxmox_clone_vmid_modele`, `proxmox_clone_source_nom`.
- Placement par défaut : `proxmox_clone_noeud`, `proxmox_clone_stockage`,
`proxmox_clone_pont`, format, complet, timeout, disque, interface, démarrer.
@ -40,7 +48,7 @@ nftables baseline · fail2ban SSH · auditd · AppArmor · sysctl · unattended-
journald (rétention) · core_dumps · systemd_ssh_auto.
### F. Endpoints des services centraux (les rôles `client_*` en dérivent)
- DNS interne : plancher `/etc/hosts` (`hosts_statiques`) + PowerDNS + `client_resolveur` (opt-in).
- DNS interne : plancher `/etc/hosts` (`hosts_statiques`) + PowerDNS (autoritatif) + `serveur_resolveur` (LE récursif du tenant), désigné sur chaque nœud par `client_resolveur` — intégration **universelle**, pas opt-in.
- AC/PKI (`client_pki_ca_url` → infra-pki, provisioner).
- IdM/LDAP : annuaire résolu par `resoudre_annuaire` (hôte + base DN dérivés du domaine).
- Relais courriel (`client_smtp_relais` → edge-mta, MTA Postfix, port 25).
@ -84,13 +92,13 @@ domaines publics, `edge`, autorité DNS, FQDN exposés.
| Intrant | Classe | Surcharge où ? |
| --- | --- | --- |
| `domaine_interne` | **Constante** | — |
| Nomenclature (supernet, CIDR, catégories, fonctions) | **Constante** | — (le plan réseau est la loi) |
| Nomenclature (**`index`**, CIDR d'hôte, catégories, fonctions) | **Constante** | — (le seed est la loi ; l'adressage en dérive) |
| Accès Proxmox (API host/user/port/token) | **Constante** | — (un seul cluster) |
| Golden template (vmid_modele, source_nom) | **Constante** | — |
| Secrets Vault | **Constante** 🔒 | — (gérés à part, jamais en clair) |
| `fuseau_horaire` | Défaut | par hôte (rare) |
| `proxmox_clone_noeud` / `stockage` / `pont` | Défaut | par hôte (`serveurs.yml`) |
| DNS internes (plancher `/etc/hosts` + PowerDNS + `client_resolveur`) | Défaut | par hôte / groupe |
| DNS internes (plancher `/etc/hosts` + PowerDNS) | Défaut | par hôte / groupe |
| Politiques durcissement (SSH, nftables, fail2ban, journald…) | Défaut | par hôte / groupe |
| Relais SMTP, TLS internes | Défaut | par hôte / groupe |
| `ciuser`, compte `ansible` | Défaut | rarement surchargé |
@ -109,7 +117,17 @@ dossier ; la forme plate `group_vars/all.yml` reste lue en compatibilité —, `
`modeles_vm.yml`, `plan/nomenclature.yml`). À détailler dans la note de conception de la
fonctionnalité GUI.
## 4. Incohérences repérées (à corriger)
- `fuseau_horaire` défini en **lab** seulement, absent de **production**.
- `group_vars/serveur_debian.yml` **référencé** (commentaire de prod `all.yml`) mais
**absent** des deux environnements.
## 4. Incohérences repérées — soldées
Les deux écarts que cette section signalait n'existent plus (vérifié le 2026-09-06), et le
modèle « deux environnements » qui les portait non plus :
- ~~`fuseau_horaire` défini en **lab** seulement, absent de **production**~~ — il est dans
`group_vars/all/10-intrants.yml` (`America/Toronto`), avec `domaine_interne`.
- ~~`group_vars/serveur_debian.yml` référencé mais absent~~ — plus aucune référence.
> **Il n'y a plus d'« environnements ».** Ce document parle de `<env>` par endroits : c'est
> le vocabulaire d'avant la séparation par instance. Une instance = **un dépôt**, avec **un**
> inventaire (le moteur en résout le nom, cf. `plan-et-generation.md`). « Lab » et
> « production » ne sont pas deux environnements d'un même écosystème : ce sont deux
> écosystèmes, chacun avec son plan, son inventaire et sa voûte.

View file

@ -37,7 +37,7 @@ flowchart TB
subgraph INST["③ INSTANCES — la flotte (inventory hosts.yml)"]
direction LR
INV[["hosts.yml"]]
VMS(("11 VM<br/>infra-pki · dns · mail · edge<br/>idm · data · obs · mon · forge · web"))
VMS(("14 VM<br/>infra-pki · infra-dns · infra-mail · infra-edge · edge-mta<br/>idm · data-sql · obs · mon · forge · collab<br/>web-frontal · web-dorsal · ops"))
end
ECO["④ ÉCOSYSTÈME souverain en service<br/>PKI · DNS · IdM/SSO · Données · Observabilité · Forge · Web"]
@ -60,8 +60,9 @@ flowchart TB
réseau (VMID·VLAN·IP·gw) · ressources (cœurs·RAM·disque) · groupes · DSN+DNS
│ = instanciation
▼
③ INSTANCES — la flotte (inventory hosts.yml : 11 VM cohérentes)
infra-pki·dns·mail·edge · idm · data · obs · mon · forge · web
③ INSTANCES — la flotte (inventory hosts.yml : 14 VM cohérentes)
infra-pki · infra-dns · infra-mail · infra-edge · edge-mta · idm · data-sql
obs · mon · forge · collab · web-frontal · web-dorsal · ops
▲ clone du golden template + identité cloud-init
│ make deployer (rôles Ansible par groupe)
▼

View file

@ -82,12 +82,15 @@ VM destinée à devenir un template, pas un serveur de production
Vérifications utiles depuis le poste Ansible :
```bash
ssh ansible@10.0.2.99
ansible -i instance/inventories/lab/hosts.yml modeles_vm -m ping
ansible -i instance/inventories/lab/hosts.yml modeles_vm -m setup
ssh ansible@<ip-de-la-VM-modele> # l'adresse est celle que tu lui as donnee, pas une derivee du plan
ansible -i "$SETOPS_INVENTAIRE" modeles_vm -m ping
ansible -i "$SETOPS_INVENTAIRE" modeles_vm -m setup
```
Adapter l'utilisateur et l'adresse IP selon `instance/inventories/lab/hosts.yml`.
`SETOPS_INVENTAIRE` est exporté par le `Makefile`, qui **résout** le nom de l'inventaire
(`INVENTAIRE_LAB` essaie `lab`, puis `principal`, puis `production`) — cette instance-ci
n'a pas de `lab/`, et un chemin écrit en dur y échouait. Adapter l'utilisateur et
l'adresse IP selon l'inventaire ainsi résolu.
### Prérequis côté dépôt
@ -107,7 +110,7 @@ modeles_vm
Les variables du template sont dans :
```text
instance/inventories/production/group_vars/modeles_vm.yml
instance/inventories/<inventaire>/group_vars/modeles_vm.yml
```
### Commande de préparation
@ -153,7 +156,7 @@ Un résultat `changed=0` à la relance est le signal que le playbook est idempot
| Symptôme | Cause probable | Action |
| --- | --- | --- |
| `UNREACHABLE` | IP, SSH, utilisateur ou clé SSH incorrecte. | Vérifier `instance/inventories/lab/hosts.yml`, cloud-init et tester `ssh`. |
| `UNREACHABLE` | IP, SSH, utilisateur ou clé SSH incorrecte. | Vérifier l'inventaire résolu (`$SETOPS_INVENTAIRE`), cloud-init et tester `ssh`. |
| échec `become` | Sudo NOPASSWD absent ou utilisateur non autorisé. | Corriger l'accès sudo initial, puis relancer `make preparer-modele`. |
| échec APT | DNS, passerelle, miroir Debian ou verrou APT. | Vérifier réseau, DNS et processus APT en cours. |
| erreur handler SSH | Handler manquant ou nom `notify` incohérent. | Vérifier les handlers du rôle SSH avant de relancer. |

View file

@ -50,11 +50,18 @@ statut fédéré/local et production ; signale toute **collision d'index** :
make instances
```
```
★ OPS-Chezlepro 13 1131-1136 fédérée prod
OPS-Technolibre 2 1021-1026 fédérée
OPS-Chezlepro-lab 1 1011-1016 local
INSTANCE INDEX VLAN FEDERE PROD
OPS-Chezlepro-lab 13 1131-1136 LOCAL non
* OPS-Chezlepro 17 1171-1176 oui oui
OPS-Technolibre 23 1231-1236 oui non
OPS-Patient0 29 1291-1296 oui ?
* = instance active (symlink 'instance'). Basculer : make instance-utiliser NOM=<depot>
```
*(Sortie réelle du 2026-09-06. **Ne pas se fier aux index d'un exemple** : ils bougent —
celui de Chezlepro a changé au moins une fois, Technolibre est passé de 11 à 23. Le seul
endroit qui dit vrai est `plan/nomenclature.yml` de chaque dépôt, et cette commande.)*
**Basculer l'active** — le symlink, avec garde-fous (le dossier existe, `instance` est
bien un symlink) :
@ -64,8 +71,9 @@ make instance-utiliser NOM=OPS-Technolibre # bascule (l'inventaire suit le
```
Rien à « recharger » : l'inventaire vit **dans** le dépôt de l'instance, il suit le lien.
Le GUI (onglet **Réseau**) montre la même flotte et les collisions ; la bascule reste au
CLI (chirurgie de symlink, mal placée dans une interface web).
Le GUI (onglet **Réseau**) montre la même flotte et les collisions — **et sait basculer**,
par le bouton « Activer » de chaque instance. *(Ce paragraphe affirmait le contraire — « la
bascule reste au CLI » — jusqu'au 2026-09-06 ; c'était vrai avant que le bouton n'existe.)*
**Deux réflexes.** (1) Avant tout déploiement, `make instance-courante` : la seule vraie
façon de se tromper est de déployer sur la mauvaise flotte (la colonne `prod` est là pour
@ -97,7 +105,7 @@ FRERES.glob("*/plan/nomenclature.yml") # tout dépôt frère ayant un plan
Une instance **est** donc un dossier : (1) **frère** du moteur (`../OPS-Chezlepro`,
`../OPS-Technolibre`…), (2) portant un **`plan/nomenclature.yml`**, (3) avec un **`index`**.
De là : **active** = ce que résout le symlink `instance` ; **fédérée** = `index` présent
*et* `federe ≠ false`. Les **modèles** (`Set-OPS-Modeles/integral/…`) sont un cran plus
*et* `federe ≠ false`. Les **modèles** (`exemples/modeles/…` ici, `Set-OPS-modeles/…` pour les modèles privés) sont un cran plus
profond — le glob ne les attrape pas, volontairement.
Conséquence : « inscrire » une instance = la déposer à côté des autres. Rien à éditer,
@ -124,6 +132,9 @@ sable met `false` et affiche « bac à sable ».
et la cible Proxmox (`proxmox.yml`).
3. Créer la voûte unique depuis le gabarit (cf. [`config-proxmox.md`](config-proxmox.md)) :
`cp exemples/vault.exemple.yml …/group_vars/all/vault.yml` puis `ansible-vault encrypt`.
**Et poser SA clé** — une voûte, une clé : `~/.config/setops-vault-ops-clientx`, nom
dérivé du dossier en minuscules. `python3 scripts/voutes.py etat` confirme que le moteur
la trouve.
4. `make instance-utiliser NOM=OPS-ClientX` puis `make instancier-appliquer`,
`make inventaire-ui`.
@ -144,7 +155,7 @@ Pour que plusieurs écosystèmes **coexistent** sur une même fabric sans collis
instance reçoit un **`index`** (unique champ d'adressage de `plan/nomenclature.yml` :
`index: N`). **Rien d'autre n'est écrit à la main** — la nomenclature ne garde que le
*modèle* (libellés de zones + placement des fonctions) ; supernet, sous-réseaux,
passerelles, VLAN et VMID se **dérivent** (`scripts/inventory_rules` : `supernet_de`,
passerelles, VLAN et VMID se **dérivent** (`scripts/inventory_rules.py` : `supernet_de`,
`base3_de`, `passerelle_de`, `vlan_de`). Changer `index` rederive tout le réseau — et la
preuve **P20** interdit tout adressage stocké.
@ -166,10 +177,13 @@ mêmes VLAN/VMID : c'est la collision que `make instances` et **P21** attrapent.
`federe: false` : il est alors **exclu du réseau convergé** (devis, `make instances` le
montre « local »). Il garde son adressage dérivé et reste déployable sur *son* infra.
**Plafond théorique : 255 écosystèmes fédérés** (index 1 à 255), borné par l'IPv4
`10.<index>` (2ᵉ octet 1→255). Le décalage de +10, retiré le 2026-08-12, en confisquait
dix — et surtout, il empêchait de lire l'index directement dans l'adresse. Le VLAN (≤ 4094) autorise jusqu'à 308, le VMID bien
plus — c'est donc l'adressage IP qui plafonne. Au-delà d'une poignée d'instances
**Plafond : le 2ᵉ octet IPv4.** `valider_index` borne l'index à **0–255**
(`INDEX_MIN`/`INDEX_MAX`, `scripts/inventory_rules.py`) — la garde est posée **à la source
de la dérivation**, donc aucune fonction ne peut fabriquer une adresse hors bornes, d'où
qu'on l'appelle. En pratique, **éviter 0** : `10.0.x` porte déjà les réseaux de service du
site. Le décalage de +10, retiré le 2026-08-12, confisquait dix valeurs — et surtout, il
empêchait de lire l'index directement dans l'adresse. Le VLAN (≤ 4094) autoriserait
jusqu'à 308, le VMID bien plus : c'est donc l'adressage IP qui plafonne. Au-delà d'une poignée d'instances
co-localisées, confier l'allocation à un **IPAM** (NetBox) plutôt qu'au moteur — cf.
[`positionnement.md`](positionnement.md).

View file

@ -21,14 +21,16 @@ infra-pki-01
infra-edge-01
infra-mail-01
infra-dns-01
edge-mta-01
idm-01
data-01
data-sql-01
obs-01
mon-01
forge-01
collab-01
web-frontal-01
web-dorsal-01
ops-01
```
La couche applicative web suit le même format. Le tier est porté par la fonction, au singulier puisqu'il nomme une instance :
@ -48,11 +50,13 @@ Les anciens noms de test `web-01` et `web-02` sont retirés. Ils ne doivent pas
| `infra-pki-01` | `serveur_step_ca` |
| `infra-edge-01` | `serveur_nginx` |
| `infra-mail-01` | `serveur_dovecot` (mail-store) |
| `infra-dns-01` | `serveur_powerdns` |
| `edge-mta-01` | `serveur_postfix`, `serveur_rspamd` (ce qui parle à l'extérieur) |
| `infra-dns-01` | `serveur_powerdns`, `serveur_resolveur` |
| `idm-01` | `serveur_openldap`, `serveur_keycloak` |
| `data-01` | `serveur_postgresql`, `serveur_redis` |
| `data-sql-01` | `serveur_postgresql`, `serveur_redis` |
| `obs-01` | `serveur_prometheus`, `serveur_loki`, `serveur_grafana` |
| `mon-01` | `serveur_icinga` |
| `mon-01` | `serveur_icinga`, `serveur_icingaweb2`, `serveur_oauth2_proxy` |
| `ops-01` | `serveur_ops`, `serveur_ops_tenant` (le runner de l'écosystème) |
| `forge-01` | `serveur_forgejo` |
| `collab-01` | `serveur_nextcloud`, `serveur_collabora` |
| `web-frontal-01`, `web-frontal-02` | `serveur_web_frontal` |
@ -60,45 +64,47 @@ Les anciens noms de test `web-01` et `web-02` sont retirés. Ils ne doivent pas
Les groupes restent fins et composables. La cohabitation se fait en associant plusieurs groupes au même hôte.
## Plages VMID
## VMID et adressage : tout dérive du seed `index`
| Plage | Usage |
| --- | --- |
| `91xxx` | fondations transversales : PKI, reverse proxy, SMTP |
| `92xxx` | identité : LDAP, SSO |
| `93xxx` | données et cache : PostgreSQL, Redis |
| `94xxx` | observabilité et supervision |
| `95xxx` | applications internes |
| `99xxx` | modèles, essais initiaux ou exceptions documentées |
> **Cette section décrivait le modèle d'avant le multi-instance**, et rien n'y était plus
> vrai : un réseau unique `10.0.0.0/16`, des VLAN 11 à 15, des plages de VMID à cinq
> chiffres (`91xxx`…`99xxx`). Il n'y a plus de plages à réserver, et il n'y a plus *un*
> réseau : chaque écosystème dérive le sien.
## Plan d'adressage interne
Réseau interne unique : `10.0.0.0/16`. Segmentation par fonction, un `/24` et un VLAN par catégorie, **3ᵉ octet = VLAN** (L2 alignée sur L3).
| Catégorie | VLAN | Sous-réseau | Passerelle |
| --- | --- | --- | --- |
| 1 — Fondations / infra | 11 | `10.0.11.0/24` | `10.0.11.1` |
| 2 — Identité | 12 | `10.0.12.0/24` | `10.0.12.1` |
| 3 — Données | 13 | `10.0.13.0/24` | `10.0.13.1` |
| 4 — Observabilité | 14 | `10.0.14.0/24` | `10.0.14.1` |
| 5 — Applications | 15 | `10.0.15.0/24` | `10.0.15.1` |
Adresse d'hôte (4ᵉ octet) : **`service × 10 + NN`**. `.1` = passerelle ; `.2`–`.9` réservés. Exemple : `web-dorsal-01` (catégorie 5, service 4, NN 01) → `10.0.15.41`.
Tout se dérive de la fonction de l'hôte, et la source unique machine-lisible est **`instance/plan/nomenclature.yml`** :
Une instance reçoit **un seul champ d'adressage** : `index`, dans
`instance/plan/nomenclature.yml`. Tout le reste s'en déduit — et la preuve **P20** interdit
de stocker un adressage quelconque (`supernet`, `sous_reseau`, `passerelle`, `vlan`).
```text
hostname = <fonction>-<NN>
VMID = 9 · catégorie · service · NN
VLAN = catégorie.vlan
IP = 10.0.<vlan>.(service × 10 + NN)
supernet = 10.<index>.0.0/16
zone (3 oct.) = 10.<index>.(15 + catégorie)
sous-réseau = 10.<index>.(15 + catégorie).0/24
passerelle = 10.<index>.(15 + catégorie).1 ← premier hôte du /24
VLAN = 1000 + index × 10 + zone ← unique sur tout le trunk convergé
VMID = <VLAN><hôte sur 3 chiffres><rang sur 2> ← neuf chiffres, miroir de l'IP
adresse IP = 10.<index>.(15 + catégorie).<hôte>
```
`make inventaire-ui` lit ce registre et **propose** automatiquement VMID, VLAN, IP et passerelle quand on nomme un hôte. La sécurité entre zones se fera par règles inter-zones (nftables / edge), pas par l'adressage.
Le VMID **est** l'adresse, relue : `117602101` se lit `1176` (VLAN) · `021` (hôte) · `01`
(rang). On retrouve la VM depuis son adresse, et l'inverse, sans registre.
Contrainte : `NN` de 01 à 09 par fonction (l'octet hôte reste dans le bloc du service). Au-delà, ouvrir une nouvelle fonction/service dans `instance/plan/nomenclature.yml`.
Exemple, l'écosystème de référence (`index: 17`) : supernet `10.17.0.0/16`, zones
`10.17.16.0/24` à `10.17.21.0/24`, VLAN `1171` à `1176`. `infra-edge-01` y vaut
`10.17.16.11`, et son VMID `117101101` se relit `1171` · `011` · `01`.
La segmentation `10.0.0.0/16` remplace l'ancienne plage d'essais `192.168.12.x`.
*(L'index se lit directement dans le second octet — c'est ce qui permet de reconnaître le
tenant d'une adresse à l'œil. `valider_index` le borne, et la même borne protège un second
plafond : à l'index 255, le VLAN vaut `3550 + zone`, sous les 4094 du 802.1Q.)*
**Changer `index` redérive tout le réseau de l'écosystème.** C'est ce qui rend un tenant
portable d'un site à l'autre, et c'est pourquoi rien ne doit être écrit à la main. La preuve
**P21** refuse que deux instances fédérées partagent un index. Détail complet :
[`multi-instances.md`](multi-instances.md).
`make inventaire-ui` lit ce registre et **propose** VMID, VLAN, IP et passerelle quand on
nomme un hôte. La sécurité entre zones ne repose pas sur l'adressage mais sur le registre
des flux (nftables de l'hôte, pare-feu de l'hyperviseur, frontière) —
[`flux-conception.md`](flux-conception.md).
## Variables de provisioning d'hôte

View file

@ -7,9 +7,18 @@ l'inventaire Ansible en est **généré**. Le dépôt est la définition ; chaqu
est une instance. Ce document décrit le modèle, les registres, les commandes et
le flux de travail.
> Règle d'or : **`instance/inventories/production/hosts.yml` est GÉNÉRÉ. Ne jamais l'éditer
> Règle d'or : **`instance/inventories/<inventaire>/hosts.yml` est GÉNÉRÉ. Ne jamais l'éditer
> à la main.** On édite le *plan* puis on régénère (`make instancier-appliquer`).
> **`<inventaire>` n'est pas un nom, c'est une place.** Le dépôt n'impose pas comment une
> instance nomme son inventaire : **le moteur le cherche**, dans l'ordre `principal`, puis
> `production` (`scripts/inventory_rules.py` : `ORDRE_INVENTAIRE` ; pour la construction du
> gabarit, `ORDRE_INVENTAIRE_MODELE` essaie `lab` d'abord). La flotte utilise `principal` ;
> le modèle public livré dans `exemples/modeles/socle/` utilise `production`, et le
> QUICKSTART l'écrit tel quel parce que c'est ce que son lecteur a sous la main. Les
> documents de doctrine, eux, écrivent `<inventaire>` : coder l'un des deux noms en dur y
> serait faux pour la moitié des lecteurs — et ça l'a été jusqu'au 2026-09-06.
---
## 1. Le modèle : deux ancres, cinq liens
@ -34,11 +43,14 @@ liaison — seulement une **capacité** qu'une VM fournit (le rôle appliqué).
## 2. Les registres (source unique de vérité)
Tous sous `docs/`, machine-lisibles, validés, consommés par le GUI, le CLI et Ansible.
Machine-lisibles, validés, consommés par le GUI, le CLI et Ansible. **Ils vivent dans
l'instance** (`instance/plan/`), pas dans le moteur — c'est toute la séparation
moteur/instance. Seul `dependances-groupes.yml` est sous `docs/`, parce qu'il décrit une
propriété des *rôles*, la même pour toutes les instances.
| Registre | Décrit | Champs clés |
| --- | --- | --- |
| `nomenclature.yml` | nommage & adressage | `fonctions` (catégorie/service), `categories` (VLAN/sous-réseau/passerelle), `supernet` |
| `nomenclature.yml` | nommage & adressage | **`index`** (le seed, seul champ d'adressage), `fonctions` (catégorie/service), `categories` (libellés de zones), `cidr_hote`, `reservations`. **Ni `supernet`, ni `vlan`, ni `passerelle` : P20 les refuse** — ils se dérivent. |
| `serveurs.yml` | les VM du plan | `fonction`, `etat` (actif/planifie), placement Proxmox (`noeud`/`stockage`/`disque`/`memoire`/`coeurs`), `integrations` (les `client_*` **facultatives** seulement — les universelles viennent du rôle, voir `integrations-vm.md`) |
| `applications.yml` | les applications | `groupe` (capacité/rôle), `hote` (VM), `port`, `requiert`, `expose`, (+ bases via consommateur) |
| `bases-donnees.yml` | serveurs de BD + bases | `serveurs_bd` ; `bases_donnees` : `serveur`/`base`/`proprietaire`/`secret`(Vault), `consommateur` + `portee` (`application`/`groupe`/`hote`), `usage` |
@ -46,9 +58,12 @@ Tous sous `docs/`, machine-lisibles, validés, consommés par le GUI, le CLI et
| `dependances-groupes.yml` | prérequis entre groupes | `requiert_groupes_actifs` |
### Dérivations clés
- **Nommage/adressage** : tout part de la `fonction` de l'hôte (`web-frontal-03`).
`VMID = 9·catégorie·service·NN`, `VLAN = catégorie.vlan`,
`IP = 10.0.<vlan>.(service×10 + NN)`. Voir `docs/nomenclature-vm.md`.
- **Nommage/adressage** : tout part du seed `index` et de la `fonction` de l'hôte.
`supernet = 10.<index>.0.0/16`, `zone = 10.<index>.(15+catégorie)`,
`VLAN = 1000 + index×10 + zone`, `VMID = <VLAN><octet-hôte><rang>` — neuf chiffres,
miroir de l'IP. Voir `docs/nomenclature-vm.md`. *(Les formules à cinq chiffres et le
réseau unique `10.0.x` qui figuraient ici décrivaient le modèle d'avant le
multi-instance.)*
- **DSN** (lien application↔base) : `<type>://<proprietaire>:<secret>@<hôte>:<port>/<base>`.
Une application reçoit les bases où `(portee=application ET consommateur=elle)`
OU `(portee=groupe ET consommateur=son groupe)` OU `(portee=hote ET consommateur=son hôte)`.
@ -83,15 +98,22 @@ c'est le feu vert pour appliquer.
```
### Via le GUI — `make inventaire-ui`
Cinq vues :
**Onze vues**, éditables ou dérivées *(la table n'en listait que cinq)* :
| Vue | Rôle |
| --- | --- |
| **Inventaire** | **lecture seule** (inventaire généré) — vue d'ensemble des hôtes |
| **Serveurs** | éditer les VM du plan : fonction/état/placement/intégrations (VMID·IP·VLAN dérivés en direct) ; bouton **« Appliquer le plan »** |
| **Chaîne** | vue holistique par hôte : groupes → rôles, et par application ses `expose` / `requiert` / bases (DSN) |
| **Applications** | éditer les applications : groupe/hôte/port/requiert/expose |
| **Bases** | éditer serveurs de BD et bases (portée + consommateur), DSN affiché |
| **Serveurs** *(éditable)* | les VM du plan : fonction/état/placement/intégrations (VMID·IP·VLAN dérivés en direct) ; bouton **« Appliquer le plan »** |
| **Applications** *(éditable)* | groupe/hôte/port/requiert/expose, et les **liens** acceptés par le rôle |
| **Bases** *(éditable)* | serveurs de BD et bases (portée + consommateur), DSN affiché (secret masqué) |
| **Domaines** *(éditable)* | zones publiques vs internes, autorité · edge · DNSSEC, expositions |
| **Intégrations** *(éditable)* | la matrice serveurs × intégrations ; les universelles en ✓ non décochables |
| **Intrants** *(éditable)* | les intrants de base — identité, Proxmox, fabric, et la liste de rappel des secrets (lecture seule) |
| **Inventaire** *(lecture seule)* | l'inventaire **généré** — vue d'ensemble des hôtes |
| **Chaîne** *(lecture seule)* | vue holistique par hôte : groupes → rôles, et par application ses `expose` / `requiert` / bases |
| **Flux** *(lecture seule)* | la matrice d'audit des flux — source de nftables et justification lisible |
| **Couches** *(lecture seule)* | l'ordre de déploiement en six couches |
| **Réseau** *(lecture seule + bascule)* | la flotte multi-instances, les collisions d'index, et le bouton **« Activer »** |
Édition → **Sauvegarder** (écrit le registre) → **Appliquer le plan** (régénère
`hosts.yml`). L'écriture directe de l'inventaire est refusée (409).
@ -137,7 +159,7 @@ registres + **`node --check` du JS du GUI**).
La règle de résolution vit **une seule fois**, en Python (`scripts/inventory_rules.py`),
et est exposée à Ansible par un *filter plugin* (`filter_plugins/registres.py`) :
`bases_de_application`, `applications_de_hote`, `expositions_des_applications`,
`chaine_connexion`. Les playbooks par application (`serveur_web_dorsal`/`_frontaux`)
`chaine_connexion`. Les playbooks par application (`serveur_web_dorsal`, `serveur_web_frontal`)
itèrent ainsi sur les applications de l'hôte et résolvent leurs DSN.
---
@ -159,7 +181,7 @@ C'est ainsi que le plan a été initialisé sans perte, avec diff vide vérifié
## 8. Moteur et instance : deux dépôts
Le **moteur** (ce dépôt, `Set-OPS`) est générique et partageable ; il ne contient
aucune donnée d'instance. Une **instance** (le plan + l'inventaire d'un loup) vit
aucune donnée d'instance. Une **instance** (le plan + l'inventaire d'une organisation) vit
dans son **propre dépôt** (ex. `OPS-monatelier`).
Le moteur localise l'instance via **`SETOPS_INSTANCE`** (défaut : `instance`). Deux
@ -168,7 +190,7 @@ modèles :
- **Modèle A — dépôts frères** (en cours) : moteur et instance côte à côte ; un
symlink `instance -> ../OPS-monatelier` (gitignoré) fait que le défaut résout
l'instance sans configuration. Idéal quand on développe le moteur *et* l'instance.
- **Modèle B — moteur en sous-module** (futur, pour la meute) : l'instance épingle
- **Modèle B — moteur en sous-module** (futur, quand plusieurs écosystèmes vivront chez des exploitants distincts) : l'instance épingle
une version du moteur ; `SETOPS_INSTANCE` pointe la racine de l'instance.
Pour brancher une instance (modèle A) :

View file

@ -44,7 +44,13 @@ Raisons assumées :
1. **Souveraineté** — mission explicite du dépôt. NetBox + AWX + Backstage = trois
applications lourdes à héberger et maintenir (Django + PostgreSQL, etc.). Set-OPS
reste possédé en entier, sans dépendance.
2. **Bon dimensionnement** — ~13 VM. NetBox est de la machinerie d'échelle entreprise.
2. **Bon dimensionnement** — mais l'argument a bougé, et il faut le dire honnêtement.
Ce document a longtemps écrit « ~13 VM ». Le moteur pilote aujourd'hui **cinq plans**
(mesuré le 2026-09-06) : Chezlepro 14 VM, Technolibre 15, le lab 15, Patient 0 5, plus
les 7 machines du site — **56 VM déclarées**, réparties sur des écosystèmes qui ne se
parlent pas. Ça reste très loin de l'échelle entreprise pour laquelle NetBox est fait,
et le seuil du §4 n'est pas franchi. Mais la courbe monte : c'est **le** chiffre à
regarder quand on se demande si la décision tient encore.
3. **Modèle sur-mesure** — NetBox exprimerait nos DSN / expositions à coups de
*custom fields* et de plugins, moins naturellement que nos registres.
4. **Maîtrise** — chaque ligne est comprise et auditable.

View file

@ -2,8 +2,12 @@
> **Pour qui :** qui **évalue le moteur** — ce qu'il sait faire, et ce qu'il ne sait pas encore.
> Bilan des capacités du moteur, au 2026-07-02.
> Fondé sur l'état réel du dépôt (≈50 rôles, ≈30 playbooks de groupe, le moteur de plan, la GUI).
> Bilan des capacités du moteur. Établi le 2026-07-02, **revu le 2026-09-06** : la
> frontière ⭐/🔧 avait cessé de dire vrai — deux reconstructions depuis zéro (2026-08-13,
> puis 2026-09-02) ont fait passer côté ⭐ presque tout ce que le §5 annonçait « à
> éprouver ». Fondé sur l'état réel du dépôt (≈65 rôles, ≈60 playbooks, le moteur de plan,
> la GUI). **Le détail rôle par rôle vit dans [`catalogue-services.md`](catalogue-services.md)** ;
> ici on ne garde que le bilan.
>
> Deux niveaux de maturité sont distingués honnêtement :
> - **⭐ Prouvé** — déployé et vérifié de bout en bout sur cluster Proxmox réel.
@ -19,8 +23,9 @@
complet — PKI, identité, DNS, web, courriel — cloné depuis un golden template durci, piloté par
une GUI utilisable sans IA, en multi-tenant, tout en logiciel libre.**
Le cœur d'infrastructure est **prouvé de bout en bout** ; la couche des services applicatifs
(SSO, données, forge, observabilité) est **outillée et prête à éprouver**.
Le cœur d'infrastructure **et** la couche des services applicatifs (SSO, données, forge,
observabilité, supervision, collaboration) sont **prouvés de bout en bout** : la flotte a
été rasée puis remontée d'un seul trait, deux fois, sans échec.
---
@ -31,8 +36,11 @@ Pouvoir central : **plan déclaratif → écosystème vivant**. On décrit *quoi
- **`scripts/instancier.py`** — lit `nomenclature / serveurs / applications / bases-donnees.yml`
et **dérive** tout : VMID, IP, groupes d'inventaire, dimensionnement. ⭐
- **Nomenclature fédérée** — un `index` + catégorie + service produit un **VMID
`{index}{cat}{svc}{seq}`** et une IP déterministes, sans collision entre instances. ⭐
- **Nomenclature fédérée** — le seul champ saisi est l'**`index`** ; supernet, VLAN, IP,
passerelle et **VMID** en dérivent, sans collision possible entre instances. Le VMID est le
**miroir de l'adresse** sur neuf chiffres — `<VLAN><octet d'hôte><rang>`, soit `117602101`
pour `1176` · `021` · `01` : on retrouve la VM depuis son adresse, et l'inverse, sans
registre. **P20** interdit d'écrire un adressage au plan. ⭐
- **Dimensionnement automatique** — chaque rôle porte une `meta/empreinte.yml`
(cœurs / RAM / disque) ; l'outil **somme les empreintes et taille la VM** hôte. ⭐
- **Multi-instance = multi-tenant** — le symlink `instance/` pointe vers un dépôt par écosystème,
@ -47,8 +55,10 @@ Impératif fondateur : un sysadmin exploite l'outil **sans IA** ; l'IA n'assiste
- **GUI souveraine** (`scripts/inventory_gui.py`, stdlib pure, aucune dépendance) : visualise le
plan, **bouton « Pousser »** (crée la VM pour un serveur / déploie pour une app ou une base),
**sonde de vivacité**, passage auto en « actif », jeton d'authentification. ⭐
- **Voûte au déploiement** — secrets chiffrés par Ansible Vault, saisis au déploiement, **jamais
en clair** dans la GUI ; usage via le fichier de mot de passe, non interactif. ⭐
- **Voûte au déploiement** — secrets chiffrés par Ansible Vault, **jamais en clair** dans la
GUI, qui n'en montre que les *noms*. **Une voûte, une clé** (2026-08-28) : chaque dépôt a la
sienne, et le `Makefile` les rassemble tout seul (`scripts/voutes.py`), sans rien exporter
ni rendre l'exécution interactive. ⭐
- **Validation intégrée** — `syntax-check`, `ansible-lint` (profil *production*), `verifier`. ⭐
## 3. Le socle — un golden template durci
@ -60,9 +70,13 @@ Impératif fondateur : un sysadmin exploite l'outil **sans IA** ; l'IA n'assiste
`template_cleanup`.
- **SSH** : `ssh_baseline` puis `ssh_hardening` (bascule mot de passe → clé, seulement après
validation de l'accès par clé).
- **Durcissement** : `apparmor`, `auditd`, `fail2ban_ssh`, `sysctl_hardening`,
`hardening_packages`, `unattended_upgrades`, `journald`, `core_dumps`,
`nftables_baseline` (installé et préparé, **non activé par défaut**).
- **Durcissement** — les **dix** rôles que compose le groupe `serveur_durci` :
`hardening_packages`, `sysctl_hardening`, `core_dumps`, `unattended_upgrades`, `apparmor`,
`auditd`, `fail2ban_ssh`, `journald`, `ssh_hardening`,
`nftables_baseline` (**éteint dans le rôle, armé par le plan** : `hotes_actifs` le met à
`true`, le gabarit doré le laisse à `false` — le pare-feu n'est pas une propriété du rôle,
c'est une décision de l'instance). Le jeu de règles n'est pas écrit à la main : il est
**dérivé du registre des flux** (`meta/flux.yml` → `scripts/resoudre_flux.py`).
- **Proxmox** : clonage depuis le golden template + redimensionnement disque (grow-only).
> Séparation nette : **Proxmox + cloud-init** donnent l'identité initiale de la VM ;
@ -83,20 +97,42 @@ par annuaire LDAP, remise LMTP réseau chiffrée step_ca vers le stockage, accè
LDAP, filtrage antispam et signature DKIM en milter. Topologie **MTA dédié en périphérie /
boîtes à l'intérieur** (défense en profondeur).
## 5. Les services outillés — rôles présents 🔧
## 5. Les services applicatifs — prouvés depuis ⭐
Construits et câblables par le plan ; **à éprouver** en déploiement réel avant de les déclarer
prouvés.
Ce paragraphe annonçait, jusqu'au 2026-09-06, des rôles « construits mais à éprouver ».
Ils l'ont été : **la reconstruction depuis zéro les a tous repris sur machine nue**
(2026-08-13, puis 15/15 et 14/14 hôtes le 2026-09-02, `make valider` à 0 échec).
- **SSO web** : `serveur_keycloak` (architecture décidée : OpenLDAP source de vérité, Keycloak
fédéré en OIDC, courriel en bind LDAP direct).
- **Données** : `serveur_postgresql`, `serveur_redis`.
- **Forge logicielle** : `serveur_forgejo`.
- **Observabilité** : `serveur_prometheus`, `serveur_grafana`, `serveur_loki`, `serveur_icinga`,
avec les clients `client_metrique`, `client_journal`, `client_supervision`.
- **Intégrations transverses** : `client_pki`, `client_backup`, `client_smtp`, `client_metrique`, `client_journal`, `client_resolveur`.
- **Méta-rôles d'agrégation** : `identity`, `applications`, `database`, `web`, `monitoring`,
`backup`, `storage`.
- **SSO web** : `serveur_keycloak` — OpenLDAP source de vérité, fédération LDAP automatisée,
courriel en bind LDAP direct. ⭐
- **Passerelle SSO** : `serveur_oauth2_proxy` — met au SSO une application sans OIDC natif
(éprouvé devant Icinga Web 2). ⭐
- **Données** : `serveur_postgresql`, `serveur_redis` — bases et comptes dérivés du registre,
TLS `verify-full` de bout en bout. ⭐
- **Forge logicielle** : `serveur_forgejo` — Git + PostgreSQL + SSO OIDC. ⭐
- **Observabilité** : `serveur_prometheus`, `serveur_grafana`, `serveur_loki`, avec les clients
`client_metrique` et `client_journal`. ⭐
- **Supervision** : `serveur_icinga`, `serveur_icingaweb2` — Icinga 2 + IcingaDB + Web 2 + BPM.
**Sans agent sur les hôtes** : contrôles actifs depuis le cœur, résultats passifs poussés par
l'API. ⭐
- **Sauvegardes** : `serveur_backup`, `client_backup` — restic hors-nœud, chaque nœud vérifiant
son **propre dépôt distant**, restauration éprouvée. ⭐
- **Plateforme webapp** : `serveur_web_frontal`, `serveur_web_dorsal` — statique et natif
(venv + systemd + nginx), zéro conteneur. ⭐
- **Intégrations transverses** : `client_pki`, `client_backup`, `client_smtp`, `client_metrique`,
`client_journal`, `client_resolveur`, `client_artefacts`. ⭐
Reste **🔧 outillé** — déployé, mais dont l'*usage* n'est pas consigné comme preuve :
- **Collaboration** : `serveur_nextcloud`, `serveur_collabora` (natif, plus de conteneur). Base
et client OIDC dérivés du plan, posés par la reconstruction ; le dépôt d'un fichier et
l'édition partagée à deux, eux, n'ont pas été consignés. 🔧
> Deux noms ont été retirés de ce paragraphe parce qu'ils n'ont jamais existé :
> `client_supervision` (la supervision est sans agent — cf. `catalogue-services.md`) et les
> « méta-rôles d'agrégation » `identity` / `storage`. La conformité passe par
> `playbooks/groupes/`, un playbook par groupe ; les répertoires `playbooks/applications/`,
> `database/`, `web/`, `monitoring/`, `backup/` ne contiennent qu'un README d'espace réservé.
## 6. Les patrons d'ingénierie — la valeur invisible ⭐
@ -119,9 +155,13 @@ prouvés.
## Prochaines frontières
- **Éprouver** les services outillés (Keycloak fédéré, observabilité, PostgreSQL, Forgejo).
- **Consigner l'usage** de la collaboration (dépôt de fichier, édition partagée) — le seul
🔧 qui reste.
- **Courriel Étape B** (public) : DNS public (MX, SPF, **DKIM** — clé `setops._domainkey` déjà
générée, DMARC), MX externe et réputation, PTR / FCrDNS.
- **Les équipements de l'hébergeur** — hyperviseurs, commutateurs, frontière : ni inventaire,
ni sauvegarde de configuration, ni supervision. Les *VM* du site en ont depuis le 2026-09-02,
les *équipements* non (cf. `hebergeur-exploitation.md`).
- **Seuils d'adoption** d'outils tiers (NetBox, AWX) plutôt que de réimplémenter le plan de
contrôle maison, qui reste volontairement gelé.

View file

@ -31,7 +31,7 @@ basse** du supernet — celle que la dérivation des tenants n'alloue jamais.
| Rôle | VLAN | Sous-réseau | MTU | Nature |
|---|---|---|---|---|
| **Gestion** | 10 | `10.<index>.0.0/24` | 1500 | **destination** — unique par site |
| **Gestion** | 10 *(ou aucun — voir ci-dessous)* | `10.<index>.0.0/24` | 1500 | **destination** — unique par site |
| Sortie des tenants | 40 | `192.168.40.0/24` | 1500 | chemin — identique partout |
| Transport VXLAN | 50 | `192.168.50.0/24` | 1500 | chemin — identique partout |
| Stockage iSCSI | 20 | `192.168.20.0/24` | 9000 | chemin — identique partout |
@ -41,6 +41,12 @@ basse** du supernet — celle que la dérivation des tenants n'alloue jamais.
Index 17 → gestion en `10.17.0.0/24` ; index 11 → `10.11.0.0/24`. Deux sites ne peuvent
donc **pas** se chevaucher, sans qu'on ait rien de plus à décider.
> **Le VLAN de gestion peut n'exister pas du tout.** Chez l'hébergeur de référence, ce plan
> est un **segment physique** — une patte dédiée sur la frontière, aucune étiquette, et
> **aucun pont d'hyperviseur ne le touche**. Conséquence recherchée : *aucune VM ne peut y
> naître*, et le validateur refuse qu'on y déclare une machine. Un port d'accès étiqueté 10
> convient aussi ; ce qui compte est que rien du monde virtuel n'y ait de patte.
**Les VLAN ne changent jamais** d'un site à l'autre : ils sont locaux, ils ne traversent
aucun lien inter-sites. Un seul modèle mental, et l'adresse de stockage porte son propre
numéro de VLAN — `192.168.20.x` est sur le VLAN 20.
@ -73,7 +79,7 @@ le seed de son site. Une seule règle, aucun cas particulier.
Le trafic des VM voyage encapsulé en VXLAN, ce qui coûte **50 octets**. Avec un transport
à 1500, les VM tournent à **1450** — automatiquement, mais à condition que 1500 passe
réellement de bout en bout sur les VLAN 11 et 40. Un MTU rogné en chemin donne le pire des
réellement de bout en bout sur les VLAN de transport et de transit — **50** et **40** (le tableau ci-dessus fait foi ; ce paragraphe disait « 11 et 40 », le numéro d'avant). Un MTU rogné en chemin donne le pire des
symptômes : les petites requêtes passent, les grosses meurent, et rien n'est signalé.
Le jumbo (9000) sur le stockage est facultatif. **À moitié configuré, il ne fonctionne
@ -216,8 +222,15 @@ réseaux **défait en silence l'isolation inter-tenant**. WireGuard s'y prête b
## 9. Ce qu'il ne faut pas faire
- **Ne pas créer de VM à la main** pour le tenant. Elles sont toutes dérivées du plan ;
une VM créée à côté est invisible pour l'outil, qui la détruira sans le savoir.
- **Ne pas créer de VM à la main** pour le tenant. Elles sont toutes dérivées du plan ; une
VM créée à côté est **invisible pour l'outil** — et le risque n'est pas celui qu'on croit.
`make raser` dérive sa liste du plan : il ne la détruira **jamais**. Elle survit donc à
tout, sans DNS, sans certificat, sans sauvegarde, sans politique de pare-feu, et **son
VMID n'est gardé par aucune preuve contre une collision**. Un VMID oublié squatte le
cluster sans que rien ne le signale. *(Cette ligne annonçait l'inverse — « l'outil la
détruira sans le savoir » — jusqu'au 2026-09-06.)* Pour une machine d'épreuve jetable, il
existe une voie prévue et documentée : `make cloner-vm`, hors plan, **à détruire à la
main** (cf. `vm-lifecycle.md` §4bis).
- **Ne pas configurer le SDN** : l'outil le fait entièrement.
- **Ne pas écrire les secrets** dans un fichier partagé ni dans un dépôt git.
- **Ne pas contourner un blocage en silence.** Un point resté ouvert et *signalé* se règle

View file

@ -51,13 +51,22 @@ Créer une nouvelle VM dans Proxmox avec des paramètres sobres.
Exemple :
```text
Nom de la VM : debian13-template
Nom de la VM : modeleSetOPS ← voir l'encadré : ce nom N'EST PAS libre
VMID : 9000 ou autre ID réservé aux modèles
OS : Debian 13
BIOS : OVMF / UEFI
Machine : q35
```
> **Le nom du modèle est un contrat, pas une étiquette.** Le clonage cherche sa source
> **par ce nom** : il doit être exactement celui que `make config` a enregistré sous
> `proxmox_clone_source_nom` — **`modeleSetOPS`** par défaut. Un nom qui ne correspond pas
> se solde par un clonage qui ne trouve rien, et le message ne dit pas que c'est le nom qui
> est en cause. *(Cette procédure proposait `debian13-template` jusqu'au 2026-09-06 :
> suivie à la lettre, elle produisait un gabarit que le moteur ne savait pas cloner.)*
>
> Le VMID, lui, est libre — c'est `proxmox_clone_vmid_modele` qui le retient.
### `q35` n'est pas un réglage — c'est la raison de cette procédure
**Ces deux lignes sont pourquoi l'installation est manuelle.** Elles expliquent aussi
@ -762,7 +771,13 @@ Exemple :
qm template 9000
```
À partir de là, le modèle peut être cloné.
À partir de là, le modèle peut être cloné — **à condition que son nom et son VMID
correspondent** à ce que `make config` a enregistré (`proxmox_clone_source_nom`,
`proxmox_clone_vmid_modele`). Vérifier avant de s'en servir :
```bash
make config # affiche la configuration lue par le moteur
```
---
@ -773,14 +788,14 @@ Après conversion du modèle, le flux normal passe par `make` et l'API Proxmox.
Les paramètres communs Proxmox sont dans :
```text
instance/inventories/production/group_vars/proxmox.yml
instance/inventories/<inventaire>/group_vars/proxmox.yml
```
Les secrets d'API vivent dans la voûte unifiée de l'instance (le token Proxmox aux côtés
des autres `vault_*`), semée par `make config` :
```text
instance/inventories/production/group_vars/all/vault.yml
instance/inventories/<inventaire>/group_vars/all/vault.yml
```
Créer le clone et l'ajouter à l'inventaire :
@ -804,7 +819,9 @@ Les paramètres par VM (VMID, IP, VLAN, passerelle) ne sont plus saisis à la ma
Après le premier démarrage, tester l'accès :
```bash
ssh ansible@10.0.15.31
# L'adresse n'est pas à retenir : elle est DÉRIVÉE, et le plan la donne.
make hote-afficher HOTE=web-frontal-01 # VMID · IP · VLAN · passerelle
ssh ansible@10.17.21.31 # (l'IP ainsi obtenue)
```
Ensuite appliquer la conformité Ansible :

View file

@ -20,10 +20,10 @@ l'edge) **n'a pas été rechargé** → il sert l'ancien cert en mémoire.
**Diagnostic-réflexe** — comparer le cert *servi* au cert *fichier* :
```bash
# SERVI (en mémoire par nginx)
echo | openssl s_client -connect infra-edge-01…:443 -servername keycloak.lab… 2>/dev/null \
echo | openssl s_client -connect infra-edge-01.chezlepro.internal:443 -servername keycloak.chezlepro.internal 2>/dev/null \
| openssl x509 -noout -enddate
# FICHIER (sur disque)
openssl x509 -in /etc/step/certs/infra-edge-01….crt -noout -enddate
openssl x509 -in /etc/step/certs/infra-edge-01.chezlepro.internal.crt -noout -enddate
```
Dates différentes (servi < fichier) ⇒ nginx sert un cert périmé.
@ -50,8 +50,15 @@ Par défaut, un utilisateur SSO est **Viewer** (dashboards seulement, pas Explor
3. L'utilisateur doit **se déconnecter/reconnecter** (Grafana applique le rôle à la connexion).
Mapping (défaut du rôle grafana) : `grafana-admin`→Admin, `grafana-editor`→Editor, sinon Viewer
(`serveur_grafana_oidc_role_path`). Idéal souverain : piloter par un **groupe d'annuaire** plutôt
qu'un utilisateur explicite. Voir l'unité wiki *Autorisation & RBAC*.
(`serveur_grafana_oidc_role_path`).
> **Préférer le groupe à la personne — et c'est construit, pas un idéal.** Les groupes LDAP
> sont projetés dans Keycloak et émis en claim (`tasks/groupes-ldap.yml`, `claim-groupes.yml`),
> et `roles/serveur_grafana/meta/acces.yml` déclare déjà `sysadmin ⇒ Admin`,
> `personnel ⇒ Viewer`. Ajouter quelqu'un au **groupe** lui ouvre Grafana, Forgejo, Icinga et
> le courriel d'un seul geste — alors qu'une assignation nominative crée une dette qu'on
> découvre le jour du départ, service par service. L'assignation explicite ci-dessus reste
> le geste de dépannage, pas la façon normale d'accorder un accès. Voir `docs/autorisation.md` §5.
---
@ -157,49 +164,54 @@ Mesuré sur `forgejo` : rejeu en **0 erreur**, **130 tables**, et les comptes r
**Ne jamais rejouer un `pg_dumpall` entier sur un cluster vivant** : il contient les
`DROP DATABASE` de toutes les bases. Une restauration réelle se fait sur un cluster neuf.
## 6. La frontière porte deux adresses de gestion (transition D-77)
## 6. La bascule d'adressage de la fabric (D-77) — FAITE
> Posée le 2026-08-12 par l'API (`interfaces/vip_settings`, mode `ipalias`). **Ce n'est
> pas une anomalie** : c'est une bascule d'adressage en cours.
> **Cette section décrivait une transition en cours jusqu'au 2026-09-06.** Elle est
> terminée : mesuré le 2026-08-22, **`10.0.0.0/24` n'existe plus** — ni `10.0.0.1`, ni
> `10.0.0.41` ne répondent. Le plan d'administration est `10.17.0.0/24` : la frontière y
> répond en `10.17.0.1` sur un port physique à elle, les commutateurs sont en `10.17.0.3`
> et `.4` avec cette passerelle par défaut, le poste de l'exploitant en `10.17.0.17`. Le
> renumérotage du tenant (`10.27` → `10.17`) est fait lui aussi.
>
> On garde la **méthode**, parce qu'elle est ce qui a permis de le faire sans coupure, et
> qu'un autre site la rejouera :
>
> **Ajouter avant de retirer, jamais l'inverse.** Un point de routage qui change d'adresse
> d'un coup coupe simultanément l'exploitant, les commutateurs qui l'ont en passerelle par
> défaut, et l'outil qui devait faire la bascule. La seconde adresse a donc vécu à côté de
> l'ancienne (`ipalias`), et l'ancienne n'est tombée qu'en **dernier**.
>
> **Distinguer une destination d'un chemin (D-78).** Un réseau qui n'est **jamais** une
> destination — seulement un chemin — n'a aucune raison d'être unique entre deux hébergeurs :
> il sort de l'espace dérivé, vers `192.168.<vlan>.0/24`. Seule la **gestion** doit rester
> unique d'un site à l'autre, parce que le poste de l'exploitant, un VPN et demain un lien
> inter-sites doivent l'atteindre.
>
> **Appliqué pour le transport VXLAN seulement, à ce jour** (mesuré le 2026-09-06) :
> `underlay-vxlan` est bien en `192.168.50.0/24`. Le **transit** (`10.0.4.0/24`, VLAN 40) et
> le **stockage** (`10.11.5-7.x`, VLAN 5/6/7) sont encore dans l'ancien espace. Ce n'est pas
> une urgence — ces réseaux ne quittent jamais leur site — mais la carte doit dire ce qui est,
> pas ce qui a été décidé. Un **site neuf** se monte directement au schéma final : il n'a
> aucune transition à subir.
>
> **Un plan d'adressage ne doit pas dépendre de l'ordre d'une migration.** Le VLAN de
> transport est passé de 11 à 50 — non parce que 11 était mauvais, mais parce que
> `192.168.11.0/24` est occupé par le contrôle de la grappe. Faire dépendre un plan
> d'adressage de l'**ordre** d'une migration est exactement la dette qui se paie un an
> plus tard.
```
10.0.0.1/24 adresse historique — TOUJOURS ACTIVE, rien ne l'a quittée
10.17.0.1/24 adresse cible (D-77 : underlay dans la bande basse du /16 du site)
```
### Ce qui reste, et qui n'est PAS un reliquat de la bascule
**Ajouter avant de retirer**, jamais l'inverse. Un point de routage qui change d'adresse
d'un coup coupe simultanément l'exploitant, les commutateurs qui l'ont en passerelle par
défaut, et l'outil qui devait faire la bascule.
Les hyperviseurs gardent **deux** plans, et c'est voulu :
### Ce qui reste à déplacer, et dans quel ordre
| Plan | Réseau | Ce qu'il porte |
|---|---|---|
| administration | `10.17.0.0/24` (`vmbr3`, segment physique) | les **équipements** et l'exploitant ; **aucune VM ne peut y naître** (aucun pont ne le touche, et le validateur refuse qu'on y déclare une machine) |
| contrôle de la grappe | `192.168.11.0/24` (`vmbr0`, carte dédiée) | l'**interface web Proxmox** et le dialogue entre nœuds — c'est par là qu'on atteint `ansible@192.168.11.4x` |
| # | À déplacer | Vers | Nature (D-78) |
|---|---|---|---|
| 1 | le poste de l'exploitant | `10.17.0.x/24` (seconde adresse) | — |
| 2 | les commutateurs `10.0.0.3/.4` **et leur `ip default-gateway`** | `10.17.0.3/.4`, passerelle `10.17.0.1` | destination |
| 3 | l'administration des hyperviseurs (`vmbr0`, aujourd'hui `192.168.11.x`), l'OOB/IPMI | `10.17.0.41/.43/.47` | destination |
| 4 | le transit `10.0.4.x` | **`192.168.40.x`** | chemin |
| 5 | le transport VXLAN `10.0.5.x`, **VLAN 11 → 50** | **`192.168.50.x`** | chemin |
| 6 | le stockage `10.0.1–3.x` | **`192.168.20/30/31.x`** | chemin |
| 7 | **en dernier seulement**, retirer `10.0.0.1` | — | — |
Les étapes 4 à 6 sortent définitivement de l'espace dérivé (D-78) : ces réseaux ne sont
jamais des destinations, seulement des chemins. Une fois faites, **seule la gestion** doit
rester unique d'un site à l'autre.
> **L'étape 5 change aussi le numéro de VLAN**, côté commutateur (trunk) et côté
> hyperviseurs (`bond3.11` → `bond3.50`). Le 11 est écarté parce que `192.168.11.0/24` est
> occupé par l'étape 3 tant qu'elle n'est pas faite — et un plan d'adressage ne doit pas
> dépendre de l'ordre d'une migration.
### Ce qui ne dépend PAS de cette bascule
**Le renumérotage du tenant** (`10.27` → `10.17`) est **indépendant**. La frontière route
le supernet du tenant vers le même prochain saut, quelle que soit sa propre adresse de
gestion : il suffit que la route et les alias suivent. Les deux chantiers peuvent donc
être menés séparément — et c'est préférable.
> **Longueur de préfixe.** Une fois les deux faits, la gestion (`10.17.0.0/24`) vit
> *à l'intérieur* du supernet du tenant (`10.17.0.0/16`). Aucun conflit : la route
> connectée du `/24` est plus spécifique que celle du `/16`. C'est la règle du préfixe le
> plus long, pas une coïncidence.
> **Le `/24` de gestion vit à l'intérieur du `/16` du tenant, et ce n'est pas un conflit.**
> Les zones d'un tenant commencent au 3ᵉ octet 16 ; la bande 0-15 est libre pour la fabric,
> et la route connectée du `/24` est plus spécifique que celle du `/16` — la règle du
> préfixe le plus long, pas une coïncidence. Il faut cependant le **déclarer**
> (`bande_basse_de:` dans `underlay.yml`), sinon le validateur ne peut pas distinguer ce
> chevauchement voulu d'un chevauchement accidentel.

View file

@ -27,26 +27,38 @@ C'est le VRF qu'on regrettait de ne pas avoir dans le matériel, obtenu en logic
## 2. La projection du modèle
Vérifiée sur les deux tenants fédérés, elle ne demande **aucun changement de dérivation** :
Vérifiée sur chaque tenant fédéré (ils étaient deux au moment de la décision, ils sont
trois), elle ne demande **aucun changement de dérivation** :
| Objet Proxmox SDN | Vient de | Exemple (Chezlepro, zone Services-infra) |
|---|---|---|
| **zone** (un VRF) | le tenant | `CHEZ17` |
| **VNet** | la zone de sécurité | `chez174` |
| **zone** (un VRF) | `zone_de(index)` → `t<index>` | `t17` |
| **VNet** | `vnet_de(index, libellé)` → `t<index><zone abrégée>` | `t17serv` |
| **tag** (VNI) | `vlan_de(index, zone)` | `1174` |
| **subnet** | `sous_reseau_de(index, zone)` | `10.17.19.0/24` |
| **gateway** | `passerelle_de(index, zone)` | `10.17.19.1` |
Les six VNets d'un tenant à l'index 17 : `t17fron`, `t17iden`, `t17donn`, `t17serv`,
`t17obse`, `t17appl`.
> **Rectification du 2026-08-03.** Ce tableau annonçait `chez17-services-infra`, qui
> aurait été **refusé à l'application** : zones et VNets sont limités à **8 caractères**
> par Proxmox — l'identifiant sert de base aux noms de bridge, veth et tap. Message
> amont : *« zone ID … can't be more length than 8 characters »*.
>
> Le nommage dérive du tenant, comme tout le reste : `<PRÉFIXE><index>` pour la zone,
> `<préfixe><index><zone>` pour le VNet. Le préfixe vient de `devis_reseau.prefixe()` —
> la même fonction que le devis des commutateurs, donc un seul endroit fabrique le nom
> court d'un tenant. Éprouvé jusqu'au pire cas de la fédération : `COOP245` = 7,
> `coop2459` = 8. **P30** refuse tout dépassement, sur les deux objets.
> **Le nommage a changé une seconde fois, et ce tableau ne l'avait pas suivi.** Il annonçait
> `CHEZ17` / `chez174`, dérivés de `devis_reseau.prefixe()` — le nom court du *dossier* du
> tenant. La forme en vigueur est plus simple et ne dépend que du seed :
> **`t<index>`** pour la zone, **`t<index><zone abrégée>`** pour le VNet
> (`scripts/devis_sdn.py` : `zone_de`, `vnet_de`). Même préfixe `t` que les IPSets du
> pare-feu Proxmox, donc un seul vocabulaire d'un bout à l'autre de la fabric ; minuscules,
> parce que cet identifiant devient une base de nom d'interface.
>
> La contrainte qui avait motivé la première rectification tient toujours : zones et VNets
> sont limités à **8 caractères** par Proxmox — l'identifiant sert de base aux noms de
> bridge, veth et tap. Message amont : *« zone ID … can't be more length than 8
> characters »*. Avec `t<index>`, la marge est confortable jusqu'à l'index 255 (`t255` = 4,
> `t255serv` = 8). **P30** refuse tout dépassement, sur les deux objets.
>
> **Les zones créées à la main (`VRF0011`, `VRF0017`) sont remplacées.** Ce nommage ne
> disait ni de quel tenant il s'agissait, ni rien qu'on puisse relier au plan : il
@ -65,8 +77,9 @@ porteur** : du SVI d'un commutateur vers la passerelle **anycast** du VNet, pré
chaque hyperviseur — donc plus proche de la VM, et sans point unique de défaillance.
Effet de bord favorable : un VNI est codé sur 24 bits là où un VLAN plafonne à 4094. La limite
du nombre de tenants n'est plus l'espace de VLAN mais le second octet IPv4 du supernet — le
plafond de 245 tenants reste, sa cause change.
du nombre de tenants n'est plus l'espace de VLAN mais le **second octet IPv4** du supernet :
`valider_index` borne l'index à **0–255**, et 0 est à éviter (les réseaux de service du site
y vivent). Le plafond reste, sa cause change.
## 3. Le partage des responsabilités
@ -81,8 +94,10 @@ Deux conséquences qui méritent d'être dites.
**L'inter-tenant ne peut plus être « oublié ».** Il ne circule pas latéralement : il doit
sortir du VRF, donc traverser la bordure, qui est en `block` par défaut. Un flux inter-tenant
légitime devra être **déclaré** pour exister — le registre des flux n'a pas encore de mot-clé
pour ça, c'est un point ouvert.
légitime doit donc être **déclaré** pour exister — et le registre des flux a désormais le
mot-clé qui manquait : **`voisins_site`**, « les tenants d'à côté »
(`scripts/resoudre_flux.py`, `MOTS_PAIR`). Ce paragraphe l'annonçait comme un point ouvert
jusqu'au 2026-09-06 ; il est fermé.
**La défense est en profondeur, sans coût de maintenance.** Le filtrage est-ouest est appliqué
deux fois : par l'hyperviseur, puis par l'hôte destinataire. Une VM compromise doit franchir
@ -168,29 +183,31 @@ Lecture seule par l'API Proxmox. **Le plan de contrôle existe, le plan de donn
| VNets | **aucun** |
| Nœuds de sortie | **aucun** — un VRF sans sortie n'a aucun chemin vers la frontière |
### Deux blocages à lever avant d'aller plus loin
### Deux blocages — levés
**Les VTEP sont adressés dans un tenant.** `vmbr3` porte `10.27.19.{41,43,47}` — le
sous-réseau *Services-infra de Chezlepro*. Le transport du cluster dérive donc de l'index d'un
tenant : un changement d'index le casse, une migration l'emporte. Et une VM de cette zone
partage son sous-réseau avec les trois VTEP, ce qui perce l'isolation à l'endroit même que
l'EVPN devait fermer.
> **Cette section décrivait l'état du 2026-08-02.** Les deux sont levés ; on la garde parce
> que le *raisonnement* explique le plan d'adressage actuel, qui paraîtrait arbitraire sans
> lui.
Le modèle **refuse d'ailleurs d'exprimer cet état** : déclarer `10.27.19.0/24` comme réseau
d'underlay ferait échouer **P23**, qui interdit tout chevauchement avec un supernet tenant. La
garde détecte la faute avant qu'on ne la documente.
**Les VTEP étaient adressés dans un tenant.** `vmbr3` portait `10.27.19.{41,43,47}` — le
sous-réseau *Services-infra de Chezlepro*. Le transport du cluster dérivait donc de l'index
d'un tenant : un changement d'index le cassait, une migration l'emportait. Et une VM de cette
zone partageait son sous-réseau avec les trois VTEP, ce qui perçait l'isolation à l'endroit
même que l'EVPN devait fermer.
`underlay.yml` déclare donc les trois hyperviseurs à leur adresse **cible** — `10.0.0.{41,43,47}`,
dernier octet conservé comme sur `vmbr0`. Le déplacement réel de l'adresse sur les nœuds reste
à faire : c'est une modification du réseau d'un hyperviseur en service.
Le modèle **refusait d'ailleurs d'exprimer cet état** : déclarer `10.27.19.0/24` comme réseau
d'underlay faisait échouer **P23**, qui interdit tout chevauchement accidentel avec un
supernet tenant. La garde a détecté la faute avant qu'on ne la documente.
**Le pont `vmbr3` n'est pas *VLAN-aware*** (pas de `bridge_vlan_aware`, contrairement à
`vmbr2`). L'adresse du VTEP y est donc **non étiquetée** : elle vit dans le VLAN natif du port
de commutateur. Déplacer le VTEP vers l'underlay suppose soit un VLAN natif 10, soit une
interface étiquetée dédiée (`bond3.10`) — ce n'est pas qu'un changement d'adresse.
**Aujourd'hui** : les VTEP vivent sur `underlay-vxlan` — `192.168.50.{41,43,47}`, VLAN 50,
sur une interface étiquetée dédiée (`bond3.50`). Le transport ne dérive plus d'aucun index.
*Pourquoi 50 et pas 11 : sous la règle `192.168.<vlan>`, le VLAN 11 aurait produit
`192.168.11.0/24` — déjà occupé par le contrôle de la grappe. Un plan d'adressage ne doit pas
dépendre de l'ordre d'une migration.*
**`vishnu` n'est pas câblé.** Son `vmbr3` n'a **aucun port physique** : le pont existe, porte
une adresse, et ne mène nulle part. Un pair VXLAN pointé sur lui ne fonctionnera jamais.
**Le pont `vmbr3` n'était pas *VLAN-aware***, et l'adresse du VTEP y vivait donc dans le VLAN
natif du port. C'est ce qui rendait le déplacement plus qu'un changement d'adresse — d'où
l'interface étiquetée dédiée retenue.
## 8. Ce qui reste à trancher
@ -199,9 +216,8 @@ une adresse, et ne mène nulle part. Un pair VXLAN pointé sur lui ne fonctionne
- **Le contrôleur EVPN** : ASN, voisins, et si l'on fait du BGP avec la bordure ou des routes
statiques comme aujourd'hui.
- **Le nombre de nœuds de sortie** et leur redondance.
- **Le mot-clé d'un flux inter-tenant** dans le registre. Aujourd'hui aucun ne l'exprime, donc
tout inter-tenant tombe dans le `block` de la bordure — un défaut sûr, mais qui rend
impossible de *déclarer* une exception légitime.
- ~~**Le mot-clé d'un flux inter-tenant** dans le registre.~~ **Tranché** : c'est
`voisins_site` (2026-08-24), né du besoin de chaîner les caches d'artefacts. Cf. §3.
- **La migration depuis l'existant** : le tenant Chezlepro tourne déjà sur des VLAN. Passer à
EVPN est un changement de plan de transport pour des VM en service — la recette de
`docs/migration-tenant.md` s'applique-t-elle, ou faut-il un chemin plus court ?

View file

@ -66,6 +66,10 @@ restaurer_cles.py le script, autonome
LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
```
> **Une clé faite avant cette version ne porte pas les compagnons.**
> `make cles-compagnons VERS=/media/…/CLE` les y dépose **sans refaire l'archive** : ni
> phrase de passe redemandée, ni risque d'écraser ce qui est déjà vérifié.
Sur la machine neuve, il ne faut que `gpg`, `python3` et la phrase de passe :
```bash

View file

@ -193,7 +193,14 @@ L'hôte est d'abord déclaré dans le **plan** (`instance/plan/serveurs.yml`) et
make deployer HOTE=web-frontal-01
```
La cible `deployer` lit les groupes de l'hôte, cherche les playbooks correspondants dans `playbooks/groupes/`, puis les applique dans l'ordre de l'inventaire.
La cible `deployer` lit les groupes de l'hôte, cherche les playbooks correspondants dans `playbooks/groupes/`, puis les applique **dans l'ordre des couches** — le socle (`serveur_debian`, `serveur_durci`) d'abord, puis le rang de `docs/couches-deploiement.yml`, le même registre que `make site`.
> **Ce n'est pas « l'ordre de l'inventaire », et la nuance a coûté un déploiement.** Un tri
> alphabétique plaçait `client_metrique` avant `serveur_step_ca` : l'intégration réclamait un
> certificat que l'autorité, pas encore déployée, ne pouvait pas avoir émis. Constaté le
> 2026-08-06 sur les deux premières VM — dont l'hôte de l'AC lui-même. La couche
> « intégrations » dit en toutes lettres *déployées en dernier, quand leurs cibles sont
> debout* ; `make deployer` l'ignorait.
Ensuite, elle lance la vérification post-déploiement :
@ -213,9 +220,9 @@ Cette commande limite le playbook au croisement entre le groupe demandé et `hot
## 9. Variables template et conformité
Les variables du template servent à construire le golden template dans `instance/inventories/production/group_vars/modeles_vm.yml`.
Les variables du template servent à construire le golden template dans `instance/inventories/<inventaire>/group_vars/modeles_vm.yml`.
Les variables de conformité servent aux VM déployées dans `instance/inventories/production/group_vars/serveur_debian.yml`.
Les variables de conformité servent aux VM déployées dans `instance/inventories/<inventaire>/group_vars/serveur_debian.yml`.
Différence attendue :

View file

@ -45,7 +45,7 @@ instance/inventories/production/group_vars/all/vault.yml
```
`make config` la sème depuis le gabarit et y place le token. Le clonage lit cette voûte
unifiée (une `proxmox.vault.yml` séparée reste acceptée en compatibilité). Détail des
unifiée. (`proxmox.vault.yml` n'est plus lue — retirée le 2026-08-03.) Détail des
paramètres : [`docs/config-proxmox.md`](../../docs/config-proxmox.md).
Créer un clone Debian depuis le modèle :

View file

@ -26,7 +26,9 @@ serveur_keycloak_admin_user: "admin"
serveur_keycloak_admin_password: "{{ vault_keycloak_admin | default('') }}" # rempli depuis la voute (vault_keycloak_admin)
# --- Fédération LDAP : OpenLDAP source de vérité (modèle d'identité A) ---
# Keycloak lit l'annuaire (READ_ONLY) ; les users LDAP se connectent via le SSO.
# Keycloak federe l'annuaire en WRITABLE (voir serveur_keycloak_ldap_edit_mode plus bas,
# et POURQUOI les deux autres modes sont des culs-de-sac) ; les users LDAP se connectent
# via le SSO.
# Requiert client_pki sur ce nœud (racine step_ca dans le bundle système pour LDAPS).
serveur_keycloak_ldap_federation: true
serveur_keycloak_realm: "{{ identite_realm | default('chezlepro') }}"

View file

@ -19,6 +19,7 @@ Usage :
from __future__ import annotations
from urllib.parse import quote
import re
import sys
from pathlib import Path
@ -119,7 +120,12 @@ def generer() -> str:
intro = intro.replace("|", "\\|")
L.append(f"| {i} | {intro or '—'} | {geste} | {_type(it)} | {_preuves(it)} |")
L.append("")
L.append(f"*Source : [{titre}]({stem}) · § À toi de jouer.*")
# LIEN RELATIF AU FICHIER GENERE, PAS AU WIKI (2026-09-06). Le lien nu `<stem>`
# est la convention du wiki Forgejo, ou les pages sont freres. Mais ce fichier-ci
# vit dans `docs/audit/` : les 22 liens ainsi produits ne menaient nulle part
# depuis le depot. Un plan de recette dont chaque source est un lien mort se lit
# une fois, puis plus jamais.
L.append(f"*Source : [{titre}](../../wiki/{quote(stem)}.md) · § À toi de jouer.*")
L.append("")
return "\n".join(L).rstrip() + "\n"

View file

@ -255,12 +255,42 @@ def preuve_authentification() -> tuple[bool, str]:
if portee == "interne-sans-auth":
lacunes.append(d.name)
# LE TABLEAU DU DOCUMENT DOIT DIRE LA MEME CHOSE QUE LES DECLARATIONS (2026-09-06).
#
# `docs/authentification.md` §5 recopie ces portees avec, en troisieme colonne, soit un
# COMPTE, soit la LISTE des roles. Les deux derivent de ce qu'on vient de lire — et les
# deux avaient cesse d'etre vrais : la table annoncait 12 roles `sans-auth-humaine`, il
# y en a 21. Personne ne l'avait vu parce que rien ne les confrontait ; la preuve lisait
# deja les declarations sans jamais regarder ce que le document en disait.
#
# On accepte les DEUX formes, parce que les deux sont legitimes : un compte quand la
# liste serait illisible, la liste quand elle tient et qu'elle informe davantage.
doc = RACINE / "docs" / "authentification.md"
if doc.is_file():
for ligne in doc.read_text(encoding="utf-8").splitlines():
m = re.match(r"\|\s*`([a-z-]+)`\s*\|.*\|\s*([^|]+?)\s*\|\s*$", ligne)
if not m or m.group(1) not in PORTEES_AUTH:
continue
portee, cellule = m.group(1), m.group(2)
reels = sorted(n.replace("serveur_", "") for n in par_portee.get(portee, []))
if cellule.isdigit():
if int(cellule) != len(reels):
fautes.append(f"docs/authentification.md : la portee '{portee}' est "
f"annoncee pour {cellule} role(s), il y en a {len(reels)}")
else:
cites = sorted(x.strip().replace("-", "_") for x in cellule.split(",")
if x.strip())
if cites != reels:
fautes.append(f"docs/authentification.md : la portee '{portee}' cite "
f"{cites}, les declarations disent {reels}")
if fautes:
return False, "; ".join(fautes[:6])
total = sum(len(v) for v in par_portee.values())
resume = ", ".join(f"{k} {len(v)}" for k, v in sorted(par_portee.items()))
suffixe = f" ; {len(lacunes)} lacune(s) nommee(s) : {', '.join(lacunes)}" if lacunes else ""
return True, f"{total} role(s) serveur declares ({resume}){suffixe}."
return True, (f"{total} role(s) serveur declares ({resume}){suffixe} ; "
f"le tableau de docs/authentification.md dit la meme chose.")
def preuve_propriete_des_intrants() -> tuple[bool, str]:
@ -1322,6 +1352,99 @@ def _carte_chiffres_mesures() -> dict[str, int]:
}
def preuve_comptes_de_la_prose() -> tuple[bool, str]:
"""Un nombre de roles ou de preuves ecrit en prose correspond a la mesure.
POURQUOI, ALORS QUE P48 GARDE DEJA LA CARTE (mesure du 2026-09-05). P48 ne regarde
que le tableau « Le depot en chiffres ». Les MEMES nombres vivent ailleurs, en pleine
phrase, et ils avaient tous derive :
docs/devis-services.md : « porte 35 preuves » -> 56
AGENTS.md : « les 35 preuves » -> 56
docs/pouvoirs-set-ops : « ≈50 roles » -> 65
docs/catalogue-services : « les 29 groupes » -> 40
Le quatrieme a ete trouve APRES coup (2026-09-06), par la revision de documentation et
non par cette preuve : elle ne regardait que `preuves` et `roles`. Un catalogue qui
annonce 29 groupes au-dessus d'un tableau qui en cite 40 se contredit A UNE LIGNE DE
DISTANCE. D'ou le troisieme compte.
Aucun de ces nombres ne fait travailler personne. Mais AGENTS.md est la source
d'autorite du depot : un document qui se trompe sur ce qu'il decrit cesse d'etre cru,
et c'est alors ses DIRECTIVES qu'on perd — pas seulement ses chiffres.
CE QU'ELLE NE FLAGUE PAS, ET POURQUOI C'EST ESSENTIEL. Une mesure DATEE est un fait
historique : « Set-OPS a longtemps eu 31 preuves » raconte pourquoi `make valider`
existe, et reecrire ce 31 detruirait le recit. On ignore donc toute ligne portant une
annee — le depot date ses mesures, c'est deja sa discipline.
Une preuve qui crie sur un cas sain finit par etre ignoree, ce qui est pire que de ne
pas l'avoir. Celle-ci a ete ecrite, puis passee sur tout le corpus jusqu'a etre
SILENCIEUSE sur les formulations legitimes avant d'etre retenue.
"""
mesures = {
"preuves": len(TOUTES_LES_PREUVES),
"roles": len([d for d in (RACINE / "roles").iterdir() if d.is_dir()]),
# UN GROUPE OPERATIONNEL EST UN PLAYBOOK, PAS UN ROLE. P04 tient deja la regle
# « groupe -> playbooks/groupes/<groupe>.yml » : c'est donc ce repertoire qui
# compte, et non `roles/`, qui contient aussi les rôles composés par un groupe
# (les onze du durcissement n'ont pas de groupe a eux).
"groupes": len(list((RACINE / "playbooks" / "groupes").glob("*.yml"))),
}
fichiers = sorted(
list((RACINE / "docs").glob("*.md")) + list((RACINE / "wiki").glob("*.md"))
+ [RACINE / "AGENTS.md", RACINE / "README.md", RACINE / "CLAUDE.md"])
ecarts: list[str] = []
for f in fichiers:
if not f.is_file():
continue
lignes = f.read_text(encoding="utf-8").splitlines()
# LA DATATION PORTE SUR LE PARAGRAPHE, PAS SUR LA LIGNE (mesure du 2026-09-05).
#
# Premiere version : on ignorait une ligne portant une annee. Elle a crie sur les
# deux seuls cas legitimes du corpus — `autorisation.md` datait son constat trois
# lignes plus haut, et `Verifier-le-deploye.md` ecrit « a longtemps eu » sans
# millesime. Une garde qui crie sur des cas sains finit par etre ignoree ; on
# regarde donc le PARAGRAPHE, qui est l'unite ou une mesure se date reellement.
bornes: list[tuple[int, int]] = []
debut = 0
for i, l in enumerate(lignes):
if not l.strip():
if i > debut:
bornes.append((debut, i))
debut = i + 1
if debut < len(lignes):
bornes.append((debut, len(lignes)))
historiques: set[int] = set()
for a, b in bornes:
para = "\n".join(lignes[a:b])
if re.search(r"\b20\d\d\b|longtemps|autrefois|jusqu'(a|au)\b", para, re.I):
historiques.update(range(a, b))
for n, ligne in enumerate(lignes, 1):
if (n - 1) in historiques:
continue
for m in re.finditer(
r"(?<![\d.])(\d{2,3})\s+(preuves|rôles|roles|groupes)\b", ligne):
valeur = int(m.group(1))
mot = m.group(2)
quoi = ("preuves" if mot == "preuves"
else "groupes" if mot == "groupes" else "roles")
if valeur != mesures[quoi]:
ecarts.append(
f"{f.relative_to(RACINE)}:{n} annonce {valeur} {quoi}, "
f"le depot en compte {mesures[quoi]}")
if ecarts:
return False, ("Des comptes ecrits en prose ne disent plus vrai :\n - "
+ "\n - ".join(ecarts)
+ "\n (dater la ligne la rend historique et l'exempte)")
return True, (f"Les comptes ecrits en prose correspondent a la mesure "
f"({mesures['preuves']} preuves, {mesures['roles']} roles, "
f"{mesures['groupes']} groupes).")
def preuve_carte_dit_vrai() -> tuple[bool, str]:
"""La carte d'orientation designe des choses qui existent, et compte juste.
@ -2321,6 +2444,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_cle_du_site_bornee_au_runner},
{"id": "P56", "titre": "Gabarit minimal, et rien de retire n'est perdu",
"refs": [], "func": preuve_gabarit_minimal_et_repris},
{"id": "P57", "titre": "Comptes en prose : les chiffres du depot sur lui-meme",
"refs": [], "func": preuve_comptes_de_la_prose},
{"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [],
"func": preuve_devis_frontiere_du_site},
{"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],
@ -2328,6 +2453,22 @@ PREUVES: list[dict] = [
]
# LA PREUVE CONDITIONNELLE VIT ICI, PAS DANS `main()` — POUR ETRE COMPTABLE (2026-09-06).
#
# P16 exige la voute : elle est SAUTEE quand la cle n'est pas la. Definie a l'interieur de
# `main()`, elle echappait a `len(PREUVES)` — et la preuve des comptes en prose (P57)
# imposait donc « 56 preuves » a toute la documentation, alors que le depot en porte 57
# (P01 a P57, sans trou). Un garde-fou qui fait respecter un chiffre faux est pire qu'aucun
# garde-fou : il donne l'assurance en plus de l'erreur.
PREUVE_CONDITIONNELLE_INVENTAIRE: dict = {
"id": "P16", "titre": "Inventaire Ansible complet (--list)",
"refs": ["AFF-030"], "func": preuve_inventaire_ansible}
# TOUTES les preuves du depot, conditionnelles comprises. C'est ce nombre que la
# documentation doit annoncer.
TOUTES_LES_PREUVES: list[dict] = PREUVES + [PREUVE_CONDITIONNELLE_INVENTAIRE]
def _vault_requis_absent() -> bool:
"""Vrai si l'inventaire chiffre un group_vars et qu'aucun mot de passe n'est fourni.
@ -2394,8 +2535,7 @@ def main(argv: list[str] | None = None) -> int:
resultats: list[tuple[dict, str, str]] = []
# Preuve conditionnelle : inventaire Ansible complet (necessite la voute).
preuve_inv = {"id": "P16", "titre": "Inventaire Ansible complet (--list)",
"refs": ["AFF-030"], "func": preuve_inventaire_ansible}
preuve_inv = PREUVE_CONDITIONNELLE_INVENTAIRE
liste = list(PREUVES)
if _vault_requis_absent():
resultats.append((preuve_inv, "SAUTE",

View file

@ -1,9 +1,16 @@
#!/usr/bin/env python3
"""Registre des serveurs (VM) du plan Set-OPS.
Phase 2 : l'inventaire reste AUTORITE. Ce registre est bootstrape depuis lui
(reconciliation en lecture). La generation de l'inventaire depuis le plan
viendra en Phase 3 (make instancier).
LE PLAN EST L'AUTORITE, ET CE N'EST PLUS L'INVERSE (corrige le 2026-09-06). Ce module a
longtemps annonce « Phase 2 : l'inventaire reste AUTORITE [...] la generation depuis le
plan viendra en Phase 3 ». La Phase 3 est faite depuis longtemps : `make instancier`
genere `hosts.yml` DEPUIS ce registre, et la REGLE D'OR d'AGENTS.md interdit d'editer
l'inventaire a la main. Lire l'ancienne phrase aujourd'hui, c'est croire l'exact contraire
de la doctrine.
`bootstrap` reste utile, mais c'est une manoeuvre de REPRISE, pas le flux normal : il
(re)constitue `plan/serveurs.yml` depuis un inventaire existant — le geste qu'on fait une
fois, quand on herite d'un ecosysteme dont le plan n'existe pas encore.
"""
from __future__ import annotations
@ -42,9 +49,12 @@ CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),
def ecrire(registre: dict) -> None:
entete = (
"# Registre des serveurs (VM) du plan Set-OPS.\n"
"# Bootstrape depuis l'inventaire (make serveurs-bootstrap). L'inventaire\n"
"# reste autorite en Phase 2 ; VMID/IP/VLAN/passerelle sont DERIVES de la\n"
"# fonction via instance/plan/nomenclature.yml (non stockes ici).\n"
"# CE FICHIER EST L'AUTORITE : l'inventaire en est GENERE (make instancier).\n"
"# Ne jamais editer instance/inventories/*/hosts.yml a la main.\n"
"# Reconstitue ici depuis un inventaire existant par `make serveurs-bootstrap` —\n"
"# manoeuvre de reprise, pas le flux normal.\n"
"# VMID/IP/VLAN/passerelle sont DERIVES de la fonction via\n"
"# instance/plan/nomenclature.yml (jamais stockes ici).\n"
"---\n"
)
with FICHIER.open("w", encoding="utf-8") as fichier:

View file

@ -41,9 +41,18 @@ Déclaratif dans Set-OPS : `serveur_keycloak_realm_roles`, `_role_mapper_clients
**zéro coupure SSO**. Une identité (`testmail`), et c'est **le rôle** — pas la connexion — qui décide
d'Explore.
> **Raffinement souverain** : ici le rôle est assigné *explicitement* à testmail. L'idéal est de le
> piloter par un **groupe d'annuaire** (LDAP → Keycloak → claim), pour que *l'appartenance* gouverne
> l'autorisation. C'est le vrai « l'annuaire gouverne l'accès ».
> **Ce « raffinement » est construit depuis le 2026-08-08.** Cette unité le présentait comme
> un idéal à atteindre ; c'est le mécanisme en vigueur. Les **groupes LDAP** sont projetés
> dans Keycloak et émis en claim (`roles/serveur_keycloak/tasks/groupes-ldap.yml` et
> `claim-groupes.yml`), et chaque service déclare le groupe qu'il reconnaît dans son
> `meta/acces.yml` — `sysadmin ⇒ Admin`, `personnel ⇒ Viewer` chez Grafana. C'est donc bien
> **l'appartenance** qui gouverne, pas une assignation nominative.
>
> La règle qui va avec : **un service nomme un groupe, jamais une personne**. Nommer
> quelqu'un créerait une dette qu'on découvre le jour du départ, service par service — et il
> faudrait un déploiement pour révoquer. L'assignation explicite à `testmail` de l'exemple
> ci-dessus reste utile pour *comprendre* la mécanique du claim ; ce n'est pas la façon dont
> on donne un accès. Voir `docs/autorisation.md` §5.
---

View file

@ -32,7 +32,10 @@ Registre ──> PostgreSQL crée la base + le compte propriétaire
Le **secret** vit dans la **voûte** (Ansible Vault), déréférencé **au déploiement**, jamais en clair
dans le plan. C'est une **liaison** *app → base*, de modalité **requise** (Keycloak sans sa base ne
démarre pas). Consommateurs prouvés : Keycloak, Forgejo, IcingaDB.
démarre pas). Le registre porte aujourd'hui **quatre bases** — `keycloak`, `forgejo`,
`icingadb`, `nextcloud` — et **cinq rôles** incluent `resoudre_base` pour bâtir leur
connexion : `serveur_keycloak`, `serveur_forgejo`, `serveur_icinga`, `serveur_icingaweb2`,
`serveur_nextcloud`.
---

View file

@ -54,10 +54,13 @@ Tu as appris **MTA/MDA/MUA, SMTP vs soumission, IMAP, la déliverabilité** —
1. **Envoie via la soumission `:587`** (client authentifié) :
```bash
swaks --server edge-mta-01.lab.chezlepro.internal:587 --tls \
--auth LOGIN --auth-user testmail@lab.chezlepro.internal --auth-password MotDePasseTest123 \
--from testmail@lab.chezlepro.internal --to testmail@lab.chezlepro.internal --header 'Subject: essai'
swaks --server edge-mta-01.chezlepro.internal:587 --tls \
--auth LOGIN --auth-user testmail@chezlepro.internal --auth-password "$MDP_ESSAI" \
--from testmail@chezlepro.internal --to testmail@chezlepro.internal --header 'Subject: essai'
```
*(`MDP_ESSAI` se saisit à la main — `read -rs MDP_ESSAI`. Un mot de passe écrit ici
partirait dans l'historique du shell et dans le dépôt public : ce document en portait un
en clair jusqu'au 2026-09-06.)*
`235 Authentication successful` puis `250 queued` = ① en action.
2. **Lis la boîte** (le MDA) : `doveadm search -u testmail mailbox INBOX all | wc -l` sur infra-mail-01.
3. **Casse & répare.** Coupe l'annuaire (arrête OpenLDAP), renvoie un courriel : Postfix **rejette le

View file

@ -7,12 +7,12 @@
## ① Le concept *(générique)*
Les machines se parlent par **adresses IP** ; les humains (et les configs) utilisent des **noms**.
La **résolution de noms** fait le pont : `keycloak.lab…` → `192.168.15.21`.
La **résolution de noms** fait le pont : `keycloak.chezlepro.internal` → `10.17.17.11`.
Plusieurs couches, de la plus locale à la plus globale :
- **Fichier hosts** (`/etc/hosts`) : une table statique, locale, **consultée en premier**, **sans
aucun réseau**. Increvable, mais manuelle.
- **DNS autoritatif** : le serveur qui **détient la vérité** d'une zone (ex. `lab.chezlepro.internal`)
- **DNS autoritatif** : le serveur qui **détient la vérité** d'une zone (ex. `chezlepro.internal`)
et répond pour ses noms (enregistrements **A**, **SOA**…).
- **DNS récursif (résolveur)** : celui que tes machines interrogent ; il **cherche pour toi**
(cache local, puis interne, puis Internet).
@ -28,14 +28,21 @@ chercher* la réponse pour toi »).
|---|---|---|
| **1. Le plancher** | `hosts_statiques` (socle) → `/etc/hosts` sur **chaque** nœud | résout **même DNS éteint**, dès le bootstrap. Le filet en dessous de tout. |
| **2. Autoritatif** | `serveur_powerdns` (PowerDNS) | la zone interne, les enregistrements **A**. |
| **3. Récursif local** | `client_resolveur` (**opt-in**) | résolveur local : stub-zone → PowerDNS pour l'interne, récursion pour le reste. |
| **3. Récursif** | `serveur_resolveur` — **un** Unbound pour tout le tenant | récurse depuis la racine, délègue la zone souveraine à PowerDNS. |
| *(l'intégration)* | `client_resolveur` — **universelle**, sur chaque nœud | n'installe rien : écrit `/etc/resolv.conf` pour désigner le résolveur ci-dessus. |
> **Le récursif n'est plus « local », et il n'est plus optionnel.** Avant le 2026-08-24,
> `client_resolveur` posait un Unbound sur *chaque* VM — N démons identiques pour un service
> unique. Il n'installe plus rien, et son intégration est **universelle, sans aucune
> exemption** : pas même l'hôte qui porte le résolveur, qui se sert lui-même.
Le **plancher** est le cœur pédagogique : parce que chaque nœud connaît *tout l'écosystème* par
`/etc/hosts`, **rien ne dépend du DNS pour démarrer** — PowerDNS devient une *commodité*, pas un
point de défaillance unique. On construit la robustesse **de bas en haut**.
C'est aussi ce que **tu** utilises depuis ta machine : mettre `192.168.15.21 keycloak.lab…` dans
ton `/etc/hosts`, c'est exactement le même « plancher ».
C'est aussi ce que **tu** utilises depuis ta machine : mettre
`10.17.16.11 keycloak.chezlepro.internal` dans ton `/etc/hosts`, c'est exactement le même
« plancher ».
---
@ -56,23 +63,28 @@ Tu as appris **la hiérarchie de résolution** (hosts → récursif → autorita
1. **Le plancher, sans DNS.** Sur un nœud :
```bash
getent hosts keycloak.lab.chezlepro.internal # répond via /etc/hosts, zéro DNS
getent hosts keycloak.chezlepro.internal # répond via /etc/hosts, zéro DNS
grep chezlepro /etc/hosts | head
```
2. **Interroge l'autoritatif.** Demande à PowerDNS directement :
```bash
dig @infra-dns-01.lab.chezlepro.internal keycloak.lab.chezlepro.internal A +short
dig @infra-dns-01.lab.chezlepro.internal lab.chezlepro.internal SOA +short
dig @infra-dns-01.chezlepro.internal keycloak.chezlepro.internal A +short
dig @infra-dns-01.chezlepro.internal chezlepro.internal SOA +short
```
3. **Vois les couches.** Compare `getent hosts` (plancher) et `dig` (DNS) : **deux chemins**, même IP.
4. **Casse & répare.** Sur un nœud **sans** `client_resolveur`, vide `/etc/hosts` de ses entrées
`chezlepro` (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne **échoue**.
Restaure `/etc/hosts` : ça remarche **sans DNS**. Tu viens de *sentir* pourquoi le plancher est le
filet de sécurité.
4. **Casse & répare.** Vide `/etc/hosts` de ses entrées `chezlepro` (garde une sauvegarde !)
**et** pointe `/etc/resolv.conf` ailleurs : la résolution interne **échoue**. Restaure
`/etc/hosts` seul : ça remarche **sans DNS**. Tu viens de *sentir* pourquoi le plancher
est le filet de sécurité.
5. **Le piège du récursif.** Demande un nom qui n'existe pas sous `internal.`, puis
redemande un nom qui existe. Si le résolveur répond NXDOMAIN aux deux, tu viens de
reproduire la panne de deux jours du 2026-09-02 : la racine étant signée, elle *prouve*
que `internal.` n'existe pas, et Unbound étend ce « non » à tout ce qui est dessous —
sans jamais interroger la stub-zone. Remède : `harden-below-nxdomain: no`.
---
## Pour aller plus loin *(dépôt)*
- Rôles : `roles/hosts_statiques` (le plancher), `roles/serveur_powerdns`, `roles/client_resolveur`.
- Conception des 3 couches + frontière publique : `docs/dns-interne.md`.
- Note : `client_resolveur` est une **liaison optionnelle** (opt-in) — voir l'unité *Liaisons*.
- Note : `client_resolveur` est une **intégration universelle** (elle suit l'existence de `serveur_resolveur`) — voir l'unité *Liaisons* et `docs/integrations-vm.md`.

View file

@ -51,10 +51,13 @@ partagée, chacune avec son *index*. Cf. [Multi-instance & fédération](Multi-i
règles de frontière, ni *VNet* ne lui sont réservés. Pour un bac à sable local, ou pour un
écosystème encore à l'état de plan.
**Modèle** — Un *plan* générique réutilisable, sans *voûte*, dans `Set-OPS-Modeles`. Les
modèles sont les **offres** : `forge`, `identite`, `collaboration`…
**Modèle** — Un *plan* générique réutilisable, sans *voûte*. Le dépôt public en fournit
**un** (`exemples/modeles/socle`) ; les modèles assemblés par offre — `forge`, `identite`,
`collaboration`… — vivent dans le dépôt privé `Set-OPS-modeles`.
**Socle** — Le modèle minimal public : DNS, PKI, edge TLS, relais courriel.
**Socle** — Le modèle minimal public, **quatre VM** : DNS interne (PowerDNS), PKI
(step-ca), edge TLS (nginx) et **magasin courriel** (Dovecot). C'est un magasin de boîtes,
pas un relais : le MTA s'ajoute ensuite.
**Liaison (binding)** — Relation déclarée entre entités du plan (app→base, app→app),
résolue par le moteur. Cf. [Liaisons (bindings)](Liaisons-bindings).
@ -177,8 +180,10 @@ ne tient de liste.
**Spanning-tree (STP)** — Le protocole qui empêche une boucle de commutateurs de saturer le
réseau. Il élit une racine ; c'est ce que désigne `routeur` dans l'*underlay*.
**MLAG** — Deux commutateurs qui se font passer pour un seul. Set-OPS n'en a pas : d'où
*un seul* commutateur qui route, et les chemins doublés ailleurs.
**MLAG** — Deux commutateurs qui se font passer pour un seul. Set-OPS n'en a pas — d'où
les chemins doublés ailleurs (MPIO côté stockage, `active-backup` côté flotte). *(Cette
entrée concluait « d'où un seul commutateur qui route » : plus aucun ne route. Le routage
des tenants vit sur l'hyperviseur, cf. l'entrée* SVI *ci-dessus.)*
**nftables** — Le pare-feu **de chaque machine** Linux. Set-OPS le dérive du registre des
flux.

View file

@ -24,7 +24,7 @@ des **méthodes 100 % génériques**.
| Section | Contenu |
|---|---|
| **Unités d'apprentissage** | Un fondamental TIC par page, toujours selon le même moule (ci-dessous). Des *services* (identité, PKI, courriel…) **et** de la *méthode* (le plan, le multi-instance, la preuve). |
| **Opérations (runbooks)** | Procédures : ajouter un service, déployer un nœud, restaurer une sauvegarde… |
| **Opérations (runbooks)** | Les procédures ne vivent **pas** ici : elles sont dans le dépôt, `docs/runbooks-exploitation.md`. Le wiki y pointe (même règle que la référence technique, ci-dessous). |
| **[Glossaire](Glossaire)** | Les concepts-clés en une phrase (seed, bindings, le plancher, hôte fantôme, voûte…). |
| **Référence technique** | Le *détail du « comment »* vit **dans le dépôt** (`docs/`, README des rôles) — ce wiki y **pointe**, ne le **recopie pas** (pour éviter la dérive). |

View file

@ -33,7 +33,7 @@ Trois pièces, un rôle chacun :
| Pièce | Rôle | Concept incarné |
|---|---|---|
| **OpenLDAP** (`serveur_openldap`) | l'**annuaire** — la source de vérité. `testmail` y vit. | annuaire |
| **Keycloak** (`serveur_keycloak`) | le **fournisseur d'identité** (IdP) SSO. Il **fédère** OpenLDAP (lit les comptes en LDAP, lecture seule). | SSO, OIDC, fédération |
| **Keycloak** (`serveur_keycloak`) | le **fournisseur d'identité** (IdP) SSO. Il **fédère** OpenLDAP en mode **`WRITABLE`** : il écrit *à travers*, vers l'annuaire. | SSO, OIDC, fédération |
| **oauth2-proxy** (`serveur_oauth2_proxy`) | une **passerelle** OIDC pour les apps **sans** OIDC natif. | motif *proxy d'authentification* |
Les applications se branchent de deux façons :
@ -52,6 +52,16 @@ Grafana ──> crée la session : "tu es testmail" ✔
Résultat : **une identité** (`testmail`, un seul mot de passe LDAP) ouvre **le courriel, Grafana,
Forgejo et Icinga**. Change le mot de passe une fois, il change partout.
> **Pourquoi `WRITABLE` et pas « lecture seule ».** Cette unité a écrit « lecture seule »
> jusqu'au 2026-09-06 ; ce serait plus prudent en apparence, et c'est un cul-de-sac. En
> `READ_ONLY`, un changement de mot de passe échoue sur *« Federated storage is not
> writable »* — or l'annuaire *exige* ce changement à la première connexion : la reprise du
> sysadmin butait dessus (constaté le 2026-08-07). Et `UNSYNCED` serait pire : Keycloak
> écrirait dans **sa** base, Dovecot et Postfix continueraient de valider l'ancien depuis
> LDAP — une identité, deux mots de passe, exactement ce que la doctrine interdit.
> `WRITABLE` écrit *à travers* : l'annuaire reste la source unique, Keycloak n'en est qu'un
> client.
---
## ③ Pourquoi c'est transférable
@ -74,14 +84,14 @@ Change les produits, le **schéma reste**. C'est ça, un savoir générique.
> Prérequis : accès au lab (VPN), `/etc/hosts` pointant les services sur l'edge.
1. **Vis le SSO.** Ouvre `https://grafana.lab.chezlepro.internal` → « *Se connecter avec
1. **Vis le SSO.** Ouvre `https://grafana.chezlepro.internal` → « *Se connecter avec
Chezlepro* » → `testmail`. Puis ouvre Forgejo, puis Icinga : **tu n'es reconnecté nulle part**.
2. **Observe le flux.** Rouvre Grafana en navigation privée, ouvre les outils dév (F12 → Réseau) :
repère la **redirection vers Keycloak**, puis le **retour avec un `code=`**. C'est ① en action.
3. **Interroge l'annuaire (la source de vérité).** Sur un nœud avec `ldap-utils` :
```bash
ldapsearch -x -H ldaps://id-ldap-01.lab.chezlepro.internal \
-b ou=people,dc=lab,dc=chezlepro,dc=internal '(uid=testmail)'
ldapsearch -x -H ldaps://idm-01.chezlepro.internal \
-b ou=people,dc=chezlepro,dc=internal '(uid=testmail)'
```
Tu vois l'entrée que Keycloak **fédère** — il ne l'a pas recopiée.
4. **Casse & répare (la fédération).** Dans la console admin Keycloak → *User Federation* →

View file

@ -26,7 +26,7 @@ Trois idées la portent :
- **Le registre** : `docs/audit/affirmations.md` — chaque affirmation du dépôt (README, docs,
aide `make`, GUI) reliée à une preuve et un statut (✅/🟡/❌/⚪).
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P35**) et écrit
- **Le harnais** : `make prouver` rejoue les preuves automatisables (**P01–P57**, sans trou dans la série) et écrit
`docs/audit/preuve-<date>.md`. `make verifier` les inclut : il **échoue** si une preuve échoue.
- **Chaque preuve garde une classe d'erreur.** Extrait :

View file

@ -38,8 +38,16 @@ Les **résolveurs partagés** font le pont : `resoudre_base` (app→base) et `re
**dans le rôle**, jamais en clair.
La **modalité** structure la robustesse : une liaison **requise** absente → le moteur **refuse
d'instancier** (ex. une base sans serveur SQL). Une **optionnelle** absente → silence (ex. un nœud
sans `client_journal` marche très bien).
d'instancier** (ex. une base sans serveur SQL). Une **optionnelle** absente → silence (ex. un
nœud sans `client_backup` : il ne détient pas d'état, il n'a rien à sauvegarder).
> **Attention à une troisième catégorie, qui n'est ni l'une ni l'autre : l'universelle.**
> `client_pki`, `client_metrique`, `client_journal`, `client_resolveur` et
> `client_artefacts` ne se déclarent **pas** — tout hôte les reçoit par dérivation, et
> **P26** refuse qu'un hôte y échappe. Cette unité citait `client_journal` en exemple
> d'optionnelle jusqu'au 2026-09-06 : c'est l'inverse. Le plan portait 28 lignes qui
> disaient « oui » à quelque chose de vrai pour tous ; elles n'existaient que pour être
> oubliées, et quatre l'avaient été.
C'est **le** concept de Set-OPS : *tu déclares les liaisons, le moteur câble.*
@ -64,9 +72,11 @@ produit. C'est un **modèle mental** qui vaut de NetScaler à Kubernetes.
*app→domaine*), et `serveurs.yml` : un nœud avec `integrations:` (liaison *nœud→service*).
2. **Vois-la se résoudre.** Après `make instancier`, regarde l'inventaire généré : la cible est
devenue une **valeur concrète** (FQDN, groupe) — le moteur a câblé.
3. **Requise vs optionnelle.** Compare : retirer `client_journal` d'un nœud → aucun problème
(optionnelle). Déclarer une base **sans** serveur → `make instancier`/la validation **échoue**
(requise). Tu *sens* la différence de modalité.
3. **Requise, optionnelle, universelle.** Compare les trois : retirer `client_backup` d'un
nœud sans état → aucun problème (**optionnelle**). Déclarer une base **sans** serveur →
`make instancier` **échoue** (**requise**). Essayer de recopier `client_metrique` dans
`serveurs.yml` → le plan **refuse** (**universelle** : elle est dérivée, la recopier
créerait une seconde source qui finirait par diverger).
4. **Casse & répare.** Casse une liaison requise (ex. réfère une base à un serveur inexistant),
relance l'instanciation : **échec clair** *avant* tout déploiement. Corrige : ça passe. Le moteur
attrape le câblage manquant à ta place.

View file

@ -30,7 +30,10 @@ rien à inscrire nulle part. Déposer une instance à côté des autres suffit.
**Le seed garantit l'unicité.** Chaque tenant a un `index` distinct → adressage dérivé sans
chevauchement possible : VLAN `1000+index×10+zone` (les VLAN de deux index différents ne peuvent
*mathématiquement* pas se croiser). Plafond : **245** écosystèmes fédérés (borne IPv4).
*mathématiquement* pas se croiser). Plafond : le **2ᵉ octet IPv4**, soit un index de **0 à
255** — en évitant 0, où vivent les réseaux de service du site. *(Cette unité annonçait 245,
un reste de l'époque où l'adressage valait `10.<10+index>` ; ce décalage a été retiré le
2026-08-12, justement pour qu'on lise l'index directement dans l'adresse.)*
**Gérer la flotte :**
- `make instances` — la vue d'ensemble : qui existe, l'active (★), index, VLAN, **collision** ;
@ -91,15 +94,23 @@ d'aucun `index`. On la décrit dans `underlay.yml`, qui vit dans le dépôt de *
plan par `instance/`. Ce lien ne suit pas `make instance-utiliser` : la fabric reste celle de
l'hébergeur, quel que soit le tenant actif. Gabarit : `underlay.yml.example`.
| Underlay | VLAN | Sous-réseau | Fabric |
|---|---|---|---|
| management (commutateurs, Proxmox, OOB) | 10 | `10.0.0.0/24` | principale |
| **transit** vers la frontière | 40 | `10.0.4.0/29` | principale |
| stockage / iSCSI | 20 | `10.0.1.0/24` | stockage |
| ceph-public | 30 | `10.0.2.0/24` | stockage |
| ceph-cluster | 31 | `10.0.3.0/24` | stockage |
> **Ne pas recopier de table ici.** Cette unité en portait une, figée, et **aucune de ses
> cinq lignes n'était encore vraie** après la bascule d'adressage de la fabric (D-77/D-78,
> terminée le 2026-08-22). Un underlay n'est pas un modèle : c'est la description d'un
> matériel particulier, propre à un hébergeur. On le **demande** :
>
> ```bash
> make underlay # affiche l'underlay monté, et le valide
> ```
>
> Ce qu'on y trouve chez l'hébergeur de référence, à titre d'illustration seulement : un
> plan d'**administration** sans VLAN (segment physique, `10.17.0.0/24`), le **contrôle de
> la grappe** à part (`vmbr0`, `192.168.11.0/24`), un **transit** vers la frontière, un
> réseau de **transport VXLAN**, trois réseaux de **stockage** sur leur propre fabric en
> MTU 9000, et les **zones du site lui-même** — l'hébergeur est aussi un exploitant, ses
> machines vivent là.
Deux choses à retenir de cette table.
Deux choses à retenir, qui ne dépendent d'aucune table.
**Les fabrics.** Tous les réseaux ne partagent pas les mêmes câbles. Le stockage jumbo peut
vivre sur ses propres commutateurs ; le devis de l'une ne déclare alors rien de l'autre, et le
@ -116,13 +127,19 @@ route vers tous les tenants par ce même saut, il ne peut donc dériver d'aucun
> elle n'est joignable. On cherche une règle de pare-feu ; c'est une route manquante à l'autre
> bout.
En mode `sdn`, ces réseaux transportent du VXLAN : leur **MTU doit atteindre 1550** au minimum,
sinon le ping passe et les transferts échouent. `make underlay` le refuse.
En mode `sdn`, le transport porte du VXLAN : le **MTU des réseaux de la fabric du routeur**
doit valoir l'overlay déclaré **plus 50 octets** d'encapsulation. `make underlay` le
**refuse** en dessous, et le dit — sous ce seuil, le ping passe et les transferts échouent,
la panne la plus coûteuse à diagnostiquer de cette couche.
`make underlay` l'affiche et le **valide** : VLAN < 1000 et sous-réseaux hors des supernets
tenant (`10.<index>.0.0/16`) — **aucune collision possible** avec les overlays. `make
devis-reseau` en émet la config (section 0) et l'ajoute au trunk. La **preuve P23** garde la
règle ; elle est *sautée* si aucun `underlay.yml` n'est défini.
`make underlay` l'affiche et le **valide** : VLAN < 1000, et pas de chevauchement
**accidentel** avec un supernet de tenant. « Accidentel », parce qu'il en existe un
**voulu** : le plan d'administration de l'hébergeur occupe la **bande basse** du `/16` d'un
tenant (D-77). Les zones d'un tenant commencent au 3ᵉ octet 16 ; la bande 0-15 lui est
libre. Ce n'est pas une collision, mais il faut le **déclarer** (`bande_basse_de:`) — sinon
le validateur ne peut pas faire la différence, et il refuse. `make devis-reseau` en émet la
config (section 0) et l'ajoute au trunk. La **preuve P23** garde la règle ; elle est
*sautée* si aucun `underlay.yml` n'est défini.
---

View file

@ -28,9 +28,19 @@ ou expédie des logs (*shipper*). Le serveur central agrège ; un tableau de bor
| **Loki** (`serveur_loki`) | **PUSH** | reçoit les journaux `journald` expédiés par `client_journal`. |
| **Grafana** (`serveur_grafana`) | — | tableaux de bord au-dessus des **deux** (au SSO). |
Le point-clé prouvé cette session : un serveur d'observabilité **sans agents** ne voit que
lui-même. En déployant `client_metrique`/`client_journal` (des **liaisons nœud, optionnelles**),
Grafana voit **toute la flotte**. *Serveur ≠ agent* : les deux sont nécessaires.
Le point-clé : un serveur d'observabilité **sans agents** ne voit que lui-même. Avec
`client_metrique`/`client_journal` sur les nœuds, Grafana voit **toute la flotte**.
*Serveur ≠ agent* : les deux sont nécessaires.
> **Ces deux liaisons ne sont PAS optionnelles**, contrairement à ce que cette unité a dit
> jusqu'au 2026-09-06. Elles portent `universelle: true` dans leur `meta/integration.yml` :
> tout hôte les reçoit **par dérivation**, sans qu'on écrive une ligne au plan, et **P26**
> refuse qu'un hôte y échappe. C'est exactement la leçon du paragraphe ci-dessus, tirée à
> son terme : le plan portait 28 lignes qui disaient « oui » à quelque chose de vrai pour
> tous — elles n'existaient que pour être oubliées, et quatre l'avaient été. Deux serveurs
> n'étaient alors ni supervisés ni journalisés, et **une machine non supervisée ne
> proteste pas**. Aujourd'hui on ne peut plus oublier : il faut **exempter**, et dire
> pourquoi.
---
@ -55,7 +65,7 @@ Tu as appris **métriques vs logs, pull vs push, le motif agent** — pas « Pro
```
2. **Vois les journaux** : `curl -s http://localhost:3100/loki/api/v1/label/host/values` → les nœuds
qui expédient leurs logs.
3. **Ouvre Grafana** (`https://grafana.lab.chezlepro.internal`) — métriques *et* logs au même endroit.
3. **Ouvre Grafana** (`https://grafana.chezlepro.internal`) — métriques *et* logs au même endroit.
4. **Casse & répare.** Arrête `prometheus-node-exporter` sur un nœud : dans Prometheus, sa cible passe
**`up=0`** (DOWN). Redémarre : elle repasse UP. Tu *sens* que c'est **l'agent** qui nourrit le
serveur (modèle pull).
@ -65,4 +75,4 @@ Tu as appris **métriques vs logs, pull vs push, le motif agent** — pas « Pro
## Pour aller plus loin *(dépôt)*
- Rôles : `roles/serveur_prometheus`, `roles/serveur_loki`, `roles/serveur_grafana`.
- Agents : `roles/client_metrique` (node_exporter), `roles/client_journal` (→ Loki).
- Ces agents sont des **liaisons optionnelles** : unité **Liaisons**.
- Ces agents sont des **intégrations universelles** (posées par dérivation, gardées par P26) : unités **Liaisons** et `docs/integrations-vm.md`.

View file

@ -9,7 +9,7 @@
**Chiffrement asymétrique** : une **paire de clés** — une **privée** (secrète) et une **publique**
(partageable). Ce que l'une chiffre, l'autre le déchiffre. La privée **signe**, la publique **vérifie**.
**Un certificat** = une clé publique + une identité (« ce serveur est `id-ldap-01` »), le tout
**Un certificat** = une clé publique + une identité (« ce serveur est `idm-01` »), le tout
**signé** par une autorité. Il répond à : *« à qui est-ce que je parle, vraiment ? »*
**L'autorité de certification (AC / CA)** signe les certificats. On lui fait confiance, donc on fait
@ -29,7 +29,7 @@ confiance à toute la chaîne.
| Pièce | Rôle | Concept incarné |
|---|---|---|
| **step-ca** (`serveur_step_ca`) | l'**autorité de certification** interne (racine + intermédiaire, base des émissions). | AC, chaîne de confiance |
| **client_pki** (`client_pki`) | intégration : un nœud **obtient un certificat** de l'AC (via ACME) et **fait confiance à la racine**. | émission ACME, magasin de confiance |
| **client_pki** (`client_pki`) | intégration **universelle** : tout nœud **obtient un certificat** de l'AC (via ACME) et **fait confiance à la racine**. Elle ne se déclare pas — elle est dérivée, et **P26** refuse qu'un hôte y échappe. Seule exemption, dérivée : l'hôte qui *est* l'AC, qui ne s'enrôle pas auprès d'elle-même. | émission ACME, magasin de confiance |
Le motif : chaque service qui doit prouver son identité (LDAPS d'OpenLDAP, HTTPS de l'edge…)
reçoit un **certificat d'hôte** signé par step-ca ; la **racine** est déposée dans le **magasin de
@ -68,7 +68,7 @@ Encrypt, Vault, une AC d'entreprise : le schéma est **identique**.
`OK` = la racine valide bien le certificat du serveur. C'est ① en action.
3. **Vois-le servir en vrai.** Le LDAPS d'OpenLDAP utilise ce certificat :
```bash
openssl s_client -connect id-ldap-01.lab.chezlepro.internal:636 \
openssl s_client -connect idm-01.chezlepro.internal:636 \
-CAfile /etc/step/certs/root_ca.crt </dev/null 2>/dev/null | grep -E 'Verify return code'
```
`0 (ok)` = confiance vérifiée.
@ -92,13 +92,25 @@ fiable. Et attention à un piège vécu en vrai :
Diagnostic-réflexe : **comparer le cert servi au cert fichier**.
```bash
echo | openssl s_client -connect EDGE:443 -servername keycloak.lab… 2>/dev/null | openssl x509 -noout -enddate # SERVI
echo | openssl s_client -connect EDGE:443 -servername keycloak.chezlepro.internal 2>/dev/null | openssl x509 -noout -enddate # SERVI
openssl x509 -in /etc/step/certs/EDGE.crt -noout -enddate # FICHIER
```
Dans Set-OPS, `client_pki_reload_services` (par nœud) fait recharger les **vrais** consommateurs
après chaque renouvellement — edge→nginx, mail→postfix/dovecot, annuaire→slapd. Fix d'urgence si
ça arrive : `systemctl reload nginx` sur l'edge.
Ce diagnostic est **outillé** depuis le 2026-08-08 : `make certificats-plan` compare, pour
toute la flotte, ce que le disque porte à ce que la mémoire sert. Il a trouvé quelque chose
à sa première exécution — sur `infra-pki-01`, **l'autorité elle-même**, le certificat était
expiré depuis plus de huit heures et le renouvellement échouait toutes les quatorze minutes
sur `'step ca renew' requires the '--ca-url' flag`. Rien ne le signalait, et le harnais
était entièrement vert.
> **Ne compare pas les empreintes, regarde l'échéance de ce qui est SERVI.** Avec des certs
> de 24 h renouvelés toutes les ~14 minutes, une empreinte servie *différente* de celle sur
> disque est l'état **normal** : un contrôle par empreinte crierait en permanence, et on
> apprendrait à l'ignorer.
**Casse & répare** : sur l'edge, arrête le timer `cert-renewer@…`, laisse le cert expirer (ou
force une horloge), observe le login SSO casser (échec TLS de l'échange OIDC), puis recharge nginx
→ tout revient. Tu *sens* que la PKI ne vit que si le renouvellement **et** le rechargement tournent.

View file

@ -14,5 +14,13 @@ reste la source, le wiki Forgejo la vue browsable et web-éditable.
**Principe** : le wiki *enseigne* et *oriente* ; il **pointe** vers `docs/` et les README de rôles
pour le détail technique — il ne les **recopie pas** (anti-dérive).
**Publication** (à faire une fois le socle validé) : cloner le dépôt wiki Forgejo et y pousser ces
pages (les noms de fichiers `Titre-Avec-Tirets.md` deviennent « Titre Avec Tirets » dans l'UI).
**Publication** : `make wiki-publier` pousse ce dossier vers le wiki Forgejo du dépôt. Les
noms de fichiers `Titre-Avec-Tirets.md` deviennent « Titre Avec Tirets » dans l'UI.
> **Sens unique.** Une page modifiée dans l'interface de la forge est **détruite** à la
> publication suivante : on lit là-bas, on écrit ici.
> Ces pages alimentent aussi `docs/audit/plan-de-recette.md`, **généré** par
> `make plan-recette` à partir des sections « ④ à toi de jouer ». La preuve **P22** refuse
> que ce plan soit en retard sur le wiki — modifier une unité sans régénérer fait échouer le
> harnais.

View file

@ -47,11 +47,24 @@ Détail de ce que chaque devis vérifie — et de ce qu'il ne vérifie pas : `do
Un prérequis et trois pièges, tous à la première connexion. Le runbook complet est dans
`docs/autorisation.md` **§6** ; voici l'ordre et la raison de chaque geste.
**1. La clé de voûte.** Le mot de passe de la voûte est lu depuis
`ANSIBLE_VAULT_PASSWORD_FILE` (`~/.config/setops-vault-pass` par défaut). **Sans ce fichier,
rien n'est possible** — ni les devis ci-dessus, ni un déploiement. C'est la première chose à
sauvegarder hors de la machine, avec les `vault.yml` de chaque instance, qui ne sont pas
versionnés.
**1. La clé de voûte — une voûte, une clé.** Chaque dépôt a **sa** clé, nommée d'après lui :
`~/.config/setops-vault-<dépôt-en-minuscules>`. Il n'y a **rien à exporter** — le `Makefile`
les rassemble seul (`scripts/voutes.py` → `ANSIBLE_VAULT_IDENTITY_LIST`). Pour voir ce que
cette machine peut ouvrir :
```
python3 scripts/voutes.py etat
```
**Sans la clé de ton écosystème, rien n'est possible** — ni les devis ci-dessus, ni un
déploiement. C'est la première chose à sortir de la machine (`make cles-exporter`, cf.
`docs/sortir-les-cles-du-poste.md`), avec les `vault.yml` de chaque instance, qui ne sont
pas versionnés.
> Ce paragraphe a désigné un `ANSIBLE_VAULT_PASSWORD_FILE` unique jusqu'au 2026-09-06. Ce
> n'est plus le mécanisme, et la raison compte : un seul mot de passe ouvrait alors *toutes*
> les voûtes de la flotte, celle de l'hébergeur comprise. Compromettre le plus petit
> locataire, c'était obtenir les secrets de tous.
**2. Le mot de passe d'amorçage, à changer avant tout le reste** (§6.1). L'annuaire refuse
toute opération tant qu'il n'est pas changé, sauf le changement lui-même. Ce n'est pas

View file

@ -19,7 +19,7 @@ Un seul endroit gère les certificats → simple et cohérent.
## ② Comment Set-OPS le fait
`serveur_nginx` déployé sur l'**edge** (`infra-edge`) est le reverse-proxy. Le point élégant :
l'**exposition est auto-dérivée**. Déclarer `expose: [icinga.lab.chezlepro.internal]` sur une app
l'**exposition est auto-dérivée**. Déclarer `expose: [icinga.chezlepro.internal]` sur une app
génère **tout** :
```
@ -49,14 +49,14 @@ Tu as appris **le reverse-proxy, le routage par nom, la terminaison TLS** — pa
## ④ À toi de jouer
1. **Route par nom.** Deux noms, un seul edge (`192.168.15.21`) :
1. **Route par nom.** Deux noms, un seul edge (`10.17.16.11`) :
```bash
curl -sI --resolve grafana.lab.chezlepro.internal:443:192.168.15.21 \
--cacert /etc/step/certs/root_ca.crt https://grafana.lab.chezlepro.internal/ | head -1
curl -sI --resolve grafana.chezlepro.internal:443:10.17.16.11 \
--cacert /etc/step/certs/root_ca.crt https://grafana.chezlepro.internal/ | head -1
```
Change `grafana` en `forge` : même IP, **backend différent**. C'est le routage par SNI.
2. **Vois la terminaison TLS.** Le certificat présenté est celui de l'**edge** (avec les SAN des
exposés) : `openssl s_client -connect 192.168.15.21:443 -servername grafana.lab… | openssl x509 -noout -text | grep -A1 'Subject Alternative'`.
exposés) : `openssl s_client -connect 10.17.16.11:443 -servername grafana.chezlepro.internal | openssl x509 -noout -text | grep -A1 'Subject Alternative'`.
3. **Casse & répare.** Arrête le backend (ex. `systemctl stop grafana-server` sur obs-01) et rouvre
Grafana : l'edge répond **502 Bad Gateway** (le proxy est là, le service non). Redémarre : ça
remarche. Tu distingues **le proxy** de **ce qu'il sert**.

View file

@ -32,8 +32,16 @@ Choix fondateur : **sauvegarder la donnée** (l'infra est reconstructible par le
| Pièce | Rôle |
|---|---|
| **restic** | l'outil : chiffrement côté client, déduplication, rétention. |
| **`serveur_backup`** | la **cible** hors-nœud (un dépôt restic par nœud). |
| **`client_backup`** | intégration par nœud : **jobs déclaratifs** (dump + chemins), timer quotidien, rétention. |
| **`serveur_backup`** | la **cible** hors-nœud (un dépôt restic par nœud) — depuis le 2026-09-01, chez l'**hébergeur** (`site-backup-01`), un compte Unix par écosystème. |
| **`client_backup`** | intégration par nœud : **jobs déclaratifs** (dump + chemins), timer quotidien, rétention — et, depuis le 2026-09-02, **vérification de son propre dépôt distant**. |
> **Qui vérifie a changé.** Le dépôt était le seul à voir ce qui arrivait vraiment : un
> nœud sait qu'il a *lancé* sa sauvegarde, pas qu'elle a *abouti*. C'était juste tant que
> le dépôt vivait dans l'écosystème. Depuis que les écosystèmes déposent chez leur
> **hébergeur** — qui héberge du chiffré côté client et ne peut rien juger — la
> vérification revient au seul qui détient la clé : **le nœud lui-même**. Il interroge son
> dépôt *distant*, et non le fait d'avoir lancé un timer. Une unité verte sur un dépôt vide
> est exactement ce qui a menti pendant un mois.
Ce qu'on protège, par **tiers** :
- **Tier 0 (vital)** : les **clés de la CA** step-ca (`/etc/step-ca`) — l'ancre de confiance, irremplaçable.
@ -64,12 +72,15 @@ Tu as appris **quoi sauvegarder, la règle 3-2-1, logique vs image, et surtout r
```bash
/usr/local/sbin/setops-sauvegarder.sh
```
2. **Liste les instantanés** (le dépôt vit hors-nœud) :
2. **Liste les instantanés** (le dépôt vit hors-nœud, et hors de l'écosystème) :
```bash
export RESTIC_REPOSITORY=sftp:restic@backup-01.lab.chezlepro.internal:$(hostname)
export RESTIC_PASSWORD_FILE=/etc/setops/restic.pass
# NE PAS RECOPIER UNE ADRESSE ICI : la cible est une valeur du plan, et elle a
# deja change. Le noeud la porte deja, gravee dans son propre script.
eval "$(grep -E '^export RESTIC_' /usr/local/sbin/setops-sauvegarder.sh)"
restic snapshots
```
*Le même dépôt est interrogé chaque nuit par `setops-verifier-mon-depot.sh`, qui
rapporte à Icinga : c'est le nœud, seul détenteur de la clé, qui juge.*
3. **Restaure — le vrai test.** Restaure dans un dossier temporaire et **compare** :
```bash
restic restore latest --target /tmp/rst
@ -85,4 +96,9 @@ Tu as appris **quoi sauvegarder, la règle 3-2-1, logique vs image, et surtout r
## Pour aller plus loin *(dépôt)*
- Rôles : `roles/serveur_backup`, `roles/client_backup`.
- Philosophie « donnée, pas VM » + les tiers : voir le CHANGELOG (entrée sauvegardes) et les jobs en host_vars.
- Complément 3-2-1 (offsite) : reste à faire — c'est l'**Étape B** (frontière publique).
- Le **1 hors-site** de la règle 3-2-1 : `make depot-hors-site VERS=<répertoire>`
(`playbooks/maintenance/depot-hors-site.yml`). Il emporte le dépôt du site — celui de
l'hébergeur **et** ceux des locataires, puisque c'est l'hébergeur qui a pris cette
promesse. La copie est opaque, et l'empreinte est prise **à la source** puis recalculée
sur la copie : sans cela on rentre chez soi avec un répertoire, pas avec une sauvegarde.
Manœuvre exécutée le 2026-09-05 — 233 fichiers, 95,7 Mo, empreintes identiques une à une.

View file

@ -49,7 +49,7 @@ Tu as appris **supervision active vs observabilité passive, l'état, l'alerting
## ④ À toi de jouer
1. **Ouvre Icinga Web 2** (`https://icinga.lab.chezlepro.internal`, via le SSO) : la liste des hôtes
1. **Ouvre Icinga Web 2** (`https://icinga.chezlepro.internal`, via le SSO) : la liste des hôtes
et services **supervisés**, avec leur **état** (vert/jaune/rouge).
2. **Vois l'impact.** Menu *Business Processes* → « **Supervision Chezlepro** » : un processus qui
**agrège** des checks (load, procs, ping…) en un état roulé. C'est l'impact, pas une case.

View file

@ -31,14 +31,28 @@ pas en option :
| Audit | `auditd` |
| Anti-force-brute SSH | `fail2ban_ssh` |
| Durcissement noyau | `sysctl_hardening` |
| Pare-feu | `nftables_baseline` (*installé et préparé, désactivé par défaut*) |
| Pare-feu | `nftables_baseline` — **règles dérivées du registre des flux**, pas écrites à la main |
| Mises à jour | `unattended_upgrades` |
| Journaux | `journald` (rétention, persistance) |
| Vidages mémoire | `core_dumps` (désactivés — un *core* peut contenir des secrets) |
| Paquets | `hardening_packages` |
| SSH | `ssh_baseline` + `ssh_hardening` (clé d'abord, `PermitRootLogin no`…) |
| Moindre privilège | compte technique `ansible` + `sudo_ansible` (pas de root direct) |
Nuance importante : le **pare-feu** est *préparé mais pas activé* dans le template — on l'active sur
un **clone/serveur final** avec des règles adaptées à son rôle (couper l'accès à l'aveugle = un
risque). Prudence par conception.
Le groupe `serveur_durci` compose **dix** de ces rôles en un seul geste ; `ssh_baseline` et
`sudo_ansible` viennent du socle `serveur_debian`, en amont.
**Le pare-feu : préparé dans le gabarit, armé sur la flotte.** `nftables_baseline` vaut
`false` par défaut *dans le rôle* — couper l'accès à l'aveugle pendant la construction d'un
gabarit serait un risque gratuit. Mais l'instance le met à `true` pour `hotes_actifs` : sur
une machine réelle, **le pare-feu tourne**.
Et ses règles ne s'écrivent pas à la main. Chaque rôle déclare dans son `meta/flux.yml` ce
qu'il écoute et ce à quoi il se connecte ; `scripts/resoudre_flux.py` en dérive le jeu de
règles, en `policy drop` par défaut. Le **même** registre alimente le pare-feu de
l'hyperviseur et la frontière OPNsense : trois couches qui ne *peuvent pas* se contredire,
parce qu'elles descendent d'une seule décision. C'est la défense en profondeur du §① prise
au mot — sans le coût habituel, qui est de tenir trois politiques cohérentes à la main.
---

View file

@ -23,8 +23,11 @@ Idée-force : la VM devient **jetable/reconstructible**. Ce qui est précieux, c
## ② Comment Set-OPS le fait
- **Proxmox** = l'hyperviseur (cluster).
- Un **golden template** (`basiqueChezlepro`) : Debian minimal, durci, avec le compte technique
`ansible`, qemu-guest-agent, cloud-init… — préparé **une fois**.
- Un **golden template** (`modeleSetOPS`) : Debian minimal, durci, avec le compte technique
`ansible`, qemu-guest-agent, cloud-init… — préparé **une fois**. *(Cette unité l'appelait
`basiqueChezlepro` jusqu'au 2026-09-06 : c'est l'ancien nom. Le nom en vigueur est celui
qu'enregistre `make config` sous `proxmox_clone_source_nom`, et il doit correspondre au
nom réel du template dans Proxmox — sinon le clonage ne trouve pas sa source.)*
- Chaque nœud = un **clone** du template. `cloud-init` pose l'identité (hostname, IP dérivée de la
nomenclature, clé SSH).
- **Puis** Set-OPS/Ansible fait la **vraie** configuration (les rôles).

View file

@ -45,6 +45,20 @@ make postgresql-plan le chiffrement est-il imposé, et à quels réseaux
make courriel-plan Postfix → LDAP → LMTP → Dovecot → IMAP, file d'attente comprise
```
Le même patron a ensuite été porté **sous** les services, au monde physique — mêmes pièces,
même refus d'écrire :
```
make frontiere-plan les règles de la frontière OPNsense contre leur devis
make proxmox-fw-plan le pare-feu est-ouest de l'hyperviseur contre le registre des flux
make sdn-plan la zone EVPN, ses VNets et la sortie des VRF
make underlay-plan l'underlay déclaré contre ce que le cluster porte vraiment
make placement-plan chaque VM est-elle là où le plan la met
```
*(Deux mesures voisines ne comparent à rien et ne portent donc pas le suffixe `-plan` :
`make mtu-mesurer` et `make versions-mesurer` relèvent un état, sans devis en face.)*
| Pièce | Rôle |
|---|---|
| un **playbook** (`playbooks/maintenance/devis-*.yml`) | **relève** le déclaré et le réel, dépose un JSON |
@ -86,8 +100,9 @@ l'éprouver dans les deux sens, en cassant volontairement ce qu'il surveille.
## ④ À toi de jouer
1. Lance les cinq devis sur ta flotte. Note le temps que ça prend : quelques minutes pour ce qui
demandait une journée d'enquête à la main.
1. Lance les **dix** devis sur ta flotte — les cinq de service, puis les cinq
d'infrastructure. Note le temps que ça prend : quelques minutes pour ce qui demandait une
journée d'enquête à la main.
2. **Casse quelque chose exprès** — arrête un service publié, change un port — et relance le
devis concerné. S'il ne dit rien, c'est *lui* qu'il faut réparer, pas le service.
3. Cherche, dans ton propre outillage, une vérification qui n'a **jamais** échoué. Demande-toi si