Set-OPS-Public/docs/decisions-architecture.md

113 lines
15 KiB
Markdown
Raw Normal View History

# Registre des décisions d'architecture
> **À quoi sert ce document.** Les décisions sont écrites là où elles s'appliquent —
> `frontiere-opnsense.md`, `sdn-evpn.md`, `migration-tenant.md`, `underlay.yml.example` — et
> leur histoire vit dans le `CHANGELOG`. Ce registre ne les répète pas : il dit **quelles
> décisions existent, pourquoi, et où lire le détail**. Sans lui, « pourquoi le `/29` et pas
> le `/30` ? » demande de relire vingt entrées de journal.
**Ce n'est pas** la doctrine de conduite (`AGENTS.md` §Principes), ni le registre des
affirmations prouvables (`docs/audit/affirmations.md`). Une décision peut n'être gardée par
aucune preuve : elle reste une décision.
**Qui décide.** Toutes ces décisions sont celles de l'opérateur du dépôt. Plusieurs ont été
prises sur recommandation — l'assistance propose et argumente, elle ne tranche pas. La
distinction compte pour la suite : une décision se renverse par celui qui l'a prise, et savoir
qu'elle a été *choisie* plutôt que *héritée* change ce qu'on s'autorise à en faire.
Les dates de la §5 sont celles de l'**historique git**, pas du moment de la conversation : ce
sont les seules vérifiables.
---
## 1. Le réseau
| # | Décision | Pourquoi | Détail | Garde |
|---|---|---|---|---|
| **D-01** | OPNsense est une frontière **nord/sud**, pas la passerelle des zones | le routage inter-zone reste au débit ligne ; la bordure ne voit pas l'est-ouest | `frontiere-opnsense.md` §1 | — |
| **D-02** | Le **lien de transit** vit dans l'underlay, pas dans un tenant | la frontière route vers tous les tenants par le même saut : il ne peut dériver d'aucun `index` | `frontiere-opnsense.md` §6 | P23 |
| **D-04** | Un point de routage porte **le même dernier octet** sur tous ses sous-réseaux | on retient une adresse, pas treize | `frontiere-opnsense.md` §4 | P23 |
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51) Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10. Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents. sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous les chemins, donc un point de panne unique du plan de données — ce qui vidait aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés, bond3 répartis. D-51 ; D-05 renversée. Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui coupait l'accès d'administration au switch en cas d'erreur. D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau, où qu'elle vive », et le devis dérive s'il doit émettre une interface routée — uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux postures : le modèle public démontre celle où le switch route. Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige passerelle » et « routeur.ip == passerelle », une règle plus forte : une passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et elle a trouvé une sous-déclaration dans le modèle public. Quatre trous corrigés, tous de la même famille (une liste figée finit par mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le transit, switches d'accès sautant sa déclaration, et le switch de tête privé d'adresse de gestion par la suppression du SVI. D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1 revient à la passerelle. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
| **D-49** | **Aucun commutateur ne route.** La frontière est le seul équipement L3 | l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie a rendu les nœuds de sortie **adjacents** à la frontière : plus un seul flux à router | `underlay.yml` | P23 |
| **D-50** | `passerelle` = la passerelle du sous-réseau, **où qu'elle vive** ; le SVI se **dérive** du rôle de son porteur | la clé signifiait « adresse du SVI du switch » — une hypothèse, pas une donnée. Un même moteur sert désormais les deux postures | `devis_reseau.py` | P23 |
| **D-51** | **Deux** commutateurs en paire, pas trois en étoile | le centre d'une étoile est sur **tous** les chemins : point de panne unique du plan de données. Deux liens vers deux commutateurs donnent la redondance qu'une étoile ne peut pas donner | `underlay.yml` | — |
| **D-06** | Les réseaux déclarent leur **fabric** ; un devis ne parle que de la sienne | le stockage jumbo vit sur ses propres commutateurs — un devis est une configuration, pas un inventaire | `sdn-evpn.md` §4 | P23 |
| **D-07** | **Pas d'ACL** sur cette fabric (`acl_inter_tenant: false`) | le matériel ne sait pas lier une ACL à un SVI ; des règles jamais liées auraient l'air d'isoler | `frontiere-opnsense.md` §1 | — |
SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30) Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
| **D-43** | Les objets SDN d'un tenant (zone, VNets, sous-réseaux) sont **dérivés du seed** et émis par `make devis-sdn` | ajouter un tenant implique 13 objets de cluster ; les créer à la main dans un dépôt où tout dérive était la dernière lacune | `sdn-evpn.md` §2 | P30 |
| **D-44** | Le **nommage SDN dérive du tenant** : zone `<PRÉFIXE><index>`, VNet `<préfixe><index><zone>` | se lit sans table de correspondance ; les zones faites à la main (`VRF0017`) sont remplacées tant qu'elles sont vides. Seule contrainte conservée : **8 caractères**, imposée par Proxmox | `sdn-evpn.md` §2 | P30 |
| **D-08** | Le **routage tenant passe au SDN EVPN** — une zone par tenant | une zone EVPN est un VRF : l'isolation devient structurelle, pas réglementaire | `sdn-evpn.md` | P23 (MTU) |
| **D-09** | Le **filtrage inter-zone d'un tenant** se fait au même niveau (EVPN) | là où le routage a lieu | `sdn-evpn.md` §3 | — |
| **D-31** | Le filtrage est-ouest est appliqué **deux fois** : hyperviseur **puis** nftables d'hôte | défense en profondeur ; les deux dérivent du même registre par les mêmes fonctions, donc ne peuvent pas diverger | `sdn-evpn.md` §3 | P25 |
| **D-32** | **Tout** ce qui entre ou sort d'un tenant passe par la frontière | un VRF n'a qu'une sortie ; conséquence : la frontière devient un **prérequis de déploiement** | `sdn-evpn.md` §3 | — |
| **D-10** | L'**inter-tenant passe obligatoirement par la frontière** | il sort du VRF, donc traverse une bordure en `block` par défaut : il ne peut plus être oublié | `sdn-evpn.md` §3 | — |
| **D-11** | La **sortie générale est déclarée** dans le registre des flux | `block out` est un vrai default-deny ; un besoin oublié ne se manifeste pas par un refus clair | `frontiere-opnsense.md` §8 | P09 |
| **D-29** | L'**overlay EVPN plafonne à 1450** ; le transport doit donc dépasser 1500 | choix d'exploitation ; l'encapsulation VXLAN coûte 50 octets | `sdn-evpn.md` §5 | P23 (MTU du transport) |
| **D-30** | L'**ICMP « fragmentation nécessaire » est déclaré**, dans les deux sens | à 1450, tout ce qui traverse la frontière dépend de la découverte de MTU de chemin ; une bordure en default-deny la casse en silence | `roles/serveur_debian/meta/flux.yml` | P09 |
| **D-12** | Nommage : **`bifrost`** aux frontières, **`sleipnir`** à la fabric | Bifröst est le pont vers l'extérieur ; Sleipnir traverse les mondes sans en sortir | `frontiere-opnsense.md` §6 | — |
## 2. Qui possède quoi
| # | Décision | Pourquoi | Détail | Garde |
|---|---|---|---|---|
| **D-13** | Un **hébergeur** sert plusieurs **tenants** et a son tenant par défaut | Chezlepro est les deux à la fois, ce qui masquait la distinction | `frontiere-opnsense.md` §2 | — |
| **D-14** | `underlay.yml` appartient à l'**hébergeur**, monté par symlink | ce sont ses commutateurs, ses câbles ; le moteur est générique, un tenant n'en possède pas | `sdn-evpn.md`, `underlay.yml.example` | — |
| **D-15** | Ce symlink **ne suit pas** `make instance-utiliser` | basculer le tenant actif ne change pas la fabric | `frontiere-opnsense.md` §2 | — |
| **D-16** | Les intrants de la **frontière** se lisent chez l'hébergeur | un hébergeur n'a qu'une frontière pour tous ses tenants | `frontiere-opnsense.md` §2 | — |
| **D-17** | L'hébergeur **n'est pas déclaré** : le symlink le désigne | une seconde déclaration ouvrirait deux valeurs contradictoires | `frontiere-opnsense.md` §2 | — |
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
| **D-35** | Le **cluster Proxmox** appartient à l'hébergeur ; seuls le **golden template** et les **défauts de placement** restent au tenant | recopié chez chaque tenant, l'inventaire du cluster avait déjà divergé — deux listes de stockages contradictoires pour le même matériel | `config-proxmox.md` | P27 |
| **D-36** | Le panneau **nomme le propriétaire** de chaque section d'intrants | éditer une section « hébergeur » vaut pour tous ses tenants ; l'écran ne le disait pas | `scripts/inventory_gui.py` (`INTRANTS_SCHEMA`) | — |
pools Proxmox : un par tenant, dérivé de l'index (D-37, P28) Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101 contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}. Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le codage. Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11. make devis-proxmox-pools rattrape la flotte existante (création du pool, puis affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes : make creer-vm dérive le pool par la même fonction et le passe à la création. Le playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le rattrapage passe par les membres. P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID — une machine appartenant à deux tenants serait pire qu'une homonymie. Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart. 27 preuves OK, 0 échec. --syntax-check du playbook de clonage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:15:14 -04:00
| **D-37** | Chaque tenant a son **pool Proxmox** ; les noms courts de VM restent **identiques** d'un tenant à l'autre | 11 serveurs sur 14 sont homonymes — c'est la preuve que la nomenclature est un gabarit ; le coût est humain (la console affiche le nom), et le pool le corrige sans rien renommer | `devis_proxmox_pools.py` | P28 |
SDN : make devis-sdn — zone, VNets et sous-réseaux dérivés du seed (D-43/44, P30) Ajouter un tenant implique 1 zone EVPN + 6 VNets + 6 sous-réseaux sur le cluster. Aucun générateur ne les produisait : dernière lacune dans un dépôt où tout dérive. 26 objets pour les deux tenants, VNI = 1000 + index×10 + zone, sous-réseau et passerelle par les mêmes fonctions que l'inventaire. Nommage, en deux temps. D'abord VRF0017 / v1174, alignés sur ce que le cluster portait — réflexe inverse du bon : cette convention venait d'une création à la main et ne disait pas de quel tenant il s'agissait. Forme retenue : t<index> pour la zone, t<index><zone abrégée> pour le VNet (t17, t17serv). C'est le préfixe que le pare-feu Proxmox utilisait déjà (t17-cli-metrique), donc un seul schéma dans tout le dépôt. Abréviation = 4 premières lettres du libellé, accents retirés. Pas de tiret entre index et zone, contrairement aux IPSets : t245-serv ferait 9 caractères, t245serv en fait 8 — la forme reste uniforme jusqu'au dernier index. Contrainte cadrante : zones ET VNets sont limités à 8 caractères par Proxmox. sdn-evpn.md annonçait chez17-services-infra (21) : il aurait été refusé à l'application. P30 refuse tout dépassement et toute collision de nom, de VNI ou de sous-réseau. Éprouvé aux bornes et par sabotage. Vérification la plus forte : avant renommage, la dérivation reproduisait à l'identique les deux zones créées à la main — nom, VNI de VRF, MTU, contrôleur. voute.py saisir : le pendant de la génération. On génère un secret dont le dépôt est la source, on saisit celui dont un tiers est la source — inventer une clé d'API OPNsense donnerait une valeur refusée à la première requête, avec P18 au vert sur une voûte inutilisable. Sans écho, double confirmation, rien sur la ligne de commande. Reste ouvert : aucun nœud de sortie déclaré. Le devis émet un marqueur, pas une valeur plausible. Deux points à trancher — le nœud de sortie route selon sa propre table (défaut actuel : 192.168.11.254, pas la frontière), et l'entrée n'est pas redondante puisqu'elle dépend d'une route statique d'OPNsense vers un seul nœud. D-45 : l'affinité de VM attend Proxmox 9 (cluster en 8.4.19), tenue à la main. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:06:42 -04:00
| **D-45** | L'**affinité de VM** (garder un groupe sur le même hyperviseur) attend **Proxmox 9** ; tenue à la main d'ici là | les *resource affinity rules* n'existent qu'en 9 ; en 8.4 seuls les groupes HA épinglent à des **nœuds**, pas des VM entre elles. Sans ressource HA déclarée, rien ne déplace ni ne sépare les VM — le sujet ne devient réel qu'en activant la HA | `sdn-evpn.md` | — |
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48) Le port du commutateur vers la frontière était figé sur le seul VLAN de transit. Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé. Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`. Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10, donc le transport tenant partageait son domaine de diffusion avec l'administration des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le remet dans sa table principale — dont la route par défaut sort par vmbr0, l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et 41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le commutateur le transporte sans le router, et le devis n'émet donc pas d'interface Vlan41. Services de l'hébergeur — décision consignée, rien n'est construit : Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière. D-46 : un hébergeur porte trois catégories — son tenant (un client comme les autres), ses opérations (supervision de la fabric, journaux, sauvegarde des configs, DNS d'underlay), et le plan de contrôle (déjà dehors). D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison de la panne. D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que pour les commutateurs (aucun agent) et la frontière (API seulement). Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux dépôts d'hébergeurs. Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et les VNI/VLAN projetés. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
| **D-46** | Un hébergeur porte **trois** catégories, pas deux : son **tenant**, ses **opérations**, le **plan de contrôle** | Chezlepro est hébergeur ET tenant, ce qui masquait des besoins n'appartenant à aucun tenant | `hebergeur-exploitation.md` §2 | — |
| **D-47** | Les **services d'exploitation** de l'hébergeur vivent dans **son dépôt**, et leurs VM se rattachent à un **pont VLAN, jamais un VNet** | un service qui observe la fabric ne peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison de la panne | `hebergeur-exploitation.md` §1, §4 | — |
| **D-48** | Les **hyperviseurs** sont gérables par Ansible ; « hors flotte » ne vaut que pour les **commutateurs** et la **frontière** | ce sont des Debian joignables en SSH ; c'est la seule façon d'y poser un exportateur de métriques | `hebergeur-exploitation.md` §5 | — |
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
authentification : SSO Keycloak devant, secours par sudo, formulaire local fermé Directive : toute authentification web passe par Keycloak, LDAP est la source unique des comptes, chaque service garde un accès de secours par sudo sur l'hôte. Les trois sont indissociables — la chaîne service → Keycloak → LDAP est en série, donc sans secours une panne exclut tout le monde, y compris pour réparer. Portée : le web seulement ; IMAP/SMTP se lient à LDAP directement et SSH est en clé seule. Posture <rôle>_connexion_locale, false par défaut. Le compte local existe — il ne peut pas dépendre de Keycloak — mais son formulaire n'est plus proposé au repos : ouvert en permanence, il contourne la politique de mot de passe, le MFA et surtout la révocation centrale. Vérifié auprès de l'amont, puis par rendu réel des gabarits dans les deux postures : - Grafana GF_AUTH_DISABLE_LOGIN_FORM → ferme ; - Forgejo ENABLE_INTERNAL_SIGNIN + ENABLE_BASIC_AUTHENTICATION → ferme, API Basic comprise. N'existe que depuis la v10 (ticket amont 7476) ; le rôle épingle 10.0.0 et un assert refuse la fermeture en deçà, car le réglage serait ignoré sans erreur ; - Nextcloud hide_login_form → MASQUE seulement : ?direct=1 reste le chemin de secours documenté par l'amont. Écrit comme tel, sans prétendre à l'équivalence. Défaut attrapé par le rendu : la condition Forgejo sans `| bool` n'émettait rien dans aucune posture — une valeur en chaîne est vraie au sens Jinja, la connexion locale serait restée ouverte en silence. docs/authentification.md, décisions D-38 à D-41. ansible-lint (production) sans échec, 28 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:50:36 -04:00
| **D-38** | Toute authentification **web** passe par Keycloak ; LDAP est la **source unique** des comptes | une identité, un mot de passe ; aucun service ne tient son propre répertoire d'humains | `authentification.md` §1-2 | — |
| **D-39** | Les protocoles qui ne parlent pas OIDC (IMAP, SMTP) se lient **directement à LDAP** | le chemin varie, la source ne varie pas | `authentification.md` §2 | — |
| **D-40** | L'accès de secours passe par **`sudo` sur l'hôte**, pas par un compte web permanent | `service → Keycloak → LDAP` est en série : sans secours, une panne exclut tout le monde, y compris pour réparer | `authentification.md` §4 | — |
| **D-41** | Le formulaire de connexion locale n'est **pas proposé au repos** (`<rôle>_connexion_locale: false`) | il contourne la politique de mot de passe, le MFA et surtout la **révocation centrale** ; `sudo` est le mécanisme de réouverture | `authentification.md` §3 | — |
authentification : chaque rôle déclare sa position, gardé par P29 Une règle qu'aucune garde ne vérifie finit par ne plus être vraie — c'est ce qui était arrivé aux 28 lignes d'intégration recopiées. Chaque rôle serveur_* porte un meta/authentification.yml, confronté à son code par P29. web-sso 5, socle-identite 2 (keycloak/openldap : ils SONT la chaîne d'identité), ldap-direct 2, interne-sans-auth 2, sans-auth-humaine 12. La preuve refuse l'oubli ET le mensonge. Éprouvée par sabotage sur sept cas : déclaration supprimée, portée inventée, secours retiré, posture de formulaire retirée, raison retirée, ldap-direct mensonger, réglage retiré des defaults. Les deux derniers passaient dans la première version : - le mensonge passait à cause d'un commentaire. Je cherchais le mot « ldap » dans le rôle, et serveur_grafana/defaults/main.yml contient « désactiver quelqu'un dans LDAP » : de la prose validait une déclaration fausse. La preuve exige maintenant un indice nommé — variable <rôle>_oidc / <rôle>_ldap, ou URI ldap:// - le réglage retiré passait parce que le gabarit citait encore la variable alors que plus rien ne lui donnait de valeur. La preuve lit defaults/main.yml en YAML et exige que la clé y soit définie, pas mentionnée. Elle a aussi forcé une valeur : oauth2-proxy était déclaré « formulaire local fermé » alors qu'il n'a aucun compte local. D'où formulaire_local: aucun, qui distingue « il n'y en a jamais eu » de « il y en a un, il est fermé ». Correction d'une note de la veille : Prometheus et Loki ne sont PAS exposés publiquement (aucun expose au plan). Seuls six groupes le sont. Le risque est intra-tenant, pas frontalier. Les deux lacunes sont comptées à chaque exécution, pas masquées. AFF-111, D-42. 29 preuves OK, ansible-lint (production) sur 375 fichiers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:05:43 -04:00
| **D-42** | Chaque rôle **déclare sa position** d'authentification (`meta/authentification.yml`), gardée par une preuve | une directive qu'aucune garde ne vérifie finit par ne plus être vraie — c'était le cas des 28 lignes d'intégration recopiées | `authentification.md` §5 | P29 |
## 3. Les secrets
| # | Décision | Pourquoi | Détail | Garde |
|---|---|---|---|---|
| **D-19** | **Une seule voûte** par instance (`group_vars/all/vault.yml`) | un mot de passe, un endroit | `config-proxmox.md` | P18 |
| **D-20** | La liste des secrets se **recense**, elle ne s'écrit pas | trois copies manuelles ont existé, toutes ont divergé | `intrants-communs.md` §H | P18 |
| **D-21** | L'**empreinte du root CA n'est pas un secret** : elle se dérive à chaud | un `from-zero` régénère l'AC ; figée en voûte, elle serait périmée | `roles/client_pki/README.md` | — |
| **D-22** | Migrer un tenant, c'est **révoquer**, pas transmettre | sinon l'ancien hébergeur garde à vie l'accès aux secrets d'un client parti | `migration-tenant.md` §6 | — |
## 4. La méthode
| # | Décision | Pourquoi | Détail | Garde |
|---|---|---|---|---|
| **D-23** | Les cibles **hors flotte** reçoivent un **devis**, pas un rôle | commutateurs, frontière et SDN sont des objets de cluster ; un rôle boucle sur des hôtes | `sdn-evpn.md` §7 | — |
| **D-24** | Un devis **n'écrit rien** : il se relit, puis s'applique | proportionné au risque — une config ratée partitionne tout un cluster | `frontiere-opnsense.md` §7 | — |
| **D-25** | Le dépôt **n'affirme pas que ses devis s'appliquent**, il affirme qu'ils **dérivent** | leur syntaxe dépend d'un matériel que le dépôt ne possède pas | `audit/affirmations.md` §10 | — |
| **D-26** | Un modèle peut porter un **underlay** ; tous les hébergeurs n'ont pas le même matériel | générique public, étoffés en privé | `exemples/modeles/socle/README.md` | P17 |
| **D-27** | **Migration** : le receveur est prouvé prêt **avant** tout gel | l'interruption se réduit au delta et à la propagation DNS | `migration-tenant.md` §4 | — |
| **D-28** | Le mandat de migration est **signé par le tenant**, pas convenu entre hébergeurs | il n'y a pas de registre central pour arbitrer ; une organisation n'est pas la propriété de son hébergeur | `migration-tenant.md` §2 | — |
intégrations : le rôle déclare sa politique ; le cluster passe à l'hébergeur Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 14:09:22 -04:00
| **D-33** | Une intégration **universelle** est déclarée par le **rôle**, jamais recopiée par serveur | 28 des 57 lignes du plan disaient oui à ce qui vaut pour tous : elles n'existaient que pour être oubliées — et quatre l'avaient été | `integrations-vm.md` §Politique | P26 |
| **D-34** | Une **exemption** se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte | l'AC ne s'enrôle pas auprès d'elle-même ; l'exemption doit suivre step-ca si on le déplace | `roles/client_pki/meta/integration.yml` | P26 |
---
## 5. Décisions renversées
Les garder évite de refaire le chemin, et explique pourquoi le code porte encore des branches
qui semblent inutiles.
| Décision | Tenue du … au … | Renversée par | Pourquoi | Ce qu'il en reste |
|---|---|---|---|---|
| L'isolation inter-tenant est portée par des **ACL de commutateur** | 2026-07-07 → 08-03 | `059d76a` | le matériel ne sait pas lier une ACL à une interface de routage | `acl_inter_tenant: true` reste le défaut : une autre fabric peut en être capable |
| Le **routage inter-zone** est porté par les commutateurs L3 | 2026-07-07 → 08-03 | `e5ce2b9` | sans ACL, l'isolation devenait déclarative ; EVPN la rend structurelle | `routage_tenants: switch` reste le défaut et reste généré |
| L'`underlay.yml` vit à la racine du moteur, gitignoré | 2026-07-24 → 08-02 | `0eae97c` | consommé par deux générateurs, validé par une preuve, versionné nulle part | `SETOPS_UNDERLAY` permet toujours de le pointer ailleurs |
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51) Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10. Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents. sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous les chemins, donc un point de panne unique du plan de données — ce qui vidait aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés, bond3 répartis. D-51 ; D-05 renversée. Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui coupait l'accès d'administration au switch en cas d'erreur. D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau, où qu'elle vive », et le devis dérive s'il doit émettre une interface routée — uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux postures : le modèle public démontre celle où le switch route. Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige passerelle » et « routeur.ip == passerelle », une règle plus forte : une passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et elle a trouvé une sous-déclaration dans le modèle public. Quatre trous corrigés, tous de la même famille (une liste figée finit par mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le transit, switches d'accès sautant sa déclaration, et le switch de tête privé d'adresse de gestion par la suppression du SVI. D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1 revient à la passerelle. 30 preuves OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
| **D-05** — un seul commutateur route, les autres en L2 pur | 2026-07-07 → 08-04 | 2026-08-04 | deux décisions séparées (EVPN, puis fusion du lien de sortie) ont vidé le rôle sans qu'on regarde leur effet cumulé : les trois SVI n'avaient plus aucun consommateur | le moteur sait toujours émettre des SVI — le modèle public s'en sert |
| **D-03** — le `/29` de transit, SVI en `.6`, `.3` réservée CARP | 2026-07-24 → 08-04 | 2026-08-04 | élargi en `/24` pour y loger les nœuds de sortie ; l'exemption d'invariant sous le `/24` tombe alors, et le `.1` revient à la passerelle | le plan `.1` passerelle / `.2 .3` pare-feux survit, décalé d'un cran |
> **Ce que ces renversements ont en commun.** Aucun ne vient d'un changement d'avis : les trois
> viennent d'un fait découvert **après** la décision — une commande absente de l'aide du
> matériel, une capacité manquante, une dizaine de modifications irrécupérables. C'est
> l'argument le plus fort pour éprouver avant de figer.