Set-OPS-Public/CHANGELOG.md
Daniel Allaire b39a3a4735 docs : rectification — trois syntaxes d'interface restent non vérifiées
J'ai annoncé « les six familles sont closes » sur la foi d'un
`spanning-tree ?` en mode configuration GLOBALE. Trois lignes du devis
vivent ailleurs et n'y figuraient donc pas :

- `spanning-tree portfast trunk` (interface) — `trunk` est un mot-clé
  Cisco ; l'équivalent s'écrit souvent `portfast` seul, voire `edged-port` ;
- `ip access-group <NOM> in` (interface) — c'est ce qui LIE l'ACL au SVI ;
  sans elle l'ACL existe et ne filtre rien ;
- `ip default-gateway <ip>` (global, absent de l'aide consultée).

`(config-if)#spanning-tree ?` et `(config-if)#ip ?` les donneraient.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:30:10 -04:00

2351 lines
165 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CHANGELOG — Set-OPS
## 2026-08-02 (suite 17) — spanning-tree vérifié : les six familles sont closes
`spanning-tree ?` en mode configuration tranche la dernière inconnue, **en faveur de la forme
émise** : `mode` et `priority` s'acceptent au niveau **global**. La priorité n'a pas besoin
d'être portée par une instance, même en MSTP — la réserve inverse, notée la veille, était
infondée et le devis ne la porte plus. Une mise en garde fausse est aussi nuisible qu'une
syntaxe fausse.
Ajouté : `spanning-tree` seul, qui **garantit l'état actif**. Sans lui, `mode` et `priority`
sur un boîtier où le protocole aurait été désactivé configureraient un arbre qui ne tourne
pas. Sans effet s'il est déjà actif — même logique déclarative que pour les trunks.
Vérifié contre le matériel : VLAN, SVI, trunks, routes, définition des ACL, spanning-tree
**global**. Deux ont révélé un défaut réel plutôt que de confirmer l'existant — les routes
(notation CIDR) et surtout les trunks, dont la forme `add` ne retranchait rien.
> **Rectification (même jour).** L'entrée ci-dessus a d'abord annoncé « les six familles sont
> closes ». C'était faux : l'aide consultée était celle du mode configuration **globale**, et
> trois lignes du devis vivent ailleurs — `spanning-tree portfast trunk` et `ip access-group
> … in` au niveau **interface**, `ip default-gateway` en global mais absent de cette aide.
> Elles restent non vérifiées, et la plus douteuse est `portfast trunk` : `trunk` est un
> mot-clé Cisco.
## 2026-08-02 (suite 16) — la syntaxe des ACL vérifiée sur le matériel
`show access-lists` confirme la dernière famille de syntaxe restée ouverte : une ACL générée
s'applique **telle quelle** sur le Binardat, ses règles dans l'ordre émis —
`ip access-list extended <NOM>`, masques normaux, `any`.
Détail de lecture consigné : le boîtier **affiche** `any-destination` là où l'on saisit `any`.
Comparer un `show access-lists` au devis ferait apparaître une différence qui n'en est pas une
— c'est le genre de faux positif qui fait perdre une heure.
**Bilan des dialectes.** Cinq familles sur six sont désormais vérifiées contre le matériel :
VLAN, SVI, trunks, routes, ACL. Ne reste que la **forme d'entrée** des commandes de
spanning-tree, que `show` ne révèle pas.
## 2026-08-02 (suite 15) — `underlay.stp.mode` aligné sur le matériel
`mode: mstp`, parce que c'est le mode d'usine du commutateur (`show spanning-tree` :
IEEE 802.1s, *Force Version 3*). Sur une étoile sans lien redondant, l'instance 0 de MSTP se
comporte comme un RSTP : changer de mode aurait donné le même résultat, au risque près de
toucher à un protocole qui fonctionne déjà.
Le devis énonce désormais sa propre incertitude là où elle est : **en MSTP la priorité se
règle souvent par instance**, alors que la forme émise est globale. Le générateur le dit
plutôt que de laisser croire à une syntaxe vérifiée — `show spanning-tree` donne l'état du
protocole, jamais la forme d'entrée des commandes.
Corrigé au passage : le commentaire de la section 6 disait « RSTP » en dur alors que le mode
est déclaré. Il le dérive.
## 2026-08-02 (suite 14) — le spanning-tree du matériel : MSTP, actif, priorité par défaut
`show spanning-tree` sur le commutateur Binardat corrige une déduction fausse et en apporte
deux faits.
**Correction.** J'avais déduit de son absence du `show running-config` que le spanning-tree
était probablement désactivé. Il est **actif** : il n'y figurait pas parce qu'il est aux
valeurs d'usine. Une absence dans une configuration ne veut pas dire une absence de fonction.
**La plateforme est en MSTP** (IEEE 802.1s, *Force Version 3*), alors que `underlay.stp.mode`
déclare `rstp`. Sur une étoile sans lien redondant les deux se comportent identiquement — la
question est de savoir si l'on aligne la déclaration sur le matériel ou le matériel sur la
déclaration.
**La priorité de pont est déjà 32768**, donc la ligne émise pour les switches d'accès est un
non-opérant : elle écrit ce qui est déjà vrai.
Reste non vérifiée la **forme d'entrée** des commandes de spanning-tree : `show` en donne
l'état, pas la syntaxe. En MSTP la priorité se règle en général **par instance**, ce que la
forme actuellement émise ne fait pas.
## 2026-08-02 (suite 13) — le trunk ne restreignait rien
`switchport trunk allowed vlan ?` sur le matériel réel confirme la syntaxe **et** révèle un
défaut : `add` *ajoute* à la liste courante, la forme sans mot-clé la *définit*.
Le devis émettait `add`. Or un port trunk neuf autorise **tous** les VLAN — dans une
configuration réelle, les ports n'avaient aucune ligne `allowed vlan`, ce qui signifie
exactement cela. Y ajouter la liste des VLAN voulus n'en retranchait aucun : le trunk
continuait de tout transporter, et le devis donnait **l'illusion de restreindre**.
C'est le pire genre de défaut — une configuration qui a l'air juste, s'applique sans erreur,
et ne fait pas ce qu'elle annonce.
La forme sans mot-clé est retenue, et elle est aussi **atomique** : `none` puis `add` aurait
coupé le trunk entre les deux commandes, ce qui suffit à perdre la session si on l'applique
sur le port de gestion.
## 2026-08-02 (suite 12) — la syntaxe des routes, vérifiée sur le matériel
Un `show running-config` du commutateur Binardat tranche la question restée ouverte : la
plateforme écrit ses routes en **notation CIDR**`ip route 0.0.0.0/0 192.168.10.254` — et
non en masque séparé comme Cisco. Le générateur produisait du Cisco quel que soit le dialecte.
`route_statique()` suit désormais le dialecte, au même titre que les masques d'ACL. Vérifié
dans les deux formes.
Restent non vérifiés faute d'apparaître dans la configuration réelle : la syntaxe des ACL,
celle de `switchport trunk allowed vlan add`, et le **spanning-tree** — totalement absent du
`show running-config`, ce qui suggère qu'il est désactivé par défaut sur cette plateforme.
## 2026-08-02 (suite 11) — le responsable prend sa section, et la symétrie est dite
### Corrigé — un renvoi ambigu
« Il ne dégèle qu'en cas de retour arrière (§6) » figurait **dans l'étape 6**. Deux « 6 » ne
désignant pas la même chose dans une seule phrase, alors que tout le document distingue
soigneusement sections et étapes. La section est désormais nommée plutôt que numérotée.
### Déplacé — le responsable désigné devient le §3
Il vivait dans « Le modèle : le transfert de nom de domaine », alors que ce n'est **pas un
emprunt aux registraires** : c'est une décision de modèle, valable migration ou pas. Le §2 ne
traite plus que de ce qui est emprunté et de là où l'analogie casse.
### Ajouté — ce qui suit le tenant, en regard de ce qui reste
Le §8 énumérait ce que la migration ne déplace **pas** — fabric, frontière, index — sans dire
ce qu'elle déplace. Or le responsable désigné, lui, **suit le tenant** : c'est exactement
l'inverse, et le dire renforce la ligne de partage.
> Si quelque chose appartenant à l'organisation ne peut pas partir, elle n'est pas vraiment
> souveraine ; si quelque chose appartenant à l'hébergeur devait partir, c'est que la
> frontière entre les deux est mal tracée.
Cette symétrie est le test le plus simple d'une migration bien conçue.
Sections renumérotées en conséquence (9 au lieu de 8) ; les six renvois internes vérifiés un
par un.
## 2026-08-02 (suite 10) — le responsable désigné, et la réversibilité nuancée
### Décidé — chaque tenant a un responsable désigné
Un domaine a un titulaire ; un tenant a un **responsable désigné** — la personne qui engage
l'organisation, et dont la signature seule vaut mandat de migration.
Ce n'est pas une formalité. Sans responsable nommé **d'avance**, la question « qui peut
décider de déménager cette organisation ? » se pose au pire moment : quand les deux hébergeurs
ont un intérêt dans la réponse. Un employé de bonne foi ne peut pas mandater le déménagement
de son employeur.
### Corrigé — la table des états laissait croire que revenir est facile jusqu'au bout
Elle annonçait une réversibilité « gratuite » entre `préparé` et `libéré`, alors que le §6
établit qu'elle change de nature à la bascule. Les deux ne se contredisent pas — on peut
effectivement revenir jusqu'à `libéré` — mais un lecteur pressé s'arrêtant au tableau en
retirait une fausse impression.
Ce qui reste gratuit est l'adressage, pas le retour : tout dérive d'un seul chiffre, dans les
deux sens.
### Ajouté aux points à trancher — trois questions de gouvernance
**Où le responsable désigné est déclaré**, et surtout **comment on en change** : c'est un acte
au moins aussi sensible que la migration, puisqu'il décide qui pourra la mandater ensuite.
**Le recouvrement de la clé du responsable** — elle se perd, se compromet, ou la personne
quitte l'organisation. Sans procédure, un tenant devient **inmigrable** : captif non par
contrat mais par accident, exactement ce que la recette existe pour empêcher.
Deux écueils symétriques y sont consignés. Trop lourde, la procédure n'aboutit jamais et le
tenant reste bloqué. Trop légère, elle devient le **chemin de moindre résistance** pour
contourner la signature — inutile de forger un mandat si l'on peut se faire attribuer la clé.
Le recouvrement doit être au moins aussi difficile que ce qu'il protège.
Piste retenue, la plus transposable des registraires : un **contact de secours nommé en même
temps que le responsable**, tant que personne n'est en situation d'urgence.
**La durée de rétention** : convenue avec qui, consignée où, attestée par qui. Sur une
séparation d'hébergeur, un flou ici finit en litige.
## 2026-08-02 (suite 9) — le retour arrière de la migration
La recette affirmait la réversibilité sans jamais décrire le retour. Or elle **change de
nature à la bascule**, et le geste évident — repointer le DNS — devient faux à cet instant.
Trois régimes, désormais écrits :
- **avant le gel** : sans conséquence, le sortant n'a jamais cessé de servir. C'est ce que la
règle d'ordre achète — tout ce qui peut échouer sans coût échoue là ;
- **pendant le gel** : dégeler, l'interruption se limite à la durée du gel ;
- **après la bascule** : ce n'est plus un retour mais une **migration inverse**. Les
utilisateurs ont écrit chez l'entrant — courriels, fichiers, commits — et ces données
n'existent nulle part ailleurs. Repointer le DNS les perdrait, et silencieusement.
Point rendu explicite : **le sortant reste gelé après la bascule**, jusqu'à confirmation. Le
dégeler « au cas où » créerait deux copies vivantes du même tenant et plus aucune vérité. En
contrepartie il n'a pas divergé, donc le delta d'un retour reste à sens unique.
Deux points de non-retour à ne pas confondre : la **bascule** fait perdre le retour *gratuit*
(il reste la migration inverse) ; la **purge** fait tout perdre.
D'où une exigence ajoutée aux points à trancher : les **critères de confirmation** de l'étape
7 se fixent par écrit **avant** la première bascule. Décider après coup ce qui compte comme
« ça marche » revient à se donner raison.
Corrigés au passage : deux renvois d'étape faux (le rattrapage est à l'étape 5, non 4 ; le
chemin de vérification hors DNS public est un prérequis de l'étape 3 elle-même).
## 2026-08-02 (suite 8) — recette de migration d'un tenant entre hébergeurs
`docs/migration-tenant.md` : la séquence, les états et les gardes. Écrite avant tout code,
délibérément — figer un enchaînement qu'on n'a jamais joué serait prématuré.
**Le modèle est le transfert de nom de domaine**, qui résout depuis trente ans les mêmes
problèmes : le mandat appartient au **client** (ni l'hébergeur sortant ni l'entrant ne peut
déplacer un tenant seul), verrou par défaut, deux actes délibérés et traçables.
Là où l'analogie casse, elle est remplacée plutôt qu'étirée : il n'y a **pas de registre**
central pour arbitrer, donc le mandat est **signé** par le tenant et vérifié contre une clé
publique de son plan — un secret partagé ne prouverait rien à l'entrant, il pourrait venir du
sortant.
**L'ordre est commandé par une règle unique** : *le receveur doit être prouvé prêt avant que
quoi que ce soit ne gèle.* L'entrant se construit et se prouve pendant que le sortant sert
normalement ; l'interruption se réduit au rattrapage du delta plus la propagation DNS. Ce
n'est pas un conseil mais une **garde de transition** — l'état `gelé` est inaccessible tant
que `préparé` n'est pas prouvé.
Deux pièges consignés parce qu'ils ne vont pas de soi : le **TTL** s'abaisse à l'étape 1, pas
à la bascule, sinon tout le bénéfice de l'ordre est perdu ; et l'entrant doit être
**vérifiable sans être public**, sinon le tenant sert des deux côtés et l'identité se
dédouble.
Enfin, la libération est une **révocation, pas une transmission** : re-clétage de la voûte et
rotation des secrets chez l'entrant. Transmettre le mot de passe laisserait à l'ancien
hébergeur un accès à vie aux secrets d'un client parti — rien ne rattrape ça après coup.
## 2026-08-02 (suite 7) — la frontière appartient à l'hébergeur, pas au tenant actif
Un hébergeur sert **plusieurs tenants** et n'a qu'**une** frontière. Or ses intrants
(`group_vars/opnsense.yml`) étaient lus chez le **tenant actif** : basculer sur un invité —
Technolibre, qui n'a pas de frontière à lui — faisait perdre au devis l'URL de gestion,
l'adresse publique et les deux interfaces. Il repartait en marqueurs, comme si le boîtier
n'existait pas. Vérifié en simulant la bascule : `AUCUN` intrant lu.
Même famille que le défaut de l'underlay corrigé plus tôt, et c'est la distinction
hébergeur/tenant qui le fait apparaître.
Le devis **et** le panneau lisent désormais la frontière chez l'hébergeur. Celui-ci n'est pas
déclaré pour autant : le symlink `underlay.yml` le **désigne déjà**, et une seconde
déclaration ouvrirait la porte à deux valeurs contradictoires. Repli sur l'instance active
quand aucun underlay n'est monté — un site sans fabric déclarée continue de fonctionner.
Vérifié : devis identique avec l'hébergeur actif, et intrants **conservés** avec un invité
actif. `docs/frontiere-opnsense.md` gagne un §2 qui pose qui possède quoi.
## 2026-08-02 (suite 6) — l'underlay devient modélisable
Un modèle décrivait jusqu'ici un **tenant** : ses services, ses zones, ses bases. Or tous les
hébergeurs n'ont pas le même matériel, et l'infrastructure physique mérite le même traitement.
### Ajouté — `exemples/modeles/socle/underlay.yml`
Le modèle public gagne un underlay **volontairement minimal** : un seul commutateur, pas de
fabric de stockage séparée. C'est le point de départ honnête d'un petit hébergeur ; les
montages plus riches (étoile à trois commutateurs, paire en MLAG, stockage jumbo dédié) sont
d'autres modèles, conformément à la doctrine — un générique public, les étoffés en privé.
Le modèle contient désormais **deux moitiés qui ne vont pas au même endroit** : `plan/` et
`inventories/` chez le tenant, `underlay.yml` chez l'hébergeur. Chez un hébergeur qui est son
propre tenant, les deux atterrissent au même dépôt — c'est le cas particulier, pas la règle.
### Étendu — `modeles.py verifier` valide l'underlay (preuve P17)
La validation est **facultative** (un modèle sans underlay reste valide) et porte sur la
cohérence **interne** seulement : VLAN sous la plage tenant, sous-réseaux disjoints,
passerelle dans son réseau, routeur déclaré, ports non dupliqués, dernier octet partagé.
Elle n'est **pas** confrontée aux tenants fédérés réels : un modèle est un gabarit, pas un
site déployé. Il a fallu pour cela rendre paramétrables deux hypothèses du validateur, qui
lisait la nomenclature de l'instance *active* et globait les dépôts frères — sur un modèle,
les deux auraient été faux. `charger_depuis()` et `plan_nomenclature=` ; comportement par
défaut inchangé.
Cinq cas de rejet exercés sur un modèle fautif : VLAN empiétant sur la plage tenant,
passerelle au mauvais dernier octet (lue dans la nomenclature **du modèle**), routeur inconnu,
sortie hors du lien, port déclaré deux fois.
## 2026-08-02 (suite 5) — l'underlay rejoint le dépôt de l'hébergeur
`underlay.yml` vivait **gitignoré** à la racine du moteur : consommé par deux générateurs,
validé par la preuve P23, et versionné nulle part. La dizaine de modifications de la journée
— transit, renumérotage du `/29`, séparation des fabrics, spanning-tree, dialecte, ports —
n'était récupérable d'aucune façon, et un clone frais repartait du gabarit.
Il appartient à l'**hébergeur** : ce sont ses switches, ses câbles, ses VLAN. Pas au moteur,
qui est générique, ni à un tenant, qui n'en possède pas. Chezlepro est ici hébergeur *et*
tenant, d'où la confusion : un tenant qui s'hébergerait sur son propre matériel aurait son
propre underlay, dans son dépôt.
Le moteur le monte par symlink, comme il monte le plan par `instance/` :
```
Set-OPS-public/underlay.yml -> ../OPS-Chezlepro/underlay.yml
```
**Ce lien ne suit pas `make instance-utiliser`.** Basculer l'instance active sur un autre
tenant ne change pas la fabric : elle reste celle de l'hébergeur. Deux symlinks, deux durées
de vie — c'est la conséquence directe de la distinction hébergeur/tenant.
Vérifié : les deux devis sortent **identiques octet pour octet** avant et après, P23 reste
verte, 24 preuves. Et un symlink **brisé** — le cas d'un clone du moteur sans le dépôt de
l'hébergeur — dégrade proprement : `exists()` suit le lien, l'underlay est vu comme absent,
et les devis omettent leurs sections au lieu d'échouer. Cas exercé.
## 2026-08-02 (suite 4) — deux devis qui se contredisaient, et une case à cocher
### Corrigé — le devis frontière certifiait des routes inexistantes
Sa section 0 annonçait trois routes de retour « DÉJÀ ÉMISES par `make devis-reseau` ». Le
devis switch n'en émettait **qu'une** : `devis_reseau` lisait `nftables_admin_ssh` de la
seule instance active, alors que la frontière était passée multi-tenant la veille. Les deux
réseaux d'administration de Technolibre n'étaient routés nulle part.
Pire qu'un silence : une affirmation fausse désamorce la vérification.
`admin_tous_tenants()` vit désormais dans `devis_reseau` et **`devis_opnsense` l'importe**
au lieu d'en refaire une copie. Les routes de retour et les règles lisent les mêmes tenants,
par construction. Vérifié : les deux listes sont identiques.
### Ajouté — l'avertissement « Block private networks »
Le SSH d'administration a une source RFC1918 arrivant sur une interface **WAN**. OPNsense
active par défaut ce filtre d'interface, qui s'applique **avant** les règles : coché, il jette
le paquet sans qu'aucune règle ne soit consultée. La config paraît juste, le SSH ne passe pas.
Le devis le signale dès qu'une source RFC1918 entre par le WAN — un réglage d'interface est
invisible dans les règles, il fallait donc l'écrire à part.
Le prédicat est **exactement** RFC1918, périmètre de cette case ; `ipaddress.is_private`
aurait été trop large (plages de documentation, CGNAT), et l'avertissement se serait déclenché
à tort. Les trois cas exercés : RFC1918 → averti ; `8.8.8.8/32` → muet ; `203.0.113.7/32`
(documentation) → muet.
## 2026-08-02 (suite 3) — chaque règle porte son interface, et l'octet est gardé
### Ajouté — l'interface d'arrivée, dérivée du sens du flux
Dans OPNsense une règle est **toujours `in` sur l'interface d'arrivée** : posée ailleurs, elle
ne s'applique jamais et le trafic est bloqué sans que la configuration paraisse anormale. Les
règles n'en portaient aucune, alors que le champ est obligatoire dans l'API.
L'attribution se dérive : un flux `ingress`/`externe` arrive par le **WAN**, un flux
`egress`/`externe` par le **lien de transit**. Le SSH d'administration suit la première ligne
— le VPN est hébergé sur le pfSense voisin et revient par l'adresse publique de la frontière.
C'était la dernière inconnue, et le montage parallèle décrit le 2026-08-02 la lève.
Le rendu abandonne `pass out` pour `pass in on <interface>`, qui est l'idiome réel d'OPNsense
et ce que le futur client d'API devra envoyer.
### Ajouté — `opnsense_wan_ip`, la face publique
L'adresse publique de la frontière (`69.70.26.62`, reprise du pfSense) est un intrant de la
section *Frontière* et s'affiche en section 1 du devis.
### Ajouté — invariant du dernier octet (preuve P23)
Convention d'exploitation : 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. `sleipnir-01` est `.1`
partout. Le chiffre n'est pas codé en dur : il vient de `reservations.passerelle` dans la
nomenclature, et `make underlay` refuse une passerelle qui s'en écarte.
Exemption assumée : les liens plus étroits qu'un `/24`. Sur le `/29` de transit, l'adressage
est dicté par les participants — les deux frontières prennent `.1` et `.2`, le switch `.6`.
Vérifié que l'invariant était **déjà respecté** sur les 13 sous-réseaux routés avant d'écrire
la garde.
## 2026-08-02 (suite 2) — la frontière porte les règles de TOUS les tenants
Le devis était multi-tenant pour ses routes et mono-tenant pour ses règles : il routait
`10.21.0.0/16` et `10.27.0.0/16`, mais ne filtrait que l'instance active. Technolibre aurait
été routé jusqu'à la bordure puis bloqué dans les deux sens, SSH d'administration compris,
sans qu'aucune ligne ne dise pourquoi. Chemin présent, politique absente — le mode de panne
du 2026-07-29, transposé.
La résolution est désormais paramétrée par tenant : `inventaire_de()` lit le `hosts.yml` de
chaque instance fédérée, `cibles_par_role()` prend l'inventaire en argument, et les alias
d'hôtes sont préfixés (`SETOPS_CHEZ17_SERVEUR_NGINX`). 11 règles par tenant, 22 au total.
### Cloisonnement du plan de gestion
Première version de ce correctif : `SETOPS_ADMIN` devenait l'**union** des réseaux
d'administration. Le plan de gestion de Technolibre aurait alors pu entrer en SSH chez
Chezlepro — la bordure rouvrait ce que les ACL de switch ferment. Corrigé avant livraison :
**un alias par tenant**, `SETOPS_ADMIN_<TENANT>`, n'ouvrant que son propre supernet.
L'union est conservée pour les routes de **retour** côté switch et la garde P24 : router
n'est pas autoriser, et le switch doit savoir revenir vers tous les plans de gestion.
### Deux omissions annoncées au lieu d'être tues
Un tenant sans inventaire généré : aucune règle, et le devis le dit. Un tenant dont
`nftables_admin_ssh` est vide : la règle SSH est **omise** plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet. Cas dégradé exercé.
## 2026-08-02 (suite) — la sortie générale est déclarée, pas subie
Le devis frontière se terminait par `block out log all` avec **une seule** règle sortante
(le relais SMTP). Appliqué tel quel, il coupait la flotte d'Internet : plus de `apt`, plus
de NTP, plus de récursion DNS. Rien ne le signalait — c'était la ligne la plus lourde de
conséquences du devis, posée à la suite des autres.
La sortie est désormais **déclarée dans le registre**, donc dérivée comme tout le reste :
- `serveur_debian` (le socle, porté par les 14 hôtes) — `443/tcp` et `80/tcp` pour les
dépôts apt, `123/udp` pour l'horloge. Une dérive d'horloge fait échouer la validation des
certificats step-ca et le SSO, des semaines après la cause.
- `client_unbound``53/udp` et `53/tcp` : `client_unbound_transitaires` est vide, donc
Unbound interroge lui-même la racine. C'est le choix souverain ; il a un coût réseau qu'il
faut déclarer. Le TCP n'est pas optionnel — c'est le repli obligatoire dès qu'une réponse
DNSSEC dépasse la taille UDP.
Le devis passe de 6 à 11 règles, et sa section 5 **énonce** le default-deny sortant, le
nombre de règles qui l'accompagnent, et où déclarer un besoin oublié — jamais à la main dans
le pare-feu, la règle serait perdue à la génération suivante.
Vérifié : les nftables d'hôte sont **inchangés**, octet pour octet. Le pair `externe` reste
sauté par `resoudre_flux.py` — ces flux relèvent de la bordure, et d'elle seule.
Non déclaré volontairement : le rôle `chrony` existe mais n'est référencé par aucun groupe,
aucun playbook ni le graphe de dépendances. Lui écrire un flux aurait créé une règle morte.
## 2026-08-02 — les interfaces se nomment par leur identifiant
`opt1`, `igb1` et `TENANTS` désignent le même port dans OPNsense : l'identifiant interne, le
périphérique FreeBSD et le libellé affiché. L'API REST ne parle que du **premier**, et c'est
lui que veulent `opnsense_if_wan` et `opnsense_if_transit`. Le libellé de ces deux intrants ne
le disait pas — la question s'est posée en pratique.
Les libellés le disent maintenant explicitement, et `docs/frontiere-opnsense.md` §6 explique
les trois couches ainsi que le motif du choix : `opt1` est le plus stable des trois, il
survit à un changement de carte réseau comme à un renommage.
La note « `opnsense_prochain_saut` dérive de l'underlay » est repliée dans l'en-tête que le
panneau régénère : une sauvegarde l'effaçait, puisque le fichier est réécrit depuis le YAML
analysé. Vérifié qu'une sauvegarde préserve valeurs **et** références de voûte.
### Corrigé — la doc portait encore l'ancien plan du `/29`
Après le renumérotage (`bifrost-1/-2` en `.1`/`.2`, SVI en `.6`), deux passages de
`docs/frontiere-opnsense.md` annonçaient toujours `10.0.4.2` comme prochain saut. La doc
contredisait le devis généré ; les `ip route` des deux coïncident désormais.
## 2026-08-01 (suite 14) — l'empreinte du root CA n'est pas un secret de voûte
`client_pki` **dérive l'empreinte à chaud** depuis l'autorité (`step certificate
fingerprint` en `delegate_to` sur `serveur_step_ca`) — précisément parce qu'un `from-zero`
régénère l'AC avec une empreinte neuve. Une empreinte figée en voûte serait périmée dès la
première reconstruction, et une empreinte périmée fait échouer le `bootstrap` de chaque hôte.
Or `defaults/main.yml` portait encore `client_pki_ca_fingerprint: "{{
vault_step_ca_fingerprint | default('') }}"`. Ce défaut était **mort** : la tâche suivante
écrase le fait sans condition. La valeur de la voûte n'avait aucun effet, quel qu'en soit le
contenu.
Le recensement de `voute.py` s'y laissait prendre — il cherche la chaîne `vault_*` dans les
fichiers, sans pouvoir savoir qu'un défaut n'est jamais lu. La « source unique » avait donc
hérité de l'erreur, et le panneau réclamait un secret impossible à fournir avant que l'AC
n'existe.
Le défaut mort est retiré, et la clé disparaît d'elle-même du recensement (25 → 24 exigés),
du gabarit et du panneau. `client_pki_ca_fingerprint_override` reste le moyen documenté
d'épingler une empreinte (AC externe, migration).
### Corrigé — une troisième copie manuelle de la liste des secrets
`docs/intrants-communs.md` §H énumérait les secrets à la main, avec les deux mêmes erreurs.
Elle renvoie désormais à `scripts/voute.py lister` et à la preuve P18 plutôt que d'entretenir
une copie de plus.
## 2026-08-01 (suite 13) — le rappel des secrets dérive de `voute.py`
`SECRETS_ATTENDUS` était une liste écrite à la main dans `inventory_gui.py`, en parallèle du
recensement que `scripts/voute.py` fait déjà depuis le plan, les rôles des groupes actifs et
les `group_vars`. Deux sources pour la même vérité, et la manuelle avait divergé :
| Clé | Panneau | Gabarit | Référencée |
|---|---|---|---|
| `vault_ldap_sssd` | annoncée | absente | **nulle part** — aucun rôle `sssd` n'existe |
| `vault_step_ca_fingerprint` | **omise** | présente | `roles/client_pki/defaults/main.yml:24` |
Un opérateur qui suivait le panneau créait donc un secret que rien ne consomme, et oubliait
celui dont `client_pki` a besoin pour vérifier l'empreinte de l'AC racine.
Le rappel dérive désormais de `voute.secrets_exiges()` — la **même source que la preuve
P18** — augmentée de `SECRETS_HORS_MOTIF` pour les jetons Proxmox, qui ne portent pas le
préfixe `vault_`. Vérifié : **27 noms, écart nul avec le gabarit**. Sur un dépôt public nu,
la liste est vide plutôt qu'en erreur.
## 2026-08-01 (suite 12) — la fabric se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui — routeur, spanning-tree, fabrics, transit —
s'éditait à la main dans un YAML, pendant que la doctrine dit qu'un sysadmin doit exploiter
l'outil **sans IA**. La frontière avait eu sa section dans le panneau ; l'underlay, non.
Une section **Fabric** couvre désormais les valeurs plates : le switch routeur, le dialecte
de CLI, le mode et la topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`,
et donc les ports) restent hors de portée du panneau — elles demandent une vue dédiée, comme
celle des serveurs.
### Le dialecte devient un intrant déclaré
Il ne vivait que dans `SETOPS_DIALECTE`, une variable d'environnement. C'est une propriété du
**matériel**, donc de la fabric : elle se déclare dans `underlay.yml`. Précédence désormais
explicite : drapeau `--dialecte` > variable d'environnement > intrant déclaré > `cisco`.
`make underlay` refuse un dialecte inconnu.
### Écriture chirurgicale, pas de `safe_dump`
`underlay.yml` porte 23 lignes de commentaires qui expliquent des décisions d'architecture —
pourquoi un seul routeur, pourquoi le transit vit dans l'underlay, pourquoi les rayons ne
sont pas des ports de bord. Un `safe_dump` les aurait toutes effacées, comme c'est arrivé aux
commentaires de `plan/applications.yml`. Le panneau remplace donc la ligne existante en
respectant son indentation. Vérifié : trois valeurs modifiées, **73 lignes et 23 commentaires
avant comme après**.
Une clef absente du fichier n'est pas créée : le panneau refuse explicitement plutôt que de
l'inventer à un endroit arbitraire.
## 2026-08-01 (suite 11) — les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés **nulle part** : `<PORT-VERS-PROXMOX>` et consorts
étaient des marqueurs littéraux dans le générateur. L'opérateur les remplaçait à la main dans
la sortie, et recommençait **à chaque régénération**. C'était le seul endroit du devis où le
travail était perdu à répétition.
Ils se déclarent désormais par équipement dans `underlay.yml`, sous quatre clefs qui
correspondent aux quatre natures de lien :
```yaml
ports:
hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3] # ports terminaux (portfast)
frontiere: [Gi1/0/23] # vers le pare-feu (portfast)
rayons: { sleipnir-02: Te1/0/47, … } # côté ROUTEUR
montante: Te1/0/48 # côté SWITCH D'ACCÈS
```
Le devis émet alors les vrais ports, y compris **plusieurs** vers les hyperviseurs — il n'en
supposait qu'un seul jusqu'ici, alors que le cluster en compte trois. Non déclarés, les
marqueurs reviennent : la dégradation est par équipement, un switch renseigné et un autre non
cohabitent sans problème.
La partie B devient **par switch** : adresse de gestion, montante et ports terminaux
différant d'une machine à l'autre, un bloc commun n'avait plus de sens.
`make underlay` refuse un port déclaré deux fois sur un même équipement, un rayon vers un
switch inconnu, des `rayons` sur autre chose que le routeur, une `montante` sur le routeur
lui-même. Les quatre cas exercés.
## 2026-08-01 (suite 10) — une interface, un bloc
La partie A déclarait ses ports de bord deux fois : l'interface en section 4, son `portfast`
en section 6. La partie B, elle, posait tout dans le même bloc. Deux conventions pour la même
chose dans un seul document — un opérateur qui applique la partie A section par section
configurait la même interface à deux endroits.
Le `portfast` est désormais posé **avec son interface** (sections 4 et 4b). La section 6 se
réduit à ce qui est global — mode, priorité du pont racine — plus les deux avertissements :
que les rayons de la section 4c n'en sont volontairement pas, et pourquoi BPDU guard n'est
pas émis.
Vérifié : aucune interface n'est déclarée deux fois dans une même partie, trois `portfast`
avec `stp`, zéro sans lui, zéro sur un rayon.
## 2026-08-01 (suite 9) — rayons de l'étoile séparés des ports terminaux
L'ajout du spanning-tree venait de rendre dangereuse une imprécision qui, jusque-là, n'était
qu'un titre approximatif. La section 4 s'appelait « Trunk vers Proxmox **+ inter-switch** »
et n'émettait qu'un port, que la section 6 déclarait en bord de réseau. Réutiliser ce
placeholder pour les rayons vers les switches d'accès revenait à mettre `portfast` sur les
liens qui portent les BPDU — c'est-à-dire à désactiver la protection anti-boucle exactement
là où elle sert.
Les rayons sont désormais **dérivés** et émis à part (section 4c côté routeur, B3a côté
accès), un par switch d'accès, avec l'avertissement qu'ils ne sont pas des ports de bord.
La section 4 ne désigne plus que les hyperviseurs, et la partie B distingue sa montante
(`B3a`) de son trunk terminal (`B3b`).
`underlay.switches_acces()` devient la source unique du « qui est un switch d'accès » —
utilisée pour leur devis **et** pour les rayons côté routeur : les deux ne peuvent pas
diverger.
Vérifié : trois commandes `portfast` émises avec `stp` déclaré, **aucune** sans lui, et
**aucune** sur un rayon.
## 2026-08-01 (suite 8) — spanning-tree dérivé de la topologie déclarée
### Ajouté — `underlay.stp` et les sections 6 / B4
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois switches, une
boucle par brassage accidentel n'est pas discrète : c'est une tempête de diffusion.
La topologie se déclare (`mode: rstp`, `topologie: etoile`) et le devis en tire la
configuration. Le **routeur est désigné pont racine** — il est le centre de l'étoile, tous
les chemins passent déjà par lui, donc l'arbre logique suit le câblage physique au lieu de
sortir d'une élection arbitraire. Les switches d'accès reçoivent une priorité volontairement
haute : ils ne doivent jamais devenir racine.
Les ports terminaux (hyperviseurs, frontière) sont déclarés en bord de réseau. **BPDU guard
n'est délibérément pas émis** : un pont Linux dont le STP serait activé enverrait des BPDU et
ferait tomber le port côté hyperviseur. Le devis dit pourquoi, et à quelle condition
l'ajouter.
En étoile, aucun lien n'est redondant : RSTP est un filet, pas une nécessité — le devis le
dit plutôt que de laisser croire à une protection indispensable.
`make underlay` valide `mode` et `topologie`, et refuse un `stp` déclaré sans `routeur` :
sans lui, aucun pont racine ne peut être désigné. Sans `stp`, la section signale l'absence
de protection au lieu de disparaître.
Réserve consignée : la forme `binardat` des lignes de spanning-tree n'a pas été confrontée au
matériel, comme les `ip route`.
## 2026-08-01 (suite 7) — l'underlay connaît ses fabrics physiques
Le stockage jumbo (iSCSI, Ceph) est porté par un **réseau indépendant de deux switches
10G**, sans câble commun avec la fabric convergée des `sleipnir`. Le modèle l'ignorait :
le devis déclarait les VLAN 20/30/31 sur les switches convergés et les mettait dans leurs
trunks. C'était faux.
Chaque réseau de l'underlay porte désormais une `fabric` (`principal` par défaut). Le devis
ne configure que celle du routeur, et **énonce ce qu'il ne couvre pas** plutôt que de le
taire :
```
! HORS PERIMETRE — fabric 'stockage' : stockage-iscsi (VLAN 20), ceph-public (VLAN 30), …
! Portee par des switches distincts, sans cable commun avec celle-ci :
! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage.
```
Conséquence directe : la question du spanning-tree ne se pose que sur la fabric principale —
trois switches — et pas sur le stockage, dont les deux switches forment un domaine séparé.
Le roster d'hôtes de la section 0 est filtré de la même façon : un équipement d'une autre
fabric n'apparaît pas, même en commentaire. Un devis est une configuration qu'on applique,
pas un inventaire. Vérifié en déclarant un hôte de la fabric stockage — il reste absent, et
la sortie du jour est inchangée.
Les `deny` de l'ACL couvrent en revanche **toutes** les fabrics, y compris celles hors
périmètre : la règle porte sur l'adresse de destination, pas sur le câblage. Si un chemin
s'ouvre un jour vers le stockage, il est déjà fermé.
## 2026-08-01 (suite 6) — nommage : `bifrost` aux frontières, `sleipnir` à la fabric
`bifrost-1` et `bifrost-2` sont réservés aux deux frontières OPNsense — Bifröst est le pont
vers l'extérieur. Les switches internes deviennent `sleipnir-01…03` : le cheval qui traverse
les mondes, pas le pont qui en sort. La division du nom suit celle de l'architecture.
Les deux boîtiers sont déclarés comme hôtes du lien de transit (`10.0.4.1`, `10.0.4.2`) :
hors flotte Ansible, la déclaration documente le lien et réserve les noms. Le `/29` choisi
plus tôt les loge tous les deux, comme prévu.
Le plan du `/29` est réorganisé en conséquence : frontières en bas (`.1`, `.2`), SVI du
switch en haut (`.6`), et `.3` laissée libre pour une future IP virtuelle CARP si les deux
OPNsense passent en haute disponibilité. Ce jour-là, `passerelle_sortie` pointera sur la VIP
plutôt que sur un boîtier nommé — un seul endroit à changer.
Conséquence à traiter : la partie B du devis switch aurait listé les deux pare-feux parmi les
« switches d'accès ». Elle ne retient désormais que les hôtes du **réseau de management**
un équipement déclaré ailleurs ne reçoit aucune ligne de configuration de switch.
Le devis frontière nomme le boîtier quand il est déclaré : `frontiere 10.0.4.1 (bifrost-1)`.
## 2026-08-01 (suite 5) — deux incohérences du devis switch
### Corrigé — le routeur avait deux adresses de gestion contradictoires
`bifrost-01` portait le SVI `Vlan10 → 10.0.0.1` **et** était déclaré dans `underlay.hotes`
à `10.0.0.2`. Une interface VLAN n'a qu'une adresse primaire : les deux ne pouvaient pas
être vraies. L'entrée datait d'avant la désignation du routeur, quand `10.0.0.1` était une
passerelle abstraite.
Le routeur est désormais déclaré à l'adresse du SVI qu'il porte, et `make underlay` **refuse**
la divergence : `ip 10.0.0.2 != passerelle 10.0.0.1 — le routeur porte ce SVI, les deux
doivent coïncider`. Le roster des trois switches reste complet.
### Corrigé — VLAN de transit déclaré sur les switches d'accès
La partie B créait `vlan 40` alors que le trunk B3 ne le transporte pas — le transit ne relie
que le routeur à la frontière. Un VLAN qui n'aurait jamais vu de trame. Il est exclu de la
partie B, comme il l'est déjà des trunks généraux.
## 2026-08-01 (suite 4) — un seul switch route, les autres en L2 pur
### Décidé — `underlay.routeur`
Sans MLAG, le routage est porté par un **unique** switch (`bifrost-01` chez Chezlepro) ; les
autres restent en L2 pur. Le devis émettait jusqu'ici un jeu unique de SVI sans dire à quel
switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses
qu'il y a de zones.
### Ajouté — le devis se scinde en deux parties
- **Partie A — switch routeur** : VLANs, SVI, ACL d'isolation, trunks, routes. Va sur lui
seul, et l'en-tête le nomme.
- **Partie B — switches d'accès (L2 pur)** : mêmes VLANs pour commuter les trames étiquetées,
une adresse de gestion par switch (tirée de `underlay.hotes`) avec `ip default-gateway`
vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route.
Deux gardes : `make underlay` refuse un `routeur` qui ne nomme aucun hôte déclaré ; et la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine — les coller tous
écraserait l'adresse. Sans `routeur` désigné, l'en-tête signale explicitement le risque de
duplication au lieu de laisser croire que le devis est applicable partout.
Reste ouvert et consigné : la syntaxe des `ip route` n'est pas dialecte-consciente,
contrairement aux ACL.
## 2026-08-01 (suite 3) — les tenants n'atteignent plus l'underlay
### Corrigé — ACL de switch : `deny` vers la fabric physique
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par `permit ip <tenant> any`.
Ce `any` autorisait `10.27.x``10.0.0.0/24` : le management des switches, celui de Proxmox
et l'**OOB/IPMI**, plus les réseaux iSCSI et Ceph. Une VM compromise atteignait la console
physique des hyperviseurs.
Le commentaire du générateur disait « Reste → passerelle OPNsense », mais c'est faux pour
l'underlay : ce trafic est routé **localement** par le switch et ne passe jamais par la
frontière, donc il n'est jamais filtré par elle.
`devis_reseau.py` émet désormais un `deny` par sous-réseau underlay avant le `permit` final,
dérivé de `underlay.yml` — dialecte respecté (masque normal ou wildcard). Aucun flux du
registre ne vise l'underlay : le blocage ne casse rien de déclaré.
Deux limites consignées dans `docs/frontiere-opnsense.md` §6 : le registre des flux n'a pas
de mot-clé `underlay`, donc un besoin légitime (superviser l'hyperviseur) ne pourrait pas
être déclaré ; et `devis_reseau.py` émet un jeu unique de SVI pour trois switches sans MLAG,
ce qui reste une décision d'architecture ouverte.
## 2026-08-01 (suite 2) — le port vers la frontière, et l'ordre d'application
### Ajouté — section 4b : le port du switch vers la frontière
Le devis étiquetait le VLAN de transit sur le trunk `<PORT-VERS-PROXMOX>` et n'émettait
aucune interface vers le pare-feu. Deux erreurs en une : les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN tenants — il route vers
eux, il ne les étiquette pas. Le VLAN de transit sort donc du trunk Proxmox et prend son
propre port, dérivé du transit déclaré.
### Ajouté — avertissement d'ordre en tête de la section 5
Les routes de la section 5 déplacent la sortie du switch, **y compris celle de ses propres
réponses**. Tant que l'adresse de la frontière ne répond pas, elles coupent l'accès
d'administration au switch lui-même — le mécanisme qui a rendu une VM muette le 2026-07-29,
appliqué cette fois à l'équipement depuis lequel on travaille.
Le devis crachait ces lignes à la suite des autres comme si elles étaient équivalentes.
Il énonce désormais les préalables (boîtier câblé, adressé, joignable depuis le switch,
session console ouverte) — inacceptable autrement pour un outil censé être exploitable sans
IA.
## 2026-08-01 (suite) — le prochain saut dérive du transit
`opnsense_prochain_saut` n'est plus un intrant du panneau : il **dérive** du réseau de
transit de l'underlay (la `passerelle` du réseau portant `passerelle_sortie`).
Un seul bloc de six lignes alimente désormais les deux devis : `10.0.4.1` devient le SVI
côté switch **et** le prochain saut des routes tenants côté frontière ; `10.0.4.2` devient
la route par défaut du switch. Le saisir en doublon dans le panneau rouvrait la possibilité
de deux valeurs contradictoires pour un seul lien — précisément le mode de panne qu'on venait
de fermer pour les réseaux d'administration.
Le devis frontière gagne au passage un bloc `transit` (JSON compris) et cesse de réclamer en
section 0 des routes que `make devis-reseau` émet maintenant : il dit lesquelles sont **déjà
émises**, ou signale l'absence de transit déclaré. Sans underlay, le marqueur
`<PROCHAIN-SAUT-SWITCH>` revient — le repli reste explicite.
Reste un seul intrant à figer au câblage : `opnsense_if_transit`, le nom de l'interface qui
porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner.
## 2026-08-01 — le lien de transit et les deux routes (la boucle est fermée)
### Ajouté — réseau de transit dans l'underlay (clé `passerelle_sortie`)
Le lien entre le routeur est-ouest (switches L3) et la frontière nord/sud manquait dans
**tous** les fichiers : le devis switch ne contenait pas une seule `ip route`.
Il vit dans l'**underlay** et non dans un tenant, pour une raison qui tranche : la frontière
route vers tous les supernets tenants par le **même** prochain saut. Le lien est donc partagé
et ne peut dériver d'aucun `index`. Un réseau underlay portant `passerelle_sortie` (l'adresse
du pare-feu sur le lien) le déclare — chez Chezlepro : VLAN 40, `10.0.4.0/29`, SVI `10.0.4.1`,
frontière `10.0.4.2`. Un `/29` et non un `/30` parce que deux pare-feux cohabitent pendant la
transition.
`underlay.py` valide la nouvelle clé (sortie sur le lien, distincte du SVI, SVI obligatoire,
un seul transit) — **preuve P23**, cas de rejet exercés un par un.
### Ajouté — `devis-reseau` émet les routes (section 5)
Deux routes dérivées, et il en faut impérativement deux :
- **l'aller** : `ip route 0.0.0.0 0.0.0.0 <sortie>` — sans elle, aucun hôte n'a de sortie ;
- **le retour** : une route par réseau d'administration — sans elle, la réponse d'une VM
revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence. C'est le piège qui a coûté la passe de déploiement du 2026-07-29.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` : **même source
unique** que la garde anti-lockout des nftables et l'alias `SETOPS_ADMIN` de la frontière —
les trois pare-feux et les routes ne peuvent pas diverger. Sans transit déclaré, la section
s'affiche en clair comme manquante plutôt que de disparaître silencieusement.
### Corrigé — le panneau refusait d'enregistrer les intrants de la frontière
`group_vars/opnsense.yml` porte à la fois des paramètres anodins et deux **références** de
voûte (`{{ vault_opnsense_api_key }}`). La fusion « préserve les clés non gérées » les
relisait, et le garde-fou, qui ne regardait que les **noms**, les prenait pour des secrets
soumis. Il regarde désormais la **valeur** : une référence de voûte est un pointeur et passe ;
toute valeur réelle sur ces clés fait toujours échouer l'écriture. Ajout d'un refus explicite
à l'entrée pour une requête forgée, au lieu d'un abandon silencieux.
### Retiré — reliquat `proxmox.vault.yml`
La voûte est **unique** (`group_vars/all/vault.yml`) ; l'ancien fichier séparé n'était plus
chargé automatiquement (aucun groupe `proxmox` dans l'inventaire) et entretenait la confusion.
Supprimé de l'instance, avec son gabarit.
Au passage : `supprimer_vm_debian.yml` ne chargeait **que** ce reliquat pour ses secrets. Le
supprimer tel quel aurait cassé `make detruire`, l'outil même du rebuild from-zero. Sa liste
est alignée sur celle du playbook de clonage (`all/vault.yml` en dernier, il l'emporte), et la
résolution du jeton depuis la voûte unique est vérifiée en exécution réelle.
## 2026-07-29 (suite) — la frontière OPNsense, dérivée du registre des flux
### Ajouté — `make devis-opnsense` (+ preuve P24)
La bordure nord/sud devient un **artefact dérivé**, comme le devis switch. Rien de saisi à la
main : ni port, ni adresse, ni nom d'hôte n'apparaît dans le générateur.
Le constat qui rend la chose évidente : `resoudre_flux.py` **saute volontairement** les flux
`pair: externe` (`scripts/resoudre_flux.py:184`) parce qu'ils ne concernent pas le pare-feu
d'hôte. Plusieurs `raison` du registre disent déjà « filtré à l'OPNsense ». **La politique de
la frontière était donc déjà écrite** — il ne restait qu'à la dériver.
- **`scripts/devis_opnsense.py`** — agrège les flux `externe`, résout les destinations depuis
l'inventaire (hôtes actifs **et** planifiés : la frontière se prépare avant les VM), les
supernets depuis les nomenclatures fédérées, et l'accès d'administration depuis l'intrant
`nftables_admin_ssh`. Sort un devis relisible ou `--json` (destiné à l'API OPNsense).
- **Garde anti-lockout (P24)** — `--verifier` **refuse** un devis dont `nftables_admin_ssh`
est vide : la règle SSH entrante n'aurait aucune source et le `block in` final fermerait
l'accès d'administration. Même intrant que les nftables d'hôte : source unique, donc pas de
divergence possible entre la bordure et les hôtes.
- **Section 0 du devis : les routes de retour à poser sur les switches.** C'est le piège qui a
coûté la passe de déploiement du jour — la passerelle de zone répond au ping, mais aucun
hôte derrière elle n'est joignable, parce que la réponse revient au pare-feu par une autre
interface que celle où l'état a été créé.
- **`docs/frontiere-opnsense.md`** — les décisions d'architecture (frontière nord/sud, les SVI
restent sur les switches L3), le partage des rôles entre les trois pare-feux, le chemin
d'application par l'API, et ce qui reste ouvert (câblage, second VPN, sortie générale).
### Corrigé — intrant `nftables_admin_ssh` vide sur l'instance Chezlepro
Il valait `[]` alors que `group_vars/hotes_actifs.yml` active `nftables_baseline_enabled`.
Autrement dit : la flotte se serait mise en `policy drop` sans **aucune** règle autorisant le
contrôleur, qui arrive par le VPN hors du sous-réseau de la flotte. Renseigné à
`192.168.255.0/24`. Le sous-réseau du second VPN (OPNsense) devra y être ajouté.
### Ajouté — la frontière devient réglable depuis la console (section *Frontière*)
Les valeurs non sensibles du pare-feu de bordure sont de **vrais intrants**, pas un fichier
YAML tenu à la main : un opérateur règle la frontière depuis le panneau « Intrants de base »,
sans IA et sans éditeur.
- **`INTRANTS_SCHEMA`** — nouvelle section *Frontière* : `opnsense_api_url`,
`opnsense_api_verifier_certs`, `opnsense_if_wan`, `opnsense_if_transit`,
`opnsense_prochain_saut`. Cible d'écriture `group_vars/opnsense.yml`, en **fusion** (comme
Proxmox) pour préserver les références de voûte du fichier. Le panneau rend ses sections
génériquement : aucune modification d'interface n'a été nécessaire.
- **Garde-fou** — `opnsense_api_key` / `opnsense_api_secret` ajoutés à
`INTRANTS_CLES_INTERDITES` : le GUI **refuse** de les écrire, donc impossible de coller un
secret d'API dans le panneau par mégarde. Ils n'apparaissent qu'en lecture seule, par leur
nom, dans `SECRETS_ATTENDUS`.
- **`devis_opnsense.py`** lit désormais ces intrants et n'affiche ses marqueurs
(`<IF-TRANSIT>`, `<PROCHAIN-SAUT-SWITCH>`) qu'en repli : le devis se complète de lui-même
dès que la console est renseignée.
### Ajouté — les identifiants d'API de la frontière, dans la voûte
Même partage que Proxmox : l'anodin en clair, le secret dans la voûte unique de l'instance.
- **`group_vars/opnsense.yml`** (instance, en clair) — URL de gestion, interfaces et prochain
saut à figer au câblage, plus les **références par nom** `{{ vault_opnsense_api_key }}` et
`{{ vault_opnsense_api_secret }}`. Aucune valeur de secret n'y figure.
- **Gabarit de voûte** — les deux clés ajoutées à `vault.yml.example`. `scripts/voute.py` les
a recensées **tout seul** depuis les `group_vars` (il ne lit jamais la voûte, il ne compare
que des noms) : le gabarit passe de 23 à 25 secrets, et la **preuve P18** reste verte.
Validation : `make verifier` vert — **24 preuves CONFORME**, 0 échec, 0 sauté.
## 2026-07-29 — dette documentaire soldée (un README par rôle) + carte revue
### Ajouté — les 12 README de rôles manquants
**Tous les rôles ont désormais un README.** Les 12 restants sont écrits, au format maison
(intention → rôle → variables → notes/limites → prérequis), et documentent surtout ce qui
ne se lit pas dans les tâches :
- **Socle et résolution** — `serveur_debian` (rôle-catégorie sans tâches : il ne porte que le
flux SSH du plan de gestion, *sans quoi les nftables couperaient l'accès Ansible*),
`hosts_statiques` (le plancher `/etc/hosts` qui rend l'ordre de reconstruction possible).
- **Rôles utilitaires** — `resoudre_base` et `resoudre_annuaire` : entrées/sorties (facts),
et *pourquoi* le FQDN plutôt que le nom court (fédération + `verify-full`).
- **Courriel** — `serveur_dovecot` (les trois réglages Dovecot 2.4 qui conditionnent la
remise ; le local-part seul comme chemin commun LMTP/IMAP), `serveur_postfix` (liens
`mailstore`/`milter`, recopie de `/etc/hosts` dans le chroot), `serveur_rspamd` (clé DKIM
idempotente ; le domaine signé doit être **public** en prod).
- **Sauvegardes** — `client_backup` (jobs déclaratifs, chiffrement côté client, *le dépôt
neuf est vide tant qu'une première sauvegarde n'a pas tourné*) et `serveur_backup`
(hors-nœud ≠ hors-site).
- **SSO et supervision** — `serveur_oauth2_proxy` (le patron réutilisable Keycloak-devant-
n'importe-quoi ; pourquoi `allow_unverified_email` est nécessaire avec un annuaire LDAP)
et `serveur_icingaweb2` (modes `ldap` vs `external`, et l'écoute à restreindre en SSO).
- **`client_unbound`** — le garde-fou de bascule du résolveur (`apply` **et** `confirm`,
validation avant de toucher `/etc/resolv.conf`).
### Corrigé — `docs/carte-set-ops.md` ne décrivait plus l'état du code
- **`expose` est consommé au déploiement** (la carte l'annonçait encore comme « Phase 3 à
venir ») : vhosts nginx dérivés via `expositions_des_applications` +
`expositions.conf.j2`, alias `/etc/hosts`, SANs d'edge dérivés par `instancier.py`.
- **Trois mécanismes transverses ajoutés au catalogue** : résolution d'annuaire
(`resoudre_annuaire`), plancher de résolution (`hosts_statiques`), et `resoudre_base`
nommé dans la ligne des bindings app→base.
- **Cinq entrées d'index ajoutées** : réseau/pare-feu, ordre de déploiement, preuve/recette,
exploitation courante, wiki — des pans entiers du corpus n'étaient pas indexés.
- Reste ouvert, explicitement : `meta/liens.yml` sur le seul `serveur_postfix`, et `requiert`
non consommé au déploiement (indice applicatif ; l'ordre, lui, vient des couches+graphe).
Validation : `make verifier` vert (ansible-lint 514 fichiers, tests, syntax-check,
**23 preuves CONFORME**).
## 2026-07-28 — figures annotées dans le wiki (console d'exploitation)
### Ajouté — les 8 vues de la console illustrées, dans le wiki
La série des figures annotées (une par vue du GUI, en **SVG auto-contenu** : capture + repères
intégrés en base64) est désormais **intégrée** à l'unité wiki
[`Le GUI (console d'exploitation)`](wiki/Le-GUI-console-d-exploitation.md), en fin de section ②.
Vues éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux, Couches, Réseau).
- **Symlink `wiki/img → ../docs/img`** — foyer unique des figures dans `docs/img/` ; les pages wiki
y réfèrent en relatif (`img/*.svg`) sans duplication dans l'arbre source.
- **`make wiki-publier`** embarque désormais les `docs/img/*-annote.svg` (déréférencés) dans le
wiki Forgejo publié — c'étaient jusqu'ici les seules `.md` qui voyageaient, donc aucune image.
## 2026-07-24 — underlay (fabric physique, cluster-global)
### Ajouté — l'underlay comme concept de premier plan
Le modèle dérive l'adressage **par tenant** (VLAN `1000+index×10+zone`), mais la **fabric
physique** qui porte la flotte — mgmt des switches, mgmt Proxmox/OOB, iSCSI, Ceph — n'appartient
à aucun tenant et ne dérive d'aucun `index`. Elle vit dans le *sous-sol* du modèle. Jusqu'ici
elle n'était pas codifiée. Elle l'est.
- **`scripts/underlay.py`** + **`make underlay`** — charge/affiche/**valide** `underlay.yml` :
réseaux (nom, VLAN, sous-réseau, passerelle optionnelle → SVI, MTU/jumbo) et hôtes fixes
documentés (les switches). La validation refuse toute **collision avec la plage tenant** :
VLAN < 1000, sous-réseaux hors des supernets `10.(10+index).0.0/16`.
- **`underlay.yml`** (racine du moteur, **gitignore** comme le vault ; gabarit public
`underlay.yml.example`), surchargeable par `SETOPS_UNDERLAY`. Absent tout reste inchangé.
- **`make devis-reseau`** émet désormais une **section 0. Underlay** (VLANs, SVI, hints jumbo,
IP des switches en commentaire) et ajoute les VLAN underlay au **trunk** Proxmox, avant les
tenants. Respecte le dialecte (`cisco`/`binardat`).
- **Preuve P23** `underlay.py --verifier` : la fabric n'empiète pas sur la plage tenant.
**Sautée** (⚪) si `underlay.yml` est absent (dépôt public), comme P16 sans vault.
Le plafond tenant (245) est inchangé : l'underlay occupe `10.0.0.0/16 .. 10.10.0.0/16`, laissé
libre par la dérivation (index 1 `10.11+`).
## 2026-07-23 (suite 7)
### Ajouté — dialecte de CLI du commutateur (`devis-reseau`)
Constat de l'opérateur : son switch est un **Binardat**, dont la CLI diffère de Cisco sur
deux points que le devis généré ignorait et deux pièges qui font passer un VLAN mais fuir
un tenant :
- **Masque d'ACL** Cisco veut un masque **inversé** (wildcard `0.0.255.255`), Binardat un
masque **normal** (`255.255.0.0`). Le SVI (`ip address 255.255.255.0`) était déjà normal,
donc valide sur les deux.
- **`remark`** Binardat n'a pas la commande de commentaire d'ACL de Cisco. Ces lignes
faisaient rejeter le bloc.
**`scripts/devis_reseau.py`** gagne un **dialecte** (`cisco` par défaut, `binardat`) :
`masque_acl()` choisit wildcard ou masque normal ; `remarque()` omet les `remark` en Binardat.
Réglable par `--dialecte`, par `SETOPS_DIALECTE`, ou `make devis-reseau DIALECTE=binardat`. Le
GUI (lecture seule) suit l'env. Le code public reste **générique** (défaut `cisco`).
## 2026-07-23 (suite 6)
### Ajouté — plan de recette (le pendant manuel de `make prouver`)
Constat de l'opérateur : les 78 exercices « À toi de jouer » du wiki forment, ensemble, un
**plan de tests d'acceptation**. Formalisé, sans dupliquer :
- **`scripts/plan_recette.py`** + **`make plan-recette`** **génère** `docs/audit/plan-de-recette.md`
depuis les exercices du wiki : une grille auto-contenue (le geste inline) par unité, colonnes
*Ce qu'on éprouve · Le geste · Type (observe/casse-répare) · **Preuve auto***. La dernière colonne
est extraite du texte (le `Pxx` que l'exercice mentionne) : elle montre quels gestes manuels sont
**aussi** gardés par la machine. Étant générée, la grille **ne peut pas dériver** du wiki.
- **Preuve P22** `plan_recette.py --verifier` échoue si le fichier committé n'est plus à jour
(le wiki a changé sans régénérer). Le plan de recette devient un artefact **auto-gardé**.
- **Honnêteté de couverture** assumée dans le document : un « » = **manuel seul** (aucune preuve
machine ne le double) ; le plan ne prétend pas à l'exhaustivité au-delà des exercices du wiki.
C'est le **pendant humain** de `make prouver` : le harnais prouve le *moteur* (P01P21), la recette
valide l'*exploitation* et sert de checklist au `protocole-operateur-independant.md` exploitable
sans IA »).
Validé : 78 gestes sur 19 unités, 5 doublés d'une preuve `Pxx` ; P22 testée (détecte une dérive) ;
`make verifier` **CONFORME 22/22** (contre une instance cohérente).
## 2026-07-23 (suite 5)
### Ajouté — wiki : l'axe « méthode » (KB enrichie)
Le wiki enseignait les fondamentaux *services* (identité, PKI, courriel…) mais pas la
*méthode* de Set-OPS. Cinq pages ajoutées, au moule à 4 temps (concept Set-OPS
transférable à toi de jouer), avec exercices concrets :
- **Le plan & l'adressage dérivé** un seed (`index`), tout en découle (DRY, source unique).
- **Multi-instance & fédération** un moteur, N écosystèmes ; découverte par convention.
- **La preuve** « ne jamais affirmer plus que ce qu'on prouve » ; le registre, `make prouver`,
P01P21.
- **Le GUI (console d'exploitation)** éditer la source, dry-run avant apply, l'invalide
impossible à saisir (le `<select>` sans hôte fantôme) ; la flotte et la bascule.
- **Glossaire** 24 concepts en une phrase (était « à venir »).
Raccordées dans `Home.md` (deux unités-pilotes : services **et** méthode) et `_Sidebar.md`
(section « Flotte & preuve »). Fidèle à la doctrine : le wiki pointe vers `docs/`, ne recopie
pas. Publié via `make wiki-publier`. Wiki : 855 1573 lignes, 22 pages, aucun lien mort.
## 2026-07-23 (suite 4)
### Ajouté — créer un MODÈLE (`make model-creer`, dépôt privé)
Symétrique de `instance-creer`, mais produit un modèle réutilisable dans le dépôt PRIVÉ
(`Set-OPS-Modeles/`, jamais `exemples/modeles/`). `scripts/model_creer.py`, deux modes :
- **`MODE=base BASE=<modele> NOM=<x>`** — copie un modèle déjà générique (copier + éditer).
- **`MODE=instance SOURCE=OPS-<x> NOM=<y>`** — **promeut une instance éprouvée en modèle** :
généralise l'identité (`domaine → exemple.internal`, organisation → `Exemple`, realm →
`exemple`), fixe `index → 1`, `setops_production → false`, `nftables_admin_ssh → []`, vide
la clé publique de sauvegarde, générique `proxmox.yml` (host/nœud/stockage/VMID vidés,
golden template → `modele-debian13`).
Sûreté : **aucun secret** ne sort (`vault.yml`/`proxmox.vault.yml` jamais copiés ; refus si
l'un subsiste ; le `.example` est conservé). Copie **ciblée** (plan/ + inventories/ seulement,
symlinks résolus) — robuste aux dépôts imbriqués et boucles de symlinks. Le modèle produit
**doit valider** (`modeles.py verifier`), sinon il est annulé. `make model-creer`.
Validé : les deux modes testés en isolement (destination tmp, dépôt privé jamais touché) —
promotion du labo (cohérent) réussie + validée, aucun `chezlepro` résiduel, aucun secret,
`proxmox.yml` génériqué ; copie base (socle) OK ; refus d'une instance INVALIDE (le garde-fou
a détecté un hôte fantôme dans une instance en cours d'édition). `node --check`, `make verifier`
rc=0 CONFORME 21/21.
## 2026-07-23 (suite 3)
### Ajouté — créer une instance depuis un modèle (CLI + GUI)
Le moteur est indépendant des instances (le symlink `instance/` est gitignoré, aucun
artefact d'instance n'est committé). Créer une instance existait seulement à la main
(`cp -r` + `ln -s`, QUICKSTART). C'est désormais une capacité de premier ordre.
- **`scripts/instance_creer.py`** — copie un modèle (`exemples/modeles/*` + `SETOPS_MODELES`)
vers un dépôt frère `../<nom>`, y fixe l'`index` (le seed), et **refuse** : un nom déjà
existant (rien n'est écrasé), un modèle inconnu, un **index en collision** avec une
instance fédérée (le garde-fou vérifie AVANT toute copie). Le `.git` et
`hosts.genere.yml` du modèle ne sont pas copiés.
- **`make instance-creer NOM=OPS-X MODELE=socle [INDEX=N]`** et **`make instance-modeles`**
(liste les modèles + les index déjà pris).
- **GUI, vue Réseau** — formulaire « Créer une instance depuis un modèle » sous la flotte :
modèle (liste), nom, index. `POST /api/instance-creer` ; `/api/instances` renvoie aussi
`modeles` et `index_pris`. Ne bascule pas l'active (geste explicite).
### Corrigé
- **Gabarit de voûte du labo complété.** P18 (voûte) a échoué en passant l'active sur le
labo : son `vault.yml.example` était resté à l'ancienne version (17 clés, sans
`vault_restic_password`, `vault_oauth2_cookie`, etc.). Aligné sur le gabarit complet
(23 secrets). P18 fait exactement son travail — attraper un gabarit incomplet, quelle que
soit l'instance active.
Validé : garde-fous de création testés en isolement (collision d'index refusée avant copie,
écrasement refusé, modèle inconnu refusé, aucune pollution des dépôts frères) ; `node --check`
du GUI ; `make verifier` rc=0 **CONFORME 21/21** (active = labo).
## 2026-07-23 (suite 2)
### Ajouté — bascule d'instance depuis le GUI (vraiment multi-instance)
Demande explicite et répétée de l'opérateur : gérer les instances **depuis le GUI**, pas
seulement en CLI. Le plan de contrôle reste maison, mais cette capacité y entre.
- **Inventaire résolu dynamiquement.** Le serveur GUI figeait l'inventaire au démarrage
(`args.inventaire.resolve()`). Il est désormais relu **à chaque requête** depuis le
symlink `instance/` (propriété `Gestionnaire.inventaire`) — la bascule prend effet sans
redémarrer. Les chemins de plan (`FICHIER_*`) étaient déjà relatifs au symlink et suivent
de même. `SETOPS_INVENTAIRE` force encore un inventaire fixe (CI).
- **`POST /api/instance-utiliser`** + `basculer_instance(nom)` — repointe le symlink avec
les garde-fous de `make instance-utiliser`, plus une **validation stricte** : `nom` doit
être une instance **découverte** (dossier frère), ce qui interdit toute traversée de
chemin (testé : `../etc` refusé).
- **GUI, vue Réseau** — la table de flotte gagne un bouton **« Activer »** par instance
(l'active affiche « active »). Il confirme (avec avertissement renforcé si l'instance est
en **production**), bascule, puis recharge la page : toutes les vues et les déploiements
visent la nouvelle instance.
L'édition du drapeau `federe` reste au CLI (rarement changé). La bascule, elle, est
maintenant CLI **et** GUI.
Validé : `node --check` du GUI ; bascule testée de bout en bout (repoint → l'inventaire
dynamique suit → traversée refusée → restauration) ; `make verifier` rc=0 CONFORME 21/21.
## 2026-07-23 (suite)
### Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision
Le mécanisme de bascule existait déjà (`make instance-utiliser`, `make instance-courante` :
repointer le symlink `instance`). Ce qui manquait : le regard d'ensemble et le filet.
- **`make instances`** (`scripts/instances.py`) — liste toutes les instances de la
fédération (dépôts frères avec `plan/nomenclature.yml`), marque l'active (`*`), et
montre pour chacune index, plage VLAN dérivée, statut fédéré/local (`federe`) et
production (`setops_production`). Lecture seule.
- **Détection de collision d'index** — signale toute paire d'instances **fédérées**
partageant un `index` (donc mêmes VLAN/VMID sur le trunk convergé). C'est exactement le
piège vécu (Chezlepro-prod et le labo tous deux à l'index 1) : désormais crié, pas
découvert par hasard. Le mode `--verifier` sort en erreur (rc=2) sur collision.
- **Preuve P21** (`make prouver`/`make verifier`) — câble ce garde-fou dans le harnais :
la cohérence de la fédération est vérifiée à chaque passage. No-op quand moins de deux
instances fédérées sont présentes (comme P17 sans `SETOPS_MODELES`).
Validé : `make instances` liste les 3 instances (labo LOCAL, Technolibre et Chezlepro
fédérées, index 2 et 13) ; détection testée en synthétique (deux fédérées au même index →
collision levée ; labo `federe: false` → cohérent) ; `make verifier` rc=0 **CONFORME 21/21**.
## 2026-07-23
### Changé — l'adressage se dérive du seul seed `index` (RUPTURE, mode compact retiré)
Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des
intrants, pas se réécrire à la main. La nomenclature dupliquait ce que `index` détermine
déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette.
- **`inventory_rules`** : nouvelles fonctions de dérivation, **source unique**
`supernet_de`, `base3_de`, `sous_reseau_de`, `passerelle_de`, `vlan_de`. Le modèle à
6 zones est encodé une fois : 2ᵉ octet = `10+index`, 3ᵉ octet de zone = `15+catégorie`,
VLAN trunk = `1000+index×10+zone`. `deriver_nomenclature` ne lit plus **aucun** adressage
stocké ; le mode `compact` est supprimé (tout est ip-miroir dérivé).
- **`devis_reseau`** importe ces helpers (plus de duplication) ; découverte des tenants sur
`index` présent (le filtre `vmid_schema` disparaît).
- **Les 10 nomenclatures** (3 instances + 7 modèles) passent au **format maigre** : `index`,
`cidr_hote`, `reservations`, libellés de zones et `fonctions` seulement. Supprimés :
`supernet`, `vmid_schema`, et par zone `sous_reseau`/`passerelle`/`vlan`. `presence-web`,
encore en `compact`, gagne `index: 1`.
- **GUI** : `index` devient un **intrant** (section « Réseau » du panneau Intrants). Il vit
dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des
modèles, hétérogènes) et le GUI l'écrit **chirurgicalement** (une ligne, sans reformater
le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de `c.vlan` stocké).
### Ajouté
- **Preuve P20** (`preuve_nomenclature_derivee`) : aucune nomenclature ne stocke d'adressage
— garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif
(une rechute simulée est bien rejetée).
Validé : **DIFF VIDE sur les 3 instances** (la dérivation reproduit exactement l'adressage
qui était stocké), les **7 modèles valident**, `devis_reseau` génère les mêmes VLAN
(1011-1016 dérivés), `make verifier` rc=0 **CONFORME 20/20**, `node --check` du GUI OK,
aller-retour d'écriture de `index` : une seule ligne touchée.
## 2026-07-22 (suite)
### Ajouté — trois preuves qui ferment les angles morts du harnais
Le constat : `make prouver` ne vérifiait qu'*une* instance et le seul modèle `socle`. Tout
ce qui vit à côté du moteur dérivait en silence — d'où l'hôte fantôme d'`integral`, les
neuf secrets absents du gabarit Chezlepro, les champs de plan que le GUI ne sait pas écrire.
- **P17 — tous les modèles valident** (`scripts/modeles.py`). Rejoue les 4 validateurs de
registres sur chaque modèle découvert (les `exemples/modeles/*` du dépôt, plus les chemins
de `SETOPS_MODELES`, ex. le dépôt privé). **A immédiatement trouvé 6 modèles invalides
sur 7** : cinq portaient `autorite: interne` (valeur périmée jamais propagée depuis la
correction de `socle` en phase 5), `presence-web` manquait son domaine interne. Corrigés.
- **P18 — gabarit de voûte complet** (`scripts/voute.py`). Confronte `vault.yml.example`
aux secrets réellement exigés par le plan (champ `secret` des bases + références `vault_*`
des rôles actifs et des group_vars). Ne déchiffre jamais la vraie voûte : compare des noms.
`--strict` signale aussi les clés devenues inutiles.
- **P19 — le GUI couvre le plan** (`scripts/couverture_gui.py`). Croise les champs présents
dans les plans réels (instance + modèles) avec `CHAMPS_ECRITS_PAR_GUI`, nouveau tableau de
`inventory_gui.py` déclarant ce que les fonctions de sauvegarde écrivent. Signale tout
champ non éditable par le GUI (**a trouvé `applications.websocket`**, comblé — case à
cocher ajoutée), et toute dérive entre le tableau et le source du GUI. La nomenclature
reste un trou connu (lecture seule), tolérable via `--tolerer nomenclature`.
### Corrigé
- **Garde-fou contre l'hôte fantôme** : `valider_applications` accepte désormais le registre
des serveurs et **refuse une application posée sur un hôte non déclaré** — la faute exacte
qu'`integral` portait. Câblé dans `scripts/applications.py` (les 4 appels), au POST du GUI,
et vérifié : re-teste le bug d'origine → rejeté avec un message actionnable.
- **6 modèles de `Set-OPS-Modeles`** : `autorite: interne``auto-heberge` (collaboration,
forge, identite, integral, observabilite) ; `presence-web` gagne son domaine interne pour
ses expositions `site.*`/`app.*`.
- **Champ `websocket` dans le GUI** (inspecteur d'application) — Collabora en a besoin.
Validé : `make verifier`**rc=0, CONFORME 19/16→19** (P01P19), `ansible-lint` 0 échec,
les 7 modèles valident (`SETOPS_MODELES` posé), gabarit de voûte complet (23/23), couverture
GUI 27/27 champs (nomenclature tolérée), DIFF VIDE conservé, JS du GUI valide.
## 2026-07-22
### Ajouté
- **Champ « Liens (bindings) » dans le GUI** (`scripts/inventory_gui.py`). L'inspecteur
d'application porte une section dédiée : une ligne par lien, `rôle → cible`, les deux en
listes déroulantes, avec bouton d'ajout et de retrait. Les **rôles proposés sont
exactement ceux que le rôle porteur déclare accepter** (`roles/<groupe>/meta/liens.yml`),
et les cibles sont les autres applications du plan. Chaque ligne affiche les **variables
que le lien injectera**. Changer le groupe d'une application vide les rôles de lien
devenus inacceptés. Comble un manque relevé à l'audit du GUI : les bindings ne pouvaient
se déclarer qu'en éditant `plan/applications.yml` à la main, ce qui rendait la
configuration du courriel (Postfix → Dovecot, Postfix → rspamd) inaccessible sans éditeur.
- **`liens_acceptes(groupe)` et `catalogue_liens()`** dans `scripts/inventory_rules.py`
source **unique** de « quels liens un rôle accepte », partagée par le validateur, le GUI
et `instancier.py`. Nouvelle clé `liens_acceptes` dans la charge utile de `/api/inventaire`.
### Modifié
- **`valider_applications` valide désormais les `liens`** : liste de tables `{vers, role}`,
champs non vides, cible connue, pas de lien vers soi-même, et **rôle accepté par le rôle
porteur** (avec la liste des rôles acceptés dans le message d'erreur). Un rôle sans
`meta/liens.yml` reste toléré au validateur — `instancier.py` tranche avec le même message.
Bénéficie aussi à `make inventaire-verifier` et à `scripts/applications.py verifier`.
- **`scripts/instancier.py`** — sa copie locale `_liens_acceptes()` est retirée au profit de
la fonction partagée. Un seul endroit lit `meta/liens.yml`.
- **`docs/bindings-conception.md`** — la Phase 4 passe à 🟡 : l'éditeur est fait, le
**graphe des liens reste à faire**.
Validé : `make verifier`**rc=0**, `ansible-lint` 0 échec sur 485 fichiers,
`prouver.py --verifier` → CONFORME 16/16, `verifier_gui.py` (node --check) OK,
**DIFF VIDE** conservé et les deux variables de binding du courriel toujours générées
(`serveur_postfix_mailstore_hote`, `serveur_postfix_rspamd_milter`). Aller-retour
d'écriture testé : les liens survivent au cycle chargement → sauvegarde → relecture.
Sept garde-fous du validateur éprouvés (rôle inconnu, cible inexistante, lien vers
soi-même, champ manquant, types invalides).
## 2026-07-21 (suite)
### Modifié
- **Palette aurore enrichie sur les quatre pages `promo/`** — ajout de deux teintes,
`--rose:#ff6fc4` (frange magenta) et `--vert:#5cff9d` (vert fluo), présentes uniquement
dans les **dégradés foncés** : fond fixe du corps, nappe `.aurora` dérivante, et filets
de séparation `.rule`. Opacités de 5,5 % à 8,5 % — une teinte, pas un motif. Les
dégradés de texte et de boutons (`--aurora`) sont **inchangés** : l'identité de marque
ne bouge pas. Appliqué identiquement aux quatre fichiers (0 conflit CSS après coup).
- **Thème Forgejo étendu au wiki** (`roles/serveur_forgejo/files/custom/public/assets/css/alliance.css`,
29 → 118 lignes). La feuille étant injectée par `templates/custom/header.tmpl` sur toutes
les pages, le wiki est couvert **sans feuille distincte**. Réorganisée en deux sections
de risque explicite : **§1 Variables** (surcharge des variables de couleur officielles,
sûr, résiste aux mises à jour) et **§2 Décor** (fond aurore, filet sous les titres,
citations, tableaux et code en ligne de `.markup` — sélecteurs internes, à revérifier à
chaque montée de version majeure). Supprimer §2 ramène au thème sobre d'origine. Le ciel
nocturne ne s'applique qu'aux thèmes sombres, pour ne pas casser le thème clair.
`roles/serveur_forgejo/README.md` documente le tout.
### Ajouté
- **`docs/theme-forgejo-hors-flotte.md`** — pose **manuelle** du thème sur une instance
Forgejo **non gérée par Set-OPS** (la forge historique qui héberge ce dépôt et son wiki).
Forgejo n'ayant aucun réglage web pour le CSS, il faut déposer la feuille et référencer
`header.tmpl` sur le serveur. La procédure **ne duplique aucun fichier** : elle pointe
vers ceux de `roles/serveur_forgejo/files/custom/`. Comprend la détection du répertoire
`custom`, un garde-fou pour ne pas écraser un `header.tmpl` existant (ajout de ligne, pas
remplacement), la vérification par `curl`, et la marche arrière.
### Corrigé
- **État de forge-01 élucidé — ce n'était pas une contradiction.** L'hôte a été **créé,
puis supprimé**. Le plan dit donc `etat: planifie` (état **courant**) et le CHANGELOG du
2026-07-03 dit le branding « prouvé sur forge-01 » (état **passé**) : les deux disent
vrai. `roles/serveur_forgejo/README.md` l'énonce désormais ainsi ; la preuve reste
valable, simplement non rejouable tant que l'hôte n'est pas recréé. (La note précédente,
qui présentait l'écart comme une contradiction à trancher, était erronée.)
- **Teintes rose et verte trop concentrées dans les coins** — nappes élargies d'environ
×2 (780×540 → 1500×1020 px pour le rose, 840×580 → 1620×1120 px pour le vert), ramenées
vers l'intérieur (97 % → 76 %, 3 % → 20 % ; nappe dérivante 95 % → 79 % et 6 % → 22 %),
extinction repoussée de 62 % à 82 % avec un **arrêt intermédiaire** pour une rampe douce,
et flou de `.aurora` porté de 64 px à 86 px. La couleur est répartie au lieu d'être tassée
aux bords.
- **Opacités des teintes réduites d'environ 40 %** dans la foulée — l'étalement les rendait
trop présentes. Rose `.075 → .044`, vert `.058 → .034`, violet `.062 → .036` (arrêts
intermédiaires abaissés dans la même proportion), et opacité de la nappe `.aurora`
`.44 → .32`. Géométrie inchangée : seule l'intensité baisse.
- **Statut du thème précisé, section par section** : §1 (variables) est **éprouvée** sur
forge-01 ; §2 (décor), ajoutée aujourd'hui, n'a **jamais été rendue** par un Forgejo réel.
L'affirmation précédente (« non rendu par un Forgejo réel », tous les cas confondus) était
fausse pour §1.
Validé : équilibre des accolades et parenthèses du CSS, 0 conflit CSS entre les quatre
pages promo, `ansible-lint` 0 échec, `prouver.py --verifier` → CONFORME 16/16.
## 2026-07-21
### Ajouté
- **Protocole d'épreuve de l'opérateur indépendant** (`docs/audit/protocole-operateur-independant.md`).
Met **AFF-002** (« Set-OPS s'exploite entièrement à la main, sans aucune IA ») à l'épreuve
d'un sysadmin qui n'est pas l'auteur, sur sa propre grappe Proxmox, à froid depuis le modèle
public `socle`. Définit : la **règle du silence** (l'observateur journalise, n'aide pas ; trois
niveaux N0/N1/N2), les **interdits** (aucune IA, aucun accès aux dépôts privés, aucune lecture
de `docs/audit/` qui divulguerait les pièges connus), les **critères de réussite R1→R6 fixés
d'avance**, le périmètre matériel et la sécurité, et le **gabarit de rapport**
(`operateur-independant-AAAA-MM-JJ.md`). Le registre peut **perdre** : le protocole prévoit
explicitement la redescente d'AFF-002 en 🟡 ou ❌ selon le verdict.
### Modifié
- **`docs/audit/affirmations.md`** — nouvelle limite consignée : **AFF-002 est ✅ *par
inspection*, pas par démonstration**. Ses écarts bloquants sont soldés, mais aucun opérateur
indépendant ne l'a exécutée ; ✅ y signifie « plus aucun défaut connu ». Renvoi vers le
protocole.
- **`docs/audit/README.md`** — « Les trois pièces » → « Les pièces » : ajout du protocole comme
quatrième pièce du dispositif (l'épreuve humaine, hors harnais automatisable).
Validé : `python3 scripts/prouver.py --verifier`**CONFORME 16/16** (voûte exportée) ;
équilibre des blocs de code du nouveau document vérifié. Aucun code touché.
## 2026-07-20
### Modifié
- **`make verifier` inclut désormais les preuves (`make prouver`).** `make verifier` se
termine par `python3 scripts/prouver.py --verifier` : nouveau mode qui exécute toutes les
preuves du registre (verdict + code de sortie) **sans écrire de rapport**, pour ne pas
écraser la pièce justificative committée `docs/audit/preuve-<date>.md`. `make verifier`
échoue donc si une preuve échoue. `make prouver` seul (sans `--verifier`) continue d'écrire
le rapport horodaté. Vérifié : `make verifier` → CONFORME 16/16 (voûte exportée), aucun
churn du rapport committé ; `make prouver` écrit toujours. Doc mise à jour
(`docs/audit/README.md` § « Rapport avec `make verifier` »).
- **Conformité Phase 2 — 3 incohérences internes corrigées (documentation d'autorité).**
Aucun code Ansible touché ; le code SSH était déjà conforme à `AGENTS.md`.
- **`CLAUDE.md` réduit à un pointeur mince** : autorité unique d'`AGENTS.md` + les cinq
règles absolues (agent unique ; `hosts.yml` généré jamais édité ; aucun secret ;
confirmation des actions destructives ; rien n'est prêt sans validation). Toute la
doctrine dupliquée (template, SSH, pare-feu, cloud-init, handlers, Makefile…) retirée.
- **Contradiction SSH levée** : `CLAUDE.md` prescrivait `PasswordAuthentication yes`
pendant la construction ; le code applique en réalité `PasswordAuthentication no` +
`AuthenticationMethods publickey` dès le départ (prouvé, cf. `docs/audit/affirmations.md`
AFF-037/050). La doctrine SSH périmée de `CLAUDE.md` est supprimée — la duplication en
était la cause racine.
- **`AGENTS.md` : « Comportement attendu de Codex » → « …des agents IA »** (la section
vaut pour tout agent IA, Claude inclus).
- Suivi dans `docs/audit/affirmations.md` (§ Journal des traitements) : AFF-040, AFF-050,
AFF-052 résolus.
### Corrigé
- **Conformité Phase 3 — lot A « doc de démarrage » (le parcours QUICKSTART marche seul).**
- **Modèle absent (AFF-020/021).** `QUICKSTART.md` proposait `cp -r
exemples/modeles/presence-web …` — modèle **inexistant** dans le dépôt public (seul
`socle` l'est). Étape 2 réécrite sur `socle` ; les modèles assemblés renvoyés au dépôt
privé `Set-OPS-modeles`, cohérent avec `exemples/modeles/README.md`.
- **Chemin de voûte faux + piège de shadowing (AFF-023).** Le chemin `lab/` (QUICKSTART,
`exemples/vault.exemple.yml`, `docs/config-proxmox.md`) est corrigé en `production/`
(le modèle `socle` n'a qu'un inventaire `production/` ; `make config` résout
`lab > principal > production`). **Piège corrigé** : le socle livrait ses intrants en
`production/group_vars/all.yml` (forme fichier) ; y ajouter `all/vault.yml` (forme
dossier) fait **ignorer silencieusement** `all.yml` par Ansible (le dossier masque le
fichier — vérifié empiriquement). Le modèle est converti en forme dossier
(`group_vars/all/10-intrants.yml`), alignée sur l'instance prouvée. Validé :
`ansible-inventory --host` charge `domaine_interne` depuis la nouvelle disposition.
- **Commandes `make` périmées (AFF-080/081/082).** `make help``make` ;
`make syntax-template``make syntaxe-modele` (dans `docs/config-proxmox.md`,
`docs/modeles_vm/debian13-proxmox.md`, `docs/MISE-A-JOUR-CODEX-CLAUDE.md`).
- **Découvert et consigné (AFF-097, non traité ici) :** `lab/` codé en dur restant dans
les docs template/clone (`vm-lifecycle`, `procedure-template…`, `proxmox/README`,
message de `cloner_vm_debian.yml`) — lot séparé à prévoir.
- **Conformité Phase 3 — lot B « doc (prose) ».**
- **Prérequis Vault rappelé (AFF-026).** `QUICKSTART.md` : note que `make
inventaire-verifier` / `make verifier` chargent l'inventaire complet et exigent
`ANSIBLE_VAULT_PASSWORD_FILE`, sinon « no vault secrets found ».
- **Sous-dossiers `playbooks/` (AFF-039).** `AGENTS.md` : précisé que seuls `groupes/`,
`maintenance/`, `modeles_vm/`, `proxmox/` sont peuplés ; les autres sont prospectifs.
- **`SOLUTION.md` remis au présent (AFF-060/061).** Bannière « document historique »
(supplanté par README/QUICKSTART/`make`) ; arborescence complétée ; commande de
construction `--ask-pass` (mot de passe) → `make preparer-modele` (accès par clé).
- **Découvert et consigné (AFF-098, traitement B, non traité ici) :** contradiction
fonctionnelle — `make config` écrit le token Proxmox dans `all/vault.yml`, mais
`cloner_vm_debian.yml` ne le lit que depuis `proxmox.vault.yml`/env. Couplé à AFF-097
dans un futur lot « voûte Proxmox ».
### Corrigé
- **Conformité Phase 3 — lot « voûte Proxmox » (AFF-098 bug fonctionnel + AFF-097).**
- **Le clonage lit la voûte unifiée (AFF-098, option a).** `make config` écrit le token
Proxmox dans `instance/inventories/<env>/group_vars/all/vault.yml`, mais
`playbooks/proxmox/cloner_vm_debian.yml` (lancé `-i localhost,`) ne le chargeait que
depuis `proxmox.vault.yml``make creer-vm` échouait l'assert `proxmox_api_token_secret`
pour qui suivait la voûte unifiée. Corrigé : `all/vault.yml` ajouté aux sources de
secrets du playbook (autoritaire) ; la détection de voûte chiffrée du `Makefile`
(`cloner-vm`) cherche d'abord `all/vault.yml` puis `proxmox.vault.yml`. `proxmox.vault.yml`
reste accepté en compatibilité. **Validé** : `--syntax-check` OK, `ansible-lint` 0 échec,
test fonctionnel (token chargé depuis `all/vault.yml`, assert vert).
- **Docs Proxmox/template alignées (AFF-097).** `lab/` codé en dur → `production/` + voûte
unifiée dans `playbooks/proxmox/README.md`, `docs/procedure-template-debian13-proxmox.md`,
`docs/vm-lifecycle.md`, `docs/modeles_vm/debian13-proxmox.md`. Plus aucune référence
`inventories/lab/group_vars` dans les fichiers suivis.
- **Conformité Phase 3 — lot C « `make verifier` vert » (AFF-006).** `ansible-lint` passe de
**33 échecs à 0** (profil `min``production`), donc `make lint` **rc=0**. Trois causes :
- **`site.yml` généré lint-propre** : `scripts/orchestrer.py` émet un `name:` avant chaque
`import_playbook` (30× `name[play]`) ; `site.yml` régénéré. Orchestration inchangée.
- **`risky-shell-pipe`** : `set -o pipefail` + `executable: /bin/bash` sur les deux tâches
shell à pipe de `playbooks/valider.yml`.
- **`name[template]`** : Jinja déplacé en fin de `name` dans `supprimer_vm_debian.yml`.
**Validé** : `make lint` rc=0 ; toutes les étapes de `make verifier` vertes (lint, test,
site-verifier, flux-verifier, syntaxe) — seule `inventaire-verifier` requiert la voûte de
l'opérateur (prérequis documenté, AFF-026). Débloque AFF-002 (« exploitable sans IA » → ✅).
### Corrigé
- **Conformité Phase 5 — boucle documentaire (le parcours QUICKSTART marche seul, prouvé).**
Relecture de cohérence bout-en-bout (README/QUICKSTART/docs/wiki ↔ code final). Deux bogues
du modèle public/outillage débusqués et corrigés, en plus des alignements de prose :
- **Modèle `socle` invalide (AFF-099).** `exemples/modeles/socle/plan/domaines.yml` :
`autorite: interne` (périmé, rejeté par le validateur) → `auto-heberge`. Le modèle valide.
- **Split-brain d'inventaire (AFF-100).** `scripts/instancier.py` et `scripts/inventory_gui.py`
retombaient sur `principal/` quand aucun `hosts.yml` n'existe encore ; or le socle est en
`production/` → la 1ʳᵉ génération écrivait dans `principal/`, à côté des `group_vars` restés
en `production/`. Corrigé : le repli vise le **répertoire d'inventaire déjà présent** (comme
`config_proxmox.py`). Instances existantes (avec `hosts.yml`) **inchangées** (non-régression
vérifiée sur `principal`).
- **Alignements de prose.** `docs/intrants-communs.md` (`group_vars/all.yml` →
`all/10-intrants.yml`, forme dossier que le GUI écrit déjà) ; `QUICKSTART.md` étape 8
(`serveur_postgresql` → `serveur_powerdns`, groupe présent dans le socle).
- **Preuve** : parcours QUICKSTART rejoué **hors-ligne** sur une copie du socle
(`instancier generer`→`comparer`→`appliquer`) — écrit `production/hosts.yml`, diff **vide**
ensuite, 4 validateurs verts. Nouvelle preuve récurrente **P15** dans `make prouver`
(« modèle public socle valide ») : `make prouver` = **15 OK, 0 échec, 1 sautée**.
### Ajouté
- **Harnais de preuve `make prouver` (Phase 4).** Nouveau `scripts/prouver.py` — un
**orchestrateur mince** qui rejoue les preuves automatisables du registre en appelant
l'outillage **existant** (les mêmes scripts que `make verifier` : lint, tests, diff-vide,
validateurs de registres, cohérence groupes/playbooks, handlers, orchestration, flux,
syntaxe, existence des runbooks, invariants structurels) — **aucune validation
réimplémentée**. Produit `docs/audit/preuve-AAAA-MM-JJ.md` : rapport **horodaté,
rejouable**, reliant chaque preuve aux affirmations couvertes, listant à part les
déclarations d'intention (⚪). Sort en erreur si une preuve échoue ; la preuve `P15`
(inventaire Ansible complet) est **sautée** proprement sans mot de passe Vault (prérequis
AFF-026). Documenté dans `README.md` (une phrase) et `docs/audit/README.md` (mode
d'emploi complet + comment ajouter une preuve). `affirmations.md` : section « Couverture
par `make prouver` » reliant chaque ✅ à sa preuve. **1re exécution : 14 preuves OK,
0 échec, 1 sautée → CONFORME.**
- **Registre des affirmations (audit de conformité, Phase 1).** Nouveau
`docs/audit/affirmations.md` : chaque affirmation publique vérifiable du dépôt
(`README`, `AGENTS`, `CLAUDE`, `QUICKSTART`, `SOLUTION`, `docs/`, `wiki/`, aide du
`Makefile`, GUI) est tracée vers une **commande de preuve reproductible** et un statut
(✅ prouvée / 🟡 partielle / ❌ fausse / ⚪ invérifiable localement). **54 affirmations
enregistrées : 30 ✅, 13 🟡, 8 ❌, 3 ⚪.** Audit **sans aucun correctif** (les
traitements relèvent des phases suivantes). Preuves exécutées localement, hors
production : diff-vide du plan, recoupement notify↔handlers, validateurs de registres
(serveurs/applications/bases/domaines/GUI/orchestrateur/flux), `--syntax-check` de tous
les playbooks, test unitaire. Dix écarts majeurs classés par risque pour un opérateur
suivant la doc à la lettre — dont : `QUICKSTART` renvoie à un modèle absent
(`presence-web`), `make verifier` échoue (ansible-lint : 33 failures), contradiction
SSH `CLAUDE.md` ↔ code/`AGENTS.md`, chemin de voûte faux, commandes `make` périmées
dans `docs/`.
## 2026-07-07 (soir)
### Modifié
- **Nomenclature `ip-miroir` : longueurs UNIFORMES.** Le VLAN dérivé passe à
`1000 + index×10 + zone`**toujours 4 chiffres** (1011..4094), donc le VMID
(`VLAN·octet·seq`) fait **toujours 9 chiffres**. Longueurs uniformes et mnémotechniques
(retirer 1000 redonne `index×10+zone`). Les deux tenants sont **convergés** sur le réseau
fédéré 10/8 : Chezlepro (idx 1) → VLANs 1011-1016 / 10.11.x ; Technolibre (idx 2) →
VLANs 1021-1026 / 10.12.x. Chezlepro quitte le bac-à-sable plat 192.168.15/VLAN 15.
### Ajouté
- **`make devis-reseau` : config switch dérivée du plan.** Nouveau `scripts/devis_reseau.py`
qui découvre les instances fédérées (`../*/plan/nomenclature.yml`, schéma `ip-miroir`) et
émet la config Cisco-like du réseau convergé — **VLANs + SVIs (passerelles) + ACLs d'isolation
inter-tenant** — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec
le plan). One-shot précédemment ; maintenant reproductible.
- **GUI : vue « Réseau » (devis switch) + bouton Copier.** Nouvel onglet lecture seule qui
affiche le devis `make devis-reseau` (VLANs + SVIs + ACLs), via `GET /api/devis-reseau`, avec
un bouton **Copier** (prêt à coller sur le switch). Un opérateur génère et transmet la config
sans CLI.
- **GUI : erreurs de déploiement en LANGAGE CLAIR.** Quand une action (créer/vérifier/déployer)
échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur —
**étape · hôte · type · message clair** — avec un bouton **« Copier »** (pour transmettre le
résumé à un humain / au mainteneur). Le backend (`_extraire_echec`) parse les `fatal:`/`UNREACHABLE!`,
privilégie la vraie cause (`stderr` plutôt que le générique « non-zero return code »), gère
`no_log` (« sortie masquée — secret ») et les injoignables. Éprouvé sur les 4 échecs réels du
from-zero du jour. `node --check` OK.
- **GUI : éditeur « Domaines » (6ᵉ onglet éditable).** Le registre des domaines publics — le seul
sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, **autorité**
(sélecteur `primaire-cache`/`auto-heberge`/`delegue`), **edge**, **DNSSEC**, **mail**, secondaires,
et affichage des **expositions (FQDN) sous la zone**. Backend : route `/api/domaines`,
`ecrire_domaines`, validation `valider_domaines` (rejette une autorité inconnue). Éprouvé :
round-trip backend OK, `node --check` OK, smoke serveur OK. (Le `autorite: interne` périmé des
instances Chezlepro/Technolibre a été corrigé en `auto-heberge` au passage.)
- **GUI : persistance des journaux d'exécution.** Chaque action lancée depuis l'interface
(créer-vm / vérifier / déployer) écrit désormais, **en plus du streaming live**, un journal
horodaté `<instance>/logs/<hôte>-<action>-<date>.log` (gitignoré). Bonus : si le navigateur se
déconnecte, l'exécution **continue** et le journal est capturé jusqu'au bout (au lieu d'être
interrompue) — utile pour un déploiement qu'on ne veut pas voir avorter à la fermeture d'un onglet.
- **GUI : vues « Flux » et « Couches » (lecture seule).** Deux nouveaux onglets rendent visibles
dans l'interface deux registres jusque-là en ligne de commande seulement : **Flux** = la matrice
d'audit réseau (rôle · sens · port · pair · chiffrement coloré · raison, depuis les `meta/flux.yml`),
et **Couches** = l'ordre de reconstruction (socle → pki → services → apps → agents, avec les groupes
triés topologiquement par couche). Alimentées par `resoudre_flux`/`orchestrer` via l'API. Éprouvé :
`node --check` OK, smoke test serveur (63 flux, 6 couches servis).
### Modifié
- **GUI : intégration des intrants de session.** Le panneau « Intrants de base » expose désormais
**`nftables_admin_ssh`** (type liste, section « Sécurité ») — la garde anti-lockout du pare-feu
était éditable en fichier mais absente du GUI. Retrait de `vault_step_ca_fingerprint` des secrets
attendus (empreinte du root CA désormais dérivée dynamiquement, plus un secret). `node --check` OK.
## 2026-07-07 — Reconstruction from-zero PROUVÉE (preuve de portabilité « sans réserve »)
Les 14 VM du lab (+ sauvegardes + AC) supprimées, puis `make myDay` a reconstruit l'écosystème
POC **de rien** : 13 hôtes déployés (0 échec), et `make valider` **entièrement vert** (Prometheus
6/6 UP, 6 vhosts HTTPS, courriel bout-en-bout remis, 6 dépôts restic restaurés) — le tout sous
pare-feu actif. 4 bugs de portabilité débusqués et corrigés dans le moteur :
### Corrigé
- **client_pki : empreinte du root CA dérivée dynamiquement** (au lieu d'une valeur figée en Vault).
Une AC régénérée (from-zero) a une empreinte neuve ; le rôle la lit désormais de l'autorité
elle-même (`step certificate fingerprint`, délégué au nœud step-ca), source de vérité.
`client_pki_ca_fingerprint_override` permet un épinglage explicite.
- **serveur_keycloak : assignation de rôle tolérante aux utilisateurs absents.** Sur un annuaire
vide (from-zero), assigner un rôle à un user inexistant échouait ; on vérifie désormais son
existence (Keycloak fédère LDAP à la demande) et on saute proprement sinon.
- **serveur_prometheus : ne scrute que les hôtes ACTIFS** (`client_metrique ∩ hotes_actifs`). Un
hôte planifié (non déployé) n'est plus une cible morte.
- **Pare-feu nftables compatible Docker.** Le ruleset résolu (a) remplace **uniquement la table
`setops_flux`** au lieu de `flush ruleset` (préserve les tables Docker : DNAT/forward des
conteneurs) et (b) autorise `docker0` + `ct established,related` dans la chaîne `forward`. Sans
ça, `forward policy drop` coupait Collabora (conteneur). Diagnostic prouvé au niveau paquet.
### Ajouté
- **`playbooks/proxmox/supprimer_vm_debian.yml`** — suppression de VM par VMID (from-zero), avec
garde-fous : n'agit que sur les VMID présents dans le cluster, refuse si le modèle est ciblé,
secrets `no_log`.
## 2026-07-07
### Ajouté
- **`make valider` — recette d'acceptation fonctionnelle (phase 4, v1).** Vérifie que les services
*fonctionnent*, pas juste qu'ils sont déployés (complète les `*-verifier` statiques). `playbooks/valider.yml`,
lecture seule : **cibles Prometheus toutes UP** (API `/targets`) + **vhosts HTTPS exposés répondent**
(dérivés des `server_name` réels de l'edge, filtrés sur `domaine_interne` — générique) + **courriel
bout-en-bout** (envoi via le MTA → LDAP → LMTP → Maildir, remise vérifiée par `doveadm` sur le
compte `testmail`, message de test nettoyé) + **restauration de sauvegarde** (pour chaque nœud
`client_backup` : restic restaure le dernier snapshot dans un dossier temporaire — lecture seule
sur le dépôt — et vérifie que des fichiers en sortent). Éprouvé sur la flotte vivante sous pare-feu
actif : 7 cibles UP, 8 vhosts OK, courriel remis, 6 dépôts restaurables. **La recette a trouvé un
vrai trou** (collab-01/Nextcloud sans `client_backup_jobs` → service de sauvegarde en échec, fichiers
non protégés), corrigé côté instance.
- **Pare-feu nftables activé sur TOUTE la flotte (14 nœuds, activation prudente).** Les 14 hôtes
actifs tournent sous `policy drop` avec leur ruleset **résolu moindre-privilège** (flux est-ouest
déclarés autorisés par source, reste refusé). Vérifié en conditions réelles : flux déclarés OPEN
(keycloak→pg, postfix→dovecot LMTP, prometheus→node_exporter…), flux non déclaré DROP
(forge→redis), 14/14 `active`+`enabled`+`policy drop`, contrôleur toujours joignable.
- **Intrant `nftables_admin_ssh`** (garde anti-lockout) — CIDR d'administration TOUJOURS
autorisés en SSH, indépendamment des flux. Le résolveur (`resoudre_flux.py`) l'injecte en tête
de chaque ruleset. **Bug de conception rattrapé avant activation** : le contrôleur Ansible arrive
par VPN (`192.168.255.2`, hors sous-réseau flotte) — sans cette règle, activer = lockout immédiat.
- **Rollout prudent** : d'abord `infra-pki-01` seul (test dead-man switch `systemd-run`, SSH
re-vérifié sous drop, puis permanent), puis les 13 autres par lot (dead-man 5 min + sonde des
flux est-ouest avant de persister). Activation pilotée par `nftables_baseline_enabled: true`
(group_vars `hotes_actifs` ; le golden template n'y est pas → reste sans pare-feu, voulu).
- Le déploiement dépose `instance/flux-genere/<hôte>.nft` dans `/etc/nftables.conf` + service
`enabled` (survit reboot ET futurs `make myDay`).
### Corrigé
- **Détection du coffre Vault : `production/` codé en dur → inventaire réel.** `deployer`,
`deployer-tout` et `verifier-deploiement` cherchaient le coffre chiffré dans
`inventories/production/group_vars`, alors qu'une instance en `principal/` (cas courant) n'a pas
ce chemin → l'invite du mot de passe Vault ne se déclenchait jamais et le déploiement échouait au
déchiffrement. Corrigé : la garde vise désormais le `group_vars` de l'inventaire **résolu**
(`$(dir $(INVENTAIRE_PRODUCTION))group_vars`). Vérifié : le coffre de `principal/` est bien détecté.
### Modifié
- **`nftables_baseline` branché sur les flux résolus (reconstruction, phase 0).** Le rôle déploie
désormais le ruleset **résolu** généré par `make flux` (`instance/flux-genere/<hôte>.nft` — règles
par source, `ip saddr` = moindre privilège) quand il est présent ; sinon repli sur le gabarit plat.
**Toujours `nftables_baseline_enabled: false` par défaut → aucune activation** (l'activation reste
un geste dédié, testé par nœud). Nouveau var `nftables_baseline_ruleset_genere`. Syntax-check OK.
### Ajouté
- **Reconstruction from-zero en une commande (reconstruction, phase 3 — outillage).** La création
de VM était unitaire (`creer-vm HOTE=…`) ; on comble le trou entre *créer* (2a) et *configurer*
(2b) :
- **`make flotte-creer CONFIRMER=true`** — boucle `creer-vm` sur tous les hôtes actifs du plan
(clone Proxmox). Nouvelle sous-commande `inventory_host.py lister-actifs`.
- **`make reconstruire CONFIRMER=true`** — enchaîne **flotte-creer → attente SSH de la flotte
(`_attendre-flotte`, `ATTENTE_MAX` réglable) → `deployer-tout`**. La reconstruction complète en
une commande, **idempotente de bout en bout** : le clone (`proxmox_kvm`) saute une VM déjà
présente (par nom), le réseau/disque sont `present`/`resized` (grow-only), le déploiement Ansible
converge. Re-lançable sans risque, qu'il reste des VM ou non.
- **`make myDay`** repointé sur `reconstruire` (le vrai « bouton rouge » ; n'était qu'un alias de
`deployer-tout`). Distinction assumée : `deployer-tout` = **converger** la config d'une flotte
existante (2b, avec `MODE_CHECK=1`) ; `reconstruire`/`myDay` = **créer les VM manquantes puis
déployer** (2a+2b).
Gardes `CONFIRMER=true` sur les trois. Non testé contre Proxmox/lab (validé : énumération des 14
hôtes actifs, refus sans `CONFIRMER`, enchaînement `make -n`).
## 2026-07-06
### Ajouté
- **`make wiki-publier` — fin du dernier geste manuel (reconstruction, phase 0).** Le wiki
pédagogique (`wiki/`, source versionnée) se publie désormais dans le wiki Forgejo par
`make wiki-publier WIKI_REMOTE=…<dépôt>.wiki.git` : clone superficiel du wiki, synchronisation des
pages (`wiki/*.md` sauf `README.md` ; suppressions propagées), commit + push seulement s'il y a du
changement. Refuse sans `WIKI_REMOTE`. Éprouvé de bout en bout contre un dépôt bare local (17
pages publiées = 17 source, diff vide, README exclu, `_Sidebar` inclus, idempotent au 2e passage).
- **Registre des flux réseau complété (reconstruction, phase 0).** Transcription du travail
zéro-confiance est-ouest dans `meta/flux.yml` : **16 rôles remplis** (step_ca, openldap, powerdns,
prometheus, loki, redis, rspamd, backup, dovecot, postfix, keycloak, forgejo, grafana, icinga,
icingaweb2, nextcloud, oauth2_proxy + le socle `serveur_debian` pour le plan de gestion SSH, +
les 5 clients pki/journal/smtp/backup/unbound). Le registre couvre désormais **29 rôles, 63 flux**
(qui-parle-à-qui : port, sens, pair, chiffrement, raison) — la base de génération nftables/OPNsense
et la matrice d'audit. Schéma enrichi (`docs/flux-conception.md`) : valeurs `ssh` (transport SSH,
restic/backup + SSH de gestion) et `tls-cible` (TLS visé, feuille de route edge→backends).
**Point critique traité** : SSH (22) déclaré au socle, sinon les nftables générés couperaient
l'accès Ansible. Validé : schéma conforme (0 erreur) et **matrice cohérente** (tout egress vers un
service a l'ingress correspondant en face). Reste phase 0 : la cible `make wiki-publier`.
- **Résolveur de flux (reconstruction, phase 0 — §Séquence 2).** `scripts/resoudre_flux.py` agrège
les `meta/flux.yml`, résout les `pair`, et produit **deux artefacts, hors-ligne, sans activation** :
- **`docs/registre-flux.md`** (généré) — la matrice d'audit source→destination (rôle, sens, port,
chiffrement, raison) + synthèse chiffrement. Artefact du label de certification.
- **aperçus nftables par hôte** (`instance/flux-genere/<hôte>.nft`, gitignorés) — règles résolues
avec IP réelles, `ip saddr` = **moindre privilège** (ex. LMTP 24 sur le mail n'accepte que l'IP
du nœud Postfix), `policy drop`. **Aperçus inspectables, NON activés** (l'activation reste un
geste dédié testé par nœud, cf. flux-conception §Activation prudente).
- Cibles **`make flux`** (registre + aperçus) et **`make flux-verifier`** (schéma + matrice,
branché dans `make verifier`). Validé : 29 rôles / 63 flux cohérents, 14 aperçus générés.
Reste : brancher `nftables_baseline` (modèle plat aujourd'hui) sur ces aperçus, et le test lab.
- **Orchestrateur ordonné (reconstruction, phase 2).** `playbooks/site.yml` n'est plus un stub :
c'est désormais un **point d'entrée ordonné généré**, qui déploie l'écosystème **couche par
couche, dans l'ordre de reconstruction**, sans intervention manuelle. Nouveautés :
- **`docs/couches-deploiement.yml`** — registre central des couches ordonnées
(`socle → pki_racine → pki_client → services → apps → agents`), les 30 groupes déployables classés.
- **`scripts/orchestrer.py`** — trie les groupes par couche (clé primaire) puis **topologiquement
intra-couche** via `dependances-groupes.yml` (ex. dovecot avant postfix, icingaweb2 après icinga,
nextcloud en dernier). Génère `site.yml` comme une séquence d'`import_playbook`. Artefact du
moteur (déterministe, sans donnée d'instance ; Ansible saute les groupes sans hôte actif).
**Deux gardes anti-dérive** (refus si violé) : bijection univers↔couches (un nouveau rôle non
classé casse la génération) et aucune arête « en arrière » (un prérequis dans une couche plus
tardive = classification fausse). Éprouvées par test négatif.
- **`make site`** (régénère + syntax-check), **`make site-verifier`** (cohérence, branché dans
`make verifier`), **`make deployer-tout CONFIRMER=true`** (déploiement orchestré de la flotte,
limité à `hotes_actifs` ; garde `CONFIRMER` car action impactante ; `MODE_CHECK=1` pour l'essai
idempotent à blanc). Validé : `verifier` OK (30 groupes, aucun cycle/arête arrière),
`--syntax-check` du `site.yml` généré OK, refus `deployer-tout` sans `CONFIRMER` (rc=2).
### Modifié
- **Audit exhaustif du codé-en-dur (reconstruction, phase 1b).** Balayage complet
`tasks + templates + defaults` de tous les rôles (noms de tenant, IP, domaines, emails, orgs).
Résultat : le moteur ne porte plus **aucun** nom de tenant en dur. Corrigé — les labels/slug
OIDC dérivent désormais de l'intrant `organisation` : `serveur_forgejo_oidc_nom` (slug de
callback, `organisation | lower | replace(' ','-')`), `serveur_grafana_oidc_nom`,
`serveur_nextcloud_oidc_nom`, `serveur_nextcloud_theme_nom` (labels d'affichage). Commentaires
« Se connecter avec Chezlepro » → génériques. Non-régression SSO : `organisation: Chezlepro`
slug `chezlepro`, **identique** à l'URI de redirection Keycloak de l'instance (pas de casse).
Conservés intentionnellement : realm `default('chezlepro')` (décision `identite_realm` actée) et
le thème visuel **Alliance Boréale** (identité par défaut assumée du réseau, pas un tenant).
Validé : re-balayage vide, rendu Jinja du slug testé (Chezlepro/Alliance Boréale/Ma Coop),
`--syntax-check` OK (playbook forgejo via inventaire `principal`).
- **Audit du graphe de dépendances (reconstruction, phase 1a).** `docs/dependances-groupes.yml`
gagne les prérequis inter-groupes confirmés dans le code, en vue de l'orchestrateur trié en
topologie. Ajouts : `serveur_keycloak`**`serveur_openldap`** (fédération LDAP via
`resoudre_annuaire_uri`, en plus de PostgreSQL) ; **`serveur_dovecot`** → `serveur_openldap`
(userdb/passdb LDAP) ; **`serveur_postfix`** → `serveur_dovecot` (remise LMTP au mailstore) ;
**`serveur_icingaweb2`** → `serveur_icinga` + `serveur_postgresql` + `serveur_openldap`
(IcingaDB + auth LDAP) ; **`serveur_nextcloud`** → `serveur_postgresql` + `serveur_keycloak`
(OIDC) + `serveur_collabora` (validation WOPI). Réconciliation `meta/liens.yml` : le seul lien
structurel (`serveur_postfix` mailstore → Dovecot) coïncide avec le graphe. **Conclusion d'archi :**
la règle « TLS vérifié ⇒ `client_pki` aux deux bouts » ne devient PAS des arêtes par-groupe
(client_pki est quasi universel) — c'est une **couche** de l'ordre de reconstruction
(`socle → step_ca → client_pki → services → apps → agents`) ; `dependances-groupes.yml` ne
capture que le fin ordonnancement intra-couche. Validé : YAML conforme, **aucun cycle**, tri-topo
réussi (19 nœuds), chargeur `charger_dependances` accepte (12 groupes, `est_groupe_operationnel`
OK), tous les groupes ont un rôle.
## 2026-07-05
### Modifié
- **VLAN dérivé du tenant (réseau convergé).** `deriver_nomenclature` (schéma `ip-miroir`) dérive
désormais le VLAN = `index × 10 + zone`**unique globalement** sur un trunk convergé (chaque
tenant son bloc de 10 ; 1-9 réservés à l'infra partagée). Le VLAN contenant déjà le tenant (1er
chiffre = index), le VMID **mène avec le VLAN** (`VLAN·octet·seq`, ≤ 9 chiffres Proxmox). Ex.
Technolibre (index 2) → VLANs 21-26, VMID 2101101. Le schéma `compact` (lab, sandbox) reste
inchangé. Champs `vlan:` codés en dur retirés des catégories ip-miroir (désormais dérivés).
### Ajouté
- **Zéro-confiance est-ouest — flux Métriques, Logs et Courriel chiffrés.** Suite du chantier
(après PostgreSQL) : **métriques** (node_exporter sert en HTTPS via cert step-ca +
`--web.config.file` + cert-sync owned prometheus ; Prometheus scrape `scheme: https` +
`tls_config`), **logs** (Loki `http_tls_config` + cert-sync owned loki ; Alloy push `https` +
`tls_config`), **courriel** (LMTP `edge-mta→infra-mail:24` en `lmtp_tls_security_level=verify` +
`lmtp_tls_CAfile` ; `client_smtp` en STARTTLS vérifié). Chacun prouvé de bout en bout (200 HTTPS,
cibles UP, livraison `status=sent`, HTTP rejeté). Motif cert-sync `.path` industrialisé. Reste :
edge→backends + DNS (DoT).
- **VMID 9 chiffres mnémotechnique (schéma `ip-miroir`, opt-in).** `vmid_schema: ip-miroir` dans la
nomenclature → VMID `I·VVV·HHH·NN` (index·VLAN·octet-hôte·séquence) : le VMID *contient* l'IP
(`10.(10+index).VLAN.hôte`) + le tenant, lisible d'un coup d'œil. Défaut `compact` rétro-compatible
(instances déployées inchangées).
- **Instance partenaire Technolibre.** Écosystème complet (12 VM, `etat: planifie`) dans
`10.12.16.0/20`, **6 zones de sécurité** (Frontière/Identité/Données/Services-infra/Observabilité/
Applications, un /24 + VLAN chacune), index de fédération 2, VMID ip-miroir. Preuve de portabilité
d'un tenant.
### Modifié
- **Références par FQDN partout (fin des IP codées en dur).** Décision d'archi : FQDN pour toute
référence inter-services (non ambigu en fédération, canonique pour TLS ; nom court = hostname OS).
`resoudre_base` renvoie le FQDN (→ keycloak/forgejo/icinga) ; `client_journal_loki_url` dérivé du
groupe `serveur_loki` ; defaults db_host IP morts nettoyés. **Aucune IP littérale dans les defaults.**
- **Découplage du tenant d'origine.** Realm SSO centralisé sur l'intrant **`identite_realm`** (défaut
`chezlepro` ; les 4 rôles keycloak/forgejo/grafana/oauth2_proxy en dérivent ; exposé dans la GUI).
Vars brandées renommées génériques : `chezlepro_timezone→fuseau_horaire`,
`chezlepro_organisation→organisation`. Le moteur ne porte plus le nom d'un tenant.
- **Modèles d'instance rafraîchis.** `integral` régénéré depuis le cas prouvé (6 zones, fonctions
éprouvées `data-sql`/`id-ldap`/`id-sso`/`sup`, ip-miroir, nouveaux intrants) ; `socle`/`identite`/
`observabilite`/`forge` réalignés sur le même moule (prouvés : dérivation + `valider_serveurs`).
`presence-web` marqué **aspirationnel** (rôles web-frontal/dorsal absents) plutôt que faussement prêt.
## 2026-07-04
### Ajouté
- **Zéro-confiance : flux PostgreSQL entièrement chiffré et vérifié.** PG sert désormais son **cert
step-ca** (vérifiable contre root_ca) au lieu du snakeoil, et **refuse** toute connexion non-TLS
du réseau (`hostssl` dans pg_hba). Les 3 clients passent en **verify-full** : keycloak
(`db-url-properties sslmode=verify-full`), forgejo (`SSL_MODE=verify-full` + `PGSSLROOTCERT`),
IcingaDB (`tls: true` + `ca`). Prérequis posés : `client_pki` sur data-sql-01 (cert), et
**`root_ca.crt` en 0644** (cert public, requis par les clients TLS non-root). cert-sync PG
(motif `.path`, owned postgres) + reload de l'instance `postgresql@NN-main`. Vars :
`serveur_postgresql_tls_actif`/`_tls_force`, `serveur_*_db_sslmode`/`_ca`. Prouvé de bout en bout
(cert Set-OPS CA servi, apps 200, non-TLS rejeté « aucun chiffrement », TLS accepté).
### Corrigé
- **PG : détection de version robuste (collision avec un répertoire non-numérique).** Placer le
`tls_dir` sous `/etc/postgresql/` faisait choisir `tls` comme « version » de cluster
(`find | sort | last`) → configs déployées au mauvais endroit (verrou hostssl inopérant).
Corrigé : détection filtrée aux dossiers **numériques** (`^[0-9]+$`) + `tls_dir` déplacé sous
`/var/lib/postgresql/tls`.
- **Renouvellement de cert : recharger le VRAI consommateur (bug latent de flotte).** Le
`cert-renewer@.service` (client_pki) renouvelait le cert sur disque mais son `ExecStartPost`
rechargeait un service nommé *d'après le cert* (`%i` = FQDN), inexistant → **nginx (et postfix,
dovecot, slapd) n'étaient jamais rechargés** et servaient l'**ancien cert jusqu'à expiration**.
Symptôme vécu : cert edge expiré en mémoire (renouvelé sur disque), échec TLS de l'échange
code→jeton OIDC → **login Grafana/SSO cassé** (tous les services derrière l'edge). Correctif :
`client_pki_reload_services` (liste des vrais consommateurs), câblée par groupe (edge→nginx,
mail→postfix/dovecot, annuaire→slapd). Appliqué + vérifié sur les 4 hôtes. Fix immédiat de
l'incident : `systemctl reload nginx` sur l'edge.
### Ajouté
- **Doc à jour : unité wiki « Autorisation & RBAC », leçon renouvellement, runbooks.** Fermeture
des dettes de doc : nouvelle **unité wiki authZ/RBAC** (pendant d'*Identité & SSO*, avec l'exemple
Grafana), **section « le renouvellement est un système »** versée dans l'unité *PKI* (comparer cert
servi vs fichier ; recharger le consommateur), et **`docs/runbooks-exploitation.md`** (cert expiré,
RBAC Grafana, branding Forgejo). 15 unités wiki désormais.
- **UI des logs Loki : dashboard Grafana provisionné.** Loki n'a pas d'UI ; son UI est Grafana.
Ajout d'un **dashboard « Journaux de la flotte »** (dossier Set-OPS) : sélecteur d'hôte multi +
filtre regex insensible à la casse + panneau logs + débit par hôte. Référence Loki par une
**variable de datasource** (`ds_loki`), pas un UID codé en dur (leçon : ajouter un uid explicite à
une datasource déjà provisionnée casse le démarrage de Grafana). **Visible par les Viewers** (dont
testmail) sans accès Explore. Prouvé sur obs-01 (dashboard chargé, Grafana actif).
- **RBAC via SSO : rôle de realm → niveau Grafana.** Machinerie *additive et idempotente* dans
`serveur_keycloak` (`rbac-oidc.yml`) : rôles de realm (`serveur_keycloak_realm_roles`), **mapper
`roles`** sur les clients choisis (`_role_mapper_clients`, rôles de realm → claim `roles` dans
ID token + userinfo), **assignations** rôle→utilisateur (`_role_assignments`). Côté Grafana,
`role_attribute_path` (`grafana-admin→Admin`, `grafana-editor→Editor`, sinon Viewer). kcadm **à
chaud, zéro coupure SSO**. Prouvé (idempotence `changed=0`) sur id-sso-01 : rôles créés, mapper
présent, `testmail` = `grafana-editor` (→ Explore). Illustre l'**authZ** (vs authN du SSO).
## 2026-07-03
### Ajouté
- **Identité visuelle Alliance Boréale sur Forgejo (léger, officiel).** Branding via le dossier
**`custom/`** de Forgejo (mécanisme *officiel* — pas de fork, résistant aux MAJ) : accent **aurore
par variables CSS** (`--color-primary`…, aucune classe interne touchée), **logo/favicon étoile**
(réutilisés du thème Keycloak), **page d'accueil brandée** (`home.tmpl` : hero aurore + accroche),
**thème sombre** par défaut, **nom + méta**. Codifié dans `serveur_forgejo`
(`serveur_forgejo_branding`, `_app_name`, `_theme`), déployé dans `{{ data }}/custom/`. **Prouvé**
sur forge-01 : accueil rend (200, « Forge Chezlepro »), `alliance.css` servi (cyan aurore), lint OK.
- **Wiki pédagogique Forgejo — 14 unités d'apprentissage.** Set-OPS comme *compagnon pédagogique* :
chaque service = une lentille sur un fondamental TIC, méthodes génériques (on apprend OIDC, pas
Keycloak). Source versionnée dans `wiki/`, publiée dans le wiki Forgejo (`eregion`). Moule à 4
temps (concept → Set-OPS → transférable → à toi de jouer, avec *casse-répare*).
- **GUI : boutons cohérents.** « Pousser » (surchargé : clonait *et* déployait) → « 🖥 Créer la VM »
pour le clone, « Déployer » partout pour le déploiement, dry-run obligatoire partout. GUI 100 %
française.
- **Agents d'observabilité/ops éprouvés — observabilité *flotte-complète*.** Les 3 intégrations
« agent » (liaisons nœud × optionnelles) déployées sur 4 nœuds (obs-01, data-sql-01, id-ldap-01,
forge-01) et **prouvées** : `client_metrique` (node_exporter → **Prometheus scrape les 4 cibles,
toutes UP**) ; `client_journal` (journald → **Loki reçoit les logs des 4 nœuds**) ; `client_smtp`
(msmtp → **courriel système d'un nœud relayé par l'edge-MTA et livré**). Comble le trou : les
*serveurs* d'observabilité (Grafana/Prometheus/Loki) étaient prouvés, mais pas la collecte
fleet-wide — désormais Grafana voit toute la flotte. Cibles corrigées (bac à sable) : Loki→obs-01,
relais→edge-mta-01 (pas infra-mail-01, qui est le store Dovecot sans SMTP :25).
- **Thème de connexion Keycloak à l'identité Alliance Boréale.** Thème de login `alliance-boreale`
(`roles/serveur_keycloak/files/themes/`, `parent=keycloak` + overlay CSS) reprenant l'identité du
site de l'Alliance (extraite de `site-alliance-boreale`) : **ciel nocturne aurore** (`#05060f`/
`#0a0d24` + dégradés), **carte glassmorphism**, **logo étoile aurore** (le `favicon.svg` du site),
**bouton dégradé aurore** (teal→cyan, pilule), liens cyan, **police système** (souveraineté, zéro
dépendance externe). Déployé dans `{{ keycloak_home }}/themes/`, appliqué au realm via
`kcadm ... -s loginTheme` (var `serveur_keycloak_login_theme`, idempotent), Keycloak rechargé
(`flush_handlers` avant la config realm). **Prouvé** : la page de login charge `alliance.css`
(HTTP 200) + le `logo.svg` (200), `loginTheme=alliance-boreale` actif sur `chezlepro`.
**Constellation animée en fond** (`scripts=js/constellation.js`) : le JS crée son propre ciel
(canvas + aurore, le template n'en ayant pas) — étoiles scintillantes qui dérivent, liens de
constellation cyan, blob d'aurore ondulant ; respecte `prefers-reduced-motion`. Prouvé :
`constellation.js` référencé + servi (200).
**Console de compte thémée aussi** (thème `account`, `parent=keycloak.v3`) : overlay CSS
surchargeant les variables PatternFly 5 (fond aurore, cartes en verre, accent aurore) + la même
constellation animée. Var `serveur_keycloak_account_theme` via `kcadm -s accountTheme`. Prouvé :
console charge (HTTP 200, `keycloak.v3` intact), `account.css` servi (200).
- **Soumission courriel `:587` interne (authentifiée) — la boucle souveraine est bouclée.**
Postfix (`edge-mta`) sert la **soumission `:587`** (bloc `master.cf` : STARTTLS requis, `SMTP AUTH`,
seuls les authentifiés relaient) ; l'auth SASL est **déléguée à Dovecot** (`infra-mail`, passdb LDAP
prouvé) via un **auth-listener réseau** (`service auth { inet_listener sasl }`, port 12345). Aucune
sortie internet : interne→interne uniquement (l'externe = déliverabilité, Étape B). **Prouvé** (swaks) :
`testmail` s'authentifie (`235 Authentication successful`), Postfix accepte (`250 queued`), et le
courriel est **livré dans la boîte** (LMTP→Dovecot). Le courriel souverain fait maintenant **recevoir
ET envoyer**. Vars : `serveur_dovecot_sasl_reseau`, `serveur_postfix_submission_actif`. Pièges :
Dovecot 2.4 exige un **nom** de section `inet_listener` ; ajouter un service `master.cf` (nouveau
listener) → handler **restart** (pas reload) ; et une config cassée peut bloquer un redéploiement si
la synchro cert/restart précède le template (corriger la config à la main pour débloquer).
- **Sauvegardes applicatives (logiques) — `serveur_backup` + `client_backup` (restic), Tier 0 prouvé.**
Choix : sauvegarder la **donnée d'état** (non régénérable) plutôt que les VM (reconstructibles par
le code + le template). Outil **restic** (chiffrement côté client, déduplication, rétention).
`serveur_backup` (nœud `backup-01`) = cible SFTP/SSH (utilisateur `restic`, clé autorisée, dépôts
sous `/srv/restic/<nœud>`). `client_backup` (intégration par nœud) = restic + **jobs déclaratifs**
(`client_backup_jobs` : `{nom, commande?, chemins}`), clé SSH + mot de passe restic en voûte,
script + **timer systemd** (quotidien) + rétention `forget --prune`. **Prouvé de bout en bout**
sur le **Tier 0** (`infra-pki-01` → `/etc/step-ca`, l'ancre de confiance) : sauvegarde **hors-nœud**
vers `backup-01`, puis **restauration byte-identique** des clés CA (`root_ca_key`,
`intermediate_ca_key`, `ca.json`). Piège corrigé : le plancher `/etc/hosts` d'un nœud existant
ignore un nœud nouvellement ajouté → rafraîchir le socle.
- **Sauvegardes Tier 1 généralisées — 5 nœuds, restauration prouvée.** `client_backup` étendu (jobs
déclaratifs en host_vars) à : `data-sql-01` (`pg_dumpall` — keycloak/forgejo/icingadb),
`id-ldap-01` (`slapcat` LDIF — les identités), `infra-mail-01` (`/var/vmail` — les boîtes),
`forge-01` (`/var/lib/forgejo` + `/etc/forgejo` — dépôts Git ; la BD est déjà couverte par PG).
**Prouvé par restauration** : dump PostgreSQL restauré contient bien les 3 bases (`CREATE DATABASE
forgejo/icingadb/keycloak`) ; LDIF restauré contient `testmail`. Les 5 dépôts restic (pki, sql,
ldap, mail, forge) sont hors-nœud sur `backup-01`, chiffrés. Ajouter un service à sauvegarder =
déclarer un job. Reste : cible **offsite** (3-2-1, Étape B — le dépôt n'est qu'une URL swappable).
- **Consolidation — binding `annuaire` (`resoudre_annuaire`) + retrait de la cruft.**
*Cruft* : supprimés les 8 dossiers-catégories inertes (`roles/{applications,backup,database,
identity,monitoring,proxmox,storage,web}/`, README seuls) et les 5 playbooks-échafaudages `debug`
sans rôle (nextcloud, collabora, client_supervision, web_frontal, web_dorsal) ; l'intention reste
documentée dans `docs/catalogue-services.md`. *Binding annuaire* : nouveau rôle utilitaire partagé
`resoudre_annuaire` (comme `resoudre_base`) qui **dérive** la connexion OpenLDAP du `domaine_interne`
+ un hôte d'annuaire surchargeable — LE seul endroit où le nom d'hôte de l'annuaire est fixé, au lieu
d'être répété. Facts `resoudre_annuaire_{uri,port,base_dn,users_dn,bind_dn,bind_password}` (secret
déréférencé, `no_log`). **Migrés + prouvés (config neutre, `changed=0`)** : `serveur_keycloak`
(fédération LDAP — testmail token HTTP 200), `serveur_dovecot` + `serveur_postfix` (flux courriel
Postfix→LDAP→LMTP→Dovecot **livré de bout en bout**). Piège appris : les *defaults* d'un rôle inclus
ne persistent pas hors de son exécution — publier via `set_fact`.
- **Binding annuaire complété — `icingaweb2` + `client_ldap` migrés vers `resoudre_annuaire`.** Fin des
2 loose ends : `serveur_icingaweb2` (connexion LDAP dormante en mode SSO) résout via
`resoudre_annuaire` (redéploiement `changed=0`, SSO intact) ; `client_ldap` (SSSD, dormant) ne pointe
plus sur un `idm-01` périmé. **Plus AUCUN rôle ne code en dur l'hôte d'annuaire** — un seul point de
vérité (`resoudre_annuaire`).
- **Rôle `serveur_oauth2_proxy` — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO.**
oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak **devant** n'importe quelle app sans OIDC
natif : elle reçoit l'utilisateur authentifié via en-tête, en auth `external`. Rôle paramétrable
(client, secret voûte, redirect, upstream, cookie voûte) — **réutilisable** pour toute app
OIDC-less. Éprouvé sur `sup-01` **devant Icinga Web 2** : client Keycloak `icingaweb2`,
oauth2-proxy `:4180` (exposé par l'edge) → upstream nginx local `:8080` → icingaweb2
`backend = external` (REMOTE_USER depuis `X-Forwarded-Preferred-Username`). **Prouvé** (flux
authorization code headless) : `testmail` → oauth2-proxy → Keycloak → **icingaweb2 `/dashboard`,
connecté** (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct
d'icingaweb2. **Réglages appris** : `insecure_oidc_allow_unverified_email` (les users LDAP n'ont
pas `email_verified` ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe
`X-Forwarded-*` (pas `X-Auth-Request-*`) ; **handler nginx en `restart` (pas `reload`)** car un
changement d'adresse d'écoute n'est pas pris par un reload gracieux.
- **Rôle `serveur_icingaweb2` — Icinga Web 2 (UI native) + module IcingaDB : éprouvé.** App PHP
(php8.4-fpm) servie par un **nginx local**, exposée par l'edge (`icinga.lab.chezlepro.internal`,
auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config **par fichiers `.ini`**
(config/resources/authentication/roles + module `icingadb`), pas d'assistant de setup. Base
IcingaDB via `resoudre_base` (registre). **Auth LDAP direct** vers OpenLDAP (LDAPS, `client_pki`
sur `sup-01`) — icingaweb2 n'a pas d'OIDC natif ; SSO-par-proxy = raffinement futur. **Prouvé** :
`testmail` (LDAP) se connecte (`/dashboard`), et le **module IcingaDB affiche la supervision**
(hôte `icinga`). Déploiement `failed=0` (le rôle est bon ; les frictions étaient dans le
simulateur de login curl : contrôle de cookie `_checkCookie`, champs `uid`/`submit_login`,
valeur CSRF avant `name`).
- **Module BPM (Business Process) éprouvé + codifié — pile Icinga complète.** `serveur_icingaweb2`
installe + active `icingaweb2-module-businessprocess` (`serveur_icingaweb2_modules`), crée le
répertoire des processus (éditable via l'UI, groupe `icingaweb2`, setgid) et **sème des processus
métier en IaC** (`serveur_icingaweb2_bpm_processes`, nom → contenu `.conf`). Format des feuilles
`host;service` (éprouvé via les fixtures du module). **Prouvé** : un processus « Supervision
Chezlepro » (agrège load/procs/swap/ping4/ssh du host `icinga` en logique ET) **rend un état**
dans l'UI (`testmail` connecté), **avec le backend IcingaDB** (pas d'IDO). Rôle re-prouvé (reset
→ recrée le processus, idempotent). BPM n'est **pas remplaçable par Grafana** (roll-up d'impact
métier). Pile Icinga = **moteur + Web 2 + BPM**, complète.
- **Cœur Icinga éprouvé (supervision active).** `serveur_icinga` (cœur : `icinga2` + `icingadb` +
`icingadb-redis`) déployé sur `sup-01` (🔧→⭐), base `icingadb` PostgreSQL via le registre.
**Prouvé** : 3 services actifs, et le moteur **supervise** — IcingaDB peuplée (1 hôte, 12
services, résultats de checks persistés en base). Zéro bug de déploiement (rôle bien bâti).
**Icinga Web 2 + module BPM restent différés** (phases dédiées : UI native + vues d'impact
métier ; le BPM n'est pas remplaçable par Grafana). Confirme aussi le **DRY `resoudre_base`**
sur icinga (les 3 rôles consommateurs validés).
- **Forgejo branché au SSO OIDC (« Se connecter avec Chezlepro ») — 2e app SSO, prouvée.**
`serveur_forgejo` (10.0.0) éprouvé sur `forge-01` (🔧→⭐), adossé à PostgreSQL (base `forgejo`
auto-provisionnée depuis le registre), exposé par l'edge (vhost + cert SAN + A PowerDNS + alias
plancher, tout auto-dérivé de `expose`). **Source OAuth2** vers Keycloak
(`forgejo admin auth add-oauth`, idempotent, realm `chezlepro`), client OIDC `forgejo`
enregistré via `serveur_keycloak_clients`. Auto-enregistrement OIDC (`[oauth2_client]
ENABLE_AUTO_REGISTRATION` + `ALLOW_ONLY_EXTERNAL_REGISTRATION` : identités depuis l'annuaire
seulement). **Prouvé** (flux authorization code headless) : `testmail` (LDAP) se connecte,
**compte auto-créé** (`testmail@lab.chezlepro.internal`), atterrit sur le tableau de bord.
**5 bugs de 1er déploiement corrigés** : dépendance périmée `serveur_sendmail`→`serveur_postfix`
(le vrai MTA) ; `app.ini` doit appartenir au user `git` (Forgejo persiste des secrets générés) ;
ordre admin/migrations (`flush_handlers` + `wait_for` avant `admin user create`) ; `HTTP_ADDR`
`127.0.0.1`→`0.0.0.0` (l'edge nginx est sur un autre hôte, 502 sinon) ; auto-enregistrement OIDC.
- **DRY : rôle utilitaire partagé `resoudre_base` (résolution BD depuis le registre).** Le bloc
copié-collé dans `serveur_keycloak`, `serveur_forgejo` et `serveur_icinga` (charger le registre,
filtrer par consommateur, déréférencer le secret via `lookup('vars', ...)`, résoudre hôte/port)
est extrait dans `roles/resoudre_base` (facts `resoudre_base_entree/db_password/db_host/db_port`,
`no_log`). Les 3 rôles l'incluent (`include_role`) et adoptent les facts. Le secret **ne quitte
toujours pas le rôle** (déréférencé au déploiement). **Fait « sur la preuve »** : re-déploiement
keycloak + forgejo `failed=0`, idempotent, `testmail` token Keycloak HTTP 200. Ferme le reste
noté de la Phase 2 des bindings (cf. `docs/bindings-conception.md`).
- **PowerDNS — A d'exposition auto-dérivés (le DNS de la Phase 3 des bindings).** `serveur_powerdns`
génère désormais, dans la zone interne, un enregistrement A pour chaque FQDN d'exposition (champ
`expose` des applications) vers l'**edge qui le sert** (`domaines.edge`) — via
`expositions_des_applications` (même source que les vhosts nginx et les SANs du cert edge).
Déclarer `expose` produit maintenant **vhost + SAN de cert + enregistrement DNS**, tout dérivé.
**Prouvé** : `dig @infra-dns-01 grafana.lab.chezlepro.internal` et `keycloak.…``192.168.15.21`
(edge). Option `serveur_powerdns_publier_expositions` (défaut true). **Limite / reste** : PowerDNS
est **autoritatif, pas récursif** — pour que les nœuds *utilisent* ces A sans casser la résolution
Internet, il faut un **récursif** (pdns-recursor : forward de la zone interne + récursion du reste)
ou garder le plancher `/etc/hosts`. Ne PAS repointer naïvement `client_dns` vers l'autoritatif.
- **`hosts_statiques` — alias d'exposition dans le plancher `/etc/hosts` (résolution client, sûre).**
Le plancher pose désormais, sur **chaque nœud**, `<IP edge> <FQDN exposé>` pour chaque `expose`
(dérivé de `domaines.edge`, même source que nginx/PowerDNS). Indépendant du DNS, aucun risque de
couper la résolution (choix retenu vs pdns-recursor). Chargement du plan **best-effort**
(`stat` **`delegate_to: localhost`** + `become: false` — les registres vivent sur le nœud de
contrôle ; ignoré si le plan est absent, ex. préparation du template). **Prouvé** : `/etc/hosts`
d'obs-01 régénéré avec `keycloak`/`grafana` → edge (ligne manuelle éliminée), `getent` OK, et le
**flux SSO Grafana fonctionne via la résolution du plancher** (`login: testmail`). Boucle Phase 3
fermée : déclarer `expose`**vhost + SAN cert + A PowerDNS + alias plancher**, tout dérivé.
Bugs corrigés en chemin : `serveur_loki` (groupe `loki` manquant), `stat` sur cible→contrôle,
`become` inutile sur le contrôle.
- **Rôle `client_unbound` — résolveur local (DNS dynamique) : éprouvé sur un nœud.** Unbound par
nœud (`127.0.0.1`) avec **stub-zone** vers l'autoritatif interne (PowerDNS) + **récursion**
Internet (ou forward via `client_unbound_transitaires`). Alternative *dynamique* au plancher
`/etc/hosts` statique, sans casser Internet. Bascule de `/etc/resolv.conf` **protégée**
(`client_unbound_apply` + `client_unbound_confirm`) **et validée AVANT** (Unbound doit résoudre
interne + Internet, sinon pas de bascule → nœud jamais coupé). **Prouvé sur data-sql-01** (rayon
d'impact minimal) : `dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal` → PowerDNS,
`deb.debian.org` → récursion, `apt` OK. Rôle sûr par défaut (`apply: false` : installe Unbound
sans toucher au resolver). Rollout flotte = opt-in par nœud. **Note direction** : OPNsense
embarque Unbound → à terme, l'Unbound *réseau* peut vivre sur l'appliance de bordure (nœud
public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local).
- **Bindings — Phase 1 : résolveur de liens dans `instancier.py` (relations service→service
déclaratives).** Une application déclare ses `liens: [{vers, role}]` dans
`plan/applications.yml` ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml`
(`setops_liens.accepte`, comme `meta/empreinte.yml`). `instancier` résout la cible (FQDN interne
**dérivé de la nomenclature** + `domaine_interne`), substitue les gabarits (`{cible.fqdn}`,
`{cible.hote}`, `{cible.ip}`) et **injecte les variables en host_vars du consommateur**.
Validation : rôle accepteur, cible existante, genre attendu. **Migration prouvée** : les liens
mail Postfix→Dovecot (`mailstore`) et Postfix→rspamd (`milter`) passent de group_vars codés en
dur à des liens déclaratifs — `make instancier` donne **DIFF VIDE** (mêmes variables générées),
puis les group_vars sont retirés. La topologie mail devient déclarative et portable.
Cf. `docs/bindings-conception.md`. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4).
- **Bindings — Phase 2 (bases) : constat + réconciliation de la note (`docs/bindings-conception.md` §5/§9).**
Inspection du code réel : le binding app→base **existe déjà****côté base** (`consommateur`/`portee`
dans `bases-donnees.yml`), résolu **dans le rôle** au déploiement (`include_vars` + filtre +
`lookup('vars', secret)`), sur 4 rôles (postgresql, forgejo, keycloak, icinga). **Délibérément
conservé** (le secret ne quitte jamais le rôle) — ne PAS dupliquer en app-side/instancier. Deux
directions assumées : app→app côté app (instancier), app→base côté base (registre). Reste (reporté
à l'épreuve de Keycloak) : factoriser le bloc de résolution copié-collé en include partagé (DRY).
- **`docs/carte-set-ops.md` — carte d'orientation (index + mécanismes transverses).** Après audit
du dépôt : point d'entrée « à lire d'abord » (index du corpus, ~22 docs), et **catalogue des
mécanismes** dispersés dans le code (les 2 directions de binding, pont de cert, résolution BD
par registre, socle-first, sûreté check-mode, voûte, dimensionnement) avec **où ils vivent**.
But : ne plus re-découvrir l'existant. Constat : la **cruft était déjà inventoriée** dans
`catalogue-services.md` (rôles-catégories inertes, échafaudages) — non dupliquée, référencée.
`catalogue-services.md` « État d'implémentation » **rafraîchi** (rôles éprouvés sur VM réelles :
socle, PKI, LDAP, DNS, nginx, pile courriel). Pointeur ajouté depuis `architecture-set-ops.md`.
- **`serveur_postgresql` et `serveur_keycloak` éprouvés sur VM réelles (🔧→⭐).** PostgreSQL
déployé (data-sql-01), écoute réseau + pg_hba VLAN, et **provisionne la base `keycloak` depuis
le registre** (`bases-donnees.yml`) — **binding app→base prouvé en réel** (base + rôle créés,
mot de passe = `vault_bd_keycloak`). Keycloak 26.0.7 déployé (id-sso-01), **mode prod**,
connecté à PostgreSQL (**87 tables** du realm master écrites), **token admin obtenu** (auth
adossée à la BD). Lacunes connues (documentées `catalogue-services.md`) : fédération LDAP et
edge nginx pas encore câblés. Reste : DRY du bloc de résolution BD (keycloak/forgejo/icinga).
- **`serveur_keycloak` — fédération LDAP (modèle d'identité A) : automatisée et prouvée.** Le rôle
configure, via `kcadm` (idempotent), un realm applicatif (`serveur_keycloak_realm`, déf.
`chezlepro`) et un **provider de stockage LDAP READ_ONLY** vers OpenLDAP (LDAPS, `uid`/`entryUUID`,
`inetOrgPerson`). TLS LDAPS validé via le **truststore système** (`truststore-paths` →
`/etc/ssl/certs/ca-certificates.crt`, racine step_ca posée par **client_pki**, désormais requis
sur le nœud). Secrets par `environment` + `no_log`. **Éprouvé avant codification** puis prouvé
par le rôle : un utilisateur LDAP (`testmail`) obtient un token via le realm (HTTP 200), et le
redéploiement est **idempotent** (`changed=0`). Nouveau : `tasks/federation-ldap.yml`. Reste :
edge nginx (accès HTTPS par nom), mappers d'attributs/groupes fins.
- **Edge nginx + exposition (Phase 3 des bindings) — prouvés avec Keycloak.** Sans changement de
code : la machinerie `serveur_nginx_publier_expositions` **existait déjà** (lit `expose` des
applications + `edge` de `domaines.yml`, dérive `amont = http://<IP hôte>:<port>`, génère le
vhost avec `X-Forwarded-*`). Le bac à sable déclare `keycloak.expose:
[keycloak.lab.chezlepro.internal]` + le domaine interne `lab.chezlepro.internal` (edge
`serveur_nginx`). **Prouvé** : le vhost s'auto-génère (`keycloak.lab.chezlepro.internal →
http://192.168.15.81:8080`), et la découverte OIDC via l'edge renvoie
`"issuer":"https://keycloak.lab.chezlepro.internal/..."` (les `X-Forwarded` passent, Keycloak
se sait derrière HTTPS). **Limite connue** : le cert TLS de l'edge est encore le snakeoil
auto-signé (avertissement navigateur). **Raffinement recommandé** (réutilise l'existant, pas de
nouveau mécanisme) : ajouter les FQDN d'exposition aux `client_pki_sans` de l'edge (client_pki
demande + renouvelle déjà le cert d'hôte), puis pointer `serveur_nginx_certificat` sur le cert
client_pki (`/etc/step/certs/<edge>.crt`).
- **Cert de l'edge : snakeoil → step_ca (HTTPS valide).** Appliqué le raffinement ci-dessus :
`client_pki` ajouté à l'edge, ses `client_pki_sans` incluent le FQDN d'exposition
(`keycloak.lab.chezlepro.internal`), et `serveur_nginx_certificat`/`_cle` pointent sur le cert
client_pki. **Prouvé** : HTTPS `HTTP 200` avec `ssl_verify_result=0` (chaîne validée contre la
racine step_ca, nom correct), émetteur `Set-OPS Internal CA`. Sans nouveau code (client_pki +
group_var). **Gaps notés** : (1) recharger nginx au **renouvellement** du cert (le cert-renewer
renouvelle en place, nginx ne recharge pas seul — hook à ajouter) ; (2) **auto-dériver** les SANs
d'exposition de l'edge depuis le plan (au lieu de les lister dans le group_var).
- **Grafana branché au SSO OIDC (« Se connecter avec Chezlepro ») — prouvé de bout en bout.**
`serveur_grafana` : config OIDC via `GF_AUTH_GENERIC_OAUTH_*` (client confidentiel `grafana`,
realm `chezlepro`, secret `vault_grafana_oidc`). `serveur_loki` + `serveur_prometheus` +
`serveur_grafana` déployés sur `obs-01` (🔧→⭐). **Bug de rôle corrigé** : `serveur_loki` créait
le répertoire en `group: loki` alors que le paquet crée l'utilisateur en `nogroup` sans groupe
`loki` → ajout de la création du groupe. **Prouvé (flux authorization code headless, via l'edge
HTTPS)** : `testmail` (user LDAP) se connecte à Grafana par le SSO — `/api/user` renvoie
`login: testmail`, email et nom **fédérés depuis LDAP**. Chaîne complète LDAP → Keycloak →
Grafana. **Gaps notés (pour rendre 100 % déclaratif)** : (1) l'**enregistrement du client OIDC**
dans Keycloak a été fait via `kcadm` **à la main** (à codifier — rôle grafana ou liste de clients
côté keycloak) ; (2) la **résolution** `keycloak.…internal → edge` sur obs-01 est un `/etc/hosts`
manuel (**PowerDNS devrait porter les A d'exposition** — chaînon récurrent) ; (3) mapping de
rôles Grafana (tous Viewer par défaut).
- **`serveur_keycloak` — enregistrement des clients OIDC codifié (gap précédent fermé).** Le rôle
gère une **liste déclarative** `serveur_keycloak_clients` (`clientId`, `redirect_uris`,
`web_origins`, `secret`) et enregistre chaque client confidentiel via **kcadm idempotent**
(`tasks/clients-oidc.yml`, create-si-absent, `no_log`). Décision : **côté Keycloak** (les creds
admin restent dans le seul rôle Keycloak, pas répandus dans chaque rôle app) ; le `secret`
référence la même variable de voûte que l'app. **Prouvé** : client `grafana` supprimé → rôle →
recréé → `testmail` se connecte à Grafana (`login: testmail`) ; redéploiement **idempotent**
(`changed=0`). Le déploiement de Grafana au SSO est désormais **autonome**.
## 2026-07-02
### Décidé
- **Bindings — conception des relations app/base/serveur/domaine (`docs/bindings-conception.md`).**
Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars
(ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue :
**liens déclarés côté application** (`liens: [{vers, role}]`), résolus par `instancier.py` en
variables Ansible ; chaque rôle décrit les liens qu'il accepte dans `meta/liens.yml` (comme
`meta/empreinte.yml`) ; FQDN cible **dérivé de la nomenclature** (jamais codé en dur). Domaines
publics traités comme lien `exposition` (écrit sur l'edge). Réconcilie l'existant (bases
`consommateur`, `domaines.edge`). Preuve de migration ciblée : les 3 liens mail. Implémentation
à suivre (phasée).
- **Licence : passage de CC BY-NC-SA 4.0 à AGPLv3.** Les licences Creative Commons ne sont pas
faites pour du logiciel (position de CC elle-même) et la clause **NonCommercial contredisait
le principe fondateur « tout est libre »** — en plus de bloquer les artisans/coopératives
visés. `LICENSE` remplacé par le **texte officiel intégral de l'AGPLv3** (verbatim, non
modifié). Attribution + modèle **libre + services/certification** documentés dans le `README`
(méthode d'attribution recommandée, sans toucher au texte de la licence). L'AGPLv3 protège la
souveraineté (anti-captation propriétaire en SaaS) **sans interdire l'usage commercial**.
- **Architecture d'identité/SSO (`docs/identite-sso.md`).** Modèle A : **OpenLDAP source de
vérité**, **Keycloak fédéré** (SSO web OIDC, MFA, self-service), **mail en bind LDAP
direct**. Une identité, un mot de passe, deux chemins d'auth (web→Keycloak, mail→LDAP),
tout adossé au même OpenLDAP. Sert la portabilité multi-tenant (chaque tenant = son LDAP +
son Keycloak fédéré). Pas Keycloak-source (casse-tête mail + moins portable).
- **Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd.** La Phase 1
Stalwart (`serveur_stalwart`) avait été prototypée et déployée (v0.16.11, install +
démarrage en mode récupération). Le prototypage a révélé un projet **trop jeune/volatil
pour un pilier mail critique** : config cassée entre 0.15 et 0.16, outil IaC
`stalwart config apply` **annoncé mais non livré** dans le binaire, API REST supprimée
(JMAP), gros backlog. Pivot vers la stack **mature Postfix/Dovecot/rspamd**, en prime
**100 % configurable par fichiers** (alignée au modèle déclaratif Set-OPS). Le rôle
`serveur_stalwart` est **retiré** (git en garde la trace) ; `docs/courriel-conception.md`
mis à jour. Réévaluer Stalwart ~2028.
### Ajouté
- **`docs/pouvoirs-set-ops.md` — bilan des capacités du moteur.** Inventaire structuré
(moteur/plan, plan de contrôle GUI, socle durci, piliers d'infrastructure, services outillés,
patrons d'ingénierie), distinguant honnêtement « prouvé sur cluster réel » de « outillé ».
- **Rôle `serveur_rspamd` (rspamd 3.x) — antispam + DKIM, en milter sur Postfix.** Installé
sur le nœud edge-mta (avec Postfix), backend **Redis** local, worker proxy en **mode milter
auto-scan** (`:11332`), **signature DKIM** sortante (clé générée par le rôle de façon
idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges
`/etc/rspamd/local.d/`. Postfix branché via `smtpd_milters` (option `serveur_postfix_rspamd_milter`,
`milter_default_action = accept` → tolérant si rspamd indisponible). **Prouvé** : un courriel
traversant le milter ressort **scanné** (`rspamc stat` : 1) et **signé DKIM** (`DKIM-Signature:
d=…`), puis livré et lu en IMAP. **Étape A (courriel interne) complète** : dovecot + postfix + rspamd.
- **Flux courriel interne PROUVÉ de bout en bout (Étape A).** Envoi → Postfix (`edge-mta`,
validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → **lu en IMAP**
(auth LDAP, TLS step_ca) : `status=sent`, message lu (sujet + corps). Réglages Dovecot 2.4
qui débloquent la remise LMTP : `userdb static { static_allow_all_users = yes }` (sinon
NOTFOUND pour l'expéditeur/raw-mail-user externe), `mail_inbox_path =` vidé (le défaut mbox
`/var/mail` root refusait l'autocréation de l'INBOX), Maildir explicite (`mail_home` +
`mail_path = %{home}/Maildir`), chemin par nom d'utilisateur (home identique côté LMTP
local-part et IMAP adresse complète ; mono-domaine, multi-domaine = raffinement Étape B).
- **Rôle `serveur_postfix` (Postfix 3.x) — MTA du nœud edge-mta.** Réception `:25`, cartes
**LDAP** (validation des boîtes via l'attribut `mail`), remise **LMTP réseau** vers le nœud
mail-store Dovecot (`virtual_transport = lmtp:inet:[…]:24`), TLS via **step_ca** (pont de
cert), aucune boîte locale. Config `main.cf` + carte `ldap-mailboxes.cf`, validée par
`postfix check`. Secret de bind : `vault_openldap_admin`. Nécessite `serveur_postfix_mailstore_hote`
(FQDN du mail-store). Validé statiquement ; déploiement réel à suivre.
- **Rôle `serveur_dovecot` (Dovecot 2.4) — déployé et prouvé.** IMAP `:993`/`:143` +
LMTP, **auth/annuaire LDAP** (vers `serveur_openldap`, filtre `mail`), stockage Maildir
(user système `vmail`), **TLS via step_ca** (pont de cert + resync au renouvellement),
neutralisation de l'auth système par défaut. Config en drop-in **syntaxe Dovecot 2.4**
(`mail_driver`, `ssl_server_cert_file`, `passdb ldap`/`userdb static`, `%{user}`),
**validée par `doveconf`** au déploiement. Sockets d'intégration Postfix **conditionnels**
(rendus si l'utilisateur `postfix` est co-localisé). Prouvé : `doveadm auth test` — bon
mot de passe accepté, mauvais refusé, sur cert step_ca. Secret de bind : `vault_openldap_admin`.
- **`serveur_openldap` durci pour la prod : TLS via step_ca + organisation en intrant.**
- **TLS (LDAPS + STARTTLS)** : le certificat d'hôte step_ca (déposé par `client_pki`,
`root:root 600`) est synchronisé vers un emplacement lisible par `openldap` (`/etc/ldap/tls`)
par un script + une unité `path` systemd qui **re-synchronise et recharge slapd à chaque
renouvellement** ; `olcTLS*` configuré dans `cn=config`, `SLAPD_SERVICES` expose `ldaps://`.
Dégrade proprement (slapd en clair local) si `client_pki` n'a pas encore posé le cert.
- **Organisation** : nouvel intrant `chezlepro_organisation` (remplace le « Exemple Inc » codé).
- *Écrit en code de prod, éprouvé statiquement ; validation par déploiement réel à suivre.*
- **`docs/courriel-conception.md`** — cadrage du futur service de courriel souverain :
full self-host, suite **Stalwart** (adoptée, enveloppée par un rôle mince), topologie
MX primaire + MX secours, intégration identité (LDAP) / DNS / PKI (Let's Encrypt public
vs step_ca interne), enregistrements DNS publics, décisions ouvertes (IP/PTR, secours,
stockage) et phasage. **Conception seulement — aucun rôle livré.**
## 2026-07-01
### Ajouté
- **État RÉEL vs plan dans le GUI (sonde de vie + auto-actif).** Le badge `planifié`/
`actif` décrit l'*intention* du plan, pas l'existence de la VM — d'où la confusion
« serveur planifié mais vivant ». Deux ajouts :
- **Sonde de vie** : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan
(`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable**
— sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan.
- **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on
**déploie** passe automatiquement `actif` dans le plan (matérialisé = actif), puis
l'inventaire est **régénéré** pour que le changement se voie partout (en-tête inclus).
- **Compteur « vivantes »** dans l'en-tête (depuis la sonde), à côté de actifs/planifiés.
### Corrigé
- **nginx ne validait pas sur Debian 13 (`server_tokens` en double).** Debian 13 livre
`server_tokens off;` **actif** dans `/etc/nginx/nginx.conf` (avant : commenté). Le
drop-in `conf.d/99-setops.conf` du rôle le redéclarait → `nginx -t` échouait
(« directive is duplicate ») et le déploiement plantait au handler de validation. Le
rôle `serveur_nginx` neutralise désormais la ligne distro (le drop-in reste l'unique
source). Trouvé en déployant nginx pour de vrai sur un hôte edge.
- **Une voûte chiffrée cassait `instancier` / « Appliquer le plan ».** `ansible-inventory
--list` (utilisé pour la comparaison sémantique du plan) tente de déchiffrer
`group_vars/all/vault.yml` et échoue sans mot de passe (`exit 4`) — alors que
l'opération est structurelle, sans secret. `instancier` utilise désormais
automatiquement le fichier conventionnel `~/.config/setops-vault-pass` (si
`ANSIBLE_VAULT_PASSWORD_FILE` n'est pas déjà défini).
- **Secrets des rôles non câblés à la voûte (échafaudage manquant).** Les rôles à
secrets déclaraient `serveur_X_password: ""` avec, en commentaire seulement, la
variable de voûte attendue (`{{ vault_X }}`) — sans mapping réel. Résultat : remplir
la voûte selon `vault.exemple.yml` ne suffisait pas, le secret restait vide et
l'assertion « secrets requis » échouait. Les 12 secrets des 8 rôles (`serveur_step_ca`,
`serveur_forgejo`, `serveur_grafana`, `serveur_keycloak`, `serveur_openldap`,
`serveur_redis`, `client_ldap`, `client_pki`) pointent désormais vers leur variable
de voûte : `serveur_X_password: "{{ vault_X | default('') }}"`. Chaque instance n'a
plus qu'à remplir ses `vault_*` dans sa voûte chiffrée ; aucun mapping par instance.
- **« Vérifier » (dry-run `--check`) échouait faussement sur un hôte frais.** Les tâches
« démarrer service » et les handlers « redémarrer / recharger / valider » des rôles
applicatifs touchent un paquet que `--check` n'installe pas réellement → le service
(ou le fichier de zone/conf) n'existe pas encore → faux `fatal`, qui **bloquait le
déploiement** (le dry-run doit réussir pour débloquer « Déployer »). Ajout de
`when: not ansible_check_mode` sur ces tâches et handlers des 13 rôles `serveur_*`
(29 gardes). En dry-run elles sont sautées ; en vrai déploiement, inchangées.
- **Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence** dès que le plan
divergeait de l'inventaire (il appelait `instancier appliquer` **sans** `--force`).
Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais
régénéré et les compteurs restaient figés. Le clic « Appliquer » **est** l'intention
explicite → le GUI force désormais (git reste le filet).
- **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet
du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault
+ confirmation renforcée en prod) :
- **Serveur** → `🖥 Pousser` clone la VM depuis le golden template (`make creer-vm`),
disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas.
- **Application** → `Pousser` déploie l'hôte porteur (`make deployer HOTE=<hôte>`).
- **Base** → `Pousser` déploie l'hôte du serveur de BD (crée la base).
Nouveaux modes `creer` / `pousser` dans `executer_flux` + routes `/api/creer` et
`/api/pousser`. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser
→ Vérifier → Déployer.
- **Premier déploiement RÉEL validé de bout en bout** (cluster asgard) : flux canonique
plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3
correctifs ci-dessous.
## 2026-06-30
### Ajouté
- **Plancher de résolution `/etc/hosts` (indépendant du DNS).** Nouveau rôle de socle
`hosts_statiques` (dans `serveur_debian`) : génère `/etc/hosts` sur **chaque** VM
depuis l'inventaire (nom + FQDN interne → IP réelle). Tout l'écosystème se résout
par nom **même serveur DNS éteint**, et le bootstrap ne dépend plus du DNS. PowerDNS
devient une **commodité** (zone/externe/dynamique) ; `client_dns` est rendu tolérant
(inerte si aucun DNS interne) et sa dépendance à `serveur_powerdns` passe **molle**.
- **Adressage fédéré : index d'instance.** Le VMID n'est plus codé `9CSNN` en dur :
il prend le préfixe d'un `index` déclaré en tête de `plan/nomenclature.yml`
(`{index}{catégorie}{service}{séq}`). Convention : `supernet = 10.(10+index).0.0/16`,
`VMID = index·CSNN`. Permet à N écosystèmes de **coexister/s'interconnecter** sans
collision (Chezlepro=1 → `10.11`/`1xxxx`, Technolibre=2 → `10.12`/`2xxxx`). Sans
index → `9CSNN` (rétro-compatible ; bacs à sable, plages ad-hoc `172.19.x`).
Code mort retiré (`deriveServeur` JS). Voir `docs/multi-instances.md`.
- **Doc `docs/multi-instances.md`** : cadrage « un moteur, N écosystèmes » — l'instance
comme dépôt autonome, la bascule, l'isolation, et le socle multi-tenant.
- **Bascule d'instance (`make instance-utiliser NOM=…`).** Repointe le symlink
`instance` vers un autre dépôt d'instance (prod ↔ bac à sable) ;
`make instance-courante` affiche l'instance montée. Permet d'exploiter plusieurs
instances (séparation **par instance**) depuis un seul moteur.
- **Identité des intrants relative à l'inventaire.** Le panneau « Intrants » lit/écrit
l'identité dans `group_vars/all/10-intrants.yml` de l'inventaire monté (fichier réel
pour une instance autonome, symlink vers une source partagée sinon — l'écriture suit
le symlink). Fonctionne donc aussi bien pour une instance « par instance » que pour
l'ancien partage lab/production.
- **Inventaire d'instance neutre et configurable (`principal` / `SETOPS_INVENTAIRE`).**
Le moteur (Makefile + `inventory_gui`, `instancier`, `config_proxmox`, `serveurs`,
`applications`) ne code plus en dur `inventories/lab` / `inventories/production` :
il vise **un inventaire par instance**, détecté de façon **rétro-compatible**
(`principal` > `production` > `lab`) et surchargeable par `SETOPS_INVENTAIRE`. Les
instances existantes (découpage lab/production) continuent de fonctionner à
l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre
de la séparation **par instance** (la 1re étant le drapeau `setops_production`).
- **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel
est désormais possible sur **toute instance** (un bac à sable déploie sur *son*
infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans
`group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une
**confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton
Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche
« bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau,
ancien repère « inventaire production »). Le GUI affiche toujours quel type
d'instance est monté.
### Modifié
- **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous
les secrets de l'instance (token Proxmox + 17 `vault_*` pour PKI, LDAP/SSO, bases,
forge, observabilité) vivent dans **un seul fichier chiffré**,
`inventories/<env>/group_vars/all/vault.yml`. Gabarit committé
`exemples/vault.exemple.yml`. `make config` (`config_proxmox.py`) écrit/édite
désormais cette voûte (semée depuis le gabarit si absente). `.gitignore` durci
(`**/vault.yml`). Docs mises à jour (config-proxmox.md avec étapes de migration,
intrants-communs.md §H, QUICKSTART). Le GUI ne stocke toujours aucun secret.
### Corrigé
- **Trois bugs trouvés au premier déploiement réel (cluster asgard).**
- **Clonage/Makefile codaient `inventories/lab` en dur** (config Proxmox + voûte) —
vestige du modèle env qui cassait les instances `principal`. Le playbook de clonage
et le Makefile détectent maintenant l'inventaire (`lab` > `principal` > `production`).
- **Redimensionnement disque non idempotent** : quand le disque dérivé du plan est
plus petit que le golden template, Proxmox refuse (`shrinking disks is not
supported`) et le clone échouait. Le resize est désormais **grow-only** (tolère le
cas, la VM garde le disque du template — le dérivé est un minimum).
- **PowerDNS refusait de démarrer** (`multiple backends 'bind'`) : le rôle
redéclarait `launch+=bind` que le paquet `pdns-backend-bind` pose déjà. Le rôle ne
déclare plus `launch` (seulement `bind-config`).
- **`chezlepro_timezone` n'était appliqué nulle part.** Cet intrant de base global
était défini mais aucun rôle ne s'en servait. Le rôle `chrony` (appliqué à tout
hôte via `serveur_debian`) règle désormais le fuseau horaire à partir de
`chezlepro_timezone` (`chrony_timezone` par défaut, vide = ne pas toucher). Les
autres défauts globaux (nœud / stockage / pont Proxmox) étaient déjà réutilisés
comme valeurs par défaut, surchargeables par hôte.
### Modifié
- **Détail GUI : section « Groupes (dérivés) » retirée (redondante).** Depuis la fusion
en atelier maître-détail, elle ne répétait que le socle (universel), les rôles des
applications (déjà dans « Applications ici ») et les intégrations (déjà cochées). Son
seul signal unique — le prérequis bloquant — est désormais nommé dans le pied
(« Prérequis manquant : … ») ; le panneau Dépendances reste pour la vue d'ensemble.
Code mort retiré (`renduGroupe`, `detailGroupe`, `titreGroupe`, CSS `.groupe*`).
- **Nettoyage CSS/HTML du GUI** après la refonte : retrait des règles et éléments
morts (`.message`, `.chips-filtre`/`.chip-f`, `.onglet*`, `.base-ligne`,
`.bases-liste`, `.ch-grp*`/`.ch-fleche`/`.ch-roles`, `.champ-val`, divs `#message`
et `#chips`).
- **GUI refondu en atelier maître-détail unifié.** Toutes les vues suivent le même
motif : tuiles à gauche, **détail + saisie à droite**, le panneau droit reflétant
la sélection de la vue courante (fin du panneau « figé » au changement de vue).
Les vues **Applications** et **Bases** passent de tableaux pleine largeur à ce
motif ; la saisie se fait dans le panneau droit, avec **liens cliquables** entre
objets (serveur → application → base). Le panneau droit est élargi (~38 %).
- **Fusion Serveur/Hôte.** Les vues « Inventaire » (hôtes, lecture seule) et
« Serveurs » (plan) faisaient doublon : elles sont fusionnées en **une seule vue
Serveurs**. Sa tuile porte le statut de réconciliation (réconcilié / divergent /
non instancié) ; son détail réunit l'**identité éditable** (plan), les **dérivés**
(VMID/IP/VLAN), les **groupes**, les **applications et bases hébergées**, et
**Vérifier/Déployer**. La navigation clavier (`1-3`, `j/k`, `v/d`, `/`) et le
filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc »
explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré
(`carte`, `renduChaine`, `ONGLETS`, `champLecture`, sélection d'hôte, etc.).
### Ajouté
- **Fluidité d'exploitation du GUI** (sans dépendance, stdlib pure) :
- **Notifications empilées (toasts)** auto-effaçables au lieu d'une bannière unique
écrasée ; les erreurs restent plus longtemps, les opérations longues mettent à
jour leur propre toast.
- **Durée d'exécution** affichée en direct dans la console (Vérifier / Déployer) et
suivi vivant de « Appliquer le plan ».
- **Navigation clavier** : `1-5` changent de vue, `j/k` parcourent les hôtes,
`v`/`d` vérifient/déploient l'hôte sélectionné, `/` cible le filtre, `Ctrl+S`
sauvegarde la vue éditable courante (ignorés pendant la saisie).
- **Validation inline** des champs du plan (vue Serveurs) : nom `fonction-NN`,
mémoire, cœurs, disque — liseré rouge et blocage avant l'envoi.
- **Garde-fou anti-perte** : confirmation `beforeunload` si des éditions de plan ne
sont pas sauvegardées.
- Le bouton **Sauvegarder** de l'en-tête devient contextuel (sauve la vue éditable,
désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort.
### Ajouté
- **Vue Serveurs : champs alimentés par les paramètres globaux.** À `+ Serveur`,
**Nœud** et **Stockage** deviennent des listes déroulantes (catalogues
`proxmox_noeuds` / `proxmox_stockages`, vide = défaut global) et **Intégrations**
une rangée de cases à cocher des rôles `client_*` disponibles (au lieu d'une saisie
texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants
(classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés
dans `group_vars/proxmox.yml` ; l'API expose `integrations_disponibles` (scan de
`roles/client_*`). Le type `liste` (chaîne virgulée → liste YAML dédoublonnée) est
ajouté au schéma des intrants.
### Supprimé
- **Vue « Chaîne » du bandeau (redondante).** Son contenu (`renduChaine`) était déjà
rendu, par hôte, dans l'onglet « Chaîne » du détail de la vue Inventaire ; la vue ne
faisait que le répéter pour tous les hôtes à la fois, sans réseau/proxmox ni boutons
d'opération. Retirée (bouton, rendu `dessinerArbre`, CSS `.arbre-*`) ; la chaîne
reste consultable hôte par hôte. Raccourcis clavier ramenés à 1-4.
### Modifié
- **Vue Inventaire (cartes) : détail converti en inspection lecture seule.** La vue
affichait « généré depuis le plan (lecture seule) » tout en exposant une surface
d'édition (+ Hôte, ✕, bascule actif/planifié, ✨ Proposer, champs et cases de
groupes éditables) qui ne pouvait pas être persistée (l'inventaire est généré ;
`/api/inventaire` renvoie 409). Ces contrôles orphelins sont retirés : nom, champs
Réseau/Proxmox et groupes en lecture seule, état affiché en badge. **Vérifier /
Déployer** et les onglets d'inspection sont conservés. L'édition reste dans les vues
**Serveurs / Applications / Bases**. Code mort supprimé (`ajouterHote`,
`supprimerHote`, `definir`, `definirNom`, `definirEtat`, `basculerGroupe`,
`proposer`, `autoProposer`, `prochainSeqLibre`, `marquerModifie`, état `modifie`).
- **Référence des paramètres de `make config`** (`docs/config-proxmox.md`).
Explique un par un les 16 paramètres Proxmox non sensibles + les secrets API
demandés par l'assistant : sens, valeur par défaut, quoi saisir, et quels champs
sont des constantes vs des défauts surchargeables par hôte. Renvois ajoutés depuis
`make help` et `QUICKSTART.md`. Comble un trou : ces invites n'étaient expliquées
nulle part de façon pérenne (impératif « exploitable sans IA »).
- **Panneau « Intrants de base » dans le GUI.** Un bouton ⚙ Intrants ouvre une fenêtre
unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction
visible entre **constantes** (valeur unique, non surchargeable) et **défauts**
(valeurs proposées, surchargeables dans les instances). Les secrets ne sont **jamais**
saisis ni affichés ici (garde-fou `INTRANTS_CLES_INTERDITES` + filtrage par schéma) :
ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à
titre informatif. Un changement de `domaine_interne` (clé de voûte) demande une
confirmation explicite ; un `domaine_interne` vide est refusé côté serveur. La
nomenclature reste en lecture seule (modifiable dans le plan). Voir
`docs/intrants-communs.md` et `docs/intrants-base-gui-conception.md`.
- **Source unique d'identité partagée.** `domaine_interne` et `chezlepro_timezone`
vivent désormais dans `inventories/partage/intrants-identite.yml`, référencé par
symlink depuis chaque environnement (`group_vars/all/10-intrants.yml`) — fin de la
duplication lab/production.
- **Info-bulles d'aide sur les champs du GUI.** Survoler la description d'un champ à
saisir affiche une bulle avec des instructions sommaires.
## 2026-06-28
### Ajouté
- **Document de présentation de l'écosystème Chezlepro** (`docs/ecosysteme-chezlepro.md`).
Description vulgarisée à destination client/partenaire : les piliers de l'écosystème
souverain, le modèle reproductible (plan → génération → clonage → conformité), et un
inventaire des **mesures de renforcement** réellement en place (SSH durci, fail2ban,
sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets,
garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut
« défini/validé vs déployé ».
### Ajouté
- **Dimensionnement dérivé des ressources VM.** Les cœurs/RAM/disque d'une VM sont
désormais **estimés depuis les logiciels hébergés + le socle SE**, au lieu d'hériter
des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant).
Chaque rôle déclare son empreinte (`roles/<rôle>/meta/empreinte.yml`) ; le
générateur somme par hôte (marge + arrondis) et écrit `proxmox_coeurs`/
`proxmox_memoire`/`proxmox_disque_taille` ; le clonage passe `cores`/`memory` à
Proxmox (`omit` si absent → aucune régression). Override par hôte possible dans le
plan (`serveurs.yml`). Voir `docs/dimensionnement-ressources.md`.
### Corrigé
- **`make instancier-appliquer FORCE=1` n'honorait pas `--force`.** La recette Makefile lançait `instancier.py appliquer` sans relayer `FORCE` ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais `$(if $(FORCE),--force)`. Trouvé en dogfooding.
## 2026-06-24 — Première publication publique
Première mise à disposition publique de **Set-OPS**, moteur Ansible d'écosystèmes
numériques souverains sur Proxmox — offert à la communauté québécoise par
l'**Alliance Boréale**, à la Saint-Jean-Baptiste 2026.
- **Moteur générique, piloté par un plan déclaratif** : on édite le plan
(`instance/plan/*.yml`), l'inventaire Ansible se génère, les VM se clonent depuis
un golden template Debian 13, les rôles s'appliquent par groupes. Tout passe par
`make`, le GUI local et la documentation.
- **Piliers d'un écosystème souverain** : socle Debian durci, AC/PKI interne, DNS
interne, identité (LDAP + SSO), relais courriel, bases de données, observabilité,
forge.
- **Catalogue de modèles prêts à déployer** (`exemples/modeles/`) : un hébergeur
copie un modèle, le renseigne à ses couleurs, et instancie.
- **Souveraineté jusqu'au bout** : Set-OPS s'exploite entièrement à la main, sans
aucune IA.
Pour démarrer : **`QUICKSTART.md`**.