# CHANGELOG — Set-OPS ## 2026-08-01 (suite 11) — les ports physiques entrent dans le modèle Les noms de ports n'étaient modélisés **nulle part** : `` 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 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 `` 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 `` 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 ` — 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 (``, ``) 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* (P01–P21), 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`, P01–P21. - **Le GUI (console d'exploitation)** — éditer la source, dry-run avant apply, l'invalide impossible à saisir (le `