diff --git a/CHANGELOG.md b/CHANGELOG.md index 3691307..ea5280a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # CHANGELOG — Set-OPS +## 2026-09-15 (1) — L'edge publie : trois noms, en TLS verifie de bout en bout + + observatoire.genese.internal http=302 tls=0 (Grafana, vers sa connexion) + vigie.genese.internal http=401 tls=0 (le vestibule tient) + forge.genese.internal http=200 tls=0 + +`tls=0` : verification reelle contre la racine du site, pas un `-k` complaisant. + +### `expositions` n'etait resolu nulle part + +`serveur_nginx` declare `egress port: derive, pair: expositions` — « je sors vers ce que +je publie ». Ce mot n'etait consomme par AUCUN moteur : ni le pare-feu d'hote, qui ne +filtre pas l'egress, ni le devis de frontiere, qui ne le connaissait pas. + +IL N'AVAIT JAMAIS EU BESOIN DE L'ETRE. Chez un locataire, les passerelles de zone sont +tenues par le SDN de Proxmox : le trafic edge -> amont ne traverse pas le boitier. Au +SITE, elles sont tenues par la frontiere — premier endroit ou un edge doit franchir la +bordure pour atteindre ce qu'il relaie. Une declaration peut dormir des mois avant que la +premiere fabric ne la reveille. + +UNE REGLE PAR AMONT, avec SON port : + + opt10 tcp 443 SERVEUR_NGINX -> SERVEUR_FORGEJO + opt10 tcp 3000 SERVEUR_NGINX -> SERVEUR_GRAFANA + opt10 tcp 8080 SERVEUR_NGINX -> SERVEUR_ICINGAWEB2 + +Jamais une regle large. Un edge qui publie trois services ne doit joindre que ces +trois-la : l'ouvrir sur « tout le site » annulerait ce qu'une zone de publication separee +cherche a obtenir. + +### LA MEME FAUTE QU'HIER, AVALEE DE LA MEME FACON + +Premiere ecriture de cette resolution : `U.lire_plan_site(...)` dans un fichier ou le +module s'importe sous `underlay_mod`. Le `NameError` est tombe dans un +`except Exception` large, et le devis a rendu une note **plausible** — « le plan du site +n'en declare aucune avec un port » — alors que le plan en declarait trois. + +C'est exactement le defaut du 2026-09-14 sur `site_inventaire.py`, refait 24 h plus tard +par la meme main. Le `except` est resserre a `(OSError, ValueError)` : une faute de frappe +doit faire du bruit, pas une phrase credible. + +### Le schema de l'amont suivait une constante, pas le port + +`proxy_pass http://…` etait ecrit en dur. La forge termine deja son TLS sur 443 : elle +recevait du clair sur un port qui attend une poignee de main, et rendait `400`. Du TLS +parfait a l'entree, une erreur de protocole a la sortie. + +Le schema suit desormais le port (`serveur_nginx_ports_tls`), **et l'amont est verifie** : +`proxy_ssl_verify on` contre l'AC interne. Un edge qui relaie en TLS sans verifier ne fait +que DEPLACER la confiance — le visiteur croit l'edge, l'edge ne croit personne. + ## 2026-09-14 (23) — Du TLS qui ressemblait a du TLS, et une regle qu'un tenant n'a jamais eu besoin d'ecrire Trois defauts de plus sur le chemin de l'edge, et le premier est le plus grave de la diff --git a/docs/audit/preuve-2026-09-15.md b/docs/audit/preuve-2026-09-15.md new file mode 100644 index 0000000..2a9fb38 --- /dev/null +++ b/docs/audit/preuve-2026-09-15.md @@ -0,0 +1,114 @@ +# Preuve de conformite — Set-OPS — 2026-09-15 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (77 OK · 0 echec · 1 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. | +| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | +| 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), 1 nom(s) surveille(s) sans reference orpheline. | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 111 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 | serveur_resolveur_site | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). | +| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 30 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 29 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 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 : 14 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 52 groupe(s), 96 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 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 : 5 pool(s) Proxmox, 43 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 33 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 22, 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 | 67 scripts expliques et atteignables, 123 cibles make documentees, 68 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 36 exigence(s) de role, toutes satisfaites (142 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 36 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 46 document(s) declarent leur lecteur (40 genere(s) exempte(s)). | +| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). | +| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 8 hote(s) de l'ecosysteme et 3 du site 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 : 41 role(s) serveur/client tous nommes, 41 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, 96 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 : 63 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre | +| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 8 machine(s) du plan retrouvees, 181 regle(s) du site. | +| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. | +| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). | +| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos | +| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). | +| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 92 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | +| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (129 lignes). | +| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 271 regles `pass`), tous non consignes et tous motives. | +| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | +| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. | +| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | +| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. | +| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. | +| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. | +| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (78 preuves, 68 roles, 41 groupes). | +| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. | +| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). | +| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `392da4d` (publie le 2026-09-14). | +| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | +| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | +| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 40 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ | +| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. | +| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 3 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (5 exposition(s)). | +| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 11 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. | +| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. | +| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. | +| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. | +| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. | +| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. | +| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 6 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). | +| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. | +| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. | +| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 155 gabarits de role : tous se rendent. | +| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. | +| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-09-15._ diff --git a/roles/serveur_nginx/defaults/main.yml b/roles/serveur_nginx/defaults/main.yml index a4d08a8..5e12168 100644 --- a/roles/serveur_nginx/defaults/main.yml +++ b/roles/serveur_nginx/defaults/main.yml @@ -50,3 +50,12 @@ serveur_nginx_proxy_busy_buffers_size: "32k" # Les ports que l'edge DOIT servir. Variable, donc mise en defaut en nommant un port qui # n'ecoute pas, sans toucher a nginx. serveur_nginx_sonde_ports: "80 443" + +# LES PORTS OU UN AMONT TERMINE DEJA LE TLS. `proxy_pass` doit alors parler `https://` : +# du clair vers un port qui attend une poignee de main rend `400 Bad Request`, et +# l'erreur parle de requete malformee, jamais de protocole. +serveur_nginx_ports_tls: [443, 8443] + +# L'AC INTERNE, POUR VERIFIER L'AMONT. Meme racine que celle qui signe le certificat que +# l'edge presente : la confiance est la meme des deux cotes de la terminaison. +serveur_nginx_ca_interne: "/etc/step/certs/root_ca.crt" diff --git a/roles/serveur_nginx/templates/expositions.conf.j2 b/roles/serveur_nginx/templates/expositions.conf.j2 index bc4abec..c834b38 100644 --- a/roles/serveur_nginx/templates/expositions.conf.j2 +++ b/roles/serveur_nginx/templates/expositions.conf.j2 @@ -12,8 +12,16 @@ map $http_upgrade $connection_upgrade { default upgrade; "" close; } {% endif %} {% for expo in serveur_nginx_expositions | default([]) %} {% set ns = namespace(amont='') %} +{#- LE SCHEMA DE L'AMONT SUIT LE PORT DE L'AMONT (2026-09-15). + `http://` etait ecrit en dur. Un service qui termine DEJA le TLS — la forge, sur son + 443 — recevait donc du clair sur un port qui attend une poignee de main, et rendait + `400 Bad Request`. Du TLS parfait a l'entree, une erreur de protocole a la sortie. + On ne devine pas : un amont declare sur un port de TLS se parle en TLS. La liste est + reglable (`serveur_nginx_ports_tls`) parce qu'un service peut servir du TLS ailleurs + que sur 443. -#} {% if expo.hote in hostvars and hostvars[expo.hote].ansible_host is defined and expo.port %} -{% set ns.amont = 'http://' ~ hostvars[expo.hote].ansible_host ~ ':' ~ expo.port %} +{% set _schema = 'https://' if (expo.port | int) in (serveur_nginx_ports_tls | default([443, 8443])) else 'http://' %} +{% set ns.amont = _schema ~ hostvars[expo.hote].ansible_host ~ ':' ~ expo.port %} {% endif %} {% if ns.amont %} @@ -37,6 +45,15 @@ server { location / { proxy_pass {{ ns.amont }}; +{% if ns.amont.startswith('https://') %} + # ON VERIFIE L'AMONT, PAS SEULEMENT LE VISITEUR. Un edge qui relaie en TLS sans + # verifier ne fait que DEPLACER la confiance : le visiteur croit l'edge, l'edge + # ne croit personne. L'AC interne est la meme des deux cotes. + proxy_ssl_verify on; + proxy_ssl_trusted_certificate {{ serveur_nginx_ca_interne }}; + proxy_ssl_name {{ expo.fqdn }}; + proxy_ssl_server_name on; +{% endif %} proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index 72a804e..31a24de 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -671,6 +671,51 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: _machines_site = _machines_du_plan_site() _services_site = _services_du_plan_site() _ports_site = _ports_du_plan_site() + + # CE QU'UN EDGE DOIT JOINDRE POUR RELAYER (2026-09-15). + # + # `serveur_nginx` declare `egress port: derive, pair: expositions` — « je sors vers + # ce que je publie ». Ce mot n'etait resolu NULLE PART : ni par le pare-feu d'hote + # (qui ne filtre pas l'egress), ni ici. La declaration existait, aucun moteur ne la + # consommait. + # + # ELLE N'A JAMAIS EU BESOIN DE L'ETRE. Chez un locataire, les passerelles de zone + # sont tenues par le SDN de Proxmox : le trafic edge -> amont ne traverse pas le + # boitier. Au SITE, elles sont tenues par la frontiere — et c'est le premier endroit + # ou un edge doit franchir la bordure pour atteindre ce qu'il relaie. + # + # Mesure du 2026-09-15 : les trois noms du site repondaient en TLS verifie, puis + # `http=000`. Le devis le disait, et c'est la seule raison qu'on l'ait vu : + # + # note : serveur_nginx declare un port `derive` que le plan du site ne resout + # pas — aucune regle emise. + # + # UNE REGLE PAR AMONT, jamais une regle large : un edge qui publie trois services ne + # doit pouvoir joindre que ces trois-la, sur LEUR port. `expositions` rendu comme + # « tout le site » aurait ouvert l'edge sur ses voisins — l'exact contraire de ce + # qu'une zone de publication separee cherche a obtenir. + # + # `underlay_mod`, ET PAS `U` — LA MEME ERREUR QU'HIER, AVALEE DE LA MEME FACON. + # Le module s'importe sous ce nom ICI ; `U` est l'alias d'un AUTRE fichier. Un + # `except Exception` large a transforme le `NameError` en « aucune exposition », et + # le devis a rendu une note plausible au lieu d'une erreur. On attrape donc ce que + # la lecture d'un fichier peut vraiment lever, et rien de plus : une faute de frappe + # doit faire du bruit. + _amonts_expositions: list[tuple[str, int]] = [] + try: + _plan_apps = (underlay_mod.lire_plan_site("applications.yml") + or {}).get("applications") or {} + except (OSError, ValueError) as _e: + print(f"note : plan du site illisible ({_e.__class__.__name__}) — aucune " + f"exposition derivee.", file=sys.stderr) + _plan_apps = {} + for _app in _plan_apps.values(): + if not isinstance(_app, dict) or not _app.get("expose"): + continue + _g, _port = str(_app.get("groupe") or ""), _app.get("port") + if _g and _g != "serveur_nginx" and str(_port or "").isdigit(): + _amonts_expositions.append((_g, int(_port))) + _amonts_expositions = sorted(set(_amonts_expositions)) if _machines_site: _res_site = {r.get("nom"): r for r in underlay_mod.reseaux(_u)} _sous_site = sorted({str(_res_site[m["reseau"]]["sous_reseau"]) @@ -908,6 +953,33 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: # tenant de ce site ne declare encore de runner. if "serveur_ops_tenant" in _pairs_fl and not _runners_tenants: continue + # UN EDGE QUI SORT VERS SES EXPOSITIONS : une regle par amont, chacune + # avec SON port. Traite ici et pas plus bas, parce que ni le port ni la + # destination ne sont ceux du role qui declare le flux — ils sont ceux + # des services qu'il relaie. + if not _entrant and "expositions" in _pairs_fl: + if not _amonts_expositions: + print(f"note : {_role} sort vers ses expositions, mais le plan du " + f"site n'en declare aucune avec un port — aucune regle " + f"emise.", file=sys.stderr) + continue + for _g_amont, _port_amont in _amonts_expositions: + if _g_amont not in _roles_site: + continue + for _src, _itf in _portees: + regles.append({ + "sens": "out", + "interface": _itf, + "protocole": _fl.get("protocole", "tcp"), + "source": _a, + "destination": nom_alias(_g_amont, "SITE"), + "ports": [str(_port_amont)], + "chiffrement": _fl.get("chiffrement"), + "role": _role, + "tenant": "SITE", + "raison": f"{_fl.get('raison', '')} (amont {_g_amont})", + }) + continue _p = _ports(_fl) # L'ICMP N'A PAS DE PORT, ET SON `port` N'EST PAS SYMBOLIQUE (2026-09-09). #