# CHANGELOG — Set-OPS ## 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 `