Commit graph

200 commits

Author SHA1 Message Date
6f04f4c6fc docs : le responsable désigné du tenant, et la réversibilité nuancée
Chaque tenant a un responsable désigné — la personne qui engage
l'organisation, et dont la signature seule vaut mandat de migration. 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.

La table des états annonçait une réversibilité « gratuite » jusqu'à
`libéré`, alors que le §6 établit qu'elle change de nature à la bascule. Pas
contradictoire, mais un lecteur pressé s'arrêtant au tableau en retirait une
fausse impression. Ce qui reste gratuit est l'adressage, pas le retour.

Deux questions de gouvernance ajoutées aux points à trancher : où le
responsable est déclaré et comment on en change — acte au moins aussi
sensible que la migration, puisqu'il décide qui pourra la mandater ensuite ;
et la rétention, convenue avec qui, consignée où, attestée par qui.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:49:33 -04:00
041e36ca7c docs : le retour arrière de la migration, et deux renvois faux
La recette affirmait la réversibilité sans décrire le retour. Or il change
de nature à la bascule, et le geste évident — repointer le DNS — devient
faux à cet instant : les utilisateurs ont écrit chez l'entrant, et ces
données n'existent nulle part ailleurs. Les perdre serait silencieux.

Trois régimes écrits : avant le gel (sans conséquence), pendant le gel
(dégeler), après la bascule (migration inverse, même outillage).

Rendu explicite : le sortant reste gelé après la bascule, jusqu'à
confirmation. Le dégeler « au cas où » créerait deux copies vivantes 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 distingués : la bascule fait perdre le retour
gratuit, la purge fait tout perdre. D'où l'exigence ajoutée : les critères
de confirmation se fixent par écrit AVANT la première bascule.

Corrigés : 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:45:56 -04:00
351e1c1e58 docs : recette de migration d'un tenant entre hébergeurs
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 : le mandat appartient au
client, verrou par défaut, deux actes délibérés et traçables. Là où
l'analogie casse elle est remplacée, pas étirée — sans registre central pour
arbitrer, 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.

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 pendant que
le sortant sert ; l'interruption se réduit au delta plus la propagation DNS.
Garde de transition, pas conseil : `gelé` est inaccessible tant que
`préparé` n'est pas prouvé.

Deux pièges consignés : le TTL s'abaisse à l'étape 1 et non à la bascule ;
l'entrant doit être vérifiable sans être public, sinon le tenant sert des
deux côtés et l'identité se dédouble.

La libération est une révocation, pas une transmission : re-clétage de la
voûte chez l'entrant, sans quoi l'ancien hébergeur garde à vie l'accès aux
secrets d'un client parti.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:41:15 -04:00
517119c7ac frontière : ses intrants appartiennent à l'hébergeur, pas au tenant actif
Un hébergeur sert plusieurs tenants et n'a qu'une frontière. Ses intrants
é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.

Même famille que le défaut de l'underlay corrigé plus tôt ; c'est la
distinction hébergeur/tenant qui le fait apparaître.

Le devis et le panneau lisent maintenant la frontière chez l'hébergeur, qui
n'est pas déclaré pour autant : le symlink `underlay.yml` le désigne déjà.
Repli sur l'instance active sans underlay monté.

Vérifié : devis identique avec l'hébergeur actif, intrants conservés avec un
invité actif.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:17:10 -04:00
7256486c9c modèles : l'underlay devient modélisable, et P17 le valide
Un modèle décrivait un tenant — ses services, ses zones, ses bases. Tous les
hébergeurs n'ont pas le même matériel : l'infrastructure physique mérite le
même traitement.

Le modèle public gagne un underlay volontairement minimal (un commutateur,
pas de fabric de stockage séparée), point de départ honnête d'un petit
hébergeur. Les montages plus riches 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.

`modeles.py verifier` le valide (P17), facultativement et sur sa cohérence
INTERNE seulement — pas contre les tenants fédérés réels, un modèle étant un
gabarit et non un site déployé. Il a fallu rendre paramétrables deux
hypothèses du validateur, qui lisait la nomenclature de l'instance active et
globait les dépôts frères ; comportement par défaut inchangé.

Cinq cas de rejet exercés : 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:11:18 -04:00
0eae97c6ec underlay : monté par symlink depuis le dépôt de l'hébergeur
Il vivait gitignoré à la racine du moteur : consommé par deux générateurs,
validé par P23, versionné nulle part. La dizaine de modifications de la
journée n'était récupérable d'aucune façon, et un clone frais repartait du
gabarit.

Il appartient à l'hébergeur — ses switches, ses câbles — 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 initiale.

Le moteur le monte par symlink comme il monte le plan par `instance/`. Ce
lien NE SUIT PAS `make instance-utiliser` : la fabric reste celle de
l'hébergeur quel que soit le tenant actif.

Vérifié : les deux devis sortent identiques octet pour octet avant et après,
P23 verte, 24 preuves. Symlink brisé (clone sans dépôt frère) : dégrade
proprement, sections omises au lieu d'échouer. Cas exercé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:05:12 -04:00
91bbad0cdf frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.

`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.

Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.

Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
26090e8acd frontière : chaque règle porte son interface d'arrivée
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 du sens du flux : `ingress`/`externe` arrive par le
WAN, `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. Le montage parallèle décrit ce jour lève la dernière
inconnue.

Le rendu abandonne `pass out` pour `pass in on <interface>`, l'idiome réel
d'OPNsense et ce que le client d'API devra envoyer.

Ajouté aussi :
- `opnsense_wan_ip`, la face publique, en section Frontière du panneau ;
- invariant du dernier octet (P23) : un point de routage porte le même
  dernier octet sur tous ses sous-réseaux. Le chiffre vient de
  `reservations.passerelle`, pas d'une constante. Exemption des liens plus
  étroits qu'un /24 — sur le /29 de transit l'adressage est dicté par les
  participants. Vérifié que l'invariant tenait déjà sur les 13 sous-réseaux
  routés avant d'écrire la garde.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:06:55 -04:00
53f6af3f1a frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, 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'une ligne dise pourquoi.

La résolution est 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, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.

Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.

Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
4d37570c0c frontière : la sortie générale est déclarée, pas subie
Le devis 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
d'apt, plus de NTP, plus de récursion DNS. Rien ne le signalait — la ligne
la plus lourde de conséquences du devis, posée à la suite des autres.

La sortie est déclarée dans le registre, donc dérivée :
- `serveur_debian` (socle, 14 hôtes) : 443 et 80 pour les dépôts apt, 123/udp
  pour l'horloge — une dérive fait échouer step-ca et le SSO des semaines
  après la cause ;
- `client_unbound` : 53 udp et tcp, la récursion depuis la racine qu'implique
  `client_unbound_transitaires: []`. Le TCP 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.

Vérifié : les nftables d'hôte sont inchangés, octet pour octet. Le pair
`externe` reste sauté par resoudre_flux.

Non déclaré volontairement : le rôle `chrony` n'est référencé par aucun
groupe ni playbook — un flux pour lui aurait été une règle morte.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:10:45 -04:00
9804573f12 frontière : les intrants d'interface demandent l'identifiant OPNsense
`opt1`, `igb1` et `TENANTS` désignent le même port — identifiant interne,
périphérique FreeBSD, libellé affiché. L'API REST ne parle que du premier.
Les libellés des deux intrants ne le disaient pas, et la question s'est
posée en pratique.

Ils le disent maintenant, et docs/frontiere-opnsense.md explique les trois
couches et pourquoi `opt1` est le plus stable : il survit à un changement de
carte comme à un renommage.

La note « prochain_saut dérive de l'underlay » est repliée dans l'en-tête
régénéré par le panneau : une sauvegarde l'effaçait, le fichier étant
réécrit depuis le YAML analysé. Vérifié qu'une sauvegarde préserve valeurs
et références de voûte.

Corrigé au passage : après le renumérotage du /29, deux passages de la doc
annonçaient encore 10.0.4.2 comme prochain saut et contredisaient le devis.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 13:02:27 -04:00
7dc7720b4b client_pki : l'empreinte du root CA n'est pas un secret de voûte
Le rôle dérive l'empreinte à chaud depuis l'autorité, parce qu'un from-zero
régénère l'AC avec une empreinte neuve. Figée en voûte, elle 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 un défaut lisant
`vault_step_ca_fingerprint`. Ce défaut était mort : la tâche suivante écrase
le fait sans condition, donc la valeur de la voûte n'avait aucun effet.

Le recensement de voute.py s'y laissait prendre — il cherche la chaîne
`vault_*` sans pouvoir savoir qu'un défaut n'est jamais lu. La « source
unique » avait hérité de l'erreur, et le panneau réclamait un secret
impossible à fournir avant que l'AC n'existe.

Défaut retiré : la clé disparaît du recensement (25 -> 24 exigés), du
gabarit et du panneau. `client_pki_ca_fingerprint_override` reste le moyen
d'épingler une empreinte.

`docs/intrants-communs.md` §H était une troisième copie manuelle de la liste
des secrets, avec les deux mêmes erreurs ; elle renvoie maintenant à
`voute.py lister` et à la preuve P18.

Preuves : 24 OK, 0 échec ; voute.py verifier --strict passe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:37:27 -04:00
8bc8589163 GUI : le rappel des secrets dérive de voute.py
`SECRETS_ATTENDUS` était une liste tenue à la main, en parallèle du
recensement que voute.py fait déjà depuis le plan, les rôles et les
group_vars. Deux sources pour la même vérité, et la manuelle avait divergé :
elle annonçait `vault_ldap_sssd`, que rien ne consomme — aucun rôle sssd
n'existe — et omettait `vault_step_ca_fingerprint`, réellement lu par
client_pki pour vérifier l'empreinte de l'AC racine.

Un opérateur qui suivait le panneau créait un secret inutile et en oubliait
un nécessaire.

Le rappel dérive maintenant de `voute.secrets_exiges()` — la même source que
la preuve P18 — augmentée de SECRETS_HORS_MOTIF pour les jetons Proxmox, qui
n'ont pas le préfixe `vault_`. Vérifié : 27 noms, écart nul avec le gabarit.
Dépôt public nu : liste vide plutôt qu'une erreur d'affichage.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:25:41 -04:00
d00b60149d GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui 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 ; l'underlay, non.

La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, 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. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.

Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
76e82bfb96 underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.

Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).

Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.

La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.

Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
f80b53b592 devis switch : 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 — alors que la partie B posait tout dans le
même bloc. Deux conventions pour la même chose dans un seul document.

Le portfast est posé avec son interface (sections 4 et 4b). La section 6 se
réduit au global — mode, priorité du pont racine — plus les deux
avertissements : les rayons de la 4c n'en sont volontairement pas, et
pourquoi BPDU guard n'est pas émis.

Vérifié : aucune interface déclarée deux fois dans une même partie,
3 portfast avec `stp`, 0 sans lui, 0 sur un rayon.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:35:32 -04:00
92d31c0406 devis switch : les rayons de l'étoile ne sont pas des ports de bord
L'ajout du spanning-tree venait de rendre dangereuse une imprécision de
titre. 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 revenait à mettre portfast sur les
liens qui portent les BPDU — désactiver la protection anti-boucle là où elle
sert.

Les rayons sont dérivés et émis à part (4c côté routeur, B3a côté accès), un
par switch d'accès, marqués comme non-bord. La section 4 ne désigne plus que
les hyperviseurs ; la partie B distingue sa montante de son trunk terminal.

`underlay.switches_acces()` devient la source unique du « qui est un switch
d'accès » — pour leur devis ET pour les rayons côté routeur.

Vérifié : 3 commandes portfast avec `stp` déclaré, 0 sans lui, 0 sur un
rayon.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:31:31 -04:00
8f61721cb3 spanning-tree : RSTP dérivé de la topologie déclarée (étoile)
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois
switches, une boucle par brassage accidentel est une tempête de diffusion.

La topologie se déclare (`stp: {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 plutôt qu'une élection arbitraire. Les
switches d'accès reçoivent une priorité haute — jamais racine.

Ports terminaux déclarés en bord de réseau. BPDU guard délibérément NON
é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 au lieu de laisser croire à une protection indispensable.

Validation : `mode` et `topologie` contrôlés, `stp` sans `routeur` refusé
(aucun pont racine désignable). Sans `stp`, la section signale l'absence de
protection au lieu de disparaître.

Réserve : la forme `binardat` du spanning-tree n'est pas vérifiée sur le
matériel, comme les `ip route`.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:50:55 -04:00
42cd269d29 devis switch : le roster d'hôtes se limite à la fabric configurée
Les VLAN et les trunks étaient déjà filtrés par fabric ; le roster de la
section 0, non. Un équipement d'une autre fabric — les switches de stockage,
le jour où ils seront déclarés — serait apparu en commentaire dans un devis
qui ne le concerne pas. 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.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:41:49 -04:00
31e5121d68 underlay : chaque réseau déclare sa fabric physique
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 porte une `fabric` (`principal` par défaut). Le devis ne
configure que celle du routeur, et énonce explicitement ce qu'il ne couvre
pas au lieu de le taire — VLAN, trunks et spanning-tree d'une autre fabric
ne le regardent pas.

Les `deny` de l'ACL couvrent en revanche toutes les fabrics : 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é.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:36:02 -04:00
e1bbeb78f8 transit : bifrost-1/-2 en .1/.2, le SVI du switch remonte en .6
Les frontières occupent le bas du /29, le SVI du switch le haut. `.3` reste
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é, et ce sera le seul changement.

Plan du lien :
  10.0.4.1  bifrost-1   frontière active, sortie par défaut de la flotte
  10.0.4.2  bifrost-2   seconde frontière
  10.0.4.3  libre       réservée VIP CARP
  10.0.4.6  sleipnir-01 SVI du switch routeur

Les deux devis suivent sans intervention : routes du switch vers 10.0.4.1,
routes tenants de la frontière via 10.0.4.6.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:22:29 -04:00
b613873c3a nommage : bifrost aux frontières, sleipnir à la fabric interne
`bifrost-0` et `bifrost-1` 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.2,
10.0.4.3) : 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.

Conséquence traitée : la partie B du devis aurait listé les deux pare-feux
parmi les « switches d'accès ». Elle ne retient plus 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é.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:18:16 -04:00
7e852b5eef underlay : le routeur porte le SVI, et le transit sort de la partie B
Deux incohérences visibles à la relecture du devis complet.

`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éclaré à l'adresse du SVI qu'il porte, et `make underlay` refuse désormais
la divergence.

La partie B créait `vlan 40` alors que son trunk ne le transporte pas — le
transit ne relie que le routeur à la frontière. VLAN exclu de la partie B,
comme il l'est déjà des trunks généraux.

Preuves : 24 OK, 0 échec ; garde de cohérence exercée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:10:42 -04:00
2d2c5c69fa devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait 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.

Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- 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.

Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
7b2ba07ffc ACL de switch : les tenants n'atteignent plus 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 iSCSI et Ceph.
Une VM compromise atteignait la console physique des hyperviseurs.

Le commentaire du générateur disait « Reste -> passerelle OPNsense », ce qui
est faux pour l'underlay : ce trafic est routé LOCALEMENT par le switch et ne
passe jamais par la frontière, donc elle ne le filtre jamais.

Un `deny` par sous-réseau underlay est désormais émis avant le `permit`
final, dérivé de underlay.yml, dialecte respecté (masque normal ou wildcard).
Vérifié qu'aucun flux du registre ne vise l'underlay : rien de déclaré ne
casse. Sans underlay déclaré, l'ACL retrouve sa forme d'avant.

Consigné en §6 : le registre n'a pas de mot-clé `underlay` (un besoin
légitime, superviser l'hyperviseur, ne pourrait pas être déclaré), et le
devis émet un jeu unique de SVI pour trois switches sans MLAG.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:59:03 -04:00
0d64d28402 devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.

Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. 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 transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.

Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que 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é à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.

Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
069e549160 frontière : le prochain saut dérive du transit, il n'est plus saisi
`opnsense_prochain_saut` quitte le panneau Intrants. Il dérive du réseau de
transit de l'underlay — la `passerelle` du réseau portant `passerelle_sortie`.

Un seul bloc alimente les deux devis : 10.0.4.1 est le SVI côté switch ET le
prochain saut des routes tenants côté frontière ; 10.0.4.2 est la route par
défaut du switch. Le saisir en doublon rouvrait la possibilité de deux
valeurs contradictoires pour un seul lien — le mode de panne qu'on venait de
fermer pour les réseaux d'administration.

Le devis frontière expose le bloc `transit` (JSON compris) et cesse de
réclamer en section 0 les routes que `devis-reseau` émet désormais : il dit
lesquelles sont déjà émises, ou signale l'absence de transit déclaré. Sans
underlay, le marqueur revient — le repli reste explicite.

Preuves : 24 OK, 0 échec.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:38:17 -04:00
3698152b6a frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.

`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.

Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.

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. Les trois pare-feux et les routes ne peuvent plus diverger.

La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.

Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
  garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
  avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
  secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
  playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
  reliquat de compatibilité.

Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
4a1d0836d0 docs : un README par rôle (12 manquants) + carte remise à l'état du code
Solde la dette documentaire repérée à l'audit du 2026-07-03. Tous les rôles ont
désormais un README, au format maison (intention → rôle → variables →
notes/limites → prérequis). Ils documentent ce qui ne se lit PAS dans les tâches :

- serveur_debian : rôle-catégorie sans tâches — il ne porte que le flux SSH du
  plan de gestion, sans quoi les nftables générés couperaient l'accès Ansible à
  toute la flotte ; le vrai travail est dans le playbook de groupe.
- hosts_statiques : le plancher /etc/hosts comme CONDITION de l'ordre de
  reconstruction (résolution par nom avant que PowerDNS existe) ; fichier
  entièrement géré, alias d'exposition best-effort.
- resoudre_base / resoudre_annuaire : entrées, sorties (facts), et pourquoi le
  FQDN plutôt que le nom court (fédération + nom canonique pour verify-full).
- serveur_dovecot : les 3 réglages Dovecot 2.4 qui conditionnent la remise
  (static_allow_all_users, mail_inbox_path vide, mail_path par utilisateur) et
  le local-part seul comme chemin commun LMTP/IMAP ; limite mono-domaine.
- 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, sinon la signature ne vaut rien pour les MX distants.
- client_backup / serveur_backup : jobs déclaratifs, chiffrement côté client,
  deux pièges (jobs vides = silencieux ; dépôt neuf vide après un from-zero) ;
  hors-nœud n'est pas hors-site.
- serveur_oauth2_proxy : le patron réutilisable Keycloak-devant-n'importe-quoi,
  et pourquoi allow_unverified_email est nécessaire avec un annuaire LDAP.
- serveur_icingaweb2 : modes ldap vs external, écoute à restreindre en SSO.
- client_unbound : le garde-fou de bascule (apply ET confirm, validation réelle
  avant de toucher /etc/resolv.conf).

docs/carte-set-ops.md ne décrivait plus l'état du code :
- expose EST consommé au déploiement (annoncé comme « Phase 3 à venir ») :
  expositions_des_applications + expositions.conf.j2, alias /etc/hosts, SANs
  d'edge dérivés par instancier.py ;
- 3 mécanismes transverses ajoutés (résolution d'annuaire, plancher de
  résolution, resoudre_base nommé dans les bindings app→base) ;
- 5 entrées d'index ajoutées : réseau/pare-feu, ordre de déploiement,
  preuve/recette, exploitation courante, wiki — pans du corpus non indexés ;
- ce qui reste ouvert est marqué : meta/liens.yml sur le seul serveur_postfix,
  requiert non consommé au déploiement (l'ordre vient des couches + du graphe).

make verifier vert : ansible-lint 514 fichiers, tests, syntax-check,
23 preuves CONFORME / 0 échec / 0 sauté.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 11:32:06 -04:00
8c401f95cf wiki : intègre les 8 vues annotées à « Le GUI (console d'exploitation) »
La série d'illustrations (commit précédent) rejoint sa page. En fin de section
② de l'unité, une galerie « Les vues, annotées » présente les 8 figures — vues
éditables (Serveurs, Applications, Bases × 2, Domaines) puis dérivées (Flux,
Couches, Réseau) — chacune un SVG auto-contenu avec ses repères + légende.

Mécanique, sans duplication dans l'arbre source :
- symlink wiki/img -> ../docs/img : foyer unique dans docs/img/, refs relatives
  (img/*.svg) qui résolvent en local et sur le navigateur de dépôt ;
- make wiki-publier embarque désormais les docs/img/*-annote.svg (déréférencés)
  dans le wiki Forgejo publié — jusqu'ici seules les .md voyageaient, donc aucune
  image n'aurait rendu une fois publiée.

Preuves : 22 OK / 0 échec / 1 sauté (dont P22 « plan de recette à jour »).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:57:39 -04:00
bbd0972f58 docs : figures annotées des 7 autres vues du GUI (série complète)
Complète la série d'illustrations commencée avec la vue Serveurs : les 7
autres vues de la console (Applications, Bases × 2 — serveur BD et fiche de
base —, Domaines, Flux, Couches, Réseau), chacune en capture PNG + version
annotée en SVG AUTO-CONTENU (PNG intégré en base64, un seul fichier portable
qui rend dans les .md, le wiki Forgejo, un navigateur).

Même moule que Set-OPS-Serveurs-annote.svg : 8 repères aux couleurs Alliance
Boréale + légende deux colonnes. Points saillants par vue :

- Applications : catalogue des applis, éditeur (rôle/hôte/port/FQDN exposé),
  relations déclaratives (requiert/liens/bases), dépendances causales.
- Bases : serveurs SGBD + bases applicatives « nom @ serveur », bases hébergées.
- Bases (fiche) : portée/consommateur/serveur, secret Vault (jamais en clair),
  DSN dérivé masqué.
- Domaines : zones DNS publique vs interne, autorité/edge/DNSSEC, expositions.
- Flux : matrice d'audit (source de nftables ET justification), sens/chiffrement.
- Couches : ordre de déploiement en 6 couches (socle → agents), lecture seule.
- Réseau : flotte multi-instances, adressage dérivé du seed, bascule, devis switches.

La série des 8 vues est désormais complète — prête à intégrer aux .md / au wiki.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 13:48:36 -04:00
272f6369d8 Underlay : fabric physique cluster-global (mgmt/iSCSI/Ceph) + preuve P23
Le modele derive l'adressage par tenant (VLAN 1000+index*10+zone), mais la
fabric physique qui porte la flotte (mgmt switches/Proxmox/OOB, iSCSI, Ceph
public+cluster) n'appartient a aucun tenant. Elle est desormais codifiee.

- scripts/underlay.py + make underlay : charge/affiche/valide underlay.yml
  (VLAN < 1000, sous-reseaux hors des supernets tenant 10.(10+index).0.0/16).
- underlay.yml gitignore (comme le vault) ; gabarit public underlay.yml.example ;
  surchargeable par SETOPS_UNDERLAY.
- devis_reseau : section 0. Underlay + VLAN underlay sur le trunk, selon dialecte.
- P23 : underlay.py --verifier ; sautee si underlay.yml absent (comme P16 sans vault).

23/23 preuves. Doc : docs/audit/README.md, wiki page reseau, CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:56:38 -04:00
02f3122b3d wiki : dialecte de CLI du commutateur (cisco|binardat) sur la page reseau
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:38:32 -04:00
412448ba55 Devis switch : dialecte cisco|binardat (masque ACL + remark)
Le switch Binardat differe de Cisco sur deux points que le devis ignorait :
- masque d'ACL normal (255.255.0.0) au lieu du wildcard inverse (0.0.255.255) ;
- pas de commande 'remark' (commentaires d'ACL).

devis_reseau.py gagne un dialecte (defaut cisco, option binardat) : masque_acl()
et remarque() adaptent la sortie. Reglable par --dialecte, SETOPS_DIALECTE, ou
make devis-reseau DIALECTE=binardat. Le GUI suit l'env. Public reste generique.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:15:27 -04:00
58015f56e0 Plan de recette (P22) : le pendant manuel de make prouver
Les 78 exercices « À toi de jouer » du wiki forment 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. Grille auto-contenue par unité, colonnes : ce qu'on
  éprouve · le geste · type (observe/casse-répare) · Preuve auto (le Pxx extrait du
  texte -> quels gestes manuels sont AUSSI gardés par la machine). Générée -> ne peut
  pas dériver du wiki.
- Preuve P22 : plan_recette.py --verifier échoue si le fichier committé est périmé.
  Le plan de recette devient auto-gardé.
- Honnêteté de couverture assumée : « — » = manuel seul ; pas d'exhaustivité au-delà
  des exercices du wiki.

Pendant humain de make prouver (le harnais prouve le moteur P01-P21, la recette valide
l'exploitation) ; checklist du protocole-operateur-independant (« exploitable sans IA »).

Validé : 78 gestes / 19 unités, 5 doublés d'un Pxx ; P22 détecte une dérive (testé) ;
make verifier -> CONFORME 22/22 (instance cohérente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:49:16 -04:00
150121089d wiki : unité « Le GUI (console d'exploitation) »
Cinquième page de la vague méthode. Au moule à 4 temps : éditer la SOURCE (le
plan) pas l'artefact, dry-run avant apply, et l'invalide rendu impossible à
saisir (le <select> « Hôte » ne propose que des hôtes réels -> pas d'hôte
fantôme, garde P19). Exercices ancrés dans le réel (dont réparer un hôte
fantôme via le select). Raccordée dans _Sidebar (Socle & méthode).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:07:52 -04:00
ac7fe93165 wiki : l'axe « méthode » (plan dérivé, multi-instance, preuve, glossaire)
Le wiki enseignait les fondamentaux SERVICES mais pas la MÉTHODE. Quatre pages
au moule à 4 temps (concept -> Set-OPS -> transférable -> à toi de jouer), avec
exercices concrets :
- Le plan & l'adressage dérivé (un seed, tout en découle).
- Multi-instance & fédération (un moteur, N écosystèmes ; découverte par
  convention, garde-fou P21).
- La preuve (ne jamais affirmer plus que ce qu'on prouve ; make prouver P01-P21).
- Glossaire (24 concepts ; était « à venir »).

Raccordées dans Home.md et _Sidebar.md (section « Flotte & preuve »). Le wiki
pointe vers docs/, ne recopie pas. 855 -> 1573 lignes, 22 pages, aucun lien mort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:05:56 -04:00
4180913c25 Créer un modèle (make model-creer) — base + promotion d'instance
Symétrique d'instance-creer, produit un modèle dans le dépôt PRIVÉ
(Set-OPS-Modeles/, jamais exemples/modeles/). scripts/model_creer.py, 2 modes :
- MODE=base : copie un modèle générique.
- MODE=instance : PROMEUT une instance en modèle (généralise identité ->
  exemple.*, index -> 1, production -> false, nftables_admin_ssh -> [], cle de
  sauvegarde videe, proxmox.yml generique [D1], inventaire principal [D2]).

Surete : aucun secret ne sort (vault.yml/proxmox.vault.yml jamais copies ; refus
si l'un subsiste ; .example conserve). Copie CIBLEE (plan/ + inventories/ seulement,
symlinks resolus) -> robuste aux depots imbriques et boucles de symlinks. Le modele
produit doit valider (modeles.py verifier), sinon annule.

Teste en isolement (depot prive jamais touche) : promotion du labo reussie +
validee (aucun chezlepro residuel, aucun secret, proxmox generique) ; copie socle
OK ; refus d'une instance INVALIDE (garde-fou : hote fantome detecte).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:43:06 -04:00
d8474d1c9f Créer une instance depuis un modèle (CLI + GUI)
Le moteur reste independant des instances ET des modeles assembles (prives) :
il ne contient que le modele public socle ; les modeles assembles sont
DECOUVERTS au runtime via SETOPS_MODELES (depot prive de l'operateur), jamais
embarques dans le code public.

- scripts/instance_creer.py : copie un modele (exemples/modeles/* + SETOPS_MODELES)
  vers un depot frere ../<nom>, y fixe l'index, et refuse un nom existant, un
  modele inconnu, ou un index en collision avec une instance federee (verifie
  AVANT toute copie). .git et hosts.genere.yml non copies.
- make instance-creer NOM=.. MODELE=.. [INDEX=N] ; make instance-modeles.
- GUI (vue Reseau) : formulaire « Creer une instance depuis un modele » sous la
  flotte. POST /api/instance-creer ; /api/instances renvoie aussi modeles +
  index_pris. Ne bascule pas l'active.

Corrige : gabarit de voute du labo complete (P18 a echoue en passant l'active
sur le labo, dont le vault.yml.example etait reste a 17 cles ; aligne sur 23).

Valide : garde-fous de creation testes en isolement (collision refusee avant
copie, ecrasement refuse, modele inconnu refuse, aucune pollution) ; node --check ;
make verifier rc=0 CONFORME 21/21 (active = labo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:13:36 -04:00
40cead2a66 gitignore : ignore .claude (config Claude Code locale)
Évite de committer une configuration Claude Code locale au dépôt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:55:33 -04:00
9223e0823e docs : figure annotée de la vue Serveurs
Première figure d'illustration de la doc : capture de la vue Serveurs du GUI
(docs/img/Set-OPS-Serveurs.png) + sa version annotée en SVG AUTO-CONTENU
(docs/img/Set-OPS-Serveurs-annote.svg) — le PNG y est intégré en base64, un
seul fichier portable (rend dans les .md, le wiki Forgejo, un navigateur).

8 repères aux couleurs Alliance Boréale + légende : inventaire actif & flotte,
onglets/registres, « Appliquer le plan », carte serveur dérivée, tuiles
VMID/IP/VLAN dérivées du seed, formulaire IDENTITE (édition du plan),
intégrations client_*, dépendances causales (garde-fou requiert/manquant).

À intégrer aux .md quand la série de captures sera complète.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:54:57 -04:00
1ad7b48483 GUI : bascule d'instance (vraiment multi-instance)
Demande explicite et repetee de l'operateur : gerer les instances DEPUIS le GUI.

- Inventaire resolu DYNAMIQUEMENT : le serveur figeait l'inventaire au demarrage
  (args.inventaire.resolve()). Il est desormais relu depuis le symlink instance/
  a chaque requete (propriete Gestionnaire.inventaire) -> la bascule prend effet
  sans redemarrer. Les FICHIER_* de plan suivaient deja le lien. 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 + validation STRICTE (nom doit etre une
  instance decouverte -> pas de traversee de chemin ; ../etc refuse, teste).
- Vue Reseau : bouton « Activer » par instance dans la table de flotte. Confirme
  (avertissement renforce si production), bascule, recharge -> toutes les vues et
  les deploiements visent la nouvelle instance.

L'edition de federe reste au CLI (rarement change) ; la bascule est CLI ET GUI.

docs/carte-set-ops.md : la ligne Multi-instance du tableau des mecanismes documente
la decouverte (glob des dossiers freres, aucun registre) et le garde-fou P21.

Valide : node --check ; bascule testee de bout en bout (repoint -> inventaire
dynamique suit -> traversee refusee -> restauration) ; make verifier rc=0
CONFORME 21/21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:12:29 -04:00
dbc78f69ec Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
  l'active (star), montre index / plage VLAN / statut federe-local / prod, et
  affiche une banniere de COLLISION d'index — le pendant visuel de make instances
  et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
  federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
  rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.

docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
  freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
  sandbox « sans index », plafond 9). Remplace par le modele derive du seed
  (tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).

Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
b1460a6dde Multi-instances : make instances (vue + collision) + preuve P21
La bascule d'instance existait deja (make instance-utiliser / instance-courante,
repointage du symlink). Manquaient la vue d'ensemble et le filet de securite.

- make instances (scripts/instances.py) : liste les instances de la federation
  (depots freres avec plan/nomenclature.yml), marque l'active (*), montre index,
  plage VLAN derivee, statut federe/local et production. Lecture seule.
- Detection de collision d'index entre instances FEDEREES (memes VLAN/VMID sur le
  trunk) : le piege exact vecu (prod + labo tous deux a l'index 1) est desormais
  crie. Mode --verifier -> rc=2 sur collision.
- Preuve P21 : cable ce garde-fou dans make prouver / make verifier. No-op quand
  moins de deux instances federees sont presentes (comme P17 sans SETOPS_MODELES).

Valide : make instances liste les 3 instances (labo LOCAL, Technolibre + Chezlepro
federees, index 2 et 13) ; detection testee en synthetique (deux federees meme
index -> collision ; labo federe:false -> coherent) ; make verifier rc=0
CONFORME 21/21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:26:01 -04:00
d2345272d4 devis-reseau : noms uniques par tenant + drapeau federe
Deux corrections, revelees quand Chezlepro a eu deux instances (labo index 1 +
prod index 13) decouvertes par le devis :

1. Collision de noms. prefixe() reduisait OPS-Chezlepro ET OPS-Chezlepro-lab au
   meme "CHEZ" (retrait de -lab + troncature 4 lettres) -> deux VLAN "CHEZ-Frontiere"
   et surtout deux ACL "CHEZ-ISOLATION", que le switch aurait FUSIONNEES (config
   cassee). Les noms sont desormais qualifies par l'index (CHEZ1-*, CHEZ13-*,
   TECH2-*) : uniques, et le tenant + son index se lisent dans le nom. Les numeros
   de VLAN encodaient deja l'index ; les noms le font maintenant aussi.

2. Drapeau federe. decouvrir() saute toute nomenclature avec `federe: false`
   (defaut true). Permet d'exclure un bac a sable LOCAL (labo, sans remote, pas sur
   la fabric de production) : on ne provisionne pas ses VLAN sur les switches de
   prod. Le labo genere toujours son propre inventaire.

Verifie : make devis-reseau -> labo (1011-1016) exclu, restent Technolibre
(1021-1026) et Chezlepro prod (1131-1136) ; SVI/ACL coherents (access-group ==
nom d'ACL) ; make verifier rc=0 CONFORME 20/20.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:21:34 -04:00
36a882b125 Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.

- inventory_rules : source unique de derivation — supernet_de, base3_de,
  sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
  (2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
  deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
  tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
  (plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
  le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
  derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.

Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.

Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
7e190e0a57 Trois preuves qui regardent au-dela d'une seule instance + champ liens/websocket au GUI
Le harnais ne verifiait qu'UNE instance et le seul modele socle. Tout ce qui vit
a cote du moteur echappait au controle. Trois preuves ferment ces angles morts :

- P17 (scripts/modeles.py) : TOUS les modeles valident, pas seulement socle.
  SETOPS_MODELES=../Set-OPS-Modeles inclut les modeles assembles prives. A trouve
  6 modeles invalides sur 7 (corriges dans Set-OPS-Modeles).
- P18 (scripts/voute.py) : le gabarit vault.yml.example couvre EXACTEMENT les secrets
  que le plan exige (bases + roles actifs + group_vars). Ne dechiffre jamais la vraie
  voute : compare des noms.
- P19 (scripts/couverture_gui.py) : tout champ present dans un plan reel est editable
  par le GUI. A trouve applications.websocket (comble). Nomenclature toleree (trou connu).

GUI :
- champ « Liens (bindings) » dans l'inspecteur d'application : role -> cible en listes
  deroulantes, les roles proposes = ceux que le role porteur accepte (meta/liens.yml).
  Comble un manque : les bindings ne se declaraient qu'en editant le YAML a la main.
- champ « WebSocket » (Collabora).
- CHAMPS_ECRITS_PAR_GUI : declaration de ce que le GUI sait ecrire, verifiee par P19.

Garde-fou de fond : valider_applications refuse une application posee sur un hote non
declare (l'hote fantome exact qu'integral portait). Cable partout + POST du GUI.

liens_acceptes()/catalogue_liens() dans inventory_rules : source unique partagee par le
validateur, le GUI et instancier.py (dont la copie locale est retiree).

Valide : make verifier rc=0, CONFORME 19/19, ansible-lint 0 echec, 7 modeles valident,
DIFF VIDE, node --check du GUI OK. Piece justificative : docs/audit/preuve-2026-07-22.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 21:32:42 -04:00
131a30075d Aurore : opacites reduites ; etat de forge-01 elucide
Palette (promo/, les 4 pages) — les teintes rose, verte et violette etaient
trop presentes une fois etalees au tour precedent. Reduction d'environ 40 % :
rose .075 -> .044, vert .058 -> .034, violet .062 -> .036, arrets
intermediaires abaisses dans la meme proportion, opacite de la nappe .aurora
.44 -> .32. Geometrie inchangee : meme largeur, meme position, seule
l'intensite baisse. 0 conflit CSS entre les quatre pages.

forge-01 — la note precedente presentait comme une contradiction a trancher
le fait que le plan dise `etat: planifie` et que le CHANGELOG du 2026-07-03
dise le branding « prouve sur forge-01 ». Ce n'en etait pas une : l'hote a ete
cree, puis supprime. Le plan decrit l'etat COURANT, le CHANGELOG un etat PASSE
— les deux disent vrai. La preuve reste valable, simplement non rejouable tant
que l'hote n'est pas recree. roles/serveur_forgejo/README.md corrige.

Valide : equilibre accolades/parentheses du CSS, 0 conflit CSS,
prouver.py --verifier -> CONFORME 16/16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:57:50 -04:00
f8e754c0b0 Aurore rose-mauve et vert fluo ; thème Forgejo étendu au wiki
Palette (promo/, les 4 pages) — ajoute --rose:#ff6fc4 (frange magenta) et
--vert:#5cff9d (vert fluo), uniquement dans les DÉGRADÉS FONCÉS : fond fixe du
corps, nappe .aurora dérivante, filets .rule. Opacités de 5,5 % à 8,5 %, une
teinte et non un motif. Le vert entre par la gauche et le rose sort par la
droite, comme une vraie aurore. --aurora (texte et boutons) est inchangé :
l'identité de marque ne bouge pas. Appliqué identiquement aux quatre fichiers,
0 conflit CSS après coup.

Thème Forgejo (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 — couleurs officielles + ciel nocturne. Sûr, résiste aux
     mises à jour. ÉPROUVÉ sur forge-01 (CHANGELOG 2026-07-03).
  §2 Décor — fond aurore, filet sous les titres, citations, tableaux et code
     en ligne de .markup. Fragile (classes internes). JAMAIS RENDU.
Supprimer §2 ramène au thème sobre. Le ciel nocturne ne s'applique qu'aux
thèmes sombres, pour ne pas casser le thème clair.

docs/theme-forgejo-hors-flotte.md — pose manuelle 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 sur
le serveur. La procédure ne duplique aucun fichier : elle pointe vers ceux du
rôle. Inclut la détection du répertoire custom, un garde-fou pour ne pas
écraser un header.tmpl existant (ajout de ligne), la vérification curl et la
marche arrière.

Corrige deux affirmations fausses de ma part :
- « thème jamais rendu par un Forgejo réel » était faux pour §1, éprouvée.
  Le statut est désormais donné section par section.
- Contradiction consignée sur forge-01 : le plan la dit `etat: planifie`, le
  CHANGELOG 2026-07-03 dit le branding « prouvé sur forge-01 ». Les deux ne
  peuvent pas être vrais ; l'écart est écrit dans le README du rôle, à trancher.

Validé : équilibre accolades/parenthèses du CSS, 0 conflit CSS entre les quatre
pages, lien de doc résolu, ansible-lint 0 échec sur 485 fichiers,
prouver.py --verifier → CONFORME 16/16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:42:52 -04:00
7cc6b1e878 Rend les quatre pages promo cohérentes
Les pages ayant été écrites l'une après l'autre, six dérives s'étaient
installées entre elles. Passe de normalisation :

- Adresse au lecteur : index.html tutoyait (17 occurrences), les trois autres
  vouvoyaient. Vouvoiement partout — ce sont des pages commerciales.
- Marque : trois pages affichaient « Alliance Boréale », tarifs affichait
  « Chezlepro · tarifs ». Désormais « Set-OPS · Chezlepro » dans la barre et
  « Chezlepro inc. · Saint-Bruno-de-Montarville, Québec » au pied.
- Navigation : quatre barres différentes (libellés et ordres distincts) →
  Capacités · Offres · Tarifs · Valeur + « Nous parler », identique partout,
  page courante marquée aria-current="page".
- Pieds de page : quatre structures → une seule, licence AGPLv3 incluse.
- Vocabulaire d'état : index utilisait .tag proven/soon, les autres
  .st ok/wip/road. Un seul système : Éprouvé · En chantier · Feuille de route.
- CSS : 35 sélecteurs partagés avaient des déclarations divergentes → 0 conflit.
  Deux étaient de vrais conflits de mise en page (.beats 4 colonnes contre 3,
  .stats grille contre flex) ; passés en repeat(auto-fit,minmax(...)), une seule
  règle qui s'adapte au nombre d'éléments.

Ajoute une section « Nos réserves » à index.html — seule page à en manquer,
alors que c'est celle qui affirme le plus de capacités. Les quatre assument
désormais leurs limites (mainteneur unique, épreuve de l'opérateur indépendant).

promo/README.md consigne les conventions communes en tableau et fournit le
script de détection des conflits CSS à lancer avant commit.

Validé : liens, balises, identifiants, équilibre CSS et node --check du JS sur
les quatre pages ; 0 conflit CSS ; prouver.py --verifier → CONFORME 16/16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:19:38 -04:00