diff --git a/CHANGELOG.md b/CHANGELOG.md index a5d29d0..9f7ff58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # CHANGELOG — Set-OPS +## 2026-08-24 — Le chaînage des caches, et la règle qui appartient au SITE + +Le runner de SITE est le seul à toucher la configuration système du matériel. Sa +responsabilité est de **préparer le terrain** — VNets, routes, flux — pour que le plan +d'un OPS puisse tenir. Ensuite le runner du tenant fait la suite. + +Cette mise au point tranche une question restée ouverte : **une règle inter-tenant +n'appartient ni à l'émetteur ni au récepteur, mais au site**. Ce n'est pas « Chezlepro +ouvre une porte chez patient 0 » — c'est le site qui autorise un flux entre deux de ses +tenants. + +### Ce que je croyais, et qui était faux + +J'avais annoncé que le générateur construisait « tenant par tenant, sur les interfaces de +ce tenant », et que des règles croisées changeraient sa forme. **Faux** : il fait une seule +passe sur tous les tenants du site, et il n'y a qu'**une interface de transit**. Les +tenants s'y distinguent par leur **alias source**, pas par leur interface. + +### Deux silences fermés dans le générateur + +`flux_frontiere()` ne retenait que les flux `externe`. Or deux tenants d'une même fabric +vivent sur des VLAN distincts, **routés par la frontière** : leur trafic la traverse, donc +elle doit le porter. Sans ça, le mot `voisins_site` était accepté par la validation et +rendait **zéro règle** — un flux déclaré que personne n'applique. + +Et un `egress` vers un voisin recevait `!SETOPS_INTERNES` en destination, comme tout flux +sortant — c'est-à-dire **le port ouvert vers l'Internet**. La destination est désormais +**nommée**. + +### Deux rôles, parce que les flux sont statiques + +Un rôle unique aurait dû déclarer l'ingress inter-tenant pour **tous** les caches. Le devis +l'a montré avant toute application : + +``` ++ TENANT_PATI29 -> CHEZ17_SERVEUR_ARTEFACTS un maillage complet, ++ TENANT_TECH23 -> CHEZ17_SERVEUR_ARTEFACTS chaque cache acceptant chaque autre +``` + +`serveur_artefacts_site` porte donc l'ingress, `serveur_artefacts` l'egress vers son amont. +Résultat au devis : **l'ingress ne vise plus que le cache du site**. + +### Ce que ça donne + +``` +VM du tenant -> cache du tenant -> cache du SITE -> Debian +``` + +Debian téléchargé **une fois pour toute la fabric**. Un seul flux nouveau par écosystème, +entre deux caches — jamais d'une VM vers le cache d'un voisin. Et le cache du site ne voit +que des requêtes **agrégées** : jamais quelle machine installe quoi. + +Vider `serveur_artefacts_amont`, c'est s'émanciper. + +### Ce qui reste large, et que je ne cache pas + +L'`egress` autorise chaque cache de tenant à joindre le 3142 de **tous** ses voisins, alors +qu'un seul lui sert d'amont. La déclaration étant statique, le rôle ne sait pas lequel de +ses voisins est le cache du site. La portée effective reste juste — seul le cache du site +accepte — mais la règle sortante est plus permissive que nécessaire. + +**Rien n'a été appliqué.** Le devis se lit avant. + ## 2026-08-24 — `voisins_site` : le vocabulaire d'un flux entre tenants, et le mur qu'il révèle Le registre des flux connaissait `flotte` (mon écosystème), `edge`, `admin` et `externe`. diff --git a/docs/audit/preuve-2026-08-24.md b/docs/audit/preuve-2026-08-24.md index 6638f01..8181112 100644 --- a/docs/audit/preuve-2026-08-24.md +++ b/docs/audit/preuve-2026-08-24.md @@ -21,7 +21,7 @@ | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | | P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 35 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 33 rôles, 84 flux, schéma + matrice OK. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 34 rôles, 86 flux, schéma + matrice OK. | | P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | @@ -36,21 +36,21 @@ | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 7 reseau(x), aucune collision avec la plage tenant. | -| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 54 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 53 groupe(s), 85 regle(s). | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 62 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,10.29.19.41/32,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 54 groupe(s), 86 regle(s). | | P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | | P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | -| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 27 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 16, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 28 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 17, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 59 roles avec README. | -| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (121 cle(s) declaree(s) par l'instance). | -| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 47 scripts expliques et atteignables, 98 cibles make documentees, 60 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (122 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (37 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (22 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | -| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 34 role(s) serveur/client tous nommes, 35 groupe(s) cite(s) en table existent tous. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 35 role(s) serveur/client tous nommes, 36 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | | P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 43 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | diff --git a/docs/catalogue-services.md b/docs/catalogue-services.md index ba9fdc7..53ff8c1 100644 --- a/docs/catalogue-services.md +++ b/docs/catalogue-services.md @@ -53,6 +53,7 @@ Ce que la reconstruction couvre, par capacité : | Source d'artefacts | `serveur_artefacts`, `client_artefacts` | cache apt de l'ecosysteme (apt-cacher-ng) : les paquets viennent de chez soi, pas de six serveurs etrangers — **mode hors ligne** pour prouver ce que le cache detient vraiment | | Runner de site | `serveur_ops_site` | le pouvoir de **materialiser** : creer et detruire des VM sur la fabric. Detient la voute du SITE, chiffree, et n'entre JAMAIS chez un tenant — reserve a l'ecosysteme de l'hebergeur | | Resolution | `serveur_resolveur`, `client_resolveur` | UN resolveur recursif par tenant (Unbound), qui recurse depuis la racine et delegue la zone souveraine a PowerDNS. Remplace les N demons locaux d'avant le 2026-08-24 | +| Cache du site | `serveur_artefacts_site` | designe LE cache que les ecosystemes voisins prennent comme amont : Debian telecharge une fois pour toute la fabric, et le cache ne voit que des requetes agregees. Reserve a l'ecosysteme de l'hebergeur | | Agents de flotte | `client_metrique`, `client_journal`, `client_smtp` | collecte et relais sur toute la flotte | *Rôles retirés (2026-07-04, supersédés ou hors conception)* : `serveur_sendmail` diff --git a/roles/serveur_artefacts/meta/flux.yml b/roles/serveur_artefacts/meta/flux.yml index 5475c05..ee30a2e 100644 --- a/roles/serveur_artefacts/meta/flux.yml +++ b/roles/serveur_artefacts/meta/flux.yml @@ -16,3 +16,15 @@ flux: pair: externe chiffrement: clair raison: "Remplir le cache depuis les dépôts Debian amont (deb.debian.org, security)." + # LE CHAINAGE, cote consommateur. Un seul port, et seulement vers un CACHE — jamais + # d'une VM vers le cache d'un autre tenant. + # + # L'INGRESS CORRESPONDANT N'EST PAS ICI : il appartient a `serveur_artefacts_site`, le + # cache designe du site. Sans cette separation, chaque cache aurait accepte chaque + # autre — un maillage complet, visible au devis du 2026-08-24. + - sens: egress + port: 3142 + protocole: tcp + pair: voisins_site + chiffrement: clair + raison: "Prendre le cache du site comme amont, plutot que d'aller chez Debian." diff --git a/roles/serveur_artefacts_site/README.md b/roles/serveur_artefacts_site/README.md new file mode 100644 index 0000000..8f56054 --- /dev/null +++ b/roles/serveur_artefacts_site/README.md @@ -0,0 +1,56 @@ +# serveur_artefacts_site + +**Le cache d'artefacts du SITE** : celui que les écosystèmes voisins prennent comme amont, +pour que Debian ne soit téléchargé qu'une fois pour toute la fabric. + +## Le chaînage + +``` +VM du tenant → cache du tenant → cache du SITE → Debian +``` + +Chaque écosystème **garde le sien** — il reste maître de ses paquets et s'en émancipe d'un +drapeau — mais il ne va plus chercher chez Debian. + +**Un cache unique où toutes les VM taperaient** aurait demandé un flux de chaque machine +de chaque tenant vers un hôte d'un autre tenant : N×M chemins à travers le default-deny. +Avec le chaînage il n'y en a **qu'un par écosystème**, entre deux caches. + +Et l'étanchéité y gagne : le cache du site ne voit que des requêtes **agrégées**, jamais +quelle machine installe quoi. + +## Pourquoi deux rôles plutôt qu'un drapeau + +Les flux se déclarent dans `meta/flux.yml`, qui est **statique**. Un rôle unique aurait dû +déclarer l'ingress inter-tenant pour **tous** les caches — et le devis l'a montré : + +``` ++ TENANT_PATI29 -> CHEZ17_SERVEUR_ARTEFACTS un maillage complet, ++ TENANT_TECH23 -> CHEZ17_SERVEUR_ARTEFACTS chaque cache acceptant chaque autre +``` + +Avec la séparation, l'ingress n'existe qu'ici, et un seul cache est joignable. + +## Qui peut le déclarer + +**L'écosystème de l'hébergeur**, celui qui exploite la fabric. Un tenant ordinaire qui le +déclarerait s'offrirait à servir ses voisins sans y être invité. + +C'est le même patron que `serveur_ops_site` : le **runner de site** prépare le terrain — +VNets, routes, flux — pour que les plans des tenants tiennent. Aucun écosystème n'ouvre de +porte chez un autre ; c'est le site qui autorise. + +## Ce qu'il vérifie + +**Qu'il ne se chaîne pas lui-même.** Il est le bout de la chaîne : le laisser pointer sur +un amont ferait tourner les paquets en rond entre deux caches, ou les ferait sortir du site +sans que personne ne l'ait décidé. + +**Que le cache écoute vraiment.** Le rôle n'installe rien : sa seule prise sur le réel est +de vérifier que le service qu'il désigne existe. Sans ça, il attribuerait une +responsabilité à un port que personne n'écoute. + +## Ce que ce rôle ne fait pas + +Il n'installe ni ne configure apt-cacher-ng — c'est `serveur_artefacts`. Il **attribue une +responsabilité** et la rend lisible dans le plan. diff --git a/roles/serveur_artefacts_site/defaults/main.yml b/roles/serveur_artefacts_site/defaults/main.yml new file mode 100644 index 0000000..172c0a8 --- /dev/null +++ b/roles/serveur_artefacts_site/defaults/main.yml @@ -0,0 +1,17 @@ +--- +# LE CACHE D'ARTEFACTS DU SITE — celui que les autres écosystèmes prennent comme amont. +# +# Même patron que `serveur_ops_site` : le rôle n'installe rien de plus, il **attribue une +# responsabilité** et la rend lisible dans le plan. `serveur_artefacts` fait le travail ; +# celui-ci déclare que ce cache-là sert au-delà de son propre écosystème. +# +# POURQUOI DEUX RÔLES PLUTÔT QU'UN DRAPEAU. Les flux se déclarent dans `meta/flux.yml`, +# qui est STATIQUE : un rôle unique aurait dû déclarer l'ingress inter-tenant pour TOUS +# les caches. Le devis obtenu le montrait — un maillage complet où chaque cache acceptait +# chaque autre, alors qu'un seul doit être joignable. +# +# QUI PEUT LE DÉCLARER : l'écosystème de l'hébergeur, celui qui exploite la fabric. Un +# tenant ordinaire qui le déclarerait s'offrirait à servir ses voisins sans y être invité. +# +# Le cache du site va DIRECTEMENT chez Debian : il est le bout de la chaîne. +serveur_artefacts_site_amont_attendu: "" diff --git a/roles/serveur_artefacts_site/meta/authentification.yml b/roles/serveur_artefacts_site/meta/authentification.yml new file mode 100644 index 0000000..f40bf65 --- /dev/null +++ b/roles/serveur_artefacts_site/meta/authentification.yml @@ -0,0 +1,12 @@ +--- +# Position de ce role dans la directive d'authentification (D-38..D-41). +# Voir docs/authentification.md. Gardee par la preuve P29. +authentification: + portee: sans-auth-humaine + mecanisme: aucun + formulaire_local: sans-objet + secours: "Acces SSH a l'hote" + raison: >- + Un cache de paquets ne sert que des machines. Ce qui le protege n'est pas une + authentification mais la LISTE de ses pairs : seuls les tenants de ce site le + joignent, et la frontiere le fait respecter. Ce qu'il sert est public par nature. diff --git a/roles/serveur_artefacts_site/meta/empreinte.yml b/roles/serveur_artefacts_site/meta/empreinte.yml new file mode 100644 index 0000000..a85308e --- /dev/null +++ b/roles/serveur_artefacts_site/meta/empreinte.yml @@ -0,0 +1,7 @@ +--- +# Empreinte ressources — ce role n'installe rien. Il attribue une responsabilite a un +# cache qui existe deja, et la rend lisible dans le plan. +setops_empreinte: + coeurs: 0 + memoire_mo: 0 + disque_go: 0 diff --git a/roles/serveur_artefacts_site/meta/flux.yml b/roles/serveur_artefacts_site/meta/flux.yml new file mode 100644 index 0000000..222d6e8 --- /dev/null +++ b/roles/serveur_artefacts_site/meta/flux.yml @@ -0,0 +1,22 @@ +--- +# Flux réseau du cache d'artefacts DU SITE. Voir docs/flux-conception.md. +# +# UN SEUL FLUX, et c'est tout son sens : accepter les caches des écosystèmes voisins. +# `serveur_artefacts` porte le reste (servir sa propre flotte, remplir depuis Debian). +# +# `voisins_site` désigne les autres tenants que CE SITE héberge. Ni `flotte` (mon +# écosystème), ni `externe` — qui signifie « depuis l'Internet » et aurait publié le cache +# au monde entier. +# +# LA RÈGLE APPARTIENT AU SITE : c'est son runner qui prépare le terrain, pas un tenant qui +# ouvrirait une porte chez un autre. +flux: + - sens: ingress + port: 3142 + protocole: tcp + pair: voisins_site + chiffrement: clair + raison: >- + Servir les caches des écosystèmes voisins. Debian n'est ainsi téléchargé qu'une + fois pour tout le site, et le cache ne voit que des requêtes AGRÉGÉES — jamais + quelle machine installe quoi. diff --git a/roles/serveur_artefacts_site/tasks/main.yml b/roles/serveur_artefacts_site/tasks/main.yml new file mode 100644 index 0000000..4a8651f --- /dev/null +++ b/roles/serveur_artefacts_site/tasks/main.yml @@ -0,0 +1,23 @@ +--- +# UN CACHE DE SITE QUI SE CHAÎNERAIT SUR UN AUTRE SERAIT UNE BOUCLE. +# +# Il est le bout de la chaîne : les caches des tenants viennent à lui, et lui va chez +# Debian. Le laisser pointer sur un amont ferait tourner les paquets en rond entre deux +# caches, ou pire, hors du site sans que personne ne l'ait décidé. +- name: Exiger que le cache du site aille directement à l'amont + ansible.builtin.assert: + that: + - (serveur_artefacts_amont | default('')) == serveur_artefacts_site_amont_attendu + fail_msg: >- + Ce cache est déclaré cache du SITE : il doit aller directement chez Debian. + `serveur_artefacts_amont` doit rester vide — sinon les caches se chaînent en boucle. + +# ÉCRIRE, PUIS RELIRE (D-68). Le rôle n'installe rien : sa seule prise sur le réel est de +# vérifier que le service qu'il désigne existe vraiment. Sans ça, il attribuerait une +# responsabilité à un port que personne n'écoute. +- name: Le cache écoute-t-il vraiment ? + ansible.builtin.wait_for: + host: "127.0.0.1" + port: "{{ serveur_artefacts_port }}" + timeout: 30 + when: not ansible_check_mode diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index 05fd0f7..7d278c8 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -477,6 +477,16 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: portees = [("any", if_wan)] if admin_par_if[etiquette]["gestion"] and fl.get("poste", True): portees.append((f"SETOPS_ADMIN_{etiquette}_GESTION", if_gestion)) + elif "voisins_site" in _pairs(fl): + # SORTANT VERS UN VOISIN : la destination est NOMMEE, pas « tout sauf + # l'interne ». Rendre `!SETOPS_INTERNES` ici aurait ouvert le port vers + # l'INTERNET — mesure du 2026-08-24, visible au devis avant application. + # + # Une regle par voisin : une regle porte une destination. + portees = [ + (f"SETOPS_TENANT_{p2}{n2['index']}", if_transit) + for nom2, p2, n2 in tenants if nom2 != nom + ] else: portees = [("any", if_transit)] for source, interface in portees: @@ -489,7 +499,11 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: # lui-meme pour qu'il apparaisse dans l'identite de la regle — sans # quoi la version niee et la version ouverte auraient la meme cle et # la seconde ne remplacerait jamais la premiere. - "destination": destination if entrant else "!SETOPS_INTERNES", + # Sortant vers un voisin : la destination est le voisin lui-meme, + # portee par `source` puisque `portees` la transporte. Sortant vers + # l'Internet : « tout sauf l'interne ». + "destination": destination if entrant + else (source if "voisins_site" in _pairs(fl) else "!SETOPS_INTERNES"), "ports": _ports(fl), "chiffrement": fl.get("chiffrement"), "role": role,