frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Devis de configuration OPNsense — la frontiere nord/sud du reseau converge.
|
|
|
|
|
|
|
|
|
|
DERIVE de deux sources deja tenues a jour, rien n'est saisi a la main :
|
|
|
|
|
- le REGISTRE DES FLUX (`roles/*/meta/flux.yml`) : les flux dont le pair est
|
|
|
|
|
`externe` sont, par construction, ceux que le pare-feu d'hote NE traite PAS
|
|
|
|
|
(scripts/resoudre_flux.py les saute) parce qu'ils relevent de la frontiere.
|
|
|
|
|
Plusieurs `raison` le disent explicitement (« filtre a l'OPNsense »).
|
|
|
|
|
- les NOMENCLATURES des instances federees (`../*/plan/nomenclature.yml`) :
|
|
|
|
|
les supernets tenants a router, decoulant du seul seed `index`.
|
|
|
|
|
|
|
|
|
|
ROLE ASSUME : frontiere NORD/SUD uniquement. Les passerelles de zone (SVI)
|
|
|
|
|
restent sur les switches L3 ; OPNsense ne porte AUCUN SVI de tenant et ne voit
|
|
|
|
|
pas le trafic est-ouest, qui reste filtre par les ACL de `make devis-reseau`.
|
|
|
|
|
|
|
|
|
|
NON destructif : ce script n'ecrit RIEN sur le pare-feu. Il produit un devis a
|
|
|
|
|
relire, puis (a venir) un JSON consommable par l'API OPNsense.
|
|
|
|
|
|
|
|
|
|
Usage :
|
|
|
|
|
python3 scripts/devis_opnsense.py # devis lisible
|
|
|
|
|
python3 scripts/devis_opnsense.py --json # meme contenu, pour l'API
|
|
|
|
|
python3 scripts/devis_opnsense.py --verifier # garde anti-lockout (P24)
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
import os
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
import ipaddress
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
|
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
|
|
2026-08-09 16:07:10 -04:00
|
|
|
from inventory_rules import sous_reseau_de, supernet_de # noqa: E402
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
# Helpers internes du resolveur de flux : reutilises VOLONTAIREMENT plutot que
|
|
|
|
|
# redupliques ici — le registre des flux et l'inventaire doivent avoir une seule
|
|
|
|
|
# lecture, sinon le devis et les nftables divergeraient en silence.
|
|
|
|
|
from resoudre_flux import ( # noqa: E402
|
|
|
|
|
charger_flux,
|
|
|
|
|
_enfants,
|
|
|
|
|
_hotes_du_groupe,
|
|
|
|
|
_inventaire,
|
|
|
|
|
_ip_par_hote,
|
|
|
|
|
_pairs,
|
|
|
|
|
_sources_admin_ssh,
|
|
|
|
|
)
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
# `inventaire_de` / `admin_de` viennent de devis_reseau : les routes de retour du
|
|
|
|
|
# switch et les regles de la frontiere doivent lire les MEMES tenants, sinon l'un
|
|
|
|
|
# certifie des routes que l'autre n'emet pas.
|
portee : les trois devis d'un site partagent enfin la meme regle
Le commit du 2026-08-14 nommait lui-meme ce qui restait : « meme hypothese ailleurs, non
corrigee — devis_sdn et devis_reseau partent du meme decouvrir(). A traiter quand ils
serviront sur un second site. » C'est fait AVANT, pas pendant la visite.
Les trois devis equipent le MATERIEL d'un site : la frontiere (regles, routes), le
commutateur (VLAN, SVI, routes) et le SDN de l'hyperviseur (zones, VNets). Un tenant
d'ailleurs y ajoutait des objets que le materiel accepte, qui ne correspondent jamais a
rien, et que rien ne signale.
UNE SEULE FONCTION AU LIEU D'UN FILTRE RECOPIE TROIS FOIS :
`devis_reseau.decouvrir_du_site()` = decouvrir() restreint par `underlay.tenants`, la
doctrine ecrite une fois. Le filtre inline de devis_opnsense est retire au profit d'elle.
`admin_tous_tenants()` la suit : le routeur d'un site n'a pas a savoir revenir vers le
plan de gestion d'un tenant qu'il ne porte pas.
EPROUVE dans les trois situations : underlay sans la cle -> les deux tenants, comme avant ;
underlay du second site -> OPS-Technolibre seul ; nom declare qu'aucun dossier ne fournit
-> ATTENTION et le reste est retenu ; filtre qui ne retient rien -> refus, code 1.
SANS EFFET SUR LE SITE ACTUEL : l'underlay de Chezlepro ne declare pas `tenants`, et cle
absente = toute la federation (verifie : decouvrir() et decouvrir_du_site() rendent la
meme liste ici).
ET LA CLE EST ENFIN DOCUMENTEE — c'etait le vrai trou. `underlay.tenants` existait depuis
le 14 sans figurer ni dans underlay.yml.example ni dans l'annexe du runbook
d'implantation : indecouvrable pour qui monte un second site.
prouver 37 OK, 0 echec, 0 saute ; make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:38:30 -04:00
|
|
|
from devis_reseau import admin_de, decouvrir, decouvrir_du_site, inventaire_de, prefixe # noqa: E402
|
2026-08-01 19:38:17 -04:00
|
|
|
# Le lien de transit (et donc le prochain saut) vit dans l'underlay : il est PARTAGE
|
|
|
|
|
# par tous les tenants, il ne peut deriver d'aucun index. Source unique, pas un intrant.
|
|
|
|
|
import underlay as underlay_mod # noqa: E402
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
# Marqueurs de repli, tant que les intrants de frontiere ne sont pas renseignes
|
|
|
|
|
# (meme idiome que devis_reseau). Le panneau « Intrants de base » du GUI les ecrit
|
|
|
|
|
# dans group_vars/opnsense.yml : le devis se complete tout seul ensuite.
|
|
|
|
|
PROCHAIN_SAUT = "<PROCHAIN-SAUT-SWITCH>"
|
2026-08-03 08:40:20 -04:00
|
|
|
NOEUD_SORTIE = "<NOEUD-DE-SORTIE-EVPN>"
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
IF_TRANSIT = "<IF-TRANSIT>"
|
|
|
|
|
IF_WAN = "<IF-WAN>"
|
2026-08-06 14:39:51 -04:00
|
|
|
IF_GESTION = "<IF-GESTION>"
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# L'interface ou arrive le trafic des MACHINES DU SITE (vlan 30 / vmbr3). Une regle
|
|
|
|
|
# posee sur la mauvaise interface ne correspond jamais : le trafic du site ne penetre pas
|
|
|
|
|
# le pare-feu par le lien de transit des tenants, mais par sa propre patte.
|
|
|
|
|
IF_SITE = "<IF-SITE>"
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
|
2026-08-02 17:17:10 -04:00
|
|
|
def depot_hebergeur() -> Path | None:
|
|
|
|
|
"""Depot de l'HEBERGEUR — celui qui possede le materiel, donc la frontiere.
|
|
|
|
|
|
|
|
|
|
Derive du symlink `underlay.yml` : il pointe vers le depot de l'hebergeur, ce qui
|
|
|
|
|
le DESIGNE deja. Le declarer une seconde fois ouvrirait la porte a deux valeurs
|
|
|
|
|
contradictoires. None si aucun underlay (site sans fabric declaree).
|
|
|
|
|
"""
|
|
|
|
|
c = underlay_mod.chemin()
|
|
|
|
|
return c.resolve().parent if c else None
|
|
|
|
|
|
|
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
def intrants_frontiere() -> dict:
|
2026-08-02 17:17:10 -04:00
|
|
|
"""Intrants NON sensibles de la frontiere (group_vars/opnsense.yml). {} si absent.
|
|
|
|
|
|
|
|
|
|
Lus chez l'HEBERGEUR, pas chez le tenant actif : un hebergeur sert plusieurs
|
|
|
|
|
tenants et n'a qu'une frontiere. Basculer l'instance active sur un invite ne doit
|
|
|
|
|
pas faire perdre au devis l'URL de gestion, l'adresse publique et les interfaces.
|
2026-08-23 00:47:32 -04:00
|
|
|
|
|
|
|
|
A LA RACINE DU DEPOT DE SITE depuis le 2026-08-22, comme `underlay.yml` et
|
|
|
|
|
`proxmox-hebergeur.yml` : la frontiere est un objet du MONDE PHYSIQUE. Tant que ce
|
|
|
|
|
fichier vivait dans les `group_vars` d'un tenant, son adresse d'API a pu rester
|
|
|
|
|
perimee sans que personne la voie — rangee chez quelqu'un qui n'en est pas
|
|
|
|
|
responsable. L'applicateur est reste suspendu sur `10.0.0.1`, une adresse morte, sans
|
|
|
|
|
message : la panne la plus couteuse a diagnostiquer.
|
|
|
|
|
|
|
|
|
|
Les deux anciens emplacements restent lus, pour qu'un site non encore migre continue
|
|
|
|
|
de fonctionner.
|
2026-08-02 17:17:10 -04:00
|
|
|
"""
|
2026-08-23 00:47:32 -04:00
|
|
|
heb = depot_hebergeur()
|
|
|
|
|
if heb is not None and (heb / "opnsense.yml").is_file():
|
|
|
|
|
return yaml.safe_load((heb / "opnsense.yml").read_text(encoding="utf-8")) or {}
|
|
|
|
|
for base in (heb, _inventaire().parent.parent.parent):
|
2026-08-02 17:17:10 -04:00
|
|
|
if base is None:
|
|
|
|
|
continue
|
|
|
|
|
for nom in ("principal", "production", "lab"):
|
|
|
|
|
p = base / "inventories" / nom / "group_vars" / "opnsense.yml"
|
|
|
|
|
if p.is_file():
|
|
|
|
|
return yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ou_marqueur(intrants: dict, cle: str, marqueur: str) -> str:
|
|
|
|
|
"""La valeur d'intrant si elle est renseignee, sinon le marqueur a completer."""
|
|
|
|
|
valeur = str(intrants.get(cle) or "").strip()
|
|
|
|
|
return valeur or marqueur
|
|
|
|
|
|
2026-08-01 19:38:17 -04:00
|
|
|
|
|
|
|
|
def transit_underlay() -> dict | None:
|
|
|
|
|
"""Le reseau de transit declare dans l'underlay, ou None."""
|
|
|
|
|
return underlay_mod.reseau_transit(underlay_mod.charger())
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 20:18:16 -04:00
|
|
|
def nom_frontiere(transit: dict | None) -> str:
|
|
|
|
|
"""Nom du boitier de bordure, s'il est declare comme hote sur le lien de transit."""
|
|
|
|
|
if not transit:
|
|
|
|
|
return ""
|
|
|
|
|
sortie = str(transit.get("passerelle_sortie") or "")
|
|
|
|
|
for h in underlay_mod.hotes(underlay_mod.charger()):
|
|
|
|
|
if h.get("reseau") == transit.get("nom") and str(h.get("ip")) == sortie:
|
|
|
|
|
return str(h.get("nom") or "")
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 19:38:17 -04:00
|
|
|
def prochain_saut(transit: dict | None) -> str:
|
|
|
|
|
"""Prochain saut des routes tenants = le SVI du switch sur le lien de transit.
|
|
|
|
|
|
|
|
|
|
DERIVE, jamais saisi : c'est le meme lien que celui d'ou `devis_reseau` tire la
|
|
|
|
|
route par defaut et les routes de retour. Le declarer deux fois (une fois ici, une
|
|
|
|
|
fois dans l'underlay) ouvrirait la porte a deux valeurs contradictoires — c'est
|
|
|
|
|
exactement le mode de panne qu'on ferme. Marqueur tant qu'aucun transit n'existe.
|
|
|
|
|
"""
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
underlay = underlay_mod.charger()
|
|
|
|
|
if underlay_mod.routage_tenants(underlay) == "sdn":
|
2026-08-03 08:40:20 -04:00
|
|
|
# En EVPN, le commutateur ne route plus les tenants : le voisin de la frontiere
|
|
|
|
|
# est un NOEUD DE SORTIE (hyperviseur), pas le SVI. Emettre le SVI produirait
|
|
|
|
|
# des routes vers un equipement qui n'a aucun chemin vers le tenant.
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
#
|
|
|
|
|
# C'est le noeud PRIMAIRE, et son adresse SUR LE LIEN DE TRANSIT — pas son
|
|
|
|
|
# adresse d'underlay ni de gestion : la frontiere n'atteint que celle-la.
|
|
|
|
|
# Une seule route statique par tenant, donc un seul saut : deux noeuds actifs
|
|
|
|
|
# en sortie avec une seule route en entree donneraient un chemin asymetrique.
|
|
|
|
|
return adresse_noeud_de_sortie(underlay, transit) or NOEUD_SORTIE
|
2026-08-01 19:38:17 -04:00
|
|
|
return str((transit or {}).get("passerelle") or "").strip() or PROCHAIN_SAUT
|
|
|
|
|
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
|
2026-08-06 14:39:51 -04:00
|
|
|
def reseaux_locaux_frontiere(underlay: dict | None, transit: dict | None) -> list[str]:
|
|
|
|
|
"""Sous-reseaux DIRECTEMENT ATTACHES a la frontiere, hors lien de transit.
|
|
|
|
|
|
|
|
|
|
C'est le plan de gestion vu du pare-feu : les reseaux ou la frontiere porte elle-meme
|
|
|
|
|
une adresse. Un paquet venu de la n'arrive donc NI par le WAN NI par le transit, mais
|
|
|
|
|
par l'interface de gestion — et c'est a cette interface que sa regle doit etre posee.
|
|
|
|
|
|
|
|
|
|
Derive de l'underlay, jamais saisi : la frontiere y declare deja ses adresses, une par
|
|
|
|
|
reseau. Un intrant supplementaire ouvrirait la porte a deux valeurs contradictoires.
|
|
|
|
|
"""
|
|
|
|
|
nom = nom_frontiere(transit)
|
|
|
|
|
if not nom:
|
|
|
|
|
return []
|
|
|
|
|
sur = {str(h.get("reseau")) for h in underlay_mod.hotes(underlay)
|
|
|
|
|
if str(h.get("nom") or "") == nom}
|
|
|
|
|
sur.discard(str((transit or {}).get("nom")))
|
|
|
|
|
cidrs = []
|
|
|
|
|
for r in underlay_mod.reseaux(underlay):
|
|
|
|
|
if str(r.get("nom")) in sur and r.get("sous_reseau"):
|
|
|
|
|
cidrs.append(str(r["sous_reseau"]))
|
|
|
|
|
return sorted(cidrs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _porte_par(cidr: str, reseaux: list[str]) -> bool:
|
|
|
|
|
"""Vrai si `cidr` est contenu dans l'un des `reseaux` (ou lui est egal)."""
|
|
|
|
|
try:
|
|
|
|
|
r = ipaddress.ip_network(cidr, strict=False)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
|
|
|
|
for n in reseaux:
|
|
|
|
|
try:
|
|
|
|
|
if r.subnet_of(ipaddress.ip_network(n, strict=False)):
|
|
|
|
|
return True
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
continue
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
def adresse_noeud_de_sortie(underlay: dict | None, transit: dict | None) -> str:
|
|
|
|
|
"""Adresse du noeud de sortie PRIMAIRE sur le lien de frontiere. '' si indeterminee.
|
|
|
|
|
|
|
|
|
|
Deux declarations doivent concorder, et c'est voulu : l'hebergeur nomme le noeud
|
|
|
|
|
(`proxmox_sdn.sortie_primaire`, propriete du cluster), l'underlay dit son adresse
|
|
|
|
|
sur ce lien. Nommer un noeud absent du lien rend le devis muet plutot que faux.
|
|
|
|
|
"""
|
|
|
|
|
primaire = str((_sdn_hebergeur() or {}).get("sortie_primaire") or "").strip()
|
|
|
|
|
if not primaire or not transit:
|
|
|
|
|
return ""
|
|
|
|
|
for h in underlay_mod.hotes(underlay):
|
|
|
|
|
if (h.get("nom") == primaire and h.get("reseau") == transit.get("nom")
|
|
|
|
|
and h.get("ip")):
|
|
|
|
|
return str(h["ip"])
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sdn_hebergeur() -> dict:
|
|
|
|
|
"""`proxmox_sdn` du depot de l'HEBERGEUR — meme derivation que devis_sdn (D-35)."""
|
|
|
|
|
lien = Path(os.environ.get("SETOPS_UNDERLAY") or (RACINE / "underlay.yml"))
|
|
|
|
|
if not lien.exists():
|
|
|
|
|
return {}
|
|
|
|
|
fichier = lien.resolve().parent / "proxmox-hebergeur.yml"
|
|
|
|
|
if not fichier.is_file():
|
|
|
|
|
return {}
|
|
|
|
|
return (yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}).get("proxmox_sdn") or {}
|
|
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
# Le socle est porte par TOUS les hotes : sa destination n'est pas une poignee
|
|
|
|
|
# de machines mais le supernet du tenant. Evite un alias de 14 adresses.
|
|
|
|
|
ROLE_SOCLE = "serveur_debian"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def flux_frontiere(flux_par_role: dict[str, list[dict]]) -> list[tuple[str, dict]]:
|
2026-08-24 17:32:35 -04:00
|
|
|
"""[(role, flux)] pour tout flux que la FRONTIERE doit porter, trie.
|
|
|
|
|
|
|
|
|
|
Deux familles, et pas une :
|
|
|
|
|
|
|
|
|
|
`externe` — le nord-sud : l'Internet d'un cote, un tenant de l'autre.
|
|
|
|
|
`voisins_site` — l'est-ouest INTER-TENANT. Deux tenants de la meme fabric vivent
|
|
|
|
|
sur des VLAN distincts, routes par la frontiere : leur trafic la
|
|
|
|
|
traverse, donc elle doit le porter.
|
|
|
|
|
|
|
|
|
|
OUBLIER LA SECONDE, C'ETAIT DECLARER UN FLUX QUE PERSONNE N'APPLIQUE (2026-08-24). Le
|
|
|
|
|
mot `voisins_site` etait accepte par la validation, le devis rendait zero regle, et
|
|
|
|
|
rien ne le signalait — le chainage des caches n'aurait simplement jamais fonctionne.
|
|
|
|
|
"""
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
retenus = [
|
|
|
|
|
(role, fl)
|
|
|
|
|
for role, flux in sorted(flux_par_role.items())
|
|
|
|
|
for fl in flux
|
2026-08-24 17:32:35 -04:00
|
|
|
if "externe" in _pairs(fl) or "voisins_site" in _pairs(fl)
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
]
|
|
|
|
|
retenus.sort(key=lambda rf: (rf[1].get("sens", ""), rf[0], str(rf[1].get("port"))))
|
|
|
|
|
return retenus
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ports(flux: dict) -> list[str]:
|
|
|
|
|
port = flux["port"]
|
|
|
|
|
return [str(p) for p in (port if isinstance(port, list) else [port])]
|
|
|
|
|
|
|
|
|
|
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
def cibles_par_role(role: str, inventaire: Path) -> list[str]:
|
|
|
|
|
"""IP des hotes qui portent ce role dans cette instance (actifs ET planifies).
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
On inclut les planifies a dessein : la frontiere se prepare avant que les VM
|
|
|
|
|
existent, sinon le devis changerait a chaque materialisation d'hote.
|
|
|
|
|
"""
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
data = yaml.safe_load(inventaire.read_text(encoding="utf-8")) or {}
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
ips = _ip_par_hote(data)
|
|
|
|
|
return sorted({ips[h] for h in _hotes_du_groupe(data, role) if h in ips})
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 17:51:29 -04:00
|
|
|
# OPNsense refuse un nom d'alias de 32 caracteres ou plus. La contrainte n'etait nulle
|
|
|
|
|
# part, et elle s'est manifestee a l'APPLICATION — pas au devis :
|
|
|
|
|
#
|
|
|
|
|
# SETOPS_PATI29_SERVEUR_ARTEFACTS_SITE (36)
|
|
|
|
|
# « The name must start with a letter [...] and be less than 32 characters »
|
|
|
|
|
#
|
|
|
|
|
# L'outil a bien refuse d'appliquer quoi que ce soit, mais le devis, lui, annoncait un
|
|
|
|
|
# objet impossible. Un devis qui promet ce que la cible refusera n'est pas un devis.
|
|
|
|
|
LONGUEUR_MAX_ALIAS = 31
|
|
|
|
|
|
|
|
|
|
# Meme abreviation que le pare-feu est-ouest (`devis_proxmox_fw` : serveur_ -> srv-), pour
|
|
|
|
|
# qu'un seul schema de lecture serve aux deux couches.
|
|
|
|
|
ABREGES = (("SERVEUR_", "SRV_"), ("CLIENT_", "CLI_"))
|
|
|
|
|
|
|
|
|
|
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
def nom_alias(role: str, etiquette: str) -> str:
|
2026-08-24 17:51:29 -04:00
|
|
|
"""Alias d'hotes, PREFIXE par le tenant : deux tenants ont chacun leur nginx.
|
|
|
|
|
|
|
|
|
|
Abrege SEULEMENT si le nom complet depasse — un nom court reste lisible tel quel, et
|
|
|
|
|
la flotte existante n'est pas renommee pour la commodite d'un seul role.
|
|
|
|
|
"""
|
|
|
|
|
nom = f"SETOPS_{etiquette}_{role.upper()}"
|
|
|
|
|
if len(nom) <= LONGUEUR_MAX_ALIAS:
|
|
|
|
|
return nom
|
|
|
|
|
court = role.upper()
|
|
|
|
|
for long, bref in ABREGES:
|
|
|
|
|
court = court.replace(long, bref)
|
|
|
|
|
nom = f"SETOPS_{etiquette}_{court}"
|
|
|
|
|
if len(nom) > LONGUEUR_MAX_ALIAS:
|
|
|
|
|
# Mieux vaut un devis qui refuse qu'un devis qui promet l'impossible.
|
|
|
|
|
raise SystemExit(
|
|
|
|
|
f"Nom d'alias trop long pour OPNsense ({len(nom)} > {LONGUEUR_MAX_ALIAS}) : {nom}.\n"
|
|
|
|
|
f"Raccourcir le nom du role `{role}`.")
|
|
|
|
|
return nom
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _instance_active() -> tuple[str, dict] | tuple[None, None]:
|
|
|
|
|
"""(nom, nomenclature) de l'instance vers laquelle pointe le symlink `instance`."""
|
|
|
|
|
cible = (RACINE / "instance").resolve().name
|
|
|
|
|
for nom, _pfx, n in decouvrir():
|
|
|
|
|
if nom == cible:
|
|
|
|
|
return nom, n
|
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 13:57:48 -04:00
|
|
|
def _plan_site() -> Path | None:
|
|
|
|
|
"""Le `plan/` du SITE, a cote de son `underlay.yml`."""
|
|
|
|
|
c = underlay_mod.chemin()
|
|
|
|
|
return (c.resolve().parent / "plan") if c else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lire_plan_site(nom: str) -> dict:
|
|
|
|
|
d = _plan_site()
|
|
|
|
|
f = (d / nom) if d else None
|
|
|
|
|
if not f or not f.is_file():
|
|
|
|
|
return {}
|
|
|
|
|
return yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _machines_du_plan_site() -> list[dict]:
|
|
|
|
|
srv = (_lire_plan_site("serveurs.yml") or {}).get("serveurs") or {}
|
|
|
|
|
return [{**s, "nom": nom} for nom, s in srv.items()
|
|
|
|
|
if str(s.get("etat", "actif")) == "actif"]
|
|
|
|
|
|
|
|
|
|
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
def _ports_du_plan_site() -> dict[str, str]:
|
|
|
|
|
"""Port declare au plan pour chaque role du site — ce que `port: derive` vaut ici.
|
|
|
|
|
|
|
|
|
|
UN PORT NON NUMERIQUE N'EST PAS UNE ECOUTE (voir `resoudre_flux`). `derive` dit que le
|
|
|
|
|
port depend du deploiement : la forge ecoute 3000 derriere un edge, 443 quand elle
|
|
|
|
|
sert son propre TLS. Le rendre tel quel produirait une regle que le boitier refuse —
|
|
|
|
|
on l'a deja paye sur le pare-feu est-ouest.
|
|
|
|
|
"""
|
|
|
|
|
apps = (_lire_plan_site("applications.yml") or {}).get("applications") or {}
|
|
|
|
|
out: dict[str, str] = {}
|
|
|
|
|
for app in apps.values():
|
|
|
|
|
if app.get("groupe") and app.get("port"):
|
|
|
|
|
out[str(app["groupe"])] = str(app["port"])
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
2026-08-25 13:57:48 -04:00
|
|
|
def _services_du_plan_site() -> dict[str, list[str]]:
|
|
|
|
|
apps = (_lire_plan_site("applications.yml") or {}).get("applications") or {}
|
|
|
|
|
out: dict[str, list[str]] = {}
|
|
|
|
|
for app in apps.values():
|
|
|
|
|
if app.get("hote") and app.get("groupe"):
|
|
|
|
|
out.setdefault(str(app["hote"]), []).append(str(app["groupe"]))
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|
|
|
|
"""Le devis, en structure — rendue ensuite en texte ou en JSON."""
|
|
|
|
|
flux_par_role = charger_flux()
|
|
|
|
|
retenus = flux_frontiere(flux_par_role)
|
2026-08-02 16:06:55 -04:00
|
|
|
_intr = intrants_frontiere()
|
|
|
|
|
if_wan = _ou_marqueur(_intr, "opnsense_if_wan", IF_WAN)
|
|
|
|
|
if_transit = _ou_marqueur(_intr, "opnsense_if_transit", IF_TRANSIT)
|
2026-08-06 14:39:51 -04:00
|
|
|
if_gestion = _ou_marqueur(_intr, "opnsense_if_gestion", IF_GESTION)
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
if_site = _ou_marqueur(_intr, "opnsense_if_site", IF_SITE)
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
# UNE PATTE PAR ZONE DU SITE (2026-08-25). Le site n'est plus un `/24` plat : chaque
|
|
|
|
|
# nature d'autorite a son VLAN, et le trafic penetre le pare-feu par SA patte. Une
|
|
|
|
|
# regle posee sur la mauvaise interface ne correspond jamais.
|
|
|
|
|
if_zones = dict(_intr.get("opnsense_if_zones") or {})
|
|
|
|
|
|
|
|
|
|
def _if_de(reseau: str | None) -> str:
|
|
|
|
|
"""L'interface ou arrive le trafic de cette zone ; l'ancienne patte a defaut."""
|
|
|
|
|
return if_zones.get(str(reseau or ""), if_site)
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
nom_actif, nomenclature_active = _instance_active()
|
|
|
|
|
|
2026-08-09 16:44:58 -04:00
|
|
|
# L'alias d'un tenant vaut ses SOUS-RESEAUX ATTRIBUES, pas son supernet.
|
|
|
|
|
#
|
|
|
|
|
# Tant qu'il valait `10.27.0.0/16`, nos propres regles autorisaient
|
|
|
|
|
# `admin -> 10.27.0.0/16:22` : le filtre laissait donc passer une connexion vers
|
|
|
|
|
# n'importe quelle adresse du /16, y compris celles ou aucune machine n'existe.
|
|
|
|
|
# Mesure du 2026-08-09, apres avoir retire les routes /16 : un `connect()` vers
|
|
|
|
|
# 10.27.99.99:22 aboutissait encore, et l'etat pf portait la description de NOTRE
|
|
|
|
|
# regle. Le symptome que nous avons attribue deux jours durant a une fonction
|
|
|
|
|
# d'anti-usurpation de la frontiere venait d'abord de nos declarations trop larges.
|
|
|
|
|
#
|
|
|
|
|
# Meme geste que pour les routes, et pour la meme raison : ne declarer que ce qui
|
|
|
|
|
# existe. L'alias sert a la fois de DESTINATION aux regles de filtrage et de SOURCE
|
|
|
|
|
# au NAT sortant ; le retrecir resserre correctement les deux.
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
alias: dict[str, dict] = {}
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
alias[f"SETOPS_TENANT_{pfx}{n['index']}"] = {
|
|
|
|
|
"type": "network",
|
2026-08-09 16:44:58 -04:00
|
|
|
"contenu": [
|
|
|
|
|
sous_reseau_de(n["index"], int(z))
|
|
|
|
|
for z in sorted((n.get("categories") or {}), key=int)
|
|
|
|
|
],
|
|
|
|
|
"description": f"Sous-reseaux attribues du tenant {nom} (index {n['index']})",
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
}
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
# « Vers Internet » ne veut pas dire « vers n'importe ou ». Les flux sortants d'un
|
|
|
|
|
# tenant (DNS, SMTP, NTP, HTTP/HTTPS) visaient `any` : la destination n'excluait donc
|
|
|
|
|
# ni le plan de gestion, ni la frontiere elle-meme, ni le supernet du VOISIN. Mesure du
|
|
|
|
|
# 2026-08-09 depuis une VM du tenant : `https://10.0.0.1/` — la console d'administration
|
|
|
|
|
# du pare-feu — repondait. Autorise par notre propre regle, pas par une regle heritee.
|
|
|
|
|
#
|
|
|
|
|
# Cet alias dit ce qui est INTERNE ; les regles sortantes le prennent en destination
|
|
|
|
|
# NIEE. Il vaut les TROIS BLOCS PRIVES (RFC 1918) et non la liste de nos reseaux : une
|
|
|
|
|
# exclusion incomplete ne protege rien, et une liste derivee de l'underlay laissait
|
|
|
|
|
# dehors `192.168.11.0/24` — le plan de gestion herite, celui-la meme ou fuyaient les
|
|
|
|
|
# paquets du 2026-08-09. Un reseau interne ajoute demain est couvert sans rien changer.
|
|
|
|
|
#
|
|
|
|
|
# C'est aussi la definition honnete de « vers Internet » : tout ce qui n'est pas prive.
|
|
|
|
|
alias["SETOPS_INTERNES"] = {
|
|
|
|
|
"type": "network",
|
|
|
|
|
"contenu": ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
|
|
|
|
|
"description": "Espaces prives RFC 1918 — sert en destination NIEE aux flux "
|
|
|
|
|
"sortants : « vers Internet » n'est pas « vers n'importe ou »",
|
|
|
|
|
}
|
|
|
|
|
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
# Reseaux d'administration : UN ALIAS PAR TENANT, jamais une union. Chaque tenant
|
|
|
|
|
# declare les siens (intrant `nftables_admin_ssh`) et ils n'ouvrent QUE son supernet :
|
|
|
|
|
# une union laisserait le plan de gestion d'un tenant entrer chez le voisin, ce que
|
|
|
|
|
# les ACL de switch interdisent par ailleurs. La frontiere ne doit pas rouvrir ce que
|
|
|
|
|
# l'isolation inter-tenant ferme.
|
2026-08-06 14:39:51 -04:00
|
|
|
#
|
|
|
|
|
# Chaque CIDR est en outre RANGE PAR INTERFACE D'ARRIVEE. Une regle OPNsense est
|
|
|
|
|
# toujours `in` sur l'interface par ou le paquet entre : posee ailleurs, elle ne peut
|
|
|
|
|
# tout simplement jamais correspondre. Un plan de gestion directement attache a la
|
|
|
|
|
# frontiere (`10.0.0.0/24` ici) arrive par l'interface de GESTION ; un plan distant,
|
|
|
|
|
# qui revient par l'adresse publique ou un VPN, arrive par le WAN.
|
|
|
|
|
reseaux_gestion = reseaux_locaux_frontiere(underlay_mod.charger(), transit_underlay())
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
admin_par_tenant: dict[str, list[str]] = {}
|
2026-08-06 14:39:51 -04:00
|
|
|
admin_par_if: dict[str, dict[str, list[str]]] = {}
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
etiquette = f"{pfx}{n['index']}"
|
|
|
|
|
cidrs = admin_de(nom)
|
|
|
|
|
admin_par_tenant[etiquette] = cidrs
|
2026-08-06 14:39:51 -04:00
|
|
|
locaux = [c for c in cidrs if _porte_par(c, reseaux_gestion)]
|
|
|
|
|
distants = [c for c in cidrs if c not in locaux]
|
|
|
|
|
admin_par_if[etiquette] = {"gestion": locaux, "wan": distants}
|
|
|
|
|
# UN ALIAS PAR INTERFACE, et le nom le dit. Un alias unique couvrant les deux
|
|
|
|
|
# cotes forcerait a poser la meme regle deux fois avec des sources qui ne peuvent
|
|
|
|
|
# pas arriver la : la moitie serait morte, sans que rien ne le signale.
|
|
|
|
|
for cle, portee in (("gestion", "GESTION"), ("wan", "WAN")):
|
|
|
|
|
part = admin_par_if[etiquette][cle]
|
|
|
|
|
if part:
|
|
|
|
|
alias[f"SETOPS_ADMIN_{etiquette}_{portee}"] = {
|
|
|
|
|
"type": "network",
|
|
|
|
|
"contenu": list(part),
|
|
|
|
|
"description": f"Reseaux d'administration de {nom} arrivant par "
|
|
|
|
|
f"{portee.lower()} (intrant nftables_admin_ssh) — "
|
|
|
|
|
f"source unique, partagee avec ses nftables",
|
|
|
|
|
}
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
# Union, pour les routes de RETOUR cote switch et la garde P24 : router n'est pas
|
|
|
|
|
# autoriser. Le switch doit savoir revenir vers tous les plans de gestion.
|
|
|
|
|
admin = sorted({c for cidrs in admin_par_tenant.values() for c in cidrs})
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
# Sources privees arrivant par le WAN : OPNsense active par defaut « Block private
|
|
|
|
|
# networks » sur cette interface, et ce filtre s'applique AVANT les regles. Une regle
|
|
|
|
|
# correcte ne serait donc jamais atteinte — panne muette, invisible dans le devis.
|
|
|
|
|
# RFC1918 exactement — c'est le perimetre de la case « Block private networks ».
|
|
|
|
|
# `ipaddress.is_private` serait trop large : il inclut les plages de documentation
|
|
|
|
|
# et le CGNAT, que la case voisine « Block bogon networks » traite separement.
|
2026-08-06 14:39:51 -04:00
|
|
|
#
|
|
|
|
|
# SEULS les CIDR ranges cote WAN comptent : conseiller de decocher le filtre pour un
|
|
|
|
|
# reseau qui arrive par la gestion affaiblirait l'interface publique sans rien ouvrir.
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
rfc1918 = [ipaddress.ip_network(b) for b in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")]
|
|
|
|
|
admin_prives = sorted({
|
2026-08-06 14:39:51 -04:00
|
|
|
c for parts in admin_par_if.values() for c in parts["wan"]
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
if any(ipaddress.ip_network(c, strict=False).subnet_of(b) for b in rfc1918)
|
|
|
|
|
})
|
|
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
supernet_actif = (
|
|
|
|
|
supernet_de(nomenclature_active["index"]) if nomenclature_active else None
|
|
|
|
|
)
|
|
|
|
|
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
# Les regles sont emises POUR CHAQUE TENANT FEDERE, pas seulement l'actif : la
|
|
|
|
|
# frontiere route deja vers eux tous (section 2). N'en filtrer qu'un laisserait les
|
|
|
|
|
# autres routes menant a un `block` muet.
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
regles: list[dict] = []
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
tenants_sans_inventaire: list[str] = []
|
|
|
|
|
tenants_sans_admin: list[str] = []
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
etiquette = f"{pfx}{n['index']}"
|
|
|
|
|
alias_tenant = f"SETOPS_TENANT_{etiquette}"
|
|
|
|
|
inv = inventaire_de(nom)
|
|
|
|
|
if inv is None:
|
|
|
|
|
tenants_sans_inventaire.append(nom)
|
|
|
|
|
continue
|
|
|
|
|
for role, fl in retenus:
|
|
|
|
|
if role == ROLE_SOCLE:
|
|
|
|
|
destination = alias_tenant
|
|
|
|
|
else:
|
|
|
|
|
cibles = cibles_par_role(role, inv)
|
|
|
|
|
if not cibles:
|
|
|
|
|
continue # role absent de ce tenant : aucune regle a poser
|
|
|
|
|
a = nom_alias(role, etiquette)
|
|
|
|
|
alias[a] = {
|
|
|
|
|
"type": "host",
|
|
|
|
|
"contenu": cibles,
|
|
|
|
|
"description": f"Hotes portant {role} (instance {nom})",
|
|
|
|
|
}
|
|
|
|
|
destination = a
|
|
|
|
|
entrant = fl.get("sens") == "ingress"
|
|
|
|
|
# Le SSH de gestion est le SEUL flux `externe` a ne pas venir d'Internet :
|
|
|
|
|
# sa `raison` dit « l'acces depuis l'exterieur est filtre a l'OPNsense ».
|
|
|
|
|
ssh_gestion = entrant and "22" in _ports(fl)
|
|
|
|
|
if ssh_gestion and not admin_par_tenant.get(etiquette):
|
|
|
|
|
# Sans reseau d'administration declare, la regle n'aurait pas de source :
|
|
|
|
|
# l'omettre ferme l'acces, l'ouvrir a `any` exposerait le SSH a Internet.
|
|
|
|
|
tenants_sans_admin.append(nom)
|
|
|
|
|
continue
|
2026-08-06 14:39:51 -04:00
|
|
|
# Interface d'ARRIVEE du paquet, celle a laquelle OPNsense attache la regle.
|
|
|
|
|
# Sortant (la flotte vers Internet) : le lien de transit, par ou le paquet
|
|
|
|
|
# penetre le pare-feu. Entrant depuis Internet : le WAN. Entrant depuis un
|
|
|
|
|
# plan d'administration : l'interface ou ce plan est REELLEMENT attache —
|
|
|
|
|
# une regle posee sur la mauvaise interface ne correspond jamais.
|
|
|
|
|
if ssh_gestion:
|
|
|
|
|
portees = []
|
|
|
|
|
if admin_par_if[etiquette]["gestion"]:
|
|
|
|
|
portees.append((f"SETOPS_ADMIN_{etiquette}_GESTION", if_gestion))
|
|
|
|
|
if admin_par_if[etiquette]["wan"]:
|
|
|
|
|
portees.append((f"SETOPS_ADMIN_{etiquette}_WAN", if_wan))
|
2026-08-24 17:32:35 -04:00
|
|
|
elif entrant and "voisins_site" in _pairs(fl):
|
|
|
|
|
# UN FLUX ENTRE TENANTS DU MEME SITE.
|
|
|
|
|
#
|
|
|
|
|
# Ce n'est ni `flotte` (mon ecosysteme) ni `externe` (l'Internet) : le
|
|
|
|
|
# paquet vient d'un VOISIN pose sur la meme fabric. Il arrive par le meme
|
|
|
|
|
# lien de transit que tout le reste — les tenants ne se distinguent pas
|
|
|
|
|
# par une interface mais par leur ALIAS SOURCE, deja construit plus haut
|
|
|
|
|
# (`SETOPS_TENANT_<etiquette>`).
|
|
|
|
|
#
|
|
|
|
|
# LA REGLE APPARTIENT AU SITE, PAS A L'UN DES DEUX TENANTS. C'est le
|
|
|
|
|
# runner de site qui prepare le terrain : il configure les VNets, les
|
|
|
|
|
# routes et les flux pour que les plans des tenants tiennent. Aucun
|
|
|
|
|
# ecosysteme n'ouvre de porte chez un autre.
|
|
|
|
|
portees = [
|
|
|
|
|
(f"SETOPS_TENANT_{p2}{n2['index']}", if_transit)
|
|
|
|
|
for nom2, p2, n2 in tenants if nom2 != nom
|
|
|
|
|
]
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
elif entrant:
|
frontiere : etanche — CONFORME sur 56 lignes, dans les deux sens
L'exploitant a retire la derniere regle heritee. `make frontiere-mesurer`
rend CONFORME (code 0) : tout ce qui est declare est livre, tout le reste est
refuse — y compris collab-01:9980, le seul qui livrait vraiment un HTTP 200
depuis le poste.
Et mon instrument avait tort, pas la frontiere. Il comptait 38 ecarts en
concluant depuis le client : « connexion etablie => la bordure a relaye ».
Faux, verifie A LA DESTINATION : pendant que le poste tenait une connexion
« etablie » vers idm-01:389, idm-01 n'en voyait aucune ; collab-01 n'en
voyait aucune sur 9980. La frontiere repond a la poignee TCP sans relayer.
Le devis raisonne desormais sur la LIVRAISON seule, et le controle ne rend le
releve NUL que s'il LIVRE des donnees — qu'il ressorte AMBIGU est attendu ici
et le rapport le dit a chaque execution. Cette relaxation rend aussi le sens
sortant mesurable : il etait declare NUL en permanence.
Nouveau mot-cle `poste: false` dans meta/flux.yml : un service publie n'est
pas forcement fait pour un poste de travail. Le 25 entrant de Postfix est un
flux serveur a serveur ; la frontiere l'etendait au VLAN d'administration ou
le nftables de l'hote le refusait. Deux regles retirees. Le mot-cle vit avec
le role qui sait ce que son port veut dire ; le generateur ne connait
toujours aucun numero de port.
Verifie : frontiere-plan sans ecart (41 regles, 12 routes), flotte 14/14,
frontiere-mesurer CONFORME, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:33:04 -04:00
|
|
|
# Un service publie est joint DEPUIS INTERNET par le WAN, et le plus
|
|
|
|
|
# souvent aussi depuis le VLAN d'administration — le poste de
|
|
|
|
|
# l'exploitant y est.
|
|
|
|
|
#
|
|
|
|
|
# Sauf quand le flux declare `poste: false`. Tous les services publies ne
|
|
|
|
|
# s'adressent pas a un humain : le `25` entrant de Postfix est un flux
|
|
|
|
|
# SERVEUR A SERVEUR (les MX distants). Le declarer joignable depuis le
|
|
|
|
|
# poste faisait dire a la frontiere ce qu'aucune autre couche n'honorait —
|
|
|
|
|
# mesure du 2026-08-09 : la bordure l'autorisait, le nftables de l'hote le
|
|
|
|
|
# refusait. Deux couches qui ne declarent pas la meme politique, c'est
|
|
|
|
|
# une politique qu'on ne peut plus lire.
|
|
|
|
|
#
|
|
|
|
|
# Le mot-cle vit dans `meta/flux.yml`, avec le role qui sait ce que son
|
|
|
|
|
# port veut dire. Le generateur, lui, ne connait aucun numero de port.
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
# Ce second chemin fonctionnait jusqu'ici par la regle d'usine
|
|
|
|
|
# `Default allow LAN to any`, qui autorise TOUT depuis le LAN : notre
|
|
|
|
|
# defaut-deny n'y avait donc aucun effet. Mesure du 2026-08-09 : depuis
|
|
|
|
|
# le poste, le 443 d'un nginx repondait alors que seul le 22 est declare.
|
|
|
|
|
#
|
|
|
|
|
# Cette regle heritee n'a AUCUNE API (verifie : zero regle non-Set-OPS
|
|
|
|
|
# visible) — elle se desactive a la main. La declarer ici est ce qui rend
|
|
|
|
|
# cette desactivation possible SANS couper l'exploitant de ses propres
|
|
|
|
|
# services. Sans elle, fermer le LAN fermerait aussi ses consoles web.
|
|
|
|
|
portees = [("any", if_wan)]
|
frontiere : etanche — CONFORME sur 56 lignes, dans les deux sens
L'exploitant a retire la derniere regle heritee. `make frontiere-mesurer`
rend CONFORME (code 0) : tout ce qui est declare est livre, tout le reste est
refuse — y compris collab-01:9980, le seul qui livrait vraiment un HTTP 200
depuis le poste.
Et mon instrument avait tort, pas la frontiere. Il comptait 38 ecarts en
concluant depuis le client : « connexion etablie => la bordure a relaye ».
Faux, verifie A LA DESTINATION : pendant que le poste tenait une connexion
« etablie » vers idm-01:389, idm-01 n'en voyait aucune ; collab-01 n'en
voyait aucune sur 9980. La frontiere repond a la poignee TCP sans relayer.
Le devis raisonne desormais sur la LIVRAISON seule, et le controle ne rend le
releve NUL que s'il LIVRE des donnees — qu'il ressorte AMBIGU est attendu ici
et le rapport le dit a chaque execution. Cette relaxation rend aussi le sens
sortant mesurable : il etait declare NUL en permanence.
Nouveau mot-cle `poste: false` dans meta/flux.yml : un service publie n'est
pas forcement fait pour un poste de travail. Le 25 entrant de Postfix est un
flux serveur a serveur ; la frontiere l'etendait au VLAN d'administration ou
le nftables de l'hote le refusait. Deux regles retirees. Le mot-cle vit avec
le role qui sait ce que son port veut dire ; le generateur ne connait
toujours aucun numero de port.
Verifie : frontiere-plan sans ecart (41 regles, 12 routes), flotte 14/14,
frontiere-mesurer CONFORME, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:33:04 -04:00
|
|
|
if admin_par_if[etiquette]["gestion"] and fl.get("poste", True):
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
portees.append((f"SETOPS_ADMIN_{etiquette}_GESTION", if_gestion))
|
2026-08-24 17:40:33 -04:00
|
|
|
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
|
|
|
|
|
]
|
2026-08-06 14:39:51 -04:00
|
|
|
else:
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
portees = [("any", if_transit)]
|
2026-08-06 14:39:51 -04:00
|
|
|
for source, interface in portees:
|
|
|
|
|
regles.append({
|
|
|
|
|
"sens": "in" if entrant else "out",
|
|
|
|
|
"interface": interface,
|
|
|
|
|
"protocole": fl.get("protocole", "tcp"),
|
|
|
|
|
"source": source if entrant else destination,
|
frontiere : « vers Internet » n'est plus « vers n'importe ou »
Validation a l'instrument de l'exigence « aucun trafic impertinent », par de
vraies requetes applicatives contre des destinations interdites.
Le transit tenant etait deja correct : hyperviseur, frontiere, poste et
Proxmox tous muets ; le 25 sortant passe depuis edge-mta-01 (banniere
220 mx.google.com) et est refuse depuis infra-dns-01.
Trou 1, a nous : les flux sortants visaient `any`, donc n'excluaient ni le
plan de gestion ni le voisin — https://10.0.0.1/ repondait depuis une VM.
Ils visent desormais !SETOPS_INTERNES, destination NIEE valant les trois
blocs prives RFC 1918. Pas la liste de nos reseaux : elle laissait dehors
192.168.11.0/24, le plan de gestion herite.
Trou 2, pas a nous : la regle d'usine « Default allow LAN to any » privait
notre defaut-deny de tout effet (mesure : le 443 d'un nginx repondait depuis
le poste alors que seul le 22 est declare). Aucune API ne l'expose. Set-OPS
declare donc les flux d'administration legitimes vers les services publies,
pour que la desactiver ne coupe pas l'exploitant de ses consoles web.
Verifie apres application : 10.0.0.1:443 bloque depuis le tenant, sortie web
+ DNS + SMTP public toujours passants, curl vers le nginx du tenant -> 302,
flotte 14/14, frontiere-plan sans ecart, prouver.py 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 17:00:08 -04:00
|
|
|
# Sortant : « tout sauf l'interne ». Le `!` est porte par le devis
|
|
|
|
|
# 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.
|
2026-08-24 17:40:33 -04:00
|
|
|
# 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"),
|
2026-08-06 14:39:51 -04:00
|
|
|
"ports": _ports(fl),
|
|
|
|
|
"chiffrement": fl.get("chiffrement"),
|
|
|
|
|
"role": role,
|
|
|
|
|
"tenant": nom,
|
|
|
|
|
"raison": fl.get("raison", ""),
|
|
|
|
|
})
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
intrants = intrants_frontiere()
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# --- LES MACHINES DU SITE -------------------------------------------------
|
|
|
|
|
#
|
|
|
|
|
# Elles ne sont dans AUCUN plan de tenant : la boucle ci-dessus ne pouvait donc pas
|
|
|
|
|
# les voir, et le devis rendait zero regle pour elles sans rien signaler. Un devis
|
|
|
|
|
# muet sur une machine qui existe n'est pas un devis.
|
|
|
|
|
#
|
|
|
|
|
# Leur trafic arrive par la patte du site (`if_site`), pas par le lien de transit.
|
|
|
|
|
# Une regle posee sur la mauvaise interface ne correspond jamais — c'est la panne la
|
|
|
|
|
# plus silencieuse de cette couche.
|
|
|
|
|
_u = underlay_mod.charger()
|
2026-08-25 13:57:48 -04:00
|
|
|
# LES MACHINES DU SITE VIENNENT DE SON PLAN, plus de l'underlay (2026-08-25).
|
|
|
|
|
#
|
|
|
|
|
# En deplacant le plan hors de `underlay.yml`, j'ai vide `machines()` sans reprendre
|
|
|
|
|
# ici : le devis a cesse de voir le site et a propose de RETIRER ses 36 objets — tous
|
|
|
|
|
# ses alias et toutes ses regles. Aucune preuve ne couvre le devis de la frontiere du
|
|
|
|
|
# site, donc `make prouver` restait vert. C'est le plan avant application qui l'a
|
|
|
|
|
# attrape, et rien d'autre ne l'aurait fait.
|
|
|
|
|
_machines_site = _machines_du_plan_site()
|
|
|
|
|
_services_site = _services_du_plan_site()
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
_ports_site = _ports_du_plan_site()
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
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"])
|
|
|
|
|
for m in _machines_site if m.get("reseau") in _res_site})
|
|
|
|
|
alias["SETOPS_SITE"] = {
|
|
|
|
|
"type": "network",
|
|
|
|
|
"contenu": _sous_site,
|
|
|
|
|
"description": "Reseau des machines de l'hebergeur (runner de SITE, cache racine)",
|
|
|
|
|
}
|
|
|
|
|
# LA FABRIC — ce que le runner du site doit joindre pour MATERIALISER : l'API des
|
|
|
|
|
# hyperviseurs, leur shell, l'API de la frontiere. C'est du RFC 1918 : le rendre
|
|
|
|
|
# par `!SETOPS_INTERNES` (la forme de « vers Internet ») l'aurait exclu tout en
|
|
|
|
|
# ayant l'air d'ouvrir le flux. D'ou le mot `fabric`, et cet alias-ci.
|
|
|
|
|
_fabric = sorted({str(h["ip"]) for h in underlay_mod.hotes(_u)
|
|
|
|
|
if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip")})
|
|
|
|
|
alias["SETOPS_FABRIC"] = {
|
|
|
|
|
"type": "host",
|
|
|
|
|
"contenu": _fabric,
|
|
|
|
|
"description": "Materiel de l'hebergeur : hyperviseurs et frontiere — "
|
|
|
|
|
"ce que le runner du SITE pilote pour materialiser",
|
|
|
|
|
}
|
|
|
|
|
# LE PLAN D'ADMINISTRATION, source du SSH vers le site. Sans lui, la regle
|
|
|
|
|
# n'aurait pas de source : l'omettre ferme l'acces, l'ouvrir a `any` exposerait
|
|
|
|
|
# le SSH du runner — la machine la plus puissante du site — a l'Internet.
|
|
|
|
|
_admin_site = [str(r["sous_reseau"]) for r in underlay_mod.reseaux(_u)
|
|
|
|
|
if r.get("nom") == "management" and r.get("sous_reseau")]
|
|
|
|
|
if _admin_site:
|
|
|
|
|
alias["SETOPS_ADMIN_SITE"] = {
|
|
|
|
|
"type": "network",
|
|
|
|
|
"contenu": _admin_site,
|
|
|
|
|
"description": "Plan d'administration — seule source du SSH vers le site",
|
|
|
|
|
}
|
|
|
|
|
# Le socle vaut aussi pour le site : ses machines sont des Debian de la flotte,
|
|
|
|
|
# et c'est lui qui porte leur SSH. `site_inventaire.py` les y range.
|
site : un ecosysteme complet — PKI, DNS, forge, cache, runner
Le site portait trois services sur les sept du modele `origine`. Sa forge servait LE
GENOME EN CLAIR et aucune de ses machines n'avait de certificat — donc aucun chiffrement
est-ouest, ce que la doctrine zero-confiance interdit. site-pki-01 (step-ca) et
site-dns-01 (PowerDNS + resolveur colocalises) rejoignent les trois autres.
Quatre defauts que le site a fait tomber, chacun invisible chez un tenant :
- DNS bloque par notre propre default-deny. Un tenant a son resolveur DANS son reseau
et ne traverse jamais la frontiere ; le site interroge la sienne. La regle est derivee
de `site.dns_amorcage`, destination declaree, jamais `any`.
- serveur_cache_site n'installe rien : il marque un cache et lit les variables de
serveur_artefacts. Les defauts d'un role ne sont en portee que dans le play qui
l'inclut — une dependance de role regle l'ordre ET la portee.
- resoudre_idp partait meme avec OIDC desactive, et exigeait un plan. La resolution
suit desormais l'usage.
- le plancher /etc/hosts etait VIDE : `hotes_actifs` n'existait pas dans l'inventaire
du site. Un role qui reussit en n'ecrivant rien est la pire forme d'echec.
Une machine du site peut desormais se configurer (`variables:`), appliquee en dernier :
ce qu'une machine declare d'elle-meme prime sur ce que le site declare pour toutes.
Verifie et non suppose : systemd disait `active` mais rien n'ecoutait sur 443 — step-ca
sert sur 8443. La zone souveraine resout et la recursion marche, mesurees sur la machine.
42 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 12:00:17 -04:00
|
|
|
# LE SITE RESOUT CHEZ SA PASSERELLE, ET IL FAUT LE DIRE (2026-08-25).
|
|
|
|
|
#
|
|
|
|
|
# Un tenant a son propre resolveur DANS son reseau : son DNS ne traverse jamais la
|
|
|
|
|
# frontiere, et aucun flux ne le declare. Le site n'en a pas — trois machines ne
|
|
|
|
|
# justifient pas un service de plus — il interroge donc la frontiere elle-meme.
|
|
|
|
|
#
|
|
|
|
|
# Sans cette regle, le default-deny bloque la requete, et la panne ne ressemble pas
|
|
|
|
|
# a un pare-feu : `/etc/resolv.conf` est correct, Unbound ecoute bien sur toutes les
|
|
|
|
|
# interfaces, le port repond au test TCP — et `apt update` echoue quand meme. On a
|
|
|
|
|
# cherche du cote du resolveur pendant que c'etait le filtre.
|
|
|
|
|
#
|
|
|
|
|
# La destination est l'adresse DECLAREE dans `site.dns_amorcage`, pas `any` : ouvrir
|
|
|
|
|
# le 53 vers le monde depuis le site serait une sortie DNS non policee.
|
|
|
|
|
_resolveur = str((_u.get("site") or {}).get("dns_amorcage") or "").strip()
|
|
|
|
|
if _resolveur:
|
|
|
|
|
alias["SETOPS_RESOLVEUR_SITE"] = {
|
|
|
|
|
"type": "host",
|
|
|
|
|
"contenu": [_resolveur],
|
|
|
|
|
"description": "Resolveur des machines du site — leur passerelle, "
|
|
|
|
|
"declaree dans `site.dns_amorcage`",
|
|
|
|
|
}
|
|
|
|
|
for _proto in ("udp", "tcp"):
|
|
|
|
|
regles.append({
|
|
|
|
|
"sens": "out",
|
|
|
|
|
"interface": if_site,
|
|
|
|
|
"protocole": _proto,
|
|
|
|
|
"source": "SETOPS_SITE",
|
|
|
|
|
"destination": "SETOPS_RESOLVEUR_SITE",
|
|
|
|
|
"ports": ["53"],
|
|
|
|
|
"chiffrement": "clair",
|
|
|
|
|
"role": "site",
|
|
|
|
|
"tenant": "SITE",
|
|
|
|
|
"raison": "Resolution DNS des machines du site aupres de leur "
|
|
|
|
|
"passerelle : le site n'a pas de resolveur a lui.",
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-25 13:57:48 -04:00
|
|
|
_roles_site = sorted({g for gs in _services_site.values() for g in gs}
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
| {"serveur_debian"})
|
|
|
|
|
for _role in _roles_site:
|
|
|
|
|
_ips = sorted({str(m["ip"]) for m in _machines_site
|
|
|
|
|
if _role == "serveur_debian"
|
2026-08-25 13:57:48 -04:00
|
|
|
or _role in _services_site.get(m["nom"], [])})
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
_a = nom_alias(_role, "SITE")
|
|
|
|
|
alias[_a] = {
|
|
|
|
|
"type": "host",
|
|
|
|
|
"contenu": _ips,
|
|
|
|
|
"description": f"Machines du site portant {_role}",
|
|
|
|
|
}
|
|
|
|
|
for _fl in flux_par_role.get(_role, []):
|
|
|
|
|
_entrant = _fl.get("sens") == "ingress"
|
|
|
|
|
_pairs_fl = _pairs(_fl)
|
|
|
|
|
if _entrant and "voisins_site" in _pairs_fl:
|
|
|
|
|
# Un tenant qui s'adresse au site : le paquet penetre le pare-feu par
|
|
|
|
|
# le lien de TRANSIT, pas par la patte du site. Une regle par voisin,
|
|
|
|
|
# parce qu'une regle porte une source.
|
|
|
|
|
_portees = [(f"SETOPS_TENANT_{p2}{n2['index']}", if_transit)
|
|
|
|
|
for _n2, p2, n2 in tenants]
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
elif _entrant and "flotte" in _pairs_fl:
|
|
|
|
|
# CE QUI ETAIT GRATUIT DEVIENT POLICE (2026-08-25).
|
|
|
|
|
#
|
|
|
|
|
# `flotte` veut dire « les machines de mon ecosysteme ». Dans un `/24`
|
|
|
|
|
# plat, ce trafic ne traversait rien : le cache, la forge, le
|
|
|
|
|
# resolveur et l'AC se joignaient en L2, sans qu'aucune regle
|
|
|
|
|
# n'existe — ni ne manque.
|
|
|
|
|
#
|
|
|
|
|
# Une zone par autorite change cela : une machine qui interroge le
|
|
|
|
|
# resolveur ou tire un paquet du cache passe desormais par la
|
|
|
|
|
# frontiere. Ce qui n'avait pas besoin d'etre dit doit l'etre.
|
|
|
|
|
#
|
|
|
|
|
# Une regle PAR ZONE SOURCE : le paquet penetre le pare-feu par la
|
|
|
|
|
# patte de la zone d'ou il part, et une regle posee ailleurs ne
|
|
|
|
|
# correspondrait jamais. La destination reste nommee — le role vise,
|
|
|
|
|
# jamais la zone entiere.
|
|
|
|
|
_portees = [("SETOPS_SITE", z) for z in
|
|
|
|
|
sorted({_if_de(m.get("reseau")) for m in _machines_site})]
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
elif _entrant and "22" in _ports(_fl) and _admin_site:
|
|
|
|
|
# Le SSH de gestion arrive par la patte d'ADMINISTRATION de la
|
|
|
|
|
# frontiere, pas par celle du site : c'est la que le paquet penetre
|
|
|
|
|
# le pare-feu. Une regle posee ailleurs ne correspondrait jamais.
|
|
|
|
|
_portees = [("SETOPS_ADMIN_SITE", if_gestion)]
|
|
|
|
|
elif _entrant:
|
|
|
|
|
continue # rien d'autre n'entre chez le site depuis l'exterieur
|
|
|
|
|
else:
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
# Une portee par ZONE ou ce role est present : deux machines d'un meme
|
|
|
|
|
# role dans deux zones differentes ne partagent pas leur patte.
|
|
|
|
|
_zones = sorted({_if_de(m.get("reseau")) for m in _machines_site
|
|
|
|
|
if _role == "serveur_debian"
|
|
|
|
|
or _role in _services_site.get(m["nom"], [])})
|
|
|
|
|
_portees = [(_a, z) for z in _zones] or [(_a, if_site)]
|
|
|
|
|
# `derive` se resout par le port declare au plan ; sans lui, on n'emet
|
|
|
|
|
# rien plutot qu'une regle que le boitier refusera.
|
|
|
|
|
_p = _ports(_fl)
|
|
|
|
|
if any(not str(x).isdigit() for x in _p):
|
|
|
|
|
_reel = _ports_site.get(_role)
|
|
|
|
|
if not _reel:
|
|
|
|
|
# SUR STDERR, JAMAIS STDOUT : ce script rend du JSON, et une
|
|
|
|
|
# note au milieu le rend illisible a `appliquer_opnsense`.
|
|
|
|
|
print(f"note : {_role} declare un port `{'/'.join(_p)}` que le "
|
|
|
|
|
f"plan du site ne resout pas — aucune regle emise.",
|
|
|
|
|
file=sys.stderr)
|
|
|
|
|
continue
|
|
|
|
|
_p = [_reel]
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
for _src, _itf in _portees:
|
|
|
|
|
regles.append({
|
|
|
|
|
"sens": "in" if _entrant else "out",
|
|
|
|
|
"interface": _itf,
|
|
|
|
|
"protocole": _fl.get("protocole", "tcp"),
|
|
|
|
|
"source": _src if _entrant else _a,
|
|
|
|
|
"destination": _a if _entrant else (
|
|
|
|
|
"SETOPS_FABRIC" if "fabric" in _pairs_fl else "!SETOPS_INTERNES"),
|
site : quatre zones d'autorite, et l'ordre inscrit dans les integrations
Le site n'est plus un /24 plat. Une zone par nature d'autorite — pilotage, autorite,
genome, service — chacune son VLAN et sa patte sur la frontiere. L'inversion corrigee,
mesuree : les cinq VM du site n'avaient AUCUN filtrage est-ouest, contre policy_in=DROP
sur une machine de tenant. La plus autoritaire etait la moins protegee.
Filtrage nord-sud par choix de l'exploitant : un seul point de police, un seul devis.
90 -> 117 regles. Chaque flux `flotte` produit une regle par zone SOURCE, destination
nommee — ce qui etait gratuit devient police.
L'ORDRE FAIT PARTIE DE L'INTEGRATION. client_pki tournait en parallele sur tous les hotes ;
sur celui qui porte l'autorite il recharge step-ca, et les quatre autres echouaient dans
cette fenetre sur `TLS handshake timeout` — un message qui accuse le reseau. Chaque
integration declare desormais sa dependance dans meta/integration.yml, les playbooks en
sont le miroir genere, et P44 refuse l'ecart (4 controles negatifs). `serveur: ~` est une
reponse valable : client_metrique pose un exportateur qu'on vient LIRE.
Autres defauts du meme soir :
- le plancher /etc/hosts venait APRES le premier apt, qui vise le cache par son NOM :
boucle fermee des que les adresses changent. Il ne s'installe pas, il rend installable.
- dns_amorcage ecrit en dur a eu tort deux fois ; il se derive de serveur_resolveur.
- l'ACL du resolveur derivait d'un seul sous-reseau : trois zones refusees sur quatre.
ET UNE ERREUR A MOI : j'ai diagnostique un trou noir de MTU et declare 1450. Faux — pas de
VXLAN sur ce chemin, tout est a 1500 de bout en bout. Ma mesure etait reelle, mon
interpretation non : je venais de debrancher la carte a chaud, et chaque `ip link set mtu`
reconfigurait l'interface. C'est la reconfiguration qui debloquait, pas la valeur.
44 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 17:31:07 -04:00
|
|
|
"ports": _p,
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
"chiffrement": _fl.get("chiffrement"),
|
|
|
|
|
"role": _role,
|
|
|
|
|
"tenant": "SITE",
|
|
|
|
|
"raison": _fl.get("raison", ""),
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-01 19:38:17 -04:00
|
|
|
transit = transit_underlay()
|
|
|
|
|
saut = prochain_saut(transit)
|
2026-08-09 16:07:10 -04:00
|
|
|
# UNE ROUTE PAR SOUS-RESEAU REELLEMENT ATTRIBUE, et non une par supernet.
|
|
|
|
|
#
|
|
|
|
|
# Router le /16 entier faisait porter a la frontiere des destinations qui n'existent
|
|
|
|
|
# nulle part. Ces paquets atteignaient le noeud de sortie, y arrivaient dans la table
|
|
|
|
|
# PRINCIPALE — pas dans le VRF, qui n'est atteint que par les /24 annonces en BGP — et
|
|
|
|
|
# repartaient vers la passerelle du reseau d'ADMINISTRATION. Mesure du 2026-08-09 :
|
|
|
|
|
# `ip route get 10.27.99.99` rendait `via 192.168.11.254`.
|
|
|
|
|
#
|
|
|
|
|
# C'est aussi ce qui faisait reussir tout `connect()` depuis le VLAN d'administration,
|
|
|
|
|
# y compris vers des adresses inexistantes — symptome que nous avons attribue pendant
|
|
|
|
|
# deux jours a une fonction d'anti-usurpation de la frontiere, alors que c'etait un
|
|
|
|
|
# routage trop large.
|
|
|
|
|
#
|
2026-08-09 16:44:58 -04:00
|
|
|
# La frontiere ne route desormais QUE ce qui existe. L'alias `SETOPS_TENANT_*`, qui
|
|
|
|
|
# porte le NAT sortant et sert de destination aux regles, enumere exactement les memes
|
|
|
|
|
# sous-reseaux : la garde P24 compare ces deux listes et reste donc satisfaite.
|
2026-08-09 16:07:10 -04:00
|
|
|
_sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn"
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
routes = [
|
|
|
|
|
{
|
2026-08-09 16:07:10 -04:00
|
|
|
"reseau": sous_reseau_de(n["index"], int(z)),
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"prochain_saut": saut,
|
2026-08-09 16:44:58 -04:00
|
|
|
"tenant": nom,
|
2026-08-09 16:07:10 -04:00
|
|
|
"description": f"{nom} — {cat.get('libelle', f'zone{z}')} (zone {z}, index "
|
|
|
|
|
f"{n['index']}) — " + ("zone EVPN du tenant" if _sdn
|
|
|
|
|
else "switches L3"),
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
}
|
|
|
|
|
for nom, _pfx, n in tenants
|
2026-08-09 16:07:10 -04:00
|
|
|
for z, cat in sorted((n.get("categories") or {}).items(), key=lambda x: int(x[0]))
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
]
|
|
|
|
|
|
2026-08-06 15:59:11 -04:00
|
|
|
# NAT sortant : une regle par tenant, source = son alias de supernet, cible = l'adresse
|
|
|
|
|
# du WAN. Le mode automatique d'OPNsense ne couvre QUE les reseaux directement attaches ;
|
|
|
|
|
# un supernet joint par route statique en sort sans avertissement. Mesure du 2026-08-06 :
|
|
|
|
|
# le filtre laissait passer, un etat s'ouvrait sans `nat_addr`, et rien ne revenait.
|
|
|
|
|
nat = [
|
|
|
|
|
{
|
|
|
|
|
"interface": if_wan,
|
|
|
|
|
"source": f"SETOPS_TENANT_{pfx}{n['index']}",
|
|
|
|
|
"destination": "any",
|
|
|
|
|
"cible": "wanip",
|
|
|
|
|
"tenant": nom,
|
|
|
|
|
}
|
|
|
|
|
for nom, pfx, n in tenants
|
|
|
|
|
]
|
frontiere : le devis voit le site — `fabric`, alias et regles
Un mot manquait au vocabulaire des flux. Le runner de SITE declarait son API Proxmox en
`externe`, qui se rend par !SETOPS_INTERNES — or les hyperviseurs SONT en RFC 1918 : la
regle les aurait exclus tout en ayant l'air d'ouvrir le flux. D'ou `fabric`.
Deux flux manquaient :
- serveur_cache_site n'avait aucun egress : la chaine de caches se terminait sur un
cache vide, et ca ne se serait vu qu'au premier apt update d'un ecosysteme neuf ;
- serveur_ops_site n'avait que l'API : le shell des noeuds et l'API de la frontiere
sont deux autres pouvoirs, declares a part.
Le devis ignorait les machines du site — dans aucun plan de tenant, donc invisibles a sa
boucle, zero regle rendue SANS RIEN SIGNALER. Il emet desormais SETOPS_SITE,
SETOPS_FABRIC, SETOPS_ADMIN_SITE et un alias par role, sur opt2 (arrivee reelle du
trafic) et lan pour le SSH, plus le NAT sortant du site.
Le socle s'applique au site : site_inventaire.py range ses machines dans serveur_debian.
Patient 0 ne declare plus serveur_ops_site ni serveur_cache_site : il est un tenant
comme les autres, c'est meme tout ce qu'il prouve. Inventaire regenere.
42 preuves vertes. Devis : 26 regles a creer, 5 a retirer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 00:49:04 -04:00
|
|
|
# LE SITE AUSSI SORT. Son reseau est directement attache a la frontiere (OPT2), donc
|
|
|
|
|
# le mode automatique d'OPNsense le couvrirait — mais nous ne sommes plus en mode
|
|
|
|
|
# automatique : declarer les tenants a la main l'a bascule. Compter sur un mode qu'on
|
|
|
|
|
# a soi-meme quitte, c'est le genre de supposition qui se paie au premier `apt update`
|
|
|
|
|
# du cache racine, quand plus rien ne se telecharge et que tout a l'air correct.
|
|
|
|
|
if _machines_site and "SETOPS_SITE" in alias:
|
|
|
|
|
nat.append({
|
|
|
|
|
"interface": if_wan,
|
|
|
|
|
"source": "SETOPS_SITE",
|
|
|
|
|
"destination": "any",
|
|
|
|
|
"cible": "wanip",
|
|
|
|
|
"tenant": "SITE",
|
|
|
|
|
})
|
2026-08-06 15:59:11 -04:00
|
|
|
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
# Adresse du noeud de sortie primaire sur le lien de frontiere, calculee une fois :
|
|
|
|
|
# elle sert au prochain saut des routes ET au commentaire qui l'explique.
|
|
|
|
|
_ip_sortie = adresse_noeud_de_sortie(underlay_mod.charger(), transit)
|
|
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
return {
|
|
|
|
|
"role_frontiere": "nord-sud",
|
|
|
|
|
"instance_active": nom_actif,
|
|
|
|
|
"supernet_actif": supernet_actif,
|
|
|
|
|
"api_url": str(intrants.get("opnsense_api_url") or "").strip(),
|
2026-08-02 16:06:55 -04:00
|
|
|
"wan_ip": str(intrants.get("opnsense_wan_ip") or "").strip(),
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"if_wan": _ou_marqueur(intrants, "opnsense_if_wan", IF_WAN),
|
|
|
|
|
"if_transit": _ou_marqueur(intrants, "opnsense_if_transit", IF_TRANSIT),
|
2026-08-06 14:39:51 -04:00
|
|
|
"if_gestion": _ou_marqueur(intrants, "opnsense_if_gestion", IF_GESTION),
|
|
|
|
|
"reseaux_gestion": reseaux_gestion,
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"alias": alias,
|
|
|
|
|
"routes": routes,
|
2026-08-06 15:59:11 -04:00
|
|
|
"nat": nat,
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"regles": regles,
|
|
|
|
|
"admin": admin,
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
"tenants_sans_inventaire": tenants_sans_inventaire,
|
|
|
|
|
"tenants_sans_admin": sorted(set(tenants_sans_admin)),
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
"admin_prives": admin_prives,
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
"noeud_sortie": ({"nom": str((_sdn_hebergeur() or {}).get("sortie_primaire") or ""),
|
|
|
|
|
"ip": _ip_sortie}
|
|
|
|
|
if _ip_sortie else None),
|
2026-08-01 19:38:17 -04:00
|
|
|
"transit": {
|
|
|
|
|
"nom": (transit or {}).get("nom"),
|
|
|
|
|
"vlan": (transit or {}).get("vlan"),
|
|
|
|
|
"sous_reseau": (transit or {}).get("sous_reseau"),
|
2026-08-03 08:42:54 -04:00
|
|
|
# L'adresse du commutateur SUR LE LIEN, pas le prochain saut des tenants :
|
|
|
|
|
# les deux ont diverge avec le SDN, ou le voisin de la frontiere devient un
|
|
|
|
|
# noeud de sortie. Ce champ decrit le cablage, pas le routage.
|
|
|
|
|
"svi_switch": (transit or {}).get("passerelle"),
|
2026-08-01 19:38:17 -04:00
|
|
|
"adresse_frontiere": (transit or {}).get("passerelle_sortie"),
|
2026-08-01 20:18:16 -04:00
|
|
|
"nom_frontiere": nom_frontiere(transit),
|
2026-08-01 19:38:17 -04:00
|
|
|
} if transit else None,
|
2026-08-03 08:40:20 -04:00
|
|
|
# Le switch route vers l'ADRESSE DU PARE-FEU sur le lien de transit
|
|
|
|
|
# (`passerelle_sortie`), pas vers son propre SVI ni vers un noeud de sortie :
|
|
|
|
|
# c'est la sortie de l'underlay. `devis_reseau` emet exactement ces lignes.
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"reciproque_switch": [
|
2026-08-03 08:40:20 -04:00
|
|
|
f"ip route {cidr} -> {(transit or {}).get('passerelle_sortie') or PROCHAIN_SAUT}"
|
|
|
|
|
f" (retour vers l'administration)"
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
for cidr in admin
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def rendre(devis: dict) -> str:
|
|
|
|
|
out = [
|
|
|
|
|
"# ============================================================",
|
|
|
|
|
"# DEVIS OPNSENSE — frontiere nord/sud (Set-OPS)",
|
|
|
|
|
"# Genere par scripts/devis_opnsense.py. NE PAS EDITER A LA MAIN.",
|
|
|
|
|
"# Derive du registre des flux (pair: externe) + des nomenclatures.",
|
2026-08-03 08:40:20 -04:00
|
|
|
("# Les passerelles de zone sont anycast sur les hyperviseurs (SDN EVPN) : aucun SVI ici."
|
|
|
|
|
if underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn"
|
|
|
|
|
else "# Les passerelles de zone restent sur les switches L3 : aucun SVI ici."),
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"# ============================================================",
|
|
|
|
|
"",
|
|
|
|
|
]
|
2026-08-01 19:38:17 -04:00
|
|
|
tr = devis.get("transit")
|
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
|
|
|
if tr and not tr.get("svi_switch"):
|
|
|
|
|
# Aucun SVI sur le lien : les switches ne routent pas, donc ils n'ont aucune
|
|
|
|
|
# route de retour a porter. Emettre la section quand meme laisserait croire
|
|
|
|
|
# qu'il reste un prerequis a satisfaire ailleurs.
|
|
|
|
|
out += [
|
|
|
|
|
"## 0. Prerequis reciproques — AUCUN",
|
|
|
|
|
"# Les switches ne routent pas : ils n'ont ni SVI sur ce lien, ni route de",
|
|
|
|
|
"# retour a porter. La frontiere est le seul equipement L3, et les noeuds de",
|
|
|
|
|
"# sortie EVPN lui sont ADJACENTS sur ce meme VLAN — rien ne s'interpose.",
|
|
|
|
|
]
|
2026-08-01 19:38:17 -04:00
|
|
|
else:
|
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
|
|
|
out += [
|
|
|
|
|
"## 0. Prerequis reciproques — COTE SWITCHES",
|
|
|
|
|
"# Sans ces routes de retour, le trafic part mais ne revient pas : la",
|
|
|
|
|
"# reponse sort par une autre interface et le pare-feu la jette (hors etat).",
|
|
|
|
|
]
|
|
|
|
|
out += [f"# {r}" for r in devis["reciproque_switch"]] or ["# (aucun reseau d'administration declare)"]
|
|
|
|
|
if tr:
|
|
|
|
|
out.append(f"# DEJA EMIS par `make devis-reseau` (section 5), derive du transit"
|
|
|
|
|
f" '{tr['nom']}' de l'underlay.")
|
|
|
|
|
else:
|
|
|
|
|
out.append("# A EMETTRE : aucun reseau de transit n'est declare dans underlay.yml")
|
|
|
|
|
out.append("# (cle `passerelle_sortie`). Sans lui, ni sortie ni retour ne sont derivables.")
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
out += [
|
|
|
|
|
"",
|
|
|
|
|
"## 1. Interfaces",
|
|
|
|
|
f"# Gestion : {devis['api_url'] or '(intrant opnsense_api_url a renseigner)'}",
|
2026-08-02 16:06:55 -04:00
|
|
|
f"# WAN : {devis['if_wan']}"
|
|
|
|
|
+ (f" — {devis['wan_ip']} (face publique de l'ecosysteme)" if devis.get("wan_ip") else ""),
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
f"# TRANSIT : {devis['if_transit']} (trunk 802.1Q vers les switches)",
|
2026-08-06 14:39:51 -04:00
|
|
|
f"# GESTION : {devis['if_gestion']}"
|
|
|
|
|
+ (f" — plan(s) attache(s) : {', '.join(devis['reseaux_gestion'])}"
|
|
|
|
|
if devis.get("reseaux_gestion") else " — aucun plan attache a la frontiere"),
|
2026-08-01 19:38:17 -04:00
|
|
|
]
|
|
|
|
|
if tr:
|
|
|
|
|
out += [
|
|
|
|
|
f"# lien '{tr['nom']}' {tr['sous_reseau']} — VLAN {tr['vlan']} a etiqueter",
|
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
|
|
|
(f"# switch {tr['svi_switch']} <-> frontiere" if tr.get("svi_switch")
|
|
|
|
|
else "# segment partage — aucun SVI de switch ; la frontiere y est")
|
|
|
|
|
+ f" {tr['adresse_frontiere']}"
|
2026-08-01 20:18:16 -04:00
|
|
|
+ (f" ({tr['nom_frontiere']})" if tr.get("nom_frontiere") else ""),
|
2026-08-01 19:38:17 -04:00
|
|
|
]
|
|
|
|
|
out += [
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"",
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
]
|
|
|
|
|
if devis.get("admin_prives"):
|
|
|
|
|
out += [
|
|
|
|
|
f"# /!\\ DECOCHER « Block private networks » SUR {devis['if_wan'].upper()}.",
|
|
|
|
|
"# Des sources PRIVEES entrent par cette interface : "
|
|
|
|
|
+ ", ".join(devis["admin_prives"]) + ".",
|
|
|
|
|
"# Ce filtre d'interface s'applique AVANT les regles : coche, il jette le",
|
|
|
|
|
"# paquet d'administration sans qu'aucune regle ne soit consultee. La config",
|
|
|
|
|
"# parait juste, le SSH ne passe pas, et rien ne dit pourquoi.",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
out += [
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"## 2. Routes statiques (vers les supernets tenants)",
|
|
|
|
|
]
|
2026-08-03 08:40:20 -04:00
|
|
|
if devis["routes"] and devis["routes"][0]["prochain_saut"] == NOEUD_SORTIE:
|
|
|
|
|
out += [
|
|
|
|
|
"# /!\\ LE PROCHAIN SAUT N'EST PAS LE COMMUTATEUR. En SDN EVPN, il ne route plus",
|
|
|
|
|
"# les tenants : une route pointee vers son SVI arriverait sur un equipement sans",
|
|
|
|
|
"# chemin vers le tenant. Le voisin de la frontiere est un NOEUD DE SORTIE — un",
|
frontière : le nœud de sortie dérive, l'identifiant d'interface est vérifié
Le prochain saut des routes tenants ne s'écrit plus <NOEUD-DE-SORTIE-EVPN> : il
dérive. Deux déclarations doivent concorder, et c'est voulu — l'hébergeur nomme
le nœud (proxmox_sdn.sortie_primaire, propriété du cluster), l'underlay dit son
adresse SUR LE LIEN DE FRONTIÈRE. Nommer un nœud absent du lien rend le devis
muet plutôt que faux.
route add 10.21.0.0/16 via 10.0.4.41
route add 10.27.0.0/16 via 10.0.4.41
Le devis explique pourquoi cette adresse-là — ni l'underlay ni la gestion, la
frontière n'atteint que celle-là — et pourquoi un seul saut : une route statique
n'en porte qu'un, et deux nœuds actifs en sortie avec une seule route en entrée
donneraient un chemin asymétrique.
Identifiant d'interface vérifié plutôt que supposé : l'API des règles donne
elle-même sa liste (lan → GESTION, opt1 → TENANTS, wan → WAN). Ni le
périphérique vlan040, ni le libellé. L'intrant opnsense_if_transit: opt1 était
juste — question ouverte depuis deux jours, tranchée par la mesure.
Consigné au CHANGELOG : l'EVPN tourne (six sessions BGP, tunnels sortis de la
carte de gestion), une alarme retirée sur des VRF que je croyais inversés, et une
brèche réelle à filtrer avant la première VM tenant — un nœud de sortie expose au
tenant tout ce que l'hyperviseur touche directement, et déplacer la route par
défaut n'y suffira pas puisqu'une route connectée l'emporte.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:46:58 -04:00
|
|
|
"# hyperviseur qui porte la sortie du VRF.",
|
|
|
|
|
"# A DECLARER : `proxmox_sdn.sortie_primaire` chez l'hebergeur, et l'adresse de",
|
|
|
|
|
"# ce noeud sur le lien de transit dans underlay.yml. Les deux doivent concorder.",
|
|
|
|
|
]
|
|
|
|
|
elif devis.get("noeud_sortie"):
|
|
|
|
|
out += [
|
|
|
|
|
f"# Prochain saut = {devis['noeud_sortie']['nom']}, noeud de sortie PRIMAIRE, a son",
|
|
|
|
|
f"# adresse SUR LE LIEN DE FRONTIERE ({devis['noeud_sortie']['ip']}) — ni son underlay,",
|
|
|
|
|
"# ni sa gestion : la frontiere n'atteint que celle-la.",
|
|
|
|
|
"#",
|
|
|
|
|
"# Un seul saut, parce qu'une route statique n'en porte qu'un. Deux noeuds actifs",
|
|
|
|
|
"# en sortie avec une seule route en entree donneraient un chemin asymetrique : la",
|
|
|
|
|
"# reponse reviendrait par une interface ou l'etat n'a pas ete cree, et serait jetee.",
|
2026-08-03 08:40:20 -04:00
|
|
|
]
|
|
|
|
|
out += [
|
|
|
|
|
]
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
for r in devis["routes"]:
|
|
|
|
|
out.append(f"route add {r['reseau']} via {r['prochain_saut']} # {r['description']}")
|
2026-08-06 15:59:11 -04:00
|
|
|
out += ["", "## 2bis. NAT sortant (traduction des supernets tenants)"]
|
|
|
|
|
if devis.get("nat"):
|
|
|
|
|
out += [
|
|
|
|
|
"# Une route ne suffit pas : sans traduction, le paquet tenant quitte le WAN avec",
|
|
|
|
|
"# sa source PRIVEE et rien ne peut lui repondre. La panne est muette — le filtre",
|
|
|
|
|
"# laisse passer, un etat s'ouvre, et aucune reponse n'arrive jamais.",
|
|
|
|
|
"#",
|
|
|
|
|
"# Le mode « automatique » d'OPNsense ne traduit que les reseaux DIRECTEMENT",
|
|
|
|
|
"# ATTACHES. Un supernet tenant est joint par une ROUTE STATIQUE : il tombe hors",
|
|
|
|
|
"# de ce perimetre sans que rien ne le signale.",
|
|
|
|
|
]
|
|
|
|
|
for n in devis["nat"]:
|
|
|
|
|
out.append(f"nat on {n['interface']:8} from {n['source']:28} to any -> {n['cible']}"
|
|
|
|
|
f" # {n['tenant']}")
|
|
|
|
|
else:
|
|
|
|
|
out.append("# Aucun tenant federe : rien a traduire.")
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
out += ["", "## 3. Alias"]
|
|
|
|
|
for nom, a in devis["alias"].items():
|
|
|
|
|
contenu = ", ".join(a["contenu"]) if a["contenu"] else "(VIDE)"
|
|
|
|
|
out.append(f"alias {nom:32} {a['type']:8} {contenu}")
|
|
|
|
|
out.append(f"# {a['description']}")
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
out += ["", "## 4. Regles — PAR TENANT FEDERE (defaut : deny + journalisation)",
|
|
|
|
|
"# La frontiere route vers tous les tenants (section 2) : elle porte donc aussi",
|
|
|
|
|
"# leurs regles. Chacun n'est joignable en SSH que depuis SES PROPRES reseaux",
|
2026-08-02 16:06:55 -04:00
|
|
|
"# d'administration — jamais ceux du voisin, que l'isolation inter-tenant ferme.",
|
|
|
|
|
"# Chaque regle est `in` sur son interface d'ARRIVEE : le WAN pour ce qui vient",
|
|
|
|
|
"# d'Internet (et du VPN d'administration, qui revient par l'adresse publique),",
|
|
|
|
|
"# le lien de transit pour ce que la flotte emet vers l'exterieur."]
|
frontière : les règles couvrent tous les tenants fédérés
Le devis était multi-tenant pour ses routes, mono-tenant pour ses règles :
il routait 10.21.0.0/16 et 10.27.0.0/16 mais ne filtrait que l'instance
active. Technolibre aurait été routé jusqu'à la bordure puis bloqué dans les
deux sens, SSH d'administration compris, sans qu'une ligne dise pourquoi.
La résolution est paramétrée par tenant : `inventaire_de()` lit le hosts.yml
de chaque instance fédérée, `cibles_par_role()` prend l'inventaire en
argument, les alias d'hôtes sont préfixés. 11 règles par tenant, 22 au total.
Cloisonnement : la première version faisait de SETOPS_ADMIN l'union des
réseaux d'administration — le plan de gestion d'un tenant serait entré chez
le voisin, la bordure rouvrant ce que les ACL de switch ferment. Corrigé
avant livraison : un alias par tenant, n'ouvrant que son propre supernet.
L'union reste pour les routes de retour et P24 : router n'est pas autoriser.
Deux omissions annoncées : tenant sans inventaire (aucune règle), tenant
sans `nftables_admin_ssh` (règle SSH omise plutôt qu'ouverte à `any`, ce qui
exposerait le SSH à Internet). Cas dégradé exercé.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 14:25:20 -04:00
|
|
|
for nom in devis.get("tenants_sans_inventaire", []):
|
|
|
|
|
out.append(f"# /!\\ {nom} : aucun inventaire genere — AUCUNE regle. Route, puis bloque.")
|
|
|
|
|
for nom in devis.get("tenants_sans_admin", []):
|
|
|
|
|
out.append(f"# /!\\ {nom} : `nftables_admin_ssh` vide — regle SSH OMISE. L'ouvrir a "
|
|
|
|
|
f"`any` exposerait le SSH a Internet ; renseigner l'intrant.")
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
for rg in devis["regles"]:
|
|
|
|
|
ports = ",".join(rg["ports"])
|
2026-08-02 16:06:55 -04:00
|
|
|
# Dans OPNsense une regle est TOUJOURS `in` sur l'interface d'arrivee : c'est
|
|
|
|
|
# la que le paquet penetre le pare-feu. Le sens du flux (entrant/sortant vis-a-vis
|
|
|
|
|
# de l'ecosysteme) se lit alors dans l'interface, pas dans un mot-cle.
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
out.append(
|
2026-08-02 16:06:55 -04:00
|
|
|
f"pass in on {rg['interface']:8} proto {rg['protocole']:3} "
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
f"from {rg['source']:24} to {rg['destination']:24} port {ports:7} "
|
|
|
|
|
f"# {rg['role']} ({rg['chiffrement']})"
|
|
|
|
|
)
|
|
|
|
|
out.append(f"# {rg['raison']}")
|
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
|
|
|
sortants = sum(1 for r in devis["regles"] if r["sens"] == "out")
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
out += [
|
|
|
|
|
"",
|
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
|
|
|
"## 5. Defaut — DENY DANS LES DEUX SENS",
|
|
|
|
|
"# `block out` est un vrai default-deny : ce qui n'est pas declare en section 4",
|
|
|
|
|
f"# ne sort PAS. {sortants} regle(s) sortante(s) y figurent, toutes derivees du",
|
|
|
|
|
"# registre des flux (`sens: egress`, `pair: externe`).",
|
|
|
|
|
"#",
|
|
|
|
|
"# Un besoin sortant oublie ne se manifeste pas par un refus clair : apt expire,",
|
|
|
|
|
"# l'horloge derive et les certificats deviennent invalides des semaines plus tard.",
|
|
|
|
|
"# Le declarer dans `roles/<role>/meta/flux.yml` — ne jamais l'ouvrir ici a la main,",
|
|
|
|
|
"# la regle serait perdue a la prochaine generation.",
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
"block in log all # tout ce qui n'est pas ci-dessus",
|
|
|
|
|
"block out log all",
|
|
|
|
|
]
|
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verifier(devis: dict) -> tuple[bool, list[str]]:
|
|
|
|
|
"""Garde anti-lockout : la frontiere ne doit pas etre generee a l'aveugle."""
|
|
|
|
|
erreurs: list[str] = []
|
|
|
|
|
if not devis["admin"]:
|
|
|
|
|
erreurs.append(
|
|
|
|
|
"Aucun reseau d'administration declare (intrant `nftables_admin_ssh` vide) : "
|
|
|
|
|
"la regle SSH entrante n'aurait aucune source et le defaut deny fermerait "
|
|
|
|
|
"l'acces d'administration. Declarer le(s) CIDR d'admin (VPN compris)."
|
|
|
|
|
)
|
|
|
|
|
if not devis["routes"]:
|
|
|
|
|
erreurs.append("Aucun tenant federe decouvert : rien a router.")
|
|
|
|
|
for nom, a in devis["alias"].items():
|
|
|
|
|
if not a["contenu"]:
|
|
|
|
|
erreurs.append(f"Alias {nom} vide : une regle le referencerait dans le vide.")
|
2026-08-06 14:39:51 -04:00
|
|
|
|
|
|
|
|
# Une regle est TOUJOURS `in` sur l'interface d'arrivee du paquet. Posee ailleurs elle
|
|
|
|
|
# ne correspond jamais : le devis affirme un acces qui n'existe pas, et la panne ne se
|
|
|
|
|
# voit qu'a l'usage. On confronte donc chaque source d'administration a l'endroit ou
|
|
|
|
|
# son reseau est reellement attache.
|
2026-08-06 15:59:11 -04:00
|
|
|
# Une route vers un supernet sans NAT correspondant est le defaut MUET du 2026-08-06 :
|
|
|
|
|
# le filtre autorise, un etat s'ouvre, et aucune reponse ne revient jamais. On exige donc
|
|
|
|
|
# que tout supernet route soit aussi traduit.
|
|
|
|
|
traduits = {n["source"] for n in devis.get("nat") or []}
|
|
|
|
|
for nom, a in devis["alias"].items():
|
|
|
|
|
if nom.startswith("SETOPS_TENANT_") and nom not in traduits:
|
|
|
|
|
erreurs.append(
|
|
|
|
|
f"Supernet {nom} route mais NON traduit : ses paquets quitteraient le WAN "
|
|
|
|
|
"avec une source privee. Le filtre laisserait passer et rien ne reviendrait."
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-09 16:44:58 -04:00
|
|
|
# Ce qui est ROUTE et ce qui est AUTORISE doivent designer exactement les memes
|
|
|
|
|
# reseaux. Un alias plus large que les routes laisse le filtre approuver des
|
|
|
|
|
# destinations qui n'existent nulle part — defaut mesure le 2026-08-09 : l'alias valait
|
|
|
|
|
# le supernet /16, nos regles autorisaient donc `admin -> tout le /16:22`, et un
|
|
|
|
|
# `connect()` vers une adresse inexistante aboutissait toujours. Un alias plus etroit
|
|
|
|
|
# que les routes est la panne symetrique : la frontiere achemine vers un reseau que
|
|
|
|
|
# le filtre refuse. Les deux se voient ici, pas a l'usage.
|
|
|
|
|
routes_declarees = {str(r["reseau"]) for r in devis.get("routes") or []}
|
|
|
|
|
autorises: set[str] = set()
|
|
|
|
|
for nom, a in devis["alias"].items():
|
|
|
|
|
if nom.startswith("SETOPS_TENANT_"):
|
|
|
|
|
autorises |= {str(c) for c in a["contenu"]}
|
|
|
|
|
if routes_declarees != autorises:
|
|
|
|
|
for large in sorted(autorises - routes_declarees):
|
|
|
|
|
erreurs.append(
|
|
|
|
|
f"{large} est autorise par un alias de tenant mais n'est pas route : le "
|
|
|
|
|
"filtre approuverait une destination que la frontiere ne sait pas joindre."
|
|
|
|
|
)
|
|
|
|
|
for etroit in sorted(routes_declarees - autorises):
|
|
|
|
|
erreurs.append(
|
|
|
|
|
f"{etroit} est route mais absent des alias de tenant : la frontiere y "
|
|
|
|
|
"acheminerait des paquets que le filtre refuserait."
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-06 14:39:51 -04:00
|
|
|
gestion = devis.get("reseaux_gestion") or []
|
|
|
|
|
for r in devis["regles"]:
|
|
|
|
|
src = str(r.get("source") or "")
|
|
|
|
|
if not src.startswith("SETOPS_ADMIN_"):
|
|
|
|
|
continue
|
|
|
|
|
cidrs = (devis["alias"].get(src) or {}).get("contenu") or []
|
|
|
|
|
locaux = [c for c in cidrs if _porte_par(c, gestion)]
|
|
|
|
|
if r["interface"] == devis.get("if_gestion") and len(locaux) != len(cidrs):
|
|
|
|
|
erreurs.append(
|
|
|
|
|
f"Regle sur {r['interface']} (gestion) avec la source {src} : "
|
|
|
|
|
+ ", ".join(c for c in cidrs if c not in locaux)
|
|
|
|
|
+ " n'est attache a aucun plan local de la frontiere — le paquet "
|
|
|
|
|
"arriverait par une autre interface et la regle ne matcherait jamais."
|
|
|
|
|
)
|
|
|
|
|
if r["interface"] == devis.get("if_wan") and locaux:
|
|
|
|
|
erreurs.append(
|
|
|
|
|
f"Regle sur {r['interface']} (WAN) avec la source {src} : "
|
|
|
|
|
+ ", ".join(locaux)
|
|
|
|
|
+ " est directement attache a la frontiere, donc arrive par "
|
|
|
|
|
f"{devis.get('if_gestion')} — cette regle est morte."
|
|
|
|
|
)
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
return (not erreurs), erreurs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
|
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
|
ap.add_argument("--json", action="store_true", help="Sortie structuree (future API OPNsense).")
|
|
|
|
|
ap.add_argument("--verifier", action="store_true", help="Valide le devis sans l'afficher.")
|
|
|
|
|
args = ap.parse_args(argv)
|
|
|
|
|
|
portee : les trois devis d'un site partagent enfin la meme regle
Le commit du 2026-08-14 nommait lui-meme ce qui restait : « meme hypothese ailleurs, non
corrigee — devis_sdn et devis_reseau partent du meme decouvrir(). A traiter quand ils
serviront sur un second site. » C'est fait AVANT, pas pendant la visite.
Les trois devis equipent le MATERIEL d'un site : la frontiere (regles, routes), le
commutateur (VLAN, SVI, routes) et le SDN de l'hyperviseur (zones, VNets). Un tenant
d'ailleurs y ajoutait des objets que le materiel accepte, qui ne correspondent jamais a
rien, et que rien ne signale.
UNE SEULE FONCTION AU LIEU D'UN FILTRE RECOPIE TROIS FOIS :
`devis_reseau.decouvrir_du_site()` = decouvrir() restreint par `underlay.tenants`, la
doctrine ecrite une fois. Le filtre inline de devis_opnsense est retire au profit d'elle.
`admin_tous_tenants()` la suit : le routeur d'un site n'a pas a savoir revenir vers le
plan de gestion d'un tenant qu'il ne porte pas.
EPROUVE dans les trois situations : underlay sans la cle -> les deux tenants, comme avant ;
underlay du second site -> OPS-Technolibre seul ; nom declare qu'aucun dossier ne fournit
-> ATTENTION et le reste est retenu ; filtre qui ne retient rien -> refus, code 1.
SANS EFFET SUR LE SITE ACTUEL : l'underlay de Chezlepro ne declare pas `tenants`, et cle
absente = toute la federation (verifie : decouvrir() et decouvrir_du_site() rendent la
meme liste ici).
ET LA CLE EST ENFIN DOCUMENTEE — c'etait le vrai trou. `underlay.tenants` existait depuis
le 14 sans figurer ni dans underlay.yml.example ni dans l'annexe du runbook
d'implantation : indecouvrable pour qui monte un second site.
prouver 37 OK, 0 echec, 0 saute ; make test inchange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:38:30 -04:00
|
|
|
# UNE FRONTIERE NE POLICE QUE LES TENANTS DE SON SITE (2026-08-14). Le filtre est
|
|
|
|
|
# devenu la source unique des trois devis qui equipent un site — commutateur, SDN et
|
|
|
|
|
# frontiere : cf. `devis_reseau.decouvrir_du_site`.
|
|
|
|
|
tenants = decouvrir_du_site()
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
if not tenants:
|
|
|
|
|
print("Aucune instance federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr)
|
|
|
|
|
return 1
|
frontiere : une frontiere ne police que les tenants de SON site
Mesure du 2026-08-14, sur le second site. `make frontiere-plan` voulait poser sur la
frontiere de Technolibre les regles ET les routes de CHEZLEPRO : 30 objets de plus,
dont six routes vers des sous-reseaux 10.17.x qui n'existent pas la-bas.
LE DEFAUT EST DE PORTEE, ET IL EST SILENCIEUX. Les devis partaient de
`devis_reseau.decouvrir()`, qui rend TOUTE la federation — tout dossier frere portant
une nomenclature avec un index. C'etait juste tant qu'il n'y avait qu'un site :
l'hebergeur unique portait bien tous les tenants. Des le second, le boitier aurait
accepte ces trente objets, aucun n'aurait jamais correspondu a un paquet, et rien ne
l'aurait signale — une politique qui a l'air complete et ne protege rien. Encore le
chèque vert sur un perimetre vide.
CORRECTIF : l'hebergeur declare dans SON underlay les tenants qu'il porte
(`underlay.tenants`), et `devis_opnsense` s'y limite. Cle ABSENTE = ancien
comportement (toute la federation) : un site unique n'a rien a declarer, c'est le
second qui doit se nommer. Un nom declare qu'aucun dossier frere ne fournit est
signale, pas ignore.
MEME HYPOTHESE AILLEURS, non corrigee ici : `devis_sdn` et `devis_reseau` partent du
meme `decouvrir()`. A traiter quand ils serviront sur un second site.
AU PASSAGE, un defaut du document ecrit la veille : le squelette d'`underlay.yml` de
`implanter-un-tenant-sur-un-site.md` omettait `index`. Sans cette cle, `make underlay`
refuse le reseau de gestion en le prenant pour le supernet d'un AUTRE site — message
deroutant, cause triviale. Trouve en s'en servant, moins de 24 h apres l'avoir ecrit.
prouver 37/37, make test 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 00:55:39 -04:00
|
|
|
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
devis = construire(tenants)
|
|
|
|
|
|
|
|
|
|
if args.verifier:
|
|
|
|
|
ok, erreurs = verifier(devis)
|
|
|
|
|
for e in erreurs:
|
|
|
|
|
print(f"ECHEC : {e}", file=sys.stderr)
|
|
|
|
|
if ok:
|
|
|
|
|
print(f"CONFORME : frontiere nord/sud, {len(devis['regles'])} regles, "
|
|
|
|
|
f"{len(devis['routes'])} routes, admin={','.join(devis['admin'])}.")
|
|
|
|
|
return 0 if ok else 1
|
|
|
|
|
|
|
|
|
|
print(json.dumps(devis, indent=2, ensure_ascii=False) if args.json else rendre(devis))
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main(sys.argv[1:]))
|