Set-OPS-Public/CHANGELOG.md
Daniel Allaire 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

143 KiB
Raw Blame History

CHANGELOG — Set-OPS

2026-08-02 (suite) — la sortie générale est déclarée, pas subie

Le devis frontière se terminait par block out log all avec une seule règle sortante (le relais SMTP). Appliqué tel quel, il coupait la flotte d'Internet : plus de apt, plus de NTP, plus de récursion DNS. Rien ne le signalait — c'était la ligne la plus lourde de conséquences du devis, posée à la suite des autres.

La sortie est désormais déclarée dans le registre, donc dérivée comme tout le reste :

  • serveur_debian (le socle, porté par les 14 hôtes) — 443/tcp et 80/tcp pour les dépôts apt, 123/udp pour l'horloge. Une dérive d'horloge fait échouer la validation des certificats step-ca et le SSO, des semaines après la cause.
  • client_unbound53/udp et 53/tcp : client_unbound_transitaires est vide, donc Unbound interroge lui-même la racine. C'est le choix souverain ; il a un coût réseau qu'il faut déclarer. Le TCP n'est pas optionnel — c'est le repli obligatoire dès qu'une réponse DNSSEC dépasse la taille UDP.

Le devis passe de 6 à 11 règles, et sa section 5 énonce le default-deny sortant, le nombre de règles qui l'accompagnent, et où déclarer un besoin oublié — jamais à la main dans le pare-feu, la règle serait perdue à la génération suivante.

Vérifié : les nftables d'hôte sont inchangés, octet pour octet. Le pair externe reste sauté par resoudre_flux.py — ces flux relèvent de la bordure, et d'elle seule.

Non déclaré volontairement : le rôle chrony existe mais n'est référencé par aucun groupe, aucun playbook ni le graphe de dépendances. Lui écrire un flux aurait créé une règle morte.

2026-08-02 — les interfaces se nomment par leur identifiant

opt1, igb1 et TENANTS désignent le même port dans OPNsense : l'identifiant interne, le périphérique FreeBSD et le libellé affiché. L'API REST ne parle que du premier, et c'est lui que veulent opnsense_if_wan et opnsense_if_transit. Le libellé de ces deux intrants ne le disait pas — la question s'est posée en pratique.

Les libellés le disent maintenant explicitement, et docs/frontiere-opnsense.md §6 explique les trois couches ainsi que le motif du choix : opt1 est le plus stable des trois, il survit à un changement de carte réseau comme à un renommage.

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

Corrigé — la doc portait encore l'ancien plan du /29

Après le renumérotage (bifrost-1/-2 en .1/.2, SVI en .6), deux passages de docs/frontiere-opnsense.md annonçaient toujours 10.0.4.2 comme prochain saut. La doc contredisait le devis généré ; les ip route des deux coïncident désormais.

2026-08-01 (suite 14) — l'empreinte du root CA n'est pas un secret de voûte

client_pki dérive l'empreinte à chaud depuis l'autorité (step certificate fingerprint en delegate_to sur serveur_step_ca) — précisément parce qu'un from-zero régénère l'AC avec une empreinte neuve. Une empreinte figée en voûte serait périmée dès la première reconstruction, et une empreinte périmée fait échouer le bootstrap de chaque hôte.

Or defaults/main.yml portait encore client_pki_ca_fingerprint: "{{ vault_step_ca_fingerprint | default('') }}". Ce défaut était mort : la tâche suivante écrase le fait sans condition. La valeur de la voûte n'avait aucun effet, quel qu'en soit le contenu.

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

Le défaut mort est retiré, et la clé disparaît d'elle-même du recensement (25 → 24 exigés), du gabarit et du panneau. client_pki_ca_fingerprint_override reste le moyen documenté d'épingler une empreinte (AC externe, migration).

Corrigé — une troisième copie manuelle de la liste des secrets

docs/intrants-communs.md §H énumérait les secrets à la main, avec les deux mêmes erreurs. Elle renvoie désormais à scripts/voute.py lister et à la preuve P18 plutôt que d'entretenir une copie de plus.

2026-08-01 (suite 13) — le rappel des secrets dérive de voute.py

SECRETS_ATTENDUS était une liste écrite à la main dans inventory_gui.py, en parallèle du recensement que scripts/voute.py fait déjà depuis le plan, les rôles des groupes actifs et les group_vars. Deux sources pour la même vérité, et la manuelle avait divergé :

Clé Panneau Gabarit Référencée
vault_ldap_sssd annoncée absente nulle part — aucun rôle sssd n'existe
vault_step_ca_fingerprint omise présente roles/client_pki/defaults/main.yml:24

Un opérateur qui suivait le panneau créait donc un secret que rien ne consomme, et oubliait celui dont client_pki a besoin pour vérifier l'empreinte de l'AC racine.

Le rappel dérive désormais de voute.secrets_exiges() — la même source que la preuve P18 — augmentée de SECRETS_HORS_MOTIF pour les jetons Proxmox, qui ne portent pas le préfixe vault_. Vérifié : 27 noms, écart nul avec le gabarit. Sur un dépôt public nu, la liste est vide plutôt qu'en erreur.

2026-08-01 (suite 12) — la fabric se règle depuis la console

Tout le modèle d'underlay bâti aujourd'hui — routeur, spanning-tree, fabrics, transit — s'éditait à la main dans un YAML, pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA. La frontière avait eu sa section dans le panneau ; l'underlay, non.

Une section Fabric couvre désormais les valeurs plates : le switch routeur, le dialecte de CLI, le mode et la topologie de spanning-tree. Les listes de tables (reseaux, hotes, et donc les ports) restent hors de portée du panneau — elles demandent une vue dédiée, comme celle des serveurs.

Le dialecte devient un intrant déclaré

Il ne vivait que dans SETOPS_DIALECTE, une variable d'environnement. C'est une propriété du matériel, donc de la fabric : elle se déclare dans underlay.yml. Précédence désormais explicite : drapeau --dialecte > variable d'environnement > intrant déclaré > cisco. make underlay refuse un dialecte inconnu.

Écriture chirurgicale, pas de safe_dump

underlay.yml porte 23 lignes de commentaires qui expliquent des décisions d'architecture — pourquoi un seul routeur, pourquoi le transit vit dans l'underlay, pourquoi les rayons ne sont pas des ports de bord. Un safe_dump les aurait toutes effacées, comme c'est arrivé aux commentaires de plan/applications.yml. Le panneau remplace donc la ligne existante en respectant son indentation. Vérifié : trois valeurs modifiées, 73 lignes et 23 commentaires avant comme après.

Une clef absente du fichier n'est pas créée : le panneau refuse explicitement plutôt que de l'inventer à un endroit arbitraire.

2026-08-01 (suite 11) — les ports physiques entrent dans le modèle

Les noms de ports n'étaient modélisés nulle part : <PORT-VERS-PROXMOX> et consorts étaient des marqueurs littéraux dans le générateur. L'opérateur les remplaçait à la main dans la sortie, et recommençait à chaque régénération. C'était le seul endroit du devis où le travail était perdu à répétition.

Ils se déclarent désormais par équipement dans underlay.yml, sous quatre clefs qui correspondent aux quatre natures de lien :

ports:
  hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3]   # ports terminaux (portfast)
  frontiere: [Gi1/0/23]                        # vers le pare-feu (portfast)
  rayons: { sleipnir-02: Te1/0/47, … }         # côté ROUTEUR
  montante: Te1/0/48                           # côté SWITCH D'ACCÈS

Le devis émet alors les vrais ports, y compris plusieurs vers les hyperviseurs — il n'en supposait qu'un seul jusqu'ici, alors que le cluster en compte trois. Non déclarés, les marqueurs reviennent : la dégradation est par équipement, un switch renseigné et un autre non cohabitent sans problème.

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

make underlay refuse un port déclaré deux fois sur un même équipement, un rayon vers un switch inconnu, des rayons sur autre chose que le routeur, une montante sur le routeur lui-même. Les quatre cas exercés.

2026-08-01 (suite 10) — une interface, un bloc

La partie A déclarait ses ports de bord deux fois : l'interface en section 4, son portfast en section 6. La partie B, elle, posait tout dans le même bloc. Deux conventions pour la même chose dans un seul document — un opérateur qui applique la partie A section par section configurait la même interface à deux endroits.

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

Vérifié : aucune interface n'est déclarée deux fois dans une même partie, trois portfast avec stp, zéro sans lui, zéro sur un rayon.

2026-08-01 (suite 9) — rayons de l'étoile séparés des ports terminaux

L'ajout du spanning-tree venait de rendre dangereuse une imprécision qui, jusque-là, n'était qu'un titre approximatif. La section 4 s'appelait « Trunk vers Proxmox + inter-switch » et n'émettait qu'un port, que la section 6 déclarait en bord de réseau. Réutiliser ce placeholder pour les rayons vers les switches d'accès revenait à mettre portfast sur les liens qui portent les BPDU — c'est-à-dire à désactiver la protection anti-boucle exactement là où elle sert.

Les rayons sont désormais dérivés et émis à part (section 4c côté routeur, B3a côté accès), un par switch d'accès, avec l'avertissement qu'ils ne sont pas des ports de bord. La section 4 ne désigne plus que les hyperviseurs, et la partie B distingue sa montante (B3a) de son trunk terminal (B3b).

underlay.switches_acces() devient la source unique du « qui est un switch d'accès » — utilisée pour leur devis et pour les rayons côté routeur : les deux ne peuvent pas diverger.

Vérifié : trois commandes portfast émises avec stp déclaré, aucune sans lui, et aucune sur un rayon.

2026-08-01 (suite 8) — spanning-tree dérivé de la topologie déclarée

Ajouté — underlay.stp et les sections 6 / B4

Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois switches, une boucle par brassage accidentel n'est pas discrète : c'est une tempête de diffusion.

La topologie se déclare (mode: rstp, topologie: etoile) et le devis en tire la configuration. Le routeur est désigné pont racine — il est le centre de l'étoile, tous les chemins passent déjà par lui, donc l'arbre logique suit le câblage physique au lieu de sortir d'une élection arbitraire. Les switches d'accès reçoivent une priorité volontairement haute : ils ne doivent jamais devenir racine.

Les ports terminaux (hyperviseurs, frontière) sont déclarés en bord de réseau. BPDU guard n'est délibérément pas émis : un pont Linux dont le STP serait activé enverrait des BPDU et ferait tomber le port côté hyperviseur. Le devis dit pourquoi, et à quelle condition l'ajouter.

En étoile, aucun lien n'est redondant : RSTP est un filet, pas une nécessité — le devis le dit plutôt que de laisser croire à une protection indispensable.

make underlay valide mode et topologie, et refuse un stp déclaré sans routeur : sans lui, aucun pont racine ne peut être désigné. Sans stp, la section signale l'absence de protection au lieu de disparaître.

Réserve consignée : la forme binardat des lignes de spanning-tree n'a pas été confrontée au matériel, comme les ip route.

2026-08-01 (suite 7) — l'underlay connaît ses fabrics physiques

Le stockage jumbo (iSCSI, Ceph) est porté par un réseau indépendant de deux switches 10G, sans câble commun avec la fabric convergée des sleipnir. Le modèle l'ignorait : le devis déclarait les VLAN 20/30/31 sur les switches convergés et les mettait dans leurs trunks. C'était faux.

Chaque réseau de l'underlay porte désormais une fabric (principal par défaut). Le devis ne configure que celle du routeur, et énonce ce qu'il ne couvre pas plutôt que de le taire :

! HORS PERIMETRE — fabric 'stockage' : stockage-iscsi (VLAN 20), ceph-public (VLAN 30), …
! Portee par des switches distincts, sans cable commun avec celle-ci :
! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage.

Conséquence directe : la question du spanning-tree ne se pose que sur la fabric principale — trois switches — et pas sur le stockage, dont les deux switches forment un domaine séparé.

Le roster d'hôtes de la section 0 est filtré de la même façon : un équipement d'une autre fabric n'apparaît pas, même en commentaire. Un devis est une configuration qu'on applique, pas un inventaire. Vérifié en déclarant un hôte de la fabric stockage — il reste absent, et la sortie du jour est inchangée.

Les deny de l'ACL couvrent en revanche toutes les fabrics, y compris celles hors périmètre : la règle porte sur l'adresse de destination, pas sur le câblage. Si un chemin s'ouvre un jour vers le stockage, il est déjà fermé.

2026-08-01 (suite 6) — nommage : bifrost aux frontières, sleipnir à la fabric

bifrost-1 et bifrost-2 sont réservés aux deux frontières OPNsense — Bifröst est le pont vers l'extérieur. Les switches internes deviennent sleipnir-01…03 : le cheval qui traverse les mondes, pas le pont qui en sort. La division du nom suit celle de l'architecture.

Les deux boîtiers sont déclarés comme hôtes du lien de transit (10.0.4.1, 10.0.4.2) : hors flotte Ansible, la déclaration documente le lien et réserve les noms. Le /29 choisi plus tôt les loge tous les deux, comme prévu.

Le plan du /29 est réorganisé en conséquence : frontières en bas (.1, .2), SVI du switch en haut (.6), et .3 laissée libre pour une future IP virtuelle CARP si les deux OPNsense passent en haute disponibilité. Ce jour-là, passerelle_sortie pointera sur la VIP plutôt que sur un boîtier nommé — un seul endroit à changer.

Conséquence à traiter : la partie B du devis switch aurait listé les deux pare-feux parmi les « switches d'accès ». Elle ne retient désormais que les hôtes du réseau de management — un équipement déclaré ailleurs ne reçoit aucune ligne de configuration de switch.

Le devis frontière nomme le boîtier quand il est déclaré : frontiere 10.0.4.1 (bifrost-1).

2026-08-01 (suite 5) — deux incohérences du devis switch

Corrigé — le routeur avait deux adresses de gestion contradictoires

bifrost-01 portait le SVI Vlan10 → 10.0.0.1 et était déclaré dans underlay.hotes à 10.0.0.2. Une interface VLAN n'a qu'une adresse primaire : les deux ne pouvaient pas être vraies. L'entrée datait d'avant la désignation du routeur, quand 10.0.0.1 était une passerelle abstraite.

Le routeur est désormais déclaré à l'adresse du SVI qu'il porte, et make underlay refuse la divergence : ip 10.0.0.2 != passerelle 10.0.0.1 — le routeur porte ce SVI, les deux doivent coïncider. Le roster des trois switches reste complet.

Corrigé — VLAN de transit déclaré sur les switches d'accès

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

2026-08-01 (suite 4) — un seul switch route, les autres en L2 pur

Décidé — underlay.routeur

Sans MLAG, le routage est porté par un unique switch (bifrost-01 chez Chezlepro) ; les autres restent en L2 pur. Le devis émettait jusqu'ici un jeu unique de SVI sans dire à quel switch il s'adressait : appliqué sur les trois, il aurait créé autant de conflits d'adresses qu'il y a de zones.

Ajouté — le devis se scinde en deux parties

  • Partie A — switch routeur : VLANs, SVI, ACL d'isolation, trunks, routes. Va sur lui seul, et l'en-tête le nomme.
  • Partie B — switches d'accès (L2 pur) : mêmes VLANs pour commuter les trames étiquetées, une adresse de gestion par switch (tirée de underlay.hotes) avec ip default-gateway vers le routeur, et les trunks. Aucun SVI de zone, aucune ACL, aucune route.

Deux gardes : make underlay refuse un routeur qui ne nomme aucun hôte déclaré ; et la partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine — les coller tous écraserait l'adresse. Sans routeur désigné, l'en-tête signale explicitement le risque de duplication au lieu de laisser croire que le devis est applicable partout.

Reste ouvert et consigné : la syntaxe des ip route n'est pas dialecte-consciente, contrairement aux ACL.

2026-08-01 (suite 3) — les tenants n'atteignent plus l'underlay

Corrigé — ACL de switch : deny vers la fabric physique

L'ACL d'isolation bloquait l'autre tenant, puis se terminait par permit ip <tenant> any. Ce any autorisait 10.27.x10.0.0.0/24 : le management des switches, celui de Proxmox et l'OOB/IPMI, plus les réseaux iSCSI et Ceph. Une VM compromise atteignait la console physique des hyperviseurs.

Le commentaire du générateur disait « Reste → passerelle OPNsense », mais c'est faux pour l'underlay : ce trafic est routé localement par le switch et ne passe jamais par la frontière, donc il n'est jamais filtré par elle.

devis_reseau.py émet désormais un deny par sous-réseau underlay avant le permit final, dérivé de underlay.yml — dialecte respecté (masque normal ou wildcard). Aucun flux du registre ne vise l'underlay : le blocage ne casse rien de déclaré.

Deux limites consignées dans docs/frontiere-opnsense.md §6 : le registre des flux n'a pas de mot-clé underlay, donc un besoin légitime (superviser l'hyperviseur) ne pourrait pas être déclaré ; et devis_reseau.py émet un jeu unique de SVI pour trois switches sans MLAG, ce qui reste une décision d'architecture ouverte.

2026-08-01 (suite 2) — le port vers la frontière, et l'ordre d'application

Ajouté — section 4b : le port du switch vers la frontière

Le devis étiquetait le VLAN de transit sur le trunk <PORT-VERS-PROXMOX> et n'émettait aucune interface vers le pare-feu. Deux erreurs en une : les hyperviseurs n'ont pas d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN tenants — il route vers eux, il ne les étiquette pas. Le VLAN de transit sort donc du trunk Proxmox et prend son propre port, dérivé du transit déclaré.

Ajouté — avertissement d'ordre en tête de la section 5

Les routes de la section 5 déplacent la sortie du switch, y compris celle de ses propres réponses. Tant que l'adresse de la frontière ne répond pas, elles coupent l'accès d'administration au switch lui-même — le mécanisme qui a rendu une VM muette le 2026-07-29, appliqué cette fois à l'équipement depuis lequel on travaille.

Le devis crachait ces lignes à la suite des autres comme si elles étaient équivalentes. Il énonce désormais les préalables (boîtier câblé, adressé, joignable depuis le switch, session console ouverte) — inacceptable autrement pour un outil censé être exploitable sans IA.

2026-08-01 (suite) — le prochain saut dérive du transit

opnsense_prochain_saut n'est plus un intrant du panneau : il dérive du réseau de transit de l'underlay (la passerelle du réseau portant passerelle_sortie).

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

Le devis frontière gagne au passage un bloc transit (JSON compris) et cesse de réclamer en section 0 des routes que make devis-reseau émet maintenant : il dit lesquelles sont déjà émises, ou signale l'absence de transit déclaré. Sans underlay, le marqueur <PROCHAIN-SAUT-SWITCH> revient — le repli reste explicite.

Reste un seul intrant à figer au câblage : opnsense_if_transit, le nom de l'interface qui porte le VLAN 40 sur le boîtier — la seule valeur que rien ne peut deviner.

2026-08-01 — le lien de transit et les deux routes (la boucle est fermée)

Ajouté — réseau de transit dans l'underlay (clé passerelle_sortie)

Le lien entre le routeur est-ouest (switches L3) et la frontière nord/sud manquait dans tous les fichiers : le devis switch ne contenait pas une seule ip route.

Il vit dans l'underlay et non dans un tenant, pour une raison qui tranche : la frontière route vers tous les supernets tenants par le même prochain saut. Le lien est donc partagé et ne peut dériver d'aucun index. Un réseau underlay portant passerelle_sortie (l'adresse du pare-feu sur le lien) le déclare — chez Chezlepro : VLAN 40, 10.0.4.0/29, SVI 10.0.4.1, frontière 10.0.4.2. Un /29 et non un /30 parce que deux pare-feux cohabitent pendant la transition.

underlay.py valide la nouvelle clé (sortie sur le lien, distincte du SVI, SVI obligatoire, un seul transit) — preuve P23, cas de rejet exercés un par un.

Ajouté — devis-reseau émet les routes (section 5)

Deux routes dérivées, et il en faut impérativement deux :

  • l'aller : ip route 0.0.0.0 0.0.0.0 <sortie> — sans elle, aucun hôte n'a de sortie ;
  • le retour : une route par réseau d'administration — sans elle, la réponse d'une VM revient au pare-feu par une autre interface que celle où l'état a été créé, et se fait jeter en silence. C'est le piège qui a coûté la passe de déploiement du 2026-07-29.

Les réseaux d'administration viennent de l'intrant nftables_admin_ssh : même source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN de la frontière — les trois pare-feux et les routes ne peuvent pas diverger. Sans transit déclaré, la section s'affiche en clair comme manquante plutôt que de disparaître silencieusement.

Corrigé — le panneau refusait d'enregistrer les intrants de la frontière

group_vars/opnsense.yml porte à la fois des paramètres anodins et deux références de voûte ({{ vault_opnsense_api_key }}). La fusion « préserve les clés non gérées » les relisait, et le garde-fou, qui ne regardait que les noms, les prenait pour des secrets soumis. Il regarde désormais la valeur : une référence de voûte est un pointeur et passe ; toute valeur réelle sur ces clés fait toujours échouer l'écriture. Ajout d'un refus explicite à l'entrée pour une requête forgée, au lieu d'un abandon silencieux.

Retiré — reliquat proxmox.vault.yml

La voûte est unique (group_vars/all/vault.yml) ; l'ancien fichier séparé n'était plus chargé automatiquement (aucun groupe proxmox dans l'inventaire) et entretenait la confusion. Supprimé de l'instance, avec son gabarit.

Au passage : supprimer_vm_debian.yml ne chargeait que ce reliquat pour ses secrets. Le supprimer tel quel aurait cassé make detruire, l'outil même du rebuild from-zero. Sa liste est alignée sur celle du playbook de clonage (all/vault.yml en dernier, il l'emporte), et la résolution du jeton depuis la voûte unique est vérifiée en exécution réelle.

2026-07-29 (suite) — la frontière OPNsense, dérivée du registre des flux

Ajouté — make devis-opnsense (+ preuve P24)

La bordure nord/sud devient un artefact dérivé, comme le devis switch. Rien de saisi à la main : ni port, ni adresse, ni nom d'hôte n'apparaît dans le générateur.

Le constat qui rend la chose évidente : resoudre_flux.py saute volontairement les flux pair: externe (scripts/resoudre_flux.py:184) parce qu'ils ne concernent pas le pare-feu d'hôte. Plusieurs raison du registre disent déjà « filtré à l'OPNsense ». La politique de la frontière était donc déjà écrite — il ne restait qu'à la dériver.

  • scripts/devis_opnsense.py — agrège les flux externe, résout les destinations depuis l'inventaire (hôtes actifs et planifiés : la frontière se prépare avant les VM), les supernets depuis les nomenclatures fédérées, et l'accès d'administration depuis l'intrant nftables_admin_ssh. Sort un devis relisible ou --json (destiné à l'API OPNsense).
  • Garde anti-lockout (P24)--verifier refuse un devis dont nftables_admin_ssh est vide : la règle SSH entrante n'aurait aucune source et le block in final fermerait l'accès d'administration. Même intrant que les nftables d'hôte : source unique, donc pas de divergence possible entre la bordure et les hôtes.
  • Section 0 du devis : les routes de retour à poser sur les switches. C'est le piège qui a coûté la passe de déploiement du jour — la passerelle de zone répond au ping, mais aucun hôte derrière elle n'est joignable, parce que la réponse revient au pare-feu par une autre interface que celle où l'état a été créé.
  • docs/frontiere-opnsense.md — les décisions d'architecture (frontière nord/sud, les SVI restent sur les switches L3), le partage des rôles entre les trois pare-feux, le chemin d'application par l'API, et ce qui reste ouvert (câblage, second VPN, sortie générale).

Corrigé — intrant nftables_admin_ssh vide sur l'instance Chezlepro

Il valait [] alors que group_vars/hotes_actifs.yml active nftables_baseline_enabled. Autrement dit : la flotte se serait mise en policy drop sans aucune règle autorisant le contrôleur, qui arrive par le VPN hors du sous-réseau de la flotte. Renseigné à 192.168.255.0/24. Le sous-réseau du second VPN (OPNsense) devra y être ajouté.

Ajouté — la frontière devient réglable depuis la console (section Frontière)

Les valeurs non sensibles du pare-feu de bordure sont de vrais intrants, pas un fichier YAML tenu à la main : un opérateur règle la frontière depuis le panneau « Intrants de base », sans IA et sans éditeur.

  • INTRANTS_SCHEMA — nouvelle section Frontière : opnsense_api_url, opnsense_api_verifier_certs, opnsense_if_wan, opnsense_if_transit, opnsense_prochain_saut. Cible d'écriture group_vars/opnsense.yml, en fusion (comme Proxmox) pour préserver les références de voûte du fichier. Le panneau rend ses sections génériquement : aucune modification d'interface n'a été nécessaire.
  • Garde-fouopnsense_api_key / opnsense_api_secret ajoutés à INTRANTS_CLES_INTERDITES : le GUI refuse de les écrire, donc impossible de coller un secret d'API dans le panneau par mégarde. Ils n'apparaissent qu'en lecture seule, par leur nom, dans SECRETS_ATTENDUS.
  • devis_opnsense.py lit désormais ces intrants et n'affiche ses marqueurs (<IF-TRANSIT>, <PROCHAIN-SAUT-SWITCH>) qu'en repli : le devis se complète de lui-même dès que la console est renseignée.

Ajouté — les identifiants d'API de la frontière, dans la voûte

Même partage que Proxmox : l'anodin en clair, le secret dans la voûte unique de l'instance.

  • group_vars/opnsense.yml (instance, en clair) — URL de gestion, interfaces et prochain saut à figer au câblage, plus les références par nom {{ vault_opnsense_api_key }} et {{ vault_opnsense_api_secret }}. Aucune valeur de secret n'y figure.
  • Gabarit de voûte — les deux clés ajoutées à vault.yml.example. scripts/voute.py les a recensées tout seul depuis les group_vars (il ne lit jamais la voûte, il ne compare que des noms) : le gabarit passe de 23 à 25 secrets, et la preuve P18 reste verte.

Validation : make verifier vert — 24 preuves CONFORME, 0 échec, 0 sauté.

2026-07-29 — dette documentaire soldée (un README par rôle) + carte revue

Ajouté — les 12 README de rôles manquants

Tous les rôles ont désormais un README. Les 12 restants sont écrits, au format maison (intention → rôle → variables → notes/limites → prérequis), et documentent surtout ce qui ne se lit pas dans les tâches :

  • Socle et résolutionserveur_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 utilitairesresoudre_base et resoudre_annuaire : entrées/sorties (facts), et pourquoi le FQDN plutôt que le nom court (fédération + verify-full).
  • Courrielserveur_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).
  • Sauvegardesclient_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 supervisionserveur_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), 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 P23underlay.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-recettegé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 P22plan_recette.py --verifier échoue si le fichier committé n'est plus à jour (le wiki a changé sans régénérer). Le plan de recette devient un artefact auto-gardé.
  • Honnêteté de couverture assumée dans le document : un « — » = manuel seul (aucune preuve machine ne le double) ; le plan ne prétend pas à l'exhaustivité au-delà des exercices du wiki.

C'est le pendant humain de make prouver : le harnais prouve le moteur (P01P21), la recette valide l'exploitation — et sert de checklist au protocole-operateur-independant.md (« exploitable sans IA »).

Validé : 78 gestes sur 19 unités, 5 doublés d'une preuve Pxx ; P22 testée (détecte une dérive) ; make verifierCONFORME 22/22 (contre une instance cohérente).

2026-07-23 (suite 5)

Ajouté — wiki : l'axe « méthode » (KB enrichie)

Le wiki enseignait les fondamentaux services (identité, PKI, courriel…) mais pas la méthode de Set-OPS. Cinq pages ajoutées, au moule à 4 temps (concept → Set-OPS → transférable → à toi de jouer), avec exercices concrets :

  • Le plan & l'adressage dérivé — un seed (index), tout en découle (DRY, source unique).
  • Multi-instance & fédération — un moteur, N écosystèmes ; découverte par convention.
  • La preuve — « ne jamais affirmer plus que ce qu'on prouve » ; le registre, make prouver, P01P21.
  • Le GUI (console d'exploitation) — éditer la source, dry-run avant apply, l'invalide impossible à saisir (le <select> sans hôte fantôme) ; la flotte et la bascule.
  • Glossaire — 24 concepts en une phrase (était « à venir »).

Raccordées dans Home.md (deux unités-pilotes : services et méthode) et _Sidebar.md (section « Flotte & preuve »). Fidèle à la doctrine : le wiki pointe vers docs/, ne recopie pas. Publié via make wiki-publier. Wiki : 855 → 1573 lignes, 22 pages, aucun lien mort.

2026-07-23 (suite 4)

Ajouté — créer un MODÈLE (make model-creer, dépôt privé)

Symétrique de instance-creer, mais produit un modèle réutilisable dans le dépôt PRIVÉ (Set-OPS-Modeles/, jamais exemples/modeles/). scripts/model_creer.py, deux modes :

  • MODE=base BASE=<modele> NOM=<x> — copie un modèle déjà générique (copier + éditer).
  • MODE=instance SOURCE=OPS-<x> NOM=<y>promeut une instance éprouvée en modèle : généralise l'identité (domaine → exemple.internal, organisation → Exemple, realm → exemple), fixe index → 1, setops_production → false, nftables_admin_ssh → [], vide la clé publique de sauvegarde, générique proxmox.yml (host/nœud/stockage/VMID vidés, golden template → modele-debian13).

Sûreté : aucun secret ne sort (vault.yml/proxmox.vault.yml jamais copiés ; refus si l'un subsiste ; le .example est conservé). Copie ciblée (plan/ + inventories/ seulement, symlinks résolus) — robuste aux dépôts imbriqués et boucles de symlinks. Le modèle produit doit valider (modeles.py verifier), sinon il est annulé. make model-creer.

Validé : les deux modes testés en isolement (destination tmp, dépôt privé jamais touché) — promotion du labo (cohérent) réussie + validée, aucun chezlepro résiduel, aucun secret, proxmox.yml génériqué ; copie base (socle) OK ; refus d'une instance INVALIDE (le garde-fou a détecté un hôte fantôme dans une instance en cours d'édition). node --check, make verifier rc=0 CONFORME 21/21.

2026-07-23 (suite 3)

Ajouté — créer une instance depuis un modèle (CLI + GUI)

Le moteur est indépendant des instances (le symlink instance/ est gitignoré, aucun artefact d'instance n'est committé). Créer une instance existait seulement à la main (cp -r + ln -s, QUICKSTART). C'est désormais une capacité de premier ordre.

  • scripts/instance_creer.py — copie un modèle (exemples/modeles/* + SETOPS_MODELES) vers un dépôt frère ../<nom>, y fixe l'index (le seed), et refuse : un nom déjà existant (rien n'est écrasé), un modèle inconnu, un index en collision avec une instance fédérée (le garde-fou vérifie AVANT toute copie). Le .git et hosts.genere.yml du modèle ne sont pas copiés.
  • make instance-creer NOM=OPS-X MODELE=socle [INDEX=N] et make instance-modeles (liste les modèles + les index déjà pris).
  • GUI, vue Réseau — formulaire « Créer une instance depuis un modèle » sous la flotte : modèle (liste), nom, index. POST /api/instance-creer ; /api/instances renvoie aussi modeles et index_pris. Ne bascule pas l'active (geste explicite).

Corrigé

  • Gabarit de voûte du labo complété. P18 (voûte) a échoué en passant l'active sur le labo : son vault.yml.example était resté à l'ancienne version (17 clés, sans vault_restic_password, vault_oauth2_cookie, etc.). Aligné sur le gabarit complet (23 secrets). P18 fait exactement son travail — attraper un gabarit incomplet, quelle que soit l'instance active.

Validé : garde-fous de création testés en isolement (collision d'index refusée avant copie, écrasement refusé, modèle inconnu refusé, aucune pollution des dépôts frères) ; node --check du GUI ; make verifier rc=0 CONFORME 21/21 (active = labo).

2026-07-23 (suite 2)

Ajouté — bascule d'instance depuis le GUI (vraiment multi-instance)

Demande explicite et répétée de l'opérateur : gérer les instances depuis le GUI, pas seulement en CLI. Le plan de contrôle reste maison, mais cette capacité y entre.

  • Inventaire résolu dynamiquement. Le serveur GUI figeait l'inventaire au démarrage (args.inventaire.resolve()). Il est désormais relu à chaque requête depuis le symlink instance/ (propriété Gestionnaire.inventaire) — la bascule prend effet sans redémarrer. Les chemins de plan (FICHIER_*) étaient déjà relatifs au symlink et suivent de même. SETOPS_INVENTAIRE force encore un inventaire fixe (CI).
  • POST /api/instance-utiliser + basculer_instance(nom) — repointe le symlink avec les garde-fous de make instance-utiliser, plus une validation stricte : nom doit être une instance découverte (dossier frère), ce qui interdit toute traversée de chemin (testé : ../etc refusé).
  • GUI, vue Réseau — la table de flotte gagne un bouton « Activer » par instance (l'active affiche « active »). Il confirme (avec avertissement renforcé si l'instance est en production), bascule, puis recharge la page : toutes les vues et les déploiements visent la nouvelle instance.

L'édition du drapeau federe reste au CLI (rarement changé). La bascule, elle, est maintenant CLI et GUI.

Validé : node --check du GUI ; bascule testée de bout en bout (repoint → l'inventaire dynamique suit → traversée refusée → restauration) ; make verifier rc=0 CONFORME 21/21.

2026-07-23 (suite)

Ajouté — gestion multi-instances : vue d'ensemble + garde-fou de collision

Le mécanisme de bascule existait déjà (make instance-utiliser, make instance-courante : repointer le symlink instance). Ce qui manquait : le regard d'ensemble et le filet.

  • make instances (scripts/instances.py) — liste toutes les instances de la fédération (dépôts frères avec plan/nomenclature.yml), marque l'active (*), et montre pour chacune index, plage VLAN dérivée, statut fédéré/local (federe) et production (setops_production). Lecture seule.
  • Détection de collision d'index — signale toute paire d'instances fédérées partageant un index (donc mêmes VLAN/VMID sur le trunk convergé). C'est exactement le piège vécu (Chezlepro-prod et le labo tous deux à l'index 1) : désormais crié, pas découvert par hasard. Le mode --verifier sort en erreur (rc=2) sur collision.
  • Preuve P21 (make prouver/make verifier) — câble ce garde-fou dans le harnais : la cohérence de la fédération est vérifiée à chaque passage. No-op quand moins de deux instances fédérées sont présentes (comme P17 sans SETOPS_MODELES).

Validé : make instances liste les 3 instances (labo LOCAL, Technolibre et Chezlepro fédérées, index 2 et 13) ; détection testée en synthétique (deux fédérées au même index → collision levée ; labo federe: false → cohérent) ; make verifier rc=0 CONFORME 21/21.

2026-07-23

Changé — l'adressage se dérive du seul seed index (RUPTURE, mode compact retiré)

Principe posé par l'utilisateur : les valeurs de configuration doivent se dériver des intrants, pas se réécrire à la main. La nomenclature dupliquait ce que index détermine déjà (supernet, sous-réseaux, passerelles, VLAN). C'est corrigé, en rupture nette.

  • inventory_rules : nouvelles fonctions de dérivation, source uniquesupernet_de, base3_de, sous_reseau_de, passerelle_de, vlan_de. Le modèle à 6 zones est encodé une fois : 2ᵉ octet = 10+index, 3ᵉ octet de zone = 15+catégorie, VLAN trunk = 1000+index×10+zone. deriver_nomenclature ne lit plus aucun adressage stocké ; le mode compact est supprimé (tout est ip-miroir dérivé).
  • devis_reseau importe ces helpers (plus de duplication) ; découverte des tenants sur index présent (le filtre vmid_schema disparaît).
  • Les 10 nomenclatures (3 instances + 7 modèles) passent au format maigre : index, cidr_hote, reservations, libellés de zones et fonctions seulement. Supprimés : supernet, vmid_schema, et par zone sous_reseau/passerelle/vlan. presence-web, encore en compact, gagne index: 1.
  • GUI : index devient un intrant (section « Réseau » du panneau Intrants). Il vit dans la nomenclature (le plan réseau, uniforme partout — contrairement aux intrants des modèles, hétérogènes) et le GUI l'écrit chirurgicalement (une ligne, sans reformater le fichier). Le miroir JS dérive le VLAN du seed (fin de la lecture de c.vlan stocké).

Ajouté

  • Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke d'adressage — garde-fou permanent contre une rechute vers l'écriture manuelle. Testée en négatif (une rechute simulée est bien rejetée).

Validé : DIFF VIDE sur les 3 instances (la dérivation reproduit exactement l'adressage qui était stocké), les 7 modèles valident, devis_reseau génère les mêmes VLAN (1011-1016 dérivés), make verifier rc=0 CONFORME 20/20, node --check du GUI OK, aller-retour d'écriture de index : une seule ligne touchée.

2026-07-22 (suite)

Ajouté — trois preuves qui ferment les angles morts du harnais

Le constat : make prouver ne vérifiait qu'une instance et le seul modèle socle. Tout ce qui vit à côté du moteur dérivait en silence — d'où l'hôte fantôme d'integral, les neuf secrets absents du gabarit Chezlepro, les champs de plan que le GUI ne sait pas écrire.

  • P17 — tous les modèles valident (scripts/modeles.py). Rejoue les 4 validateurs de registres sur chaque modèle découvert (les exemples/modeles/* du dépôt, plus les chemins de SETOPS_MODELES, ex. le dépôt privé). A immédiatement trouvé 6 modèles invalides sur 7 : cinq portaient autorite: interne (valeur périmée jamais propagée depuis la correction de socle en phase 5), presence-web manquait son domaine interne. Corrigés.
  • P18 — gabarit de voûte complet (scripts/voute.py). Confronte vault.yml.example aux secrets réellement exigés par le plan (champ secret des bases + références vault_* des rôles actifs et des group_vars). Ne déchiffre jamais la vraie voûte : compare des noms. --strict signale aussi les clés devenues inutiles.
  • P19 — le GUI couvre le plan (scripts/couverture_gui.py). Croise les champs présents dans les plans réels (instance + modèles) avec CHAMPS_ECRITS_PAR_GUI, nouveau tableau de inventory_gui.py déclarant ce que les fonctions de sauvegarde écrivent. Signale tout champ non éditable par le GUI (a trouvé applications.websocket, comblé — case à cocher ajoutée), et toute dérive entre le tableau et le source du GUI. La nomenclature reste un trou connu (lecture seule), tolérable via --tolerer nomenclature.

Corrigé

  • Garde-fou contre l'hôte fantôme : valider_applications accepte désormais le registre des serveurs et refuse une application posée sur un hôte non déclaré — la faute exacte qu'integral portait. Câblé dans scripts/applications.py (les 4 appels), au POST du GUI, et vérifié : re-teste le bug d'origine → rejeté avec un message actionnable.
  • 6 modèles de Set-OPS-Modeles : autorite: interneauto-heberge (collaboration, forge, identite, integral, observabilite) ; presence-web gagne son domaine interne pour ses expositions site.*/app.*.
  • Champ websocket dans le GUI (inspecteur d'application) — Collabora en a besoin.

Validé : make verifierrc=0, CONFORME 19/16→19 (P01P19), ansible-lint 0 échec, les 7 modèles valident (SETOPS_MODELES posé), gabarit de voûte complet (23/23), couverture GUI 27/27 champs (nomenclature tolérée), DIFF VIDE conservé, JS du GUI valide.

2026-07-22

Ajouté

  • Champ « Liens (bindings) » dans le GUI (scripts/inventory_gui.py). L'inspecteur d'application porte une section dédiée : une ligne par lien, rôle → cible, les deux en listes déroulantes, avec bouton d'ajout et de retrait. Les rôles proposés sont exactement ceux que le rôle porteur déclare accepter (roles/<groupe>/meta/liens.yml), et les cibles sont les autres applications du plan. Chaque ligne affiche les variables que le lien injectera. Changer le groupe d'une application vide les rôles de lien devenus inacceptés. Comble un manque relevé à l'audit du GUI : les bindings ne pouvaient se déclarer qu'en éditant plan/applications.yml à la main, ce qui rendait la configuration du courriel (Postfix → Dovecot, Postfix → rspamd) inaccessible sans éditeur.
  • liens_acceptes(groupe) et catalogue_liens() dans scripts/inventory_rules.py — source unique de « quels liens un rôle accepte », partagée par le validateur, le GUI et instancier.py. Nouvelle clé liens_acceptes dans la charge utile de /api/inventaire.

Modifié

  • valider_applications valide désormais les liens : liste de tables {vers, role}, champs non vides, cible connue, pas de lien vers soi-même, et rôle accepté par le rôle porteur (avec la liste des rôles acceptés dans le message d'erreur). Un rôle sans meta/liens.yml reste toléré au validateur — instancier.py tranche avec le même message. Bénéficie aussi à make inventaire-verifier et à scripts/applications.py verifier.
  • scripts/instancier.py — sa copie locale _liens_acceptes() est retirée au profit de la fonction partagée. Un seul endroit lit meta/liens.yml.
  • docs/bindings-conception.md — la Phase 4 passe à 🟡 : l'éditeur est fait, le graphe des liens reste à faire.

Validé : make verifierrc=0, ansible-lint 0 échec sur 485 fichiers, prouver.py --verifier → CONFORME 16/16, verifier_gui.py (node --check) OK, DIFF VIDE conservé et les deux variables de binding du courriel toujours générées (serveur_postfix_mailstore_hote, serveur_postfix_rspamd_milter). Aller-retour d'écriture testé : les liens survivent au cycle chargement → sauvegarde → relecture. Sept garde-fous du validateur éprouvés (rôle inconnu, cible inexistante, lien vers soi-même, champ manquant, types invalides).

2026-07-21 (suite)

Modifié

  • Palette aurore enrichie sur les quatre pages promo/ — ajout de deux teintes, --rose:#ff6fc4 (frange magenta) et --vert:#5cff9d (vert fluo), présentes uniquement dans les dégradés foncés : fond fixe du corps, nappe .aurora dérivante, et filets de séparation .rule. Opacités de 5,5 % à 8,5 % — une teinte, pas un motif. Les dégradés de texte et de boutons (--aurora) sont inchangés : l'identité de marque ne bouge pas. Appliqué identiquement aux quatre fichiers (0 conflit CSS après coup).
  • Thème Forgejo étendu au wiki (roles/serveur_forgejo/files/custom/public/assets/css/alliance.css, 29 → 118 lignes). La feuille étant injectée par templates/custom/header.tmpl sur toutes les pages, le wiki est couvert sans feuille distincte. Réorganisée en deux sections de risque explicite : §1 Variables (surcharge des variables de couleur officielles, sûr, résiste aux mises à jour) et §2 Décor (fond aurore, filet sous les titres, citations, tableaux et code en ligne de .markup — sélecteurs internes, à revérifier à chaque montée de version majeure). Supprimer §2 ramène au thème sobre d'origine. Le ciel nocturne ne s'applique qu'aux thèmes sombres, pour ne pas casser le thème clair. roles/serveur_forgejo/README.md documente le tout.

Ajouté

  • docs/theme-forgejo-hors-flotte.md — pose manuelle du thème sur une instance Forgejo non gérée par Set-OPS (la forge historique qui héberge ce dépôt et son wiki). Forgejo n'ayant aucun réglage web pour le CSS, il faut déposer la feuille et référencer header.tmpl sur le serveur. La procédure ne duplique aucun fichier : elle pointe vers ceux de roles/serveur_forgejo/files/custom/. Comprend la détection du répertoire custom, un garde-fou pour ne pas écraser un header.tmpl existant (ajout de ligne, pas remplacement), la vérification par curl, et la marche arrière.

Corrigé

  • État de forge-01 élucidé — ce n'était pas une contradiction. L'hôte a été créé, puis supprimé. Le plan dit donc etat: planifie (état courant) et le CHANGELOG du 2026-07-03 dit le branding « prouvé sur forge-01 » (état passé) : les deux disent vrai. roles/serveur_forgejo/README.md l'énonce désormais ainsi ; la preuve reste valable, simplement non rejouable tant que l'hôte n'est pas recréé. (La note précédente, qui présentait l'écart comme une contradiction à trancher, était erronée.)
  • Teintes rose et verte trop concentrées dans les coins — nappes élargies d'environ ×2 (780×540 → 1500×1020 px pour le rose, 840×580 → 1620×1120 px pour le vert), ramenées vers l'intérieur (97 % → 76 %, 3 % → 20 % ; nappe dérivante 95 % → 79 % et 6 % → 22 %), extinction repoussée de 62 % à 82 % avec un arrêt intermédiaire pour une rampe douce, et flou de .aurora porté de 64 px à 86 px. La couleur est répartie au lieu d'être tassée aux bords.
  • Opacités des teintes réduites d'environ 40 % dans la foulée — l'étalement les rendait trop présentes. Rose .075 → .044, vert .058 → .034, violet .062 → .036 (arrêts intermédiaires abaissés dans la même proportion), et opacité de la nappe .aurora .44 → .32. Géométrie inchangée : seule l'intensité baisse.
  • Statut du thème précisé, section par section : §1 (variables) est éprouvée sur forge-01 ; §2 (décor), ajoutée aujourd'hui, n'a jamais été rendue par un Forgejo réel. L'affirmation précédente (« non rendu par un Forgejo réel », tous les cas confondus) était fausse pour §1.

Validé : équilibre des accolades et parenthèses du CSS, 0 conflit CSS entre les quatre pages promo, ansible-lint 0 échec, prouver.py --verifier → CONFORME 16/16.

2026-07-21

Ajouté

  • Protocole d'épreuve de l'opérateur indépendant (docs/audit/protocole-operateur-independant.md). Met AFF-002 (« Set-OPS s'exploite entièrement à la main, sans aucune IA ») à l'épreuve d'un sysadmin qui n'est pas l'auteur, sur sa propre grappe Proxmox, à froid depuis le modèle public socle. Définit : la règle du silence (l'observateur journalise, n'aide pas ; trois niveaux N0/N1/N2), les interdits (aucune IA, aucun accès aux dépôts privés, aucune lecture de docs/audit/ qui divulguerait les pièges connus), les critères de réussite R1→R6 fixés d'avance, le périmètre matériel et la sécurité, et le gabarit de rapport (operateur-independant-AAAA-MM-JJ.md). Le registre peut perdre : le protocole prévoit explicitement la redescente d'AFF-002 en 🟡 ou selon le verdict.

Modifié

  • docs/audit/affirmations.md — nouvelle limite consignée : AFF-002 est par inspection, pas par démonstration. Ses écarts bloquants sont soldés, mais aucun opérateur indépendant ne l'a exécutée ; y signifie « plus aucun défaut connu ». Renvoi vers le protocole.
  • docs/audit/README.md — « Les trois pièces » → « Les pièces » : ajout du protocole comme quatrième pièce du dispositif (l'épreuve humaine, hors harnais automatisable).

Validé : python3 scripts/prouver.py --verifierCONFORME 16/16 (voûte exportée) ; équilibre des blocs de code du nouveau document vérifié. Aucun code touché.

2026-07-20

Modifié

  • make verifier inclut désormais les preuves (make prouver). make verifier se termine par python3 scripts/prouver.py --verifier : nouveau mode qui exécute toutes les preuves du registre (verdict + code de sortie) sans écrire de rapport, pour ne pas écraser la pièce justificative committée docs/audit/preuve-<date>.md. make verifier échoue donc si une preuve échoue. make prouver seul (sans --verifier) continue d'écrire le rapport horodaté. Vérifié : make verifier → CONFORME 16/16 (voûte exportée), aucun churn du rapport committé ; make prouver écrit toujours. Doc mise à jour (docs/audit/README.md § « Rapport avec make verifier »).
  • Conformité Phase 2 — 3 incohérences internes corrigées (documentation d'autorité). Aucun code Ansible touché ; le code SSH était déjà conforme à AGENTS.md.
    • CLAUDE.md réduit à un pointeur mince : autorité unique d'AGENTS.md + les cinq règles absolues (agent unique ; hosts.yml généré jamais édité ; aucun secret ; confirmation des actions destructives ; rien n'est prêt sans validation). Toute la doctrine dupliquée (template, SSH, pare-feu, cloud-init, handlers, Makefile…) retirée.
    • Contradiction SSH levée : CLAUDE.md prescrivait PasswordAuthentication yes pendant la construction ; le code applique en réalité PasswordAuthentication no + AuthenticationMethods publickey dès le départ (prouvé, cf. docs/audit/affirmations.md AFF-037/050). La doctrine SSH périmée de CLAUDE.md est supprimée — la duplication en était la cause racine.
    • AGENTS.md : « Comportement attendu de Codex » → « …des agents IA » (la section vaut pour tout agent IA, Claude inclus).
    • Suivi dans docs/audit/affirmations.md (§ Journal des traitements) : AFF-040, AFF-050, AFF-052 résolus.

Corrigé

  • Conformité Phase 3 — lot A « doc de démarrage » (le parcours QUICKSTART marche seul).
    • Modèle absent (AFF-020/021). QUICKSTART.md proposait cp -r exemples/modeles/presence-web … — modèle inexistant dans le dépôt public (seul socle l'est). Étape 2 réécrite sur socle ; les modèles assemblés renvoyés au dépôt privé Set-OPS-modeles, cohérent avec exemples/modeles/README.md.
    • Chemin de voûte faux + piège de shadowing (AFF-023). Le chemin lab/ (QUICKSTART, exemples/vault.exemple.yml, docs/config-proxmox.md) est corrigé en production/ (le modèle socle n'a qu'un inventaire production/ ; make config résout lab > principal > production). Piège corrigé : le socle livrait ses intrants en production/group_vars/all.yml (forme fichier) ; y ajouter all/vault.yml (forme dossier) fait ignorer silencieusement all.yml par Ansible (le dossier masque le fichier — vérifié empiriquement). Le modèle est converti en forme dossier (group_vars/all/10-intrants.yml), alignée sur l'instance prouvée. Validé : ansible-inventory --host charge domaine_interne depuis la nouvelle disposition.
    • Commandes make périmées (AFF-080/081/082). make helpmake ; make syntax-templatemake syntaxe-modele (dans docs/config-proxmox.md, docs/modeles_vm/debian13-proxmox.md, docs/MISE-A-JOUR-CODEX-CLAUDE.md).
    • Découvert et consigné (AFF-097, non traité ici) : lab/ codé en dur restant dans les docs template/clone (vm-lifecycle, procedure-template…, proxmox/README, message de cloner_vm_debian.yml) — lot séparé à prévoir.
  • Conformité Phase 3 — lot B « doc (prose) ».
    • Prérequis Vault rappelé (AFF-026). QUICKSTART.md : note que make inventaire-verifier / make verifier chargent l'inventaire complet et exigent ANSIBLE_VAULT_PASSWORD_FILE, sinon « no vault secrets found ».
    • Sous-dossiers playbooks/ (AFF-039). AGENTS.md : précisé que seuls groupes/, maintenance/, modeles_vm/, proxmox/ sont peuplés ; les autres sont prospectifs.
    • SOLUTION.md remis au présent (AFF-060/061). Bannière « document historique » (supplanté par README/QUICKSTART/make) ; arborescence complétée ; commande de construction --ask-pass (mot de passe) → make preparer-modele (accès par clé).
    • Découvert et consigné (AFF-098, traitement B, non traité ici) : contradiction fonctionnelle — make config écrit le token Proxmox dans all/vault.yml, mais cloner_vm_debian.yml ne le lit que depuis proxmox.vault.yml/env. Couplé à AFF-097 dans un futur lot « voûte Proxmox ».

Corrigé

  • Conformité Phase 3 — lot « voûte Proxmox » (AFF-098 bug fonctionnel + AFF-097).
    • Le clonage lit la voûte unifiée (AFF-098, option a). make config écrit le token Proxmox dans instance/inventories/<env>/group_vars/all/vault.yml, mais playbooks/proxmox/cloner_vm_debian.yml (lancé -i localhost,) ne le chargeait que depuis proxmox.vault.ymlmake creer-vm échouait l'assert proxmox_api_token_secret pour qui suivait la voûte unifiée. Corrigé : all/vault.yml ajouté aux sources de secrets du playbook (autoritaire) ; la détection de voûte chiffrée du Makefile (cloner-vm) cherche d'abord all/vault.yml puis proxmox.vault.yml. proxmox.vault.yml reste accepté en compatibilité. Validé : --syntax-check OK, ansible-lint 0 échec, test fonctionnel (token chargé depuis all/vault.yml, assert vert).
    • Docs Proxmox/template alignées (AFF-097). lab/ codé en dur → production/ + voûte unifiée dans playbooks/proxmox/README.md, docs/procedure-template-debian13-proxmox.md, docs/vm-lifecycle.md, docs/modeles_vm/debian13-proxmox.md. Plus aucune référence inventories/lab/group_vars dans les fichiers suivis.
  • Conformité Phase 3 — lot C « make verifier vert » (AFF-006). ansible-lint passe de 33 échecs à 0 (profil minproduction), donc make lint rc=0. Trois causes :
    • site.yml généré lint-propre : scripts/orchestrer.py émet un name: avant chaque import_playbook (30× name[play]) ; site.yml régénéré. Orchestration inchangée.
    • risky-shell-pipe : set -o pipefail + executable: /bin/bash sur les deux tâches shell à pipe de playbooks/valider.yml.
    • name[template] : Jinja déplacé en fin de name dans supprimer_vm_debian.yml. Validé : make lint rc=0 ; toutes les étapes de make verifier vertes (lint, test, site-verifier, flux-verifier, syntaxe) — seule inventaire-verifier requiert la voûte de l'opérateur (prérequis documenté, AFF-026). Débloque AFF-002 (« exploitable sans IA » → ).

Corrigé

  • Conformité Phase 5 — boucle documentaire (le parcours QUICKSTART marche seul, prouvé). Relecture de cohérence bout-en-bout (README/QUICKSTART/docs/wiki ↔ code final). Deux bogues du modèle public/outillage débusqués et corrigés, en plus des alignements de prose :
    • Modèle socle invalide (AFF-099). exemples/modeles/socle/plan/domaines.yml : autorite: interne (périmé, rejeté par le validateur) → auto-heberge. Le modèle valide.
    • Split-brain d'inventaire (AFF-100). scripts/instancier.py et scripts/inventory_gui.py retombaient sur principal/ quand aucun hosts.yml n'existe encore ; or le socle est en production/ → la 1ʳᵉ génération écrivait dans principal/, à côté des group_vars restés en production/. Corrigé : le repli vise le répertoire d'inventaire déjà présent (comme config_proxmox.py). Instances existantes (avec hosts.yml) inchangées (non-régression vérifiée sur principal).
    • Alignements de prose. docs/intrants-communs.md (group_vars/all.ymlall/10-intrants.yml, forme dossier que le GUI écrit déjà) ; QUICKSTART.md étape 8 (serveur_postgresqlserveur_powerdns, groupe présent dans le socle).
    • Preuve : parcours QUICKSTART rejoué hors-ligne sur une copie du socle (instancier generercomparerappliquer) — écrit production/hosts.yml, diff vide ensuite, 4 validateurs verts. Nouvelle preuve récurrente P15 dans make prouver (« modèle public socle valide ») : make prouver = 15 OK, 0 échec, 1 sautée.

Ajouté

  • Harnais de preuve make prouver (Phase 4). Nouveau scripts/prouver.py — un orchestrateur mince qui rejoue les preuves automatisables du registre en appelant l'outillage existant (les mêmes scripts que make verifier : lint, tests, diff-vide, validateurs de registres, cohérence groupes/playbooks, handlers, orchestration, flux, syntaxe, existence des runbooks, invariants structurels) — aucune validation réimplémentée. Produit docs/audit/preuve-AAAA-MM-JJ.md : rapport horodaté, rejouable, reliant chaque preuve aux affirmations couvertes, listant à part les déclarations d'intention (). Sort en erreur si une preuve échoue ; la preuve P15 (inventaire Ansible complet) est sautée proprement sans mot de passe Vault (prérequis AFF-026). Documenté dans README.md (une phrase) et docs/audit/README.md (mode d'emploi complet + comment ajouter une preuve). affirmations.md : section « Couverture par make prouver » reliant chaque à sa preuve. 1re exécution : 14 preuves OK, 0 échec, 1 sautée → CONFORME.
  • Registre des affirmations (audit de conformité, Phase 1). Nouveau docs/audit/affirmations.md : chaque affirmation publique vérifiable du dépôt (README, AGENTS, CLAUDE, QUICKSTART, SOLUTION, docs/, wiki/, aide du Makefile, GUI) est tracée vers une commande de preuve reproductible et un statut ( prouvée / 🟡 partielle / fausse / invérifiable localement). 54 affirmations enregistrées : 30 , 13 🟡, 8 , 3 . Audit sans aucun correctif (les traitements relèvent des phases suivantes). Preuves exécutées localement, hors production : diff-vide du plan, recoupement notify↔handlers, validateurs de registres (serveurs/applications/bases/domaines/GUI/orchestrateur/flux), --syntax-check de tous les playbooks, test unitaire. Dix écarts majeurs classés par risque pour un opérateur suivant la doc à la lettre — dont : QUICKSTART renvoie à un modèle absent (presence-web), make verifier échoue (ansible-lint : 33 failures), contradiction SSH CLAUDE.md ↔ code/AGENTS.md, chemin de voûte faux, commandes make périmées dans docs/.

2026-07-07 (soir)

Modifié

  • Nomenclature ip-miroir : longueurs UNIFORMES. Le VLAN dérivé passe à 1000 + index×10 + zonetoujours 4 chiffres (1011..4094), donc le VMID (VLAN·octet·seq) fait toujours 9 chiffres. Longueurs uniformes et mnémotechniques (retirer 1000 redonne index×10+zone). Les deux tenants sont convergés sur le réseau fédéré 10/8 : Chezlepro (idx 1) → VLANs 1011-1016 / 10.11.x ; Technolibre (idx 2) → VLANs 1021-1026 / 10.12.x. Chezlepro quitte le bac-à-sable plat 192.168.15/VLAN 15.

Ajouté

  • make devis-reseau : config switch dérivée du plan. Nouveau scripts/devis_reseau.py qui découvre les instances fédérées (../*/plan/nomenclature.yml, schéma ip-miroir) et émet la config Cisco-like du réseau convergé — VLANs + SVIs (passerelles) + ACLs d'isolation inter-tenant — dérivée des nomenclatures, jamais saisie à la main (toujours synchrone avec le plan). One-shot précédemment ; maintenant reproductible.
  • GUI : vue « Réseau » (devis switch) + bouton Copier. Nouvel onglet lecture seule qui affiche le devis make devis-reseau (VLANs + SVIs + ACLs), via GET /api/devis-reseau, avec un bouton Copier (prêt à coller sur le switch). Un opérateur génère et transmet la config sans CLI.
  • GUI : erreurs de déploiement en LANGAGE CLAIR. Quand une action (créer/vérifier/déployer) échoue, le GUI n'oblige plus à lire le dump Ansible : un encadré résume les tâches en erreur — étape · hôte · type · message clair — avec un bouton « Copier » (pour transmettre le résumé à un humain / au mainteneur). Le backend (_extraire_echec) parse les fatal:/UNREACHABLE!, privilégie la vraie cause (stderr plutôt que le générique « non-zero return code »), gère no_log (« sortie masquée — secret ») et les injoignables. Éprouvé sur les 4 échecs réels du from-zero du jour. node --check OK.
  • GUI : éditeur « Domaines » (6ᵉ onglet éditable). Le registre des domaines publics — le seul sans édition dans le GUI — a désormais son onglet : ajouter/retirer une zone DNS, autorité (sélecteur primaire-cache/auto-heberge/delegue), edge, DNSSEC, mail, secondaires, et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines, ecrire_domaines, validation valider_domaines (rejette une autorité inconnue). Éprouvé : round-trip backend OK, node --check OK, smoke serveur OK. (Le autorite: interne périmé des instances Chezlepro/Technolibre a été corrigé en auto-heberge au passage.)
  • GUI : persistance des journaux d'exécution. Chaque action lancée depuis l'interface (créer-vm / vérifier / déployer) écrit désormais, en plus du streaming live, un journal horodaté <instance>/logs/<hôte>-<action>-<date>.log (gitignoré). Bonus : si le navigateur se déconnecte, l'exécution continue et le journal est capturé jusqu'au bout (au lieu d'être interrompue) — utile pour un déploiement qu'on ne veut pas voir avorter à la fermeture d'un onglet.
  • GUI : vues « Flux » et « Couches » (lecture seule). Deux nouveaux onglets rendent visibles dans l'interface deux registres jusque-là en ligne de commande seulement : Flux = la matrice d'audit réseau (rôle · sens · port · pair · chiffrement coloré · raison, depuis les meta/flux.yml), et Couches = l'ordre de reconstruction (socle → pki → services → apps → agents, avec les groupes triés topologiquement par couche). Alimentées par resoudre_flux/orchestrer via l'API. Éprouvé : node --check OK, smoke test serveur (63 flux, 6 couches servis).

Modifié

  • GUI : intégration des intrants de session. Le panneau « Intrants de base » expose désormais nftables_admin_ssh (type liste, section « Sécurité ») — la garde anti-lockout du pare-feu était éditable en fichier mais absente du GUI. Retrait de vault_step_ca_fingerprint des secrets attendus (empreinte du root CA désormais dérivée dynamiquement, plus un secret). node --check OK.

2026-07-07 — Reconstruction from-zero PROUVÉE (preuve de portabilité « sans réserve »)

Les 14 VM du lab (+ sauvegardes + AC) supprimées, puis make myDay a reconstruit l'écosystème POC de rien : 13 hôtes déployés (0 échec), et make valider entièrement vert (Prometheus 6/6 UP, 6 vhosts HTTPS, courriel bout-en-bout remis, 6 dépôts restic restaurés) — le tout sous pare-feu actif. 4 bugs de portabilité débusqués et corrigés dans le moteur :

Corrigé

  • client_pki : empreinte du root CA dérivée dynamiquement (au lieu d'une valeur figée en Vault). Une AC régénérée (from-zero) a une empreinte neuve ; le rôle la lit désormais de l'autorité elle-même (step certificate fingerprint, délégué au nœud step-ca), source de vérité. client_pki_ca_fingerprint_override permet un épinglage explicite.
  • serveur_keycloak : assignation de rôle tolérante aux utilisateurs absents. Sur un annuaire vide (from-zero), assigner un rôle à un user inexistant échouait ; on vérifie désormais son existence (Keycloak fédère LDAP à la demande) et on saute proprement sinon.
  • serveur_prometheus : ne scrute que les hôtes ACTIFS (client_metrique ∩ hotes_actifs). Un hôte planifié (non déployé) n'est plus une cible morte.
  • Pare-feu nftables compatible Docker. Le ruleset résolu (a) remplace uniquement la table setops_flux au lieu de flush ruleset (préserve les tables Docker : DNAT/forward des conteneurs) et (b) autorise docker0 + ct established,related dans la chaîne forward. Sans ça, forward policy drop coupait Collabora (conteneur). Diagnostic prouvé au niveau paquet.

Ajouté

  • playbooks/proxmox/supprimer_vm_debian.yml — suppression de VM par VMID (from-zero), avec garde-fous : n'agit que sur les VMID présents dans le cluster, refuse si le modèle est ciblé, secrets no_log.

2026-07-07

Ajouté

  • make valider — recette d'acceptation fonctionnelle (phase 4, v1). Vérifie que les services fonctionnent, pas juste qu'ils sont déployés (complète les *-verifier statiques). playbooks/valider.yml, lecture seule : cibles Prometheus toutes UP (API /targets) + vhosts HTTPS exposés répondent (dérivés des server_name réels de l'edge, filtrés sur domaine_interne — générique) + courriel bout-en-bout (envoi via le MTA → LDAP → LMTP → Maildir, remise vérifiée par doveadm sur le compte testmail, message de test nettoyé) + restauration de sauvegarde (pour chaque nœud client_backup : restic restaure le dernier snapshot dans un dossier temporaire — lecture seule sur le dépôt — et vérifie que des fichiers en sortent). Éprouvé sur la flotte vivante sous pare-feu actif : 7 cibles UP, 8 vhosts OK, courriel remis, 6 dépôts restaurables. La recette a trouvé un vrai trou (collab-01/Nextcloud sans client_backup_jobs → service de sauvegarde en échec, fichiers non protégés), corrigé côté instance.
  • Pare-feu nftables activé sur TOUTE la flotte (14 nœuds, activation prudente). Les 14 hôtes actifs tournent sous policy drop avec leur ruleset résolu moindre-privilège (flux est-ouest déclarés autorisés par source, reste refusé). Vérifié en conditions réelles : flux déclarés OPEN (keycloak→pg, postfix→dovecot LMTP, prometheus→node_exporter…), flux non déclaré DROP (forge→redis), 14/14 active+enabled+policy drop, contrôleur toujours joignable.
    • Intrant nftables_admin_ssh (garde anti-lockout) — CIDR d'administration TOUJOURS autorisés en SSH, indépendamment des flux. Le résolveur (resoudre_flux.py) l'injecte en tête de chaque ruleset. Bug de conception rattrapé avant activation : le contrôleur Ansible arrive par VPN (192.168.255.2, hors sous-réseau flotte) — sans cette règle, activer = lockout immédiat.
    • Rollout prudent : d'abord infra-pki-01 seul (test dead-man switch systemd-run, SSH re-vérifié sous drop, puis permanent), puis les 13 autres par lot (dead-man 5 min + sonde des flux est-ouest avant de persister). Activation pilotée par nftables_baseline_enabled: true (group_vars hotes_actifs ; le golden template n'y est pas → reste sans pare-feu, voulu).
    • Le déploiement dépose instance/flux-genere/<hôte>.nft dans /etc/nftables.conf + service enabled (survit reboot ET futurs make myDay).

Corrigé

  • Détection du coffre Vault : production/ codé en dur → inventaire réel. deployer, deployer-tout et verifier-deploiement cherchaient le coffre chiffré dans inventories/production/group_vars, alors qu'une instance en principal/ (cas courant) n'a pas ce chemin → l'invite du mot de passe Vault ne se déclenchait jamais et le déploiement échouait au déchiffrement. Corrigé : la garde vise désormais le group_vars de l'inventaire résolu ($(dir $(INVENTAIRE_PRODUCTION))group_vars). Vérifié : le coffre de principal/ est bien détecté.

Modifié

  • nftables_baseline branché sur les flux résolus (reconstruction, phase 0). Le rôle déploie désormais le ruleset résolu généré par make flux (instance/flux-genere/<hôte>.nft — règles par source, ip saddr = moindre privilège) quand il est présent ; sinon repli sur le gabarit plat. Toujours nftables_baseline_enabled: false par défaut → aucune activation (l'activation reste un geste dédié, testé par nœud). Nouveau var nftables_baseline_ruleset_genere. Syntax-check OK.

Ajouté

  • Reconstruction from-zero en une commande (reconstruction, phase 3 — outillage). La création de VM était unitaire (creer-vm HOTE=…) ; on comble le trou entre créer (2a) et configurer (2b) :
    • make flotte-creer CONFIRMER=true — boucle creer-vm sur tous les hôtes actifs du plan (clone Proxmox). Nouvelle sous-commande inventory_host.py lister-actifs.
    • make reconstruire CONFIRMER=true — enchaîne flotte-creer → attente SSH de la flotte (_attendre-flotte, ATTENTE_MAX réglable) → deployer-tout. La reconstruction complète en une commande, idempotente de bout en bout : le clone (proxmox_kvm) saute une VM déjà présente (par nom), le réseau/disque sont present/resized (grow-only), le déploiement Ansible converge. Re-lançable sans risque, qu'il reste des VM ou non.
    • make myDay repointé sur reconstruire (le vrai « bouton rouge » ; n'était qu'un alias de deployer-tout). Distinction assumée : deployer-tout = converger la config d'une flotte existante (2b, avec MODE_CHECK=1) ; reconstruire/myDay = créer les VM manquantes puis déployer (2a+2b). Gardes CONFIRMER=true sur les trois. Non testé contre Proxmox/lab (validé : énumération des 14 hôtes actifs, refus sans CONFIRMER, enchaînement make -n).

2026-07-06

Ajouté

  • make wiki-publier — fin du dernier geste manuel (reconstruction, phase 0). Le wiki pédagogique (wiki/, source versionnée) se publie désormais dans le wiki Forgejo par make wiki-publier WIKI_REMOTE=…<dépôt>.wiki.git : clone superficiel du wiki, synchronisation des pages (wiki/*.md sauf README.md ; suppressions propagées), commit + push seulement s'il y a du changement. Refuse sans WIKI_REMOTE. Éprouvé de bout en bout contre un dépôt bare local (17 pages publiées = 17 source, diff vide, README exclu, _Sidebar inclus, idempotent au 2e passage).

  • Registre des flux réseau complété (reconstruction, phase 0). Transcription du travail zéro-confiance est-ouest dans meta/flux.yml : 16 rôles remplis (step_ca, openldap, powerdns, prometheus, loki, redis, rspamd, backup, dovecot, postfix, keycloak, forgejo, grafana, icinga, icingaweb2, nextcloud, oauth2_proxy + le socle serveur_debian pour le plan de gestion SSH, + les 5 clients pki/journal/smtp/backup/unbound). Le registre couvre désormais 29 rôles, 63 flux (qui-parle-à-qui : port, sens, pair, chiffrement, raison) — la base de génération nftables/OPNsense et la matrice d'audit. Schéma enrichi (docs/flux-conception.md) : valeurs ssh (transport SSH, restic/backup + SSH de gestion) et tls-cible (TLS visé, feuille de route edge→backends). Point critique traité : SSH (22) déclaré au socle, sinon les nftables générés couperaient l'accès Ansible. Validé : schéma conforme (0 erreur) et matrice cohérente (tout egress vers un service a l'ingress correspondant en face). Reste phase 0 : la cible make wiki-publier.

  • Résolveur de flux (reconstruction, phase 0 — §Séquence 2). scripts/resoudre_flux.py agrège les meta/flux.yml, résout les pair, et produit deux artefacts, hors-ligne, sans activation :

    • docs/registre-flux.md (généré) — la matrice d'audit source→destination (rôle, sens, port, chiffrement, raison) + synthèse chiffrement. Artefact du label de certification.
    • aperçus nftables par hôte (instance/flux-genere/<hôte>.nft, gitignorés) — règles résolues avec IP réelles, ip saddr = moindre privilège (ex. LMTP 24 sur le mail n'accepte que l'IP du nœud Postfix), policy drop. Aperçus inspectables, NON activés (l'activation reste un geste dédié testé par nœud, cf. flux-conception §Activation prudente).
    • Cibles make flux (registre + aperçus) et make flux-verifier (schéma + matrice, branché dans make verifier). Validé : 29 rôles / 63 flux cohérents, 14 aperçus générés. Reste : brancher nftables_baseline (modèle plat aujourd'hui) sur ces aperçus, et le test lab.
  • Orchestrateur ordonné (reconstruction, phase 2). playbooks/site.yml n'est plus un stub : c'est désormais un point d'entrée ordonné généré, qui déploie l'écosystème couche par couche, dans l'ordre de reconstruction, sans intervention manuelle. Nouveautés :

    • docs/couches-deploiement.yml — registre central des couches ordonnées (socle → pki_racine → pki_client → services → apps → agents), les 30 groupes déployables classés.
    • scripts/orchestrer.py — trie les groupes par couche (clé primaire) puis topologiquement intra-couche via dependances-groupes.yml (ex. dovecot avant postfix, icingaweb2 après icinga, nextcloud en dernier). Génère site.yml comme une séquence d'import_playbook. Artefact du moteur (déterministe, sans donnée d'instance ; Ansible saute les groupes sans hôte actif). Deux gardes anti-dérive (refus si violé) : bijection univers↔couches (un nouveau rôle non classé casse la génération) et aucune arête « en arrière » (un prérequis dans une couche plus tardive = classification fausse). Éprouvées par test négatif.
    • make site (régénère + syntax-check), make site-verifier (cohérence, branché dans make verifier), make deployer-tout CONFIRMER=true (déploiement orchestré de la flotte, limité à hotes_actifs ; garde CONFIRMER car action impactante ; MODE_CHECK=1 pour l'essai idempotent à blanc). Validé : verifier OK (30 groupes, aucun cycle/arête arrière), --syntax-check du site.yml généré OK, refus deployer-tout sans CONFIRMER (rc=2).

Modifié

  • Audit exhaustif du codé-en-dur (reconstruction, phase 1b). Balayage complet tasks + templates + defaults de tous les rôles (noms de tenant, IP, domaines, emails, orgs). Résultat : le moteur ne porte plus aucun nom de tenant en dur. Corrigé — les labels/slug OIDC dérivent désormais de l'intrant organisation : serveur_forgejo_oidc_nom (slug de callback, organisation | lower | replace(' ','-')), serveur_grafana_oidc_nom, serveur_nextcloud_oidc_nom, serveur_nextcloud_theme_nom (labels d'affichage). Commentaires « Se connecter avec Chezlepro » → génériques. Non-régression SSO : organisation: Chezlepro → slug chezlepro, identique à l'URI de redirection Keycloak de l'instance (pas de casse). Conservés intentionnellement : realm default('chezlepro') (décision identite_realm actée) et le thème visuel Alliance Boréale (identité par défaut assumée du réseau, pas un tenant). Validé : re-balayage vide, rendu Jinja du slug testé (Chezlepro/Alliance Boréale/Ma Coop), --syntax-check OK (playbook forgejo via inventaire principal).
  • Audit du graphe de dépendances (reconstruction, phase 1a). docs/dependances-groupes.yml gagne les prérequis inter-groupes confirmés dans le code, en vue de l'orchestrateur trié en topologie. Ajouts : serveur_keycloakserveur_openldap (fédération LDAP via resoudre_annuaire_uri, en plus de PostgreSQL) ; serveur_dovecotserveur_openldap (userdb/passdb LDAP) ; serveur_postfixserveur_dovecot (remise LMTP au mailstore) ; serveur_icingaweb2serveur_icinga + serveur_postgresql + serveur_openldap (IcingaDB + auth LDAP) ; serveur_nextcloudserveur_postgresql + serveur_keycloak (OIDC) + serveur_collabora (validation WOPI). Réconciliation meta/liens.yml : le seul lien structurel (serveur_postfix mailstore → Dovecot) coïncide avec le graphe. Conclusion d'archi : la règle « TLS vérifié ⇒ client_pki aux deux bouts » ne devient PAS des arêtes par-groupe (client_pki est quasi universel) — c'est une couche de l'ordre de reconstruction (socle → step_ca → client_pki → services → apps → agents) ; dependances-groupes.yml ne capture que le fin ordonnancement intra-couche. Validé : YAML conforme, aucun cycle, tri-topo réussi (19 nœuds), chargeur charger_dependances accepte (12 groupes, est_groupe_operationnel OK), tous les groupes ont un rôle.

2026-07-05

Modifié

  • VLAN dérivé du tenant (réseau convergé). deriver_nomenclature (schéma ip-miroir) dérive désormais le VLAN = index × 10 + zoneunique globalement sur un trunk convergé (chaque tenant son bloc de 10 ; 1-9 réservés à l'infra partagée). Le VLAN contenant déjà le tenant (1er chiffre = index), le VMID mène avec le VLAN (VLAN·octet·seq, ≤ 9 chiffres Proxmox). Ex. Technolibre (index 2) → VLANs 21-26, VMID 2101101. Le schéma compact (lab, sandbox) reste inchangé. Champs vlan: codés en dur retirés des catégories ip-miroir (désormais dérivés).

Ajouté

  • Zéro-confiance est-ouest — flux Métriques, Logs et Courriel chiffrés. Suite du chantier (après PostgreSQL) : métriques (node_exporter sert en HTTPS via cert step-ca + --web.config.file + cert-sync owned prometheus ; Prometheus scrape scheme: https + tls_config), logs (Loki http_tls_config + cert-sync owned loki ; Alloy push https + tls_config), courriel (LMTP edge-mta→infra-mail:24 en lmtp_tls_security_level=verify + lmtp_tls_CAfile ; client_smtp en STARTTLS vérifié). Chacun prouvé de bout en bout (200 HTTPS, cibles UP, livraison status=sent, HTTP rejeté). Motif cert-sync .path industrialisé. Reste : edge→backends + DNS (DoT).
  • VMID 9 chiffres mnémotechnique (schéma ip-miroir, opt-in). vmid_schema: ip-miroir dans la nomenclature → VMID I·VVV·HHH·NN (index·VLAN·octet-hôte·séquence) : le VMID contient l'IP (10.(10+index).VLAN.hôte) + le tenant, lisible d'un coup d'œil. Défaut compact rétro-compatible (instances déployées inchangées).
  • Instance partenaire Technolibre. Écosystème complet (12 VM, etat: planifie) dans 10.12.16.0/20, 6 zones de sécurité (Frontière/Identité/Données/Services-infra/Observabilité/ Applications, un /24 + VLAN chacune), index de fédération 2, VMID ip-miroir. Preuve de portabilité d'un tenant.

Modifié

  • Références par FQDN partout (fin des IP codées en dur). Décision d'archi : FQDN pour toute référence inter-services (non ambigu en fédération, canonique pour TLS ; nom court = hostname OS). resoudre_base renvoie le FQDN (→ keycloak/forgejo/icinga) ; client_journal_loki_url dérivé du groupe serveur_loki ; defaults db_host IP morts nettoyés. Aucune IP littérale dans les defaults.
  • Découplage du tenant d'origine. Realm SSO centralisé sur l'intrant identite_realm (défaut chezlepro ; les 4 rôles keycloak/forgejo/grafana/oauth2_proxy en dérivent ; exposé dans la GUI). Vars brandées renommées génériques : chezlepro_timezone→fuseau_horaire, chezlepro_organisation→organisation. Le moteur ne porte plus le nom d'un tenant.
  • Modèles d'instance rafraîchis. integral régénéré depuis le cas prouvé (6 zones, fonctions éprouvées data-sql/id-ldap/id-sso/sup, ip-miroir, nouveaux intrants) ; socle/identite/ observabilite/forge réalignés sur le même moule (prouvés : dérivation + valider_serveurs). presence-web marqué aspirationnel (rôles web-frontal/dorsal absents) plutôt que faussement prêt.

2026-07-04

Ajouté

  • Zéro-confiance : flux PostgreSQL entièrement chiffré et vérifié. PG sert désormais son cert step-ca (vérifiable contre root_ca) au lieu du snakeoil, et refuse toute connexion non-TLS du réseau (hostssl dans pg_hba). Les 3 clients passent en verify-full : keycloak (db-url-properties sslmode=verify-full), forgejo (SSL_MODE=verify-full + PGSSLROOTCERT), IcingaDB (tls: true + ca). Prérequis posés : client_pki sur data-sql-01 (cert), et root_ca.crt en 0644 (cert public, requis par les clients TLS non-root). cert-sync PG (motif .path, owned postgres) + reload de l'instance postgresql@NN-main. Vars : serveur_postgresql_tls_actif/_tls_force, serveur_*_db_sslmode/_ca. Prouvé de bout en bout (cert Set-OPS CA servi, apps 200, non-TLS rejeté « aucun chiffrement », TLS accepté).

Corrigé

  • PG : détection de version robuste (collision avec un répertoire non-numérique). Placer le tls_dir sous /etc/postgresql/ faisait choisir tls comme « version » de cluster (find | sort | last) → configs déployées au mauvais endroit (verrou hostssl inopérant). Corrigé : détection filtrée aux dossiers numériques (^[0-9]+$) + tls_dir déplacé sous /var/lib/postgresql/tls.
  • Renouvellement de cert : recharger le VRAI consommateur (bug latent de flotte). Le cert-renewer@.service (client_pki) renouvelait le cert sur disque mais son ExecStartPost rechargeait un service nommé d'après le cert (%i = FQDN), inexistant → nginx (et postfix, dovecot, slapd) n'étaient jamais rechargés et servaient l'ancien cert jusqu'à expiration. Symptôme vécu : cert edge expiré en mémoire (renouvelé sur disque), échec TLS de l'échange code→jeton OIDC → login Grafana/SSO cassé (tous les services derrière l'edge). Correctif : client_pki_reload_services (liste des vrais consommateurs), câblée par groupe (edge→nginx, mail→postfix/dovecot, annuaire→slapd). Appliqué + vérifié sur les 4 hôtes. Fix immédiat de l'incident : systemctl reload nginx sur l'edge.

Ajouté

  • Doc à jour : unité wiki « Autorisation & RBAC », leçon renouvellement, runbooks. Fermeture des dettes de doc : nouvelle unité wiki authZ/RBAC (pendant d'Identité & SSO, avec l'exemple Grafana), section « le renouvellement est un système » versée dans l'unité PKI (comparer cert servi vs fichier ; recharger le consommateur), et docs/runbooks-exploitation.md (cert expiré, RBAC Grafana, branding Forgejo). 15 unités wiki désormais.
  • UI des logs Loki : dashboard Grafana provisionné. Loki n'a pas d'UI ; son UI est Grafana. Ajout d'un dashboard « Journaux de la flotte » (dossier Set-OPS) : sélecteur d'hôte multi + filtre regex insensible à la casse + panneau logs + débit par hôte. Référence Loki par une variable de datasource (ds_loki), pas un UID codé en dur (leçon : ajouter un uid explicite à une datasource déjà provisionnée casse le démarrage de Grafana). Visible par les Viewers (dont testmail) sans accès Explore. Prouvé sur obs-01 (dashboard chargé, Grafana actif).
  • RBAC via SSO : rôle de realm → niveau Grafana. Machinerie additive et idempotente dans serveur_keycloak (rbac-oidc.yml) : rôles de realm (serveur_keycloak_realm_roles), mapper roles sur les clients choisis (_role_mapper_clients, rôles de realm → claim roles dans ID token + userinfo), assignations rôle→utilisateur (_role_assignments). Côté Grafana, role_attribute_path (grafana-admin→Admin, grafana-editor→Editor, sinon Viewer). kcadm à chaud, zéro coupure SSO. Prouvé (idempotence changed=0) sur id-sso-01 : rôles créés, mapper présent, testmail = grafana-editor (→ Explore). Illustre l'authZ (vs authN du SSO).

2026-07-03

Ajouté

  • Identité visuelle Alliance Boréale sur Forgejo (léger, officiel). Branding via le dossier custom/ de Forgejo (mécanisme officiel — pas de fork, résistant aux MAJ) : accent aurore par variables CSS (--color-primary…, aucune classe interne touchée), logo/favicon étoile (réutilisés du thème Keycloak), page d'accueil brandée (home.tmpl : hero aurore + accroche), thème sombre par défaut, nom + méta. Codifié dans serveur_forgejo (serveur_forgejo_branding, _app_name, _theme), déployé dans {{ data }}/custom/. Prouvé sur forge-01 : accueil rend (200, « Forge Chezlepro »), alliance.css servi (cyan aurore), lint OK.
  • Wiki pédagogique Forgejo — 14 unités d'apprentissage. Set-OPS comme compagnon pédagogique : chaque service = une lentille sur un fondamental TIC, méthodes génériques (on apprend OIDC, pas Keycloak). Source versionnée dans wiki/, publiée dans le wiki Forgejo (eregion). Moule à 4 temps (concept → Set-OPS → transférable → à toi de jouer, avec casse-répare).
  • GUI : boutons cohérents. « Pousser » (surchargé : clonait et déployait) → « 🖥 Créer la VM » pour le clone, « Déployer » partout pour le déploiement, dry-run obligatoire partout. GUI 100 % française.
  • Agents d'observabilité/ops éprouvés — observabilité flotte-complète. Les 3 intégrations « agent » (liaisons nœud × optionnelles) déployées sur 4 nœuds (obs-01, data-sql-01, id-ldap-01, forge-01) et prouvées : client_metrique (node_exporter → Prometheus scrape les 4 cibles, toutes UP) ; client_journal (journald → Loki reçoit les logs des 4 nœuds) ; client_smtp (msmtp → courriel système d'un nœud relayé par l'edge-MTA et livré). Comble le trou : les serveurs d'observabilité (Grafana/Prometheus/Loki) étaient prouvés, mais pas la collecte fleet-wide — désormais Grafana voit toute la flotte. Cibles corrigées (bac à sable) : Loki→obs-01, relais→edge-mta-01 (pas infra-mail-01, qui est le store Dovecot sans SMTP :25).
  • Thème de connexion Keycloak à l'identité Alliance Boréale. Thème de login alliance-boreale (roles/serveur_keycloak/files/themes/, parent=keycloak + overlay CSS) reprenant l'identité du site de l'Alliance (extraite de site-alliance-boreale) : ciel nocturne aurore (#05060f/ #0a0d24 + dégradés), carte glassmorphism, logo étoile aurore (le favicon.svg du site), bouton dégradé aurore (teal→cyan, pilule), liens cyan, police système (souveraineté, zéro dépendance externe). Déployé dans {{ keycloak_home }}/themes/, appliqué au realm via kcadm ... -s loginTheme (var serveur_keycloak_login_theme, idempotent), Keycloak rechargé (flush_handlers avant la config realm). Prouvé : la page de login charge alliance.css (HTTP 200) + le logo.svg (200), loginTheme=alliance-boreale actif sur chezlepro. Constellation animée en fond (scripts=js/constellation.js) : le JS crée son propre ciel (canvas + aurore, le template n'en ayant pas) — étoiles scintillantes qui dérivent, liens de constellation cyan, blob d'aurore ondulant ; respecte prefers-reduced-motion. Prouvé : constellation.js référencé + servi (200). Console de compte thémée aussi (thème account, parent=keycloak.v3) : overlay CSS surchargeant les variables PatternFly 5 (fond aurore, cartes en verre, accent aurore) + la même constellation animée. Var serveur_keycloak_account_theme via kcadm -s accountTheme. Prouvé : console charge (HTTP 200, keycloak.v3 intact), account.css servi (200).
  • Soumission courriel :587 interne (authentifiée) — la boucle souveraine est bouclée. Postfix (edge-mta) sert la soumission :587 (bloc master.cf : STARTTLS requis, SMTP AUTH, seuls les authentifiés relaient) ; l'auth SASL est déléguée à Dovecot (infra-mail, passdb LDAP prouvé) via un auth-listener réseau (service auth { inet_listener sasl }, port 12345). Aucune sortie internet : interne→interne uniquement (l'externe = déliverabilité, Étape B). Prouvé (swaks) : testmail s'authentifie (235 Authentication successful), Postfix accepte (250 queued), et le courriel est livré dans la boîte (LMTP→Dovecot). Le courriel souverain fait maintenant recevoir ET envoyer. Vars : serveur_dovecot_sasl_reseau, serveur_postfix_submission_actif. Pièges : Dovecot 2.4 exige un nom de section inet_listener ; ajouter un service master.cf (nouveau listener) → handler restart (pas reload) ; et une config cassée peut bloquer un redéploiement si la synchro cert/restart précède le template (corriger la config à la main pour débloquer).
  • Sauvegardes applicatives (logiques) — serveur_backup + client_backup (restic), Tier 0 prouvé. Choix : sauvegarder la donnée d'état (non régénérable) plutôt que les VM (reconstructibles par le code + le template). Outil restic (chiffrement côté client, déduplication, rétention). serveur_backup (nœud backup-01) = cible SFTP/SSH (utilisateur restic, clé autorisée, dépôts sous /srv/restic/<nœud>). client_backup (intégration par nœud) = restic + jobs déclaratifs (client_backup_jobs : {nom, commande?, chemins}), clé SSH + mot de passe restic en voûte, script + timer systemd (quotidien) + rétention forget --prune. Prouvé de bout en bout sur le Tier 0 (infra-pki-01/etc/step-ca, l'ancre de confiance) : sauvegarde hors-nœud vers backup-01, puis restauration byte-identique des clés CA (root_ca_key, intermediate_ca_key, ca.json). Piège corrigé : le plancher /etc/hosts d'un nœud existant ignore un nœud nouvellement ajouté → rafraîchir le socle.
  • Sauvegardes Tier 1 généralisées — 5 nœuds, restauration prouvée. client_backup étendu (jobs déclaratifs en host_vars) à : data-sql-01 (pg_dumpall — keycloak/forgejo/icingadb), id-ldap-01 (slapcat LDIF — les identités), infra-mail-01 (/var/vmail — les boîtes), forge-01 (/var/lib/forgejo + /etc/forgejo — dépôts Git ; la BD est déjà couverte par PG). Prouvé par restauration : dump PostgreSQL restauré contient bien les 3 bases (CREATE DATABASE forgejo/icingadb/keycloak) ; LDIF restauré contient testmail. Les 5 dépôts restic (pki, sql, ldap, mail, forge) sont hors-nœud sur backup-01, chiffrés. Ajouter un service à sauvegarder = déclarer un job. Reste : cible offsite (3-2-1, Étape B — le dépôt n'est qu'une URL swappable).
  • Consolidation — binding annuaire (resoudre_annuaire) + retrait de la cruft. Cruft : supprimés les 8 dossiers-catégories inertes (roles/{applications,backup,database, identity,monitoring,proxmox,storage,web}/, README seuls) et les 5 playbooks-échafaudages debug sans rôle (nextcloud, collabora, client_supervision, web_frontal, web_dorsal) ; l'intention reste documentée dans docs/catalogue-services.md. Binding annuaire : nouveau rôle utilitaire partagé resoudre_annuaire (comme resoudre_base) qui dérive la connexion OpenLDAP du domaine_interne
    • un hôte d'annuaire surchargeable — LE seul endroit où le nom d'hôte de l'annuaire est fixé, au lieu d'être répété. Facts resoudre_annuaire_{uri,port,base_dn,users_dn,bind_dn,bind_password} (secret déréférencé, no_log). Migrés + prouvés (config neutre, changed=0) : serveur_keycloak (fédération LDAP — testmail token HTTP 200), serveur_dovecot + serveur_postfix (flux courriel Postfix→LDAP→LMTP→Dovecot livré de bout en bout). Piège appris : les defaults d'un rôle inclus ne persistent pas hors de son exécution — publier via set_fact.
  • Binding annuaire complété — icingaweb2 + client_ldap migrés vers resoudre_annuaire. Fin des 2 loose ends : serveur_icingaweb2 (connexion LDAP dormante en mode SSO) résout via resoudre_annuaire (redéploiement changed=0, SSO intact) ; client_ldap (SSSD, dormant) ne pointe plus sur un idm-01 périmé. Plus AUCUN rôle ne code en dur l'hôte d'annuaire — un seul point de vérité (resoudre_annuaire).
  • Rôle serveur_oauth2_proxy — passerelle SSO OIDC générique (Keycloak) + Icinga Web 2 au SSO. oauth2-proxy (v7.15.3, binaire GitHub) place Keycloak devant n'importe quelle app sans OIDC natif : elle reçoit l'utilisateur authentifié via en-tête, en auth external. Rôle paramétrable (client, secret voûte, redirect, upstream, cookie voûte) — réutilisable pour toute app OIDC-less. Éprouvé sur sup-01 devant Icinga Web 2 : client Keycloak icingaweb2, oauth2-proxy :4180 (exposé par l'edge) → upstream nginx local :8080 → icingaweb2 backend = external (REMOTE_USER depuis X-Forwarded-Preferred-Username). Prouvé (flux authorization code headless) : testmail → oauth2-proxy → Keycloak → icingaweb2 /dashboard, connecté (« Se connecter avec Chezlepro », comme Grafana/Forgejo). Ferme le gap LDAP-direct d'icingaweb2. Réglages appris : insecure_oidc_allow_unverified_email (les users LDAP n'ont pas email_verified ; IdP interne de confiance) ; en reverse-proxy oauth2-proxy passe X-Forwarded-* (pas X-Auth-Request-*) ; handler nginx en restart (pas reload) car un changement d'adresse d'écoute n'est pas pris par un reload gracieux.
  • Rôle serveur_icingaweb2 — Icinga Web 2 (UI native) + module IcingaDB : éprouvé. App PHP (php8.4-fpm) servie par un nginx local, exposée par l'edge (icinga.lab.chezlepro.internal, auto-dérivé : vhost + cert SAN + A PowerDNS + alias plancher). Config par fichiers .ini (config/resources/authentication/roles + module icingadb), pas d'assistant de setup. Base IcingaDB via resoudre_base (registre). Auth LDAP direct vers OpenLDAP (LDAPS, client_pki sur sup-01) — icingaweb2 n'a pas d'OIDC natif ; SSO-par-proxy = raffinement futur. Prouvé : testmail (LDAP) se connecte (/dashboard), et le module IcingaDB affiche la supervision (hôte icinga). Déploiement failed=0 (le rôle est bon ; les frictions étaient dans le simulateur de login curl : contrôle de cookie _checkCookie, champs uid/submit_login, valeur CSRF avant name).
  • Module BPM (Business Process) éprouvé + codifié — pile Icinga complète. serveur_icingaweb2 installe + active icingaweb2-module-businessprocess (serveur_icingaweb2_modules), crée le répertoire des processus (éditable via l'UI, groupe icingaweb2, setgid) et sème des processus métier en IaC (serveur_icingaweb2_bpm_processes, nom → contenu .conf). Format des feuilles host;service (éprouvé via les fixtures du module). Prouvé : un processus « Supervision Chezlepro » (agrège load/procs/swap/ping4/ssh du host icinga en logique ET) rend un état dans l'UI (testmail connecté), avec le backend IcingaDB (pas d'IDO). Rôle re-prouvé (reset → recrée le processus, idempotent). BPM n'est pas remplaçable par Grafana (roll-up d'impact métier). Pile Icinga = moteur + Web 2 + BPM, complète.
  • Cœur Icinga éprouvé (supervision active). serveur_icinga (cœur : icinga2 + icingadb + icingadb-redis) déployé sur sup-01 (🔧), base icingadb PostgreSQL via le registre. Prouvé : 3 services actifs, et le moteur supervise — IcingaDB peuplée (1 hôte, 12 services, résultats de checks persistés en base). Zéro bug de déploiement (rôle bien bâti). Icinga Web 2 + module BPM restent différés (phases dédiées : UI native + vues d'impact métier ; le BPM n'est pas remplaçable par Grafana). Confirme aussi le DRY resoudre_base sur icinga (les 3 rôles consommateurs validés).
  • Forgejo branché au SSO OIDC (« Se connecter avec Chezlepro ») — 2e app SSO, prouvée. serveur_forgejo (10.0.0) éprouvé sur forge-01 (🔧), adossé à PostgreSQL (base forgejo auto-provisionnée depuis le registre), exposé par l'edge (vhost + cert SAN + A PowerDNS + alias plancher, tout auto-dérivé de expose). Source OAuth2 vers Keycloak (forgejo admin auth add-oauth, idempotent, realm chezlepro), client OIDC forgejo enregistré via serveur_keycloak_clients. Auto-enregistrement OIDC ([oauth2_client] ENABLE_AUTO_REGISTRATION + ALLOW_ONLY_EXTERNAL_REGISTRATION : identités depuis l'annuaire seulement). Prouvé (flux authorization code headless) : testmail (LDAP) se connecte, compte auto-créé (testmail@lab.chezlepro.internal), atterrit sur le tableau de bord. 5 bugs de 1er déploiement corrigés : dépendance périmée serveur_sendmailserveur_postfix (le vrai MTA) ; app.ini doit appartenir au user git (Forgejo persiste des secrets générés) ; ordre admin/migrations (flush_handlers + wait_for avant admin user create) ; HTTP_ADDR 127.0.0.10.0.0.0 (l'edge nginx est sur un autre hôte, 502 sinon) ; auto-enregistrement OIDC.
  • DRY : rôle utilitaire partagé resoudre_base (résolution BD depuis le registre). Le bloc copié-collé dans serveur_keycloak, serveur_forgejo et serveur_icinga (charger le registre, filtrer par consommateur, déréférencer le secret via lookup('vars', ...), résoudre hôte/port) est extrait dans roles/resoudre_base (facts resoudre_base_entree/db_password/db_host/db_port, no_log). Les 3 rôles l'incluent (include_role) et adoptent les facts. Le secret ne quitte toujours pas le rôle (déréférencé au déploiement). Fait « sur la preuve » : re-déploiement keycloak + forgejo failed=0, idempotent, testmail token Keycloak HTTP 200. Ferme le reste noté de la Phase 2 des bindings (cf. docs/bindings-conception.md).
  • PowerDNS — A d'exposition auto-dérivés (le DNS de la Phase 3 des bindings). serveur_powerdns génère désormais, dans la zone interne, un enregistrement A pour chaque FQDN d'exposition (champ expose des applications) vers l'edge qui le sert (domaines.edge) — via expositions_des_applications (même source que les vhosts nginx et les SANs du cert edge). Déclarer expose produit maintenant vhost + SAN de cert + enregistrement DNS, tout dérivé. Prouvé : dig @infra-dns-01 grafana.lab.chezlepro.internal et keycloak.…192.168.15.21 (edge). Option serveur_powerdns_publier_expositions (défaut true). Limite / reste : PowerDNS est autoritatif, pas récursif — pour que les nœuds utilisent ces A sans casser la résolution Internet, il faut un récursif (pdns-recursor : forward de la zone interne + récursion du reste) ou garder le plancher /etc/hosts. Ne PAS repointer naïvement client_dns vers l'autoritatif.
  • hosts_statiques — alias d'exposition dans le plancher /etc/hosts (résolution client, sûre). Le plancher pose désormais, sur chaque nœud, <IP edge> <FQDN exposé> pour chaque expose (dérivé de domaines.edge, même source que nginx/PowerDNS). Indépendant du DNS, aucun risque de couper la résolution (choix retenu vs pdns-recursor). Chargement du plan best-effort (stat delegate_to: localhost + become: false — les registres vivent sur le nœud de contrôle ; ignoré si le plan est absent, ex. préparation du template). Prouvé : /etc/hosts d'obs-01 régénéré avec keycloak/grafana → edge (ligne manuelle éliminée), getent OK, et le flux SSO Grafana fonctionne via la résolution du plancher (login: testmail). Boucle Phase 3 fermée : déclarer exposevhost + SAN cert + A PowerDNS + alias plancher, tout dérivé. Bugs corrigés en chemin : serveur_loki (groupe loki manquant), stat sur cible→contrôle, become inutile sur le contrôle.
  • Rôle client_unbound — résolveur local (DNS dynamique) : éprouvé sur un nœud. Unbound par nœud (127.0.0.1) avec stub-zone vers l'autoritatif interne (PowerDNS) + récursion Internet (ou forward via client_unbound_transitaires). Alternative dynamique au plancher /etc/hosts statique, sans casser Internet. Bascule de /etc/resolv.conf protégée (client_unbound_apply + client_unbound_confirm) et validée AVANT (Unbound doit résoudre interne + Internet, sinon pas de bascule → nœud jamais coupé). Prouvé sur data-sql-01 (rayon d'impact minimal) : dig @127.0.0.1 keycloak/id-sso-01.lab.chezlepro.internal → PowerDNS, deb.debian.org → récursion, apt OK. Rôle sûr par défaut (apply: false : installe Unbound sans toucher au resolver). Rollout flotte = opt-in par nœud. Note direction : OPNsense embarque Unbound → à terme, l'Unbound réseau peut vivre sur l'appliance de bordure (nœud public, Étape B) ; le rôle par-nœud reste portable et complémentaire (cache local).
  • Bindings — Phase 1 : résolveur de liens dans instancier.py (relations service→service déclaratives). Une application déclare ses liens: [{vers, role}] dans plan/applications.yml ; chaque rôle décrit les liens qu'il accepte dans meta/liens.yml (setops_liens.accepte, comme meta/empreinte.yml). instancier résout la cible (FQDN interne dérivé de la nomenclature + domaine_interne), substitue les gabarits ({cible.fqdn}, {cible.hote}, {cible.ip}) et injecte les variables en host_vars du consommateur. Validation : rôle accepteur, cible existante, genre attendu. Migration prouvée : les liens mail Postfix→Dovecot (mailstore) et Postfix→rspamd (milter) passent de group_vars codés en dur à des liens déclaratifs — make instancier donne DIFF VIDE (mêmes variables générées), puis les group_vars sont retirés. La topologie mail devient déclarative et portable. Cf. docs/bindings-conception.md. Suite : bases (Phase 2), exposition/domaines (Phase 3), GUI (Phase 4).
  • Bindings — Phase 2 (bases) : constat + réconciliation de la note (docs/bindings-conception.md §5/§9). Inspection du code réel : le binding app→base existe déjàcôté base (consommateur/portee dans bases-donnees.yml), résolu dans le rôle au déploiement (include_vars + filtre + lookup('vars', secret)), sur 4 rôles (postgresql, forgejo, keycloak, icinga). Délibérément conservé (le secret ne quitte jamais le rôle) — ne PAS dupliquer en app-side/instancier. Deux directions assumées : app→app côté app (instancier), app→base côté base (registre). Reste (reporté à l'épreuve de Keycloak) : factoriser le bloc de résolution copié-collé en include partagé (DRY).
  • docs/carte-set-ops.md — carte d'orientation (index + mécanismes transverses). Après audit du dépôt : point d'entrée « à lire d'abord » (index du corpus, ~22 docs), et catalogue des mécanismes dispersés dans le code (les 2 directions de binding, pont de cert, résolution BD par registre, socle-first, sûreté check-mode, voûte, dimensionnement) avec où ils vivent. But : ne plus re-découvrir l'existant. Constat : la cruft était déjà inventoriée dans catalogue-services.md (rôles-catégories inertes, échafaudages) — non dupliquée, référencée. catalogue-services.md « État d'implémentation » rafraîchi (rôles éprouvés sur VM réelles : socle, PKI, LDAP, DNS, nginx, pile courriel). Pointeur ajouté depuis architecture-set-ops.md.
  • serveur_postgresql et serveur_keycloak éprouvés sur VM réelles (🔧). PostgreSQL déployé (data-sql-01), écoute réseau + pg_hba VLAN, et provisionne la base keycloak depuis le registre (bases-donnees.yml) — binding app→base prouvé en réel (base + rôle créés, mot de passe = vault_bd_keycloak). Keycloak 26.0.7 déployé (id-sso-01), mode prod, connecté à PostgreSQL (87 tables du realm master écrites), token admin obtenu (auth adossée à la BD). Lacunes connues (documentées catalogue-services.md) : fédération LDAP et edge nginx pas encore câblés. Reste : DRY du bloc de résolution BD (keycloak/forgejo/icinga).
  • serveur_keycloak — fédération LDAP (modèle d'identité A) : automatisée et prouvée. Le rôle configure, via kcadm (idempotent), un realm applicatif (serveur_keycloak_realm, déf. chezlepro) et un provider de stockage LDAP READ_ONLY vers OpenLDAP (LDAPS, uid/entryUUID, inetOrgPerson). TLS LDAPS validé via le truststore système (truststore-paths/etc/ssl/certs/ca-certificates.crt, racine step_ca posée par client_pki, désormais requis sur le nœud). Secrets par environment + no_log. Éprouvé avant codification puis prouvé par le rôle : un utilisateur LDAP (testmail) obtient un token via le realm (HTTP 200), et le redéploiement est idempotent (changed=0). Nouveau : tasks/federation-ldap.yml. Reste : edge nginx (accès HTTPS par nom), mappers d'attributs/groupes fins.
  • Edge nginx + exposition (Phase 3 des bindings) — prouvés avec Keycloak. Sans changement de code : la machinerie serveur_nginx_publier_expositions existait déjà (lit expose des applications + edge de domaines.yml, dérive amont = http://<IP hôte>:<port>, génère le vhost avec X-Forwarded-*). Le bac à sable déclare keycloak.expose: [keycloak.lab.chezlepro.internal] + le domaine interne lab.chezlepro.internal (edge serveur_nginx). Prouvé : le vhost s'auto-génère (keycloak.lab.chezlepro.internal → http://192.168.15.81:8080), et la découverte OIDC via l'edge renvoie "issuer":"https://keycloak.lab.chezlepro.internal/..." (les X-Forwarded passent, Keycloak se sait derrière HTTPS). Limite connue : le cert TLS de l'edge est encore le snakeoil auto-signé (avertissement navigateur). Raffinement recommandé (réutilise l'existant, pas de nouveau mécanisme) : ajouter les FQDN d'exposition aux client_pki_sans de l'edge (client_pki demande + renouvelle déjà le cert d'hôte), puis pointer serveur_nginx_certificat sur le cert client_pki (/etc/step/certs/<edge>.crt).
  • Cert de l'edge : snakeoil → step_ca (HTTPS valide). Appliqué le raffinement ci-dessus : client_pki ajouté à l'edge, ses client_pki_sans incluent le FQDN d'exposition (keycloak.lab.chezlepro.internal), et serveur_nginx_certificat/_cle pointent sur le cert client_pki. Prouvé : HTTPS HTTP 200 avec ssl_verify_result=0 (chaîne validée contre la racine step_ca, nom correct), émetteur Set-OPS Internal CA. Sans nouveau code (client_pki + group_var). Gaps notés : (1) recharger nginx au renouvellement du cert (le cert-renewer renouvelle en place, nginx ne recharge pas seul — hook à ajouter) ; (2) auto-dériver les SANs d'exposition de l'edge depuis le plan (au lieu de les lister dans le group_var).
  • Grafana branché au SSO OIDC (« Se connecter avec Chezlepro ») — prouvé de bout en bout. serveur_grafana : config OIDC via GF_AUTH_GENERIC_OAUTH_* (client confidentiel grafana, realm chezlepro, secret vault_grafana_oidc). serveur_loki + serveur_prometheus + serveur_grafana déployés sur obs-01 (🔧). Bug de rôle corrigé : serveur_loki créait le répertoire en group: loki alors que le paquet crée l'utilisateur en nogroup sans groupe loki → ajout de la création du groupe. Prouvé (flux authorization code headless, via l'edge HTTPS) : testmail (user LDAP) se connecte à Grafana par le SSO — /api/user renvoie login: testmail, email et nom fédérés depuis LDAP. Chaîne complète LDAP → Keycloak → Grafana. Gaps notés (pour rendre 100 % déclaratif) : (1) l'enregistrement du client OIDC dans Keycloak a été fait via kcadm à la main (à codifier — rôle grafana ou liste de clients côté keycloak) ; (2) la résolution keycloak.…internal → edge sur obs-01 est un /etc/hosts manuel (PowerDNS devrait porter les A d'exposition — chaînon récurrent) ; (3) mapping de rôles Grafana (tous Viewer par défaut).
  • serveur_keycloak — enregistrement des clients OIDC codifié (gap précédent fermé). Le rôle gère une liste déclarative serveur_keycloak_clients (clientId, redirect_uris, web_origins, secret) et enregistre chaque client confidentiel via kcadm idempotent (tasks/clients-oidc.yml, create-si-absent, no_log). Décision : côté Keycloak (les creds admin restent dans le seul rôle Keycloak, pas répandus dans chaque rôle app) ; le secret référence la même variable de voûte que l'app. Prouvé : client grafana supprimé → rôle → recréé → testmail se connecte à Grafana (login: testmail) ; redéploiement idempotent (changed=0). Le déploiement de Grafana au SSO est désormais autonome.

2026-07-02

Décidé

  • Bindings — conception des relations app/base/serveur/domaine (docs/bindings-conception.md). Les relations service→service sont aujourd'hui codées en dur, éparpillées dans des group_vars (ex. Postfix→Dovecot/rspamd/LDAP), ce qui casse la portabilité multi-tenant. Direction retenue : liens déclarés côté application (liens: [{vers, role}]), résolus par instancier.py en variables Ansible ; chaque rôle décrit les liens qu'il accepte dans meta/liens.yml (comme meta/empreinte.yml) ; FQDN cible dérivé de la nomenclature (jamais codé en dur). Domaines publics traités comme lien exposition (écrit sur l'edge). Réconcilie l'existant (bases consommateur, domaines.edge). Preuve de migration ciblée : les 3 liens mail. Implémentation à suivre (phasée).
  • Licence : passage de CC BY-NC-SA 4.0 à AGPLv3. Les licences Creative Commons ne sont pas faites pour du logiciel (position de CC elle-même) et la clause NonCommercial contredisait le principe fondateur « tout est libre » — en plus de bloquer les artisans/coopératives visés. LICENSE remplacé par le texte officiel intégral de l'AGPLv3 (verbatim, non modifié). Attribution + modèle libre + services/certification documentés dans le README (méthode d'attribution recommandée, sans toucher au texte de la licence). L'AGPLv3 protège la souveraineté (anti-captation propriétaire en SaaS) sans interdire l'usage commercial.
  • Architecture d'identité/SSO (docs/identite-sso.md). Modèle A : OpenLDAP source de vérité, Keycloak fédéré (SSO web OIDC, MFA, self-service), mail en bind LDAP direct. Une identité, un mot de passe, deux chemins d'auth (web→Keycloak, mail→LDAP), tout adossé au même OpenLDAP. Sert la portabilité multi-tenant (chaque tenant = son LDAP + son Keycloak fédéré). Pas Keycloak-source (casse-tête mail + moins portable).
  • Service courriel : pivot de Stalwart vers Postfix + Dovecot + rspamd. La Phase 1 Stalwart (serveur_stalwart) avait été prototypée et déployée (v0.16.11, install + démarrage en mode récupération). Le prototypage a révélé un projet trop jeune/volatil pour un pilier mail critique : config cassée entre 0.15 et 0.16, outil IaC stalwart config apply annoncé mais non livré dans le binaire, API REST supprimée (JMAP), gros backlog. Pivot vers la stack mature Postfix/Dovecot/rspamd, en prime 100 % configurable par fichiers (alignée au modèle déclaratif Set-OPS). Le rôle serveur_stalwart est retiré (git en garde la trace) ; docs/courriel-conception.md mis à jour. Réévaluer Stalwart ~2028.

Ajouté

  • docs/pouvoirs-set-ops.md — bilan des capacités du moteur. Inventaire structuré (moteur/plan, plan de contrôle GUI, socle durci, piliers d'infrastructure, services outillés, patrons d'ingénierie), distinguant honnêtement « prouvé sur cluster réel » de « outillé ».
  • Rôle serveur_rspamd (rspamd 3.x) — antispam + DKIM, en milter sur Postfix. Installé sur le nœud edge-mta (avec Postfix), backend Redis local, worker proxy en mode milter auto-scan (:11332), signature DKIM sortante (clé générée par le rôle de façon idempotente ; enregistrement DNS public affiché pour l'Étape B). Config par surcharges /etc/rspamd/local.d/. Postfix branché via smtpd_milters (option serveur_postfix_rspamd_milter, milter_default_action = accept → tolérant si rspamd indisponible). Prouvé : un courriel traversant le milter ressort scanné (rspamc stat : 1) et signé DKIM (DKIM-Signature: d=…), puis livré et lu en IMAP. Étape A (courriel interne) complète : dovecot + postfix + rspamd.
  • Flux courriel interne PROUVÉ de bout en bout (Étape A). Envoi → Postfix (edge-mta, validation LDAP) → LMTP réseau → Dovecot (mail-store) → boîte Maildir → lu en IMAP (auth LDAP, TLS step_ca) : status=sent, message lu (sujet + corps). Réglages Dovecot 2.4 qui débloquent la remise LMTP : userdb static { static_allow_all_users = yes } (sinon NOTFOUND pour l'expéditeur/raw-mail-user externe), mail_inbox_path = vidé (le défaut mbox /var/mail root refusait l'autocréation de l'INBOX), Maildir explicite (mail_home + mail_path = %{home}/Maildir), chemin par nom d'utilisateur (home identique côté LMTP local-part et IMAP adresse complète ; mono-domaine, multi-domaine = raffinement Étape B).
  • Rôle serveur_postfix (Postfix 3.x) — MTA du nœud edge-mta. Réception :25, cartes LDAP (validation des boîtes via l'attribut mail), remise LMTP réseau vers le nœud mail-store Dovecot (virtual_transport = lmtp:inet:[…]:24), TLS via step_ca (pont de cert), aucune boîte locale. Config main.cf + carte ldap-mailboxes.cf, validée par postfix check. Secret de bind : vault_openldap_admin. Nécessite serveur_postfix_mailstore_hote (FQDN du mail-store). Validé statiquement ; déploiement réel à suivre.
  • Rôle serveur_dovecot (Dovecot 2.4) — déployé et prouvé. IMAP :993/:143 + LMTP, auth/annuaire LDAP (vers serveur_openldap, filtre mail), stockage Maildir (user système vmail), TLS via step_ca (pont de cert + resync au renouvellement), neutralisation de l'auth système par défaut. Config en drop-in syntaxe Dovecot 2.4 (mail_driver, ssl_server_cert_file, passdb ldap/userdb static, %{user}), validée par doveconf au déploiement. Sockets d'intégration Postfix conditionnels (rendus si l'utilisateur postfix est co-localisé). Prouvé : doveadm auth test — bon mot de passe accepté, mauvais refusé, sur cert step_ca. Secret de bind : vault_openldap_admin.
  • serveur_openldap durci pour la prod : TLS via step_ca + organisation en intrant.
    • TLS (LDAPS + STARTTLS) : le certificat d'hôte step_ca (déposé par client_pki, root:root 600) est synchronisé vers un emplacement lisible par openldap (/etc/ldap/tls) par un script + une unité path systemd qui re-synchronise et recharge slapd à chaque renouvellement ; olcTLS* configuré dans cn=config, SLAPD_SERVICES expose ldaps://. Dégrade proprement (slapd en clair local) si client_pki n'a pas encore posé le cert.
    • Organisation : nouvel intrant chezlepro_organisation (remplace le « Exemple Inc » codé).
    • Écrit en code de prod, éprouvé statiquement ; validation par déploiement réel à suivre.
  • docs/courriel-conception.md — cadrage du futur service de courriel souverain : full self-host, suite Stalwart (adoptée, enveloppée par un rôle mince), topologie MX primaire + MX secours, intégration identité (LDAP) / DNS / PKI (Let's Encrypt public vs step_ca interne), enregistrements DNS publics, décisions ouvertes (IP/PTR, secours, stockage) et phasage. Conception seulement — aucun rôle livré.

2026-07-01

Ajouté

  • État RÉEL vs plan dans le GUI (sonde de vie + auto-actif). Le badge planifié/ actif décrit l'intention du plan, pas l'existence de la VM — d'où la confusion « serveur planifié mais vivant ». Deux ajouts :
    • Sonde de vie : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan (/api/sondes, en parallèle) et affiche un état réel — ● vivante / ● injoignable — sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan.
    • Auto-actif : un hôte qu'on matérialise (clone creer réussi) ou qu'on déploie passe automatiquement actif dans le plan (matérialisé = actif), puis l'inventaire est régénéré pour que le changement se voie partout (en-tête inclus).
    • Compteur « vivantes » dans l'en-tête (depuis la sonde), à côté de actifs/planifiés.

Corrigé

  • nginx ne validait pas sur Debian 13 (server_tokens en double). Debian 13 livre server_tokens off; actif dans /etc/nginx/nginx.conf (avant : commenté). Le drop-in conf.d/99-setops.conf du rôle le redéclarait → nginx -t échouait (« directive is duplicate ») et le déploiement plantait au handler de validation. Le rôle serveur_nginx neutralise désormais la ligne distro (le drop-in reste l'unique source). Trouvé en déployant nginx pour de vrai sur un hôte edge.

  • Une voûte chiffrée cassait instancier / « Appliquer le plan ». ansible-inventory --list (utilisé pour la comparaison sémantique du plan) tente de déchiffrer group_vars/all/vault.yml et échoue sans mot de passe (exit 4) — alors que l'opération est structurelle, sans secret. instancier utilise désormais automatiquement le fichier conventionnel ~/.config/setops-vault-pass (si ANSIBLE_VAULT_PASSWORD_FILE n'est pas déjà défini).

  • Secrets des rôles non câblés à la voûte (échafaudage manquant). Les rôles à secrets déclaraient serveur_X_password: "" avec, en commentaire seulement, la variable de voûte attendue ({{ vault_X }}) — sans mapping réel. Résultat : remplir la voûte selon vault.exemple.yml ne suffisait pas, le secret restait vide et l'assertion « secrets requis » échouait. Les 12 secrets des 8 rôles (serveur_step_ca, serveur_forgejo, serveur_grafana, serveur_keycloak, serveur_openldap, serveur_redis, client_ldap, client_pki) pointent désormais vers leur variable de voûte : serveur_X_password: "{{ vault_X | default('') }}". Chaque instance n'a plus qu'à remplir ses vault_* dans sa voûte chiffrée ; aucun mapping par instance.

  • « Vérifier » (dry-run --check) échouait faussement sur un hôte frais. Les tâches « démarrer service » et les handlers « redémarrer / recharger / valider » des rôles applicatifs touchent un paquet que --check n'installe pas réellement → le service (ou le fichier de zone/conf) n'existe pas encore → faux fatal, qui bloquait le déploiement (le dry-run doit réussir pour débloquer « Déployer »). Ajout de when: not ansible_check_mode sur ces tâches et handlers des 13 rôles serveur_* (29 gardes). En dry-run elles sont sautées ; en vrai déploiement, inchangées.

  • Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence dès que le plan divergeait de l'inventaire (il appelait instancier appliquer sans --force). Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais régénéré et les compteurs restaient figés. Le clic « Appliquer » est l'intention explicite → le GUI force désormais (git reste le filet).

  • Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable. Chaque objet du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault

    • confirmation renforcée en prod) :
    • Serveur🖥 Pousser clone la VM depuis le golden template (make creer-vm), disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas.
    • ApplicationPousser déploie l'hôte porteur (make deployer HOTE=<hôte>).
    • BasePousser déploie l'hôte du serveur de BD (crée la base). Nouveaux modes creer / pousser dans executer_flux + routes /api/creer et /api/pousser. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser → Vérifier → Déployer.
  • Premier déploiement RÉEL validé de bout en bout (cluster asgard) : flux canonique plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3 correctifs ci-dessous.

2026-06-30

Ajouté

  • Plancher de résolution /etc/hosts (indépendant du DNS). Nouveau rôle de socle hosts_statiques (dans serveur_debian) : génère /etc/hosts sur chaque VM depuis l'inventaire (nom + FQDN interne → IP réelle). Tout l'écosystème se résout par nom même serveur DNS éteint, et le bootstrap ne dépend plus du DNS. PowerDNS devient une commodité (zone/externe/dynamique) ; client_dns est rendu tolérant (inerte si aucun DNS interne) et sa dépendance à serveur_powerdns passe molle.
  • Adressage fédéré : index d'instance. Le VMID n'est plus codé 9CSNN en dur : il prend le préfixe d'un index déclaré en tête de plan/nomenclature.yml ({index}{catégorie}{service}{séq}). Convention : supernet = 10.(10+index).0.0/16, VMID = index·CSNN. Permet à N écosystèmes de coexister/s'interconnecter sans collision (Chezlepro=1 → 10.11/1xxxx, Technolibre=2 → 10.12/2xxxx). Sans index → 9CSNN (rétro-compatible ; bacs à sable, plages ad-hoc 172.19.x). Code mort retiré (deriveServeur JS). Voir docs/multi-instances.md.
  • Doc docs/multi-instances.md : cadrage « un moteur, N écosystèmes » — l'instance comme dépôt autonome, la bascule, l'isolation, et le socle multi-tenant.
  • Bascule d'instance (make instance-utiliser NOM=…). Repointe le symlink instance vers un autre dépôt d'instance (prod ↔ bac à sable) ; make instance-courante affiche l'instance montée. Permet d'exploiter plusieurs instances (séparation par instance) depuis un seul moteur.
  • Identité des intrants relative à l'inventaire. Le panneau « Intrants » lit/écrit l'identité dans group_vars/all/10-intrants.yml de l'inventaire monté (fichier réel pour une instance autonome, symlink vers une source partagée sinon — l'écriture suit le symlink). Fonctionne donc aussi bien pour une instance « par instance » que pour l'ancien partage lab/production.
  • Inventaire d'instance neutre et configurable (principal / SETOPS_INVENTAIRE). Le moteur (Makefile + inventory_gui, instancier, config_proxmox, serveurs, applications) ne code plus en dur inventories/lab / inventories/production : il vise un inventaire par instance, détecté de façon rétro-compatible (principal > production > lab) et surchargeable par SETOPS_INVENTAIRE. Les instances existantes (découpage lab/production) continuent de fonctionner à l'identique ; les nouvelles peuvent adopter inventories/principal/. Deuxième pierre de la séparation par instance (la 1re étant le drapeau setops_production).
  • Garde-fou de prudence par instance (setops_production). Le déploiement réel est désormais possible sur toute instance (un bac à sable déploie sur son infra lab — il est isolé et fonctionnel). Le drapeau setops_production dans group_vars/all/ ne bloque plus rien : il marque la PRODUCTION pour exiger une confirmation renforcée au déploiement (bannière/badge rouge « PROD », bouton Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (false) affiche « bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau, ancien repère « inventaire production »). Le GUI affiche toujours quel type d'instance est monté.

Modifié

  • Voûte de secrets unique par environnement. Fini les voûtes éparpillées : tous les secrets de l'instance (token Proxmox + 17 vault_* pour PKI, LDAP/SSO, bases, forge, observabilité) vivent dans un seul fichier chiffré, inventories/<env>/group_vars/all/vault.yml. Gabarit committé exemples/vault.exemple.yml. make config (config_proxmox.py) écrit/édite désormais cette voûte (semée depuis le gabarit si absente). .gitignore durci (**/vault.yml). Docs mises à jour (config-proxmox.md avec étapes de migration, intrants-communs.md §H, QUICKSTART). Le GUI ne stocke toujours aucun secret.

Corrigé

  • Trois bugs trouvés au premier déploiement réel (cluster asgard).
    • Clonage/Makefile codaient inventories/lab en dur (config Proxmox + voûte) — vestige du modèle env qui cassait les instances principal. Le playbook de clonage et le Makefile détectent maintenant l'inventaire (lab > principal > production).
    • Redimensionnement disque non idempotent : quand le disque dérivé du plan est plus petit que le golden template, Proxmox refuse (shrinking disks is not supported) et le clone échouait. Le resize est désormais grow-only (tolère le cas, la VM garde le disque du template — le dérivé est un minimum).
    • PowerDNS refusait de démarrer (multiple backends 'bind') : le rôle redéclarait launch+=bind que le paquet pdns-backend-bind pose déjà. Le rôle ne déclare plus launch (seulement bind-config).
  • chezlepro_timezone n'était appliqué nulle part. Cet intrant de base global était défini mais aucun rôle ne s'en servait. Le rôle chrony (appliqué à tout hôte via serveur_debian) règle désormais le fuseau horaire à partir de chezlepro_timezone (chrony_timezone par défaut, vide = ne pas toucher). Les autres défauts globaux (nœud / stockage / pont Proxmox) étaient déjà réutilisés comme valeurs par défaut, surchargeables par hôte.

Modifié

  • Détail GUI : section « Groupes (dérivés) » retirée (redondante). Depuis la fusion en atelier maître-détail, elle ne répétait que le socle (universel), les rôles des applications (déjà dans « Applications ici ») et les intégrations (déjà cochées). Son seul signal unique — le prérequis bloquant — est désormais nommé dans le pied (« Prérequis manquant : … ») ; le panneau Dépendances reste pour la vue d'ensemble. Code mort retiré (renduGroupe, detailGroupe, titreGroupe, CSS .groupe*).
  • Nettoyage CSS/HTML du GUI après la refonte : retrait des règles et éléments morts (.message, .chips-filtre/.chip-f, .onglet*, .base-ligne, .bases-liste, .ch-grp*/.ch-fleche/.ch-roles, .champ-val, divs #message et #chips).
  • GUI refondu en atelier maître-détail unifié. Toutes les vues suivent le même motif : tuiles à gauche, détail + saisie à droite, le panneau droit reflétant la sélection de la vue courante (fin du panneau « figé » au changement de vue). Les vues Applications et Bases passent de tableaux pleine largeur à ce motif ; la saisie se fait dans le panneau droit, avec liens cliquables entre objets (serveur → application → base). Le panneau droit est élargi (~38 %).
  • Fusion Serveur/Hôte. Les vues « Inventaire » (hôtes, lecture seule) et « Serveurs » (plan) faisaient doublon : elles sont fusionnées en une seule vue Serveurs. Sa tuile porte le statut de réconciliation (réconcilié / divergent / non instancié) ; son détail réunit l'identité éditable (plan), les dérivés (VMID/IP/VLAN), les groupes, les applications et bases hébergées, et Vérifier/Déployer. La navigation clavier (1-3, j/k, v/d, /) et le filtre opèrent désormais sur les serveurs. Un bandeau « Comment lire ce parc » explicite le modèle serveur·hôte·groupe·application·base. Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection d'hôte, etc.).

Ajouté

  • Fluidité d'exploitation du GUI (sans dépendance, stdlib pure) :
    • Notifications empilées (toasts) auto-effaçables au lieu d'une bannière unique écrasée ; les erreurs restent plus longtemps, les opérations longues mettent à jour leur propre toast.
    • Durée d'exécution affichée en direct dans la console (Vérifier / Déployer) et suivi vivant de « Appliquer le plan ».
    • Navigation clavier : 1-5 changent de vue, j/k parcourent les hôtes, v/d vérifient/déploient l'hôte sélectionné, / cible le filtre, Ctrl+S sauvegarde la vue éditable courante (ignorés pendant la saisie).
    • Validation inline des champs du plan (vue Serveurs) : nom fonction-NN, mémoire, cœurs, disque — liseré rouge et blocage avant l'envoi.
    • Garde-fou anti-perte : confirmation beforeunload si des éditions de plan ne sont pas sauvegardées.
    • Le bouton Sauvegarder de l'en-tête devient contextuel (sauve la vue éditable, désactivé en lecture seule) — fin de l'impasse 409 ; suppression du code mort.

Ajouté

  • Vue Serveurs : champs alimentés par les paramètres globaux. À + Serveur, Nœud et Stockage deviennent des listes déroulantes (catalogues proxmox_noeuds / proxmox_stockages, vide = défaut global) et Intégrations une rangée de cases à cocher des rôles client_* disponibles (au lieu d'une saisie texte). Les catalogues Nœuds / Stockages / Ponts sont de nouveaux intrants (classe « Catalogue ») éditables dans le panneau « Intrants de base » et stockés dans group_vars/proxmox.yml ; l'API expose integrations_disponibles (scan de roles/client_*). Le type liste (chaîne virgulée → liste YAML dédoublonnée) est ajouté au schéma des intrants.

Supprimé

  • Vue « Chaîne » du bandeau (redondante). Son contenu (renduChaine) était déjà rendu, par hôte, dans l'onglet « Chaîne » du détail de la vue Inventaire ; la vue ne faisait que le répéter pour tous les hôtes à la fois, sans réseau/proxmox ni boutons d'opération. Retirée (bouton, rendu dessinerArbre, CSS .arbre-*) ; la chaîne reste consultable hôte par hôte. Raccourcis clavier ramenés à 1-4.

Modifié

  • Vue Inventaire (cartes) : détail converti en inspection lecture seule. La vue affichait « généré depuis le plan (lecture seule) » tout en exposant une surface d'édition (+ Hôte, ✕, bascule actif/planifié, Proposer, champs et cases de groupes éditables) qui ne pouvait pas être persistée (l'inventaire est généré ; /api/inventaire renvoie 409). Ces contrôles orphelins sont retirés : nom, champs Réseau/Proxmox et groupes en lecture seule, état affiché en badge. Vérifier / Déployer et les onglets d'inspection sont conservés. L'édition reste dans les vues Serveurs / Applications / Bases. Code mort supprimé (ajouterHote, supprimerHote, definir, definirNom, definirEtat, basculerGroupe, proposer, autoProposer, prochainSeqLibre, marquerModifie, état modifie).
  • Référence des paramètres de make config (docs/config-proxmox.md). Explique un par un les 16 paramètres Proxmox non sensibles + les secrets API demandés par l'assistant : sens, valeur par défaut, quoi saisir, et quels champs sont des constantes vs des défauts surchargeables par hôte. Renvois ajoutés depuis make help et QUICKSTART.md. Comble un trou : ces invites n'étaient expliquées nulle part de façon pérenne (impératif « exploitable sans IA »).
  • Panneau « Intrants de base » dans le GUI. Un bouton ⚙ Intrants ouvre une fenêtre unique pour saisir les valeurs communes à tout l'écosystème, avec une distinction visible entre constantes (valeur unique, non surchargeable) et défauts (valeurs proposées, surchargeables dans les instances). Les secrets ne sont jamais saisis ni affichés ici (garde-fou INTRANTS_CLES_INTERDITES + filtrage par schéma) : ils restent dans le Vault Ansible, édités en CLI ; le panneau les liste seulement à titre informatif. Un changement de domaine_interne (clé de voûte) demande une confirmation explicite ; un domaine_interne vide est refusé côté serveur. La nomenclature reste en lecture seule (modifiable dans le plan). Voir docs/intrants-communs.md et docs/intrants-base-gui-conception.md.
  • Source unique d'identité partagée. domaine_interne et chezlepro_timezone vivent désormais dans inventories/partage/intrants-identite.yml, référencé par symlink depuis chaque environnement (group_vars/all/10-intrants.yml) — fin de la duplication lab/production.
  • Info-bulles d'aide sur les champs du GUI. Survoler la description d'un champ à saisir affiche une bulle avec des instructions sommaires.

2026-06-28

Ajouté

  • Document de présentation de l'écosystème Chezlepro (docs/ecosysteme-chezlepro.md). Description vulgarisée à destination client/partenaire : les piliers de l'écosystème souverain, le modèle reproductible (plan → génération → clonage → conformité), et un inventaire des mesures de renforcement réellement en place (SSH durci, fail2ban, sysctl, nftables, AppArmor, auditd, mises à jour automatiques, gestion des secrets, garde-fous destructifs, sécurité par l'architecture). Honnête sur le statut « défini/validé vs déployé ».

Ajouté

  • Dimensionnement dérivé des ressources VM. Les cœurs/RAM/disque d'une VM sont désormais estimés depuis les logiciels hébergés + le socle SE, au lieu d'hériter des specs du golden template (CPU/RAM identiques pour toutes les VM auparavant). Chaque rôle déclare son empreinte (roles/<rôle>/meta/empreinte.yml) ; le générateur somme par hôte (marge + arrondis) et écrit proxmox_coeurs/ proxmox_memoire/proxmox_disque_taille ; le clonage passe cores/memory à Proxmox (omit si absent → aucune régression). Override par hôte possible dans le plan (serveurs.yml). Voir docs/dimensionnement-ressources.md.

Corrigé

  • make instancier-appliquer FORCE=1 n'honorait pas --force. La recette Makefile lançait instancier.py appliquer sans relayer FORCE ; le message « Utilise FORCE=1 » était donc trompeur (un changement de plan intentionnel restait bloqué). La recette passe désormais $(if $(FORCE),--force). Trouvé en dogfooding.

2026-06-24 — Première publication publique

Première mise à disposition publique de Set-OPS, moteur Ansible d'écosystèmes numériques souverains sur Proxmox — offert à la communauté québécoise par l'Alliance Boréale, à la Saint-Jean-Baptiste 2026.

  • Moteur générique, piloté par un plan déclaratif : on édite le plan (instance/plan/*.yml), l'inventaire Ansible se génère, les VM se clonent depuis un golden template Debian 13, les rôles s'appliquent par groupes. Tout passe par make, le GUI local et la documentation.
  • Piliers d'un écosystème souverain : socle Debian durci, AC/PKI interne, DNS interne, identité (LDAP + SSO), relais courriel, bases de données, observabilité, forge.
  • Catalogue de modèles prêts à déployer (exemples/modeles/) : un hébergeur copie un modèle, le renseigne à ses couleurs, et instancie.
  • Souveraineté jusqu'au bout : Set-OPS s'exploite entièrement à la main, sans aucune IA.

Pour démarrer : QUICKSTART.md.