Compare commits

..

No commits in common. "7339cc64b46f908a89ae3ec709943d1bd08e33be" and "1d875f50ec29fa36ec1c7e619b4e23c778035e3e" have entirely different histories.

6 changed files with 5 additions and 150 deletions

View file

@ -30,7 +30,7 @@
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 24 secret(s) exige(s), tous presents. Voute reelle : 27 cle(s), aucun manque. |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
@ -44,7 +44,7 @@
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 45 scripts expliques et atteignables, 93 cibles make documentees, 54 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (118 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 40 document(s) declarent leur lecteur (16 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |

View file

@ -1,73 +0,0 @@
# Preuve de conformite — Set-OPS — 2026-08-14
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
> [`docs/audit/README.md`](README.md), et le registre trace :
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (37 OK · 0 echec · 0 saute)
## Preuves
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  |
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 77 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml |
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. |
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). |
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 3 reseau(x), aucune collision avec la plage tenant. |
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 15 regles, 6 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. |
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 42 groupe(s), 68 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 0 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision ; /!\ aucun noeud de sortie declare (VRF sans chemin vers l'exterieur). |
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 45 scripts expliques et atteignables, 93 cibles make documentees, 54 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 40 document(s) declarent leur lecteur (16 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (OPS-Technolibre) : — tous offerts. |
## Couverture des affirmations ✅ du registre
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
elles restent hors du harnais recurrent (rien d'executable a rejouer).
## Declarations d'intention (⚪ invérifiables localement — assumees)
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
comme declarations d'intention, non comme preuves :
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
contre une flotte vivante.
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
_Rapport genere le 2026-08-14._

View file

@ -203,13 +203,7 @@ make frontiere-plan make sdn-plan make placement-plan
```yaml
---
underlay:
# LE SEED DU SITE — le même que celui de son tenant, et la clé qu'on oublie.
# C'est elle qui dit au validateur que 10.<index>.0.0/16 est SON supernet, donc que
# la bande basse lui appartient. Sans elle, `make underlay` refuse le réseau de
# gestion en le prenant pour celui d'un AUTRE site — message déroutant, cause triviale.
index: <index>
routeur: <nom-du-commutateur> # racine du spanning-tree, pas un routeur
# `stp` exige `routeur` : ne pas le déclarer tant qu'aucun commutateur ne l'est.
routage_tenants: sdn # le routage inter-zone vit sur l'hyperviseur
mtu_overlay: 1450 # 1450 + 50 de VXLAN = 1500 de transport
acl_inter_tenant: false # true seulement si le matériel sait lier une ACL à un SVI

View file

@ -29,7 +29,6 @@ import ssl
import subprocess
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
@ -64,35 +63,11 @@ class Frontiere:
self.auth = "Basic " + base64.b64encode(f"{cle}:{secret}".encode()).decode()
self.ctx = None if verifier else ssl._create_unverified_context()
@staticmethod
def _formulaire(corps: dict) -> bytes:
"""Encode `{racine: {champ: valeur}}` en `racine[champ]=valeur`.
POURQUOI PAS DU JSON (mesure du 2026-08-13, sur un OPNsense 24.7 neuf).
Le controleur d'OPNsense lit ses champs avec `hasPost(<racine>)`. Quand le
corps arrive en `application/json` et que le boitier ne le decompose pas en
variables de POST, ce test est FAUX : le controleur renvoie `{"result":
"failed"}` NU — sans la moindre validation, donc sans dire ce qui manque.
Toute ecriture echouait ainsi : alias, regles, NAT, routes. Le meme corps en
formulaire encode passe du premier coup.
C'est la forme que poste l'interface web elle-meme : aucune version
d'OPNsense ne peut la refuser, alors que le JSON depend du boitier.
"""
champs: dict[str, str] = {}
for racine, valeurs in corps.items():
if isinstance(valeurs, dict):
for k, v in valeurs.items():
champs[f"{racine}[{k}]"] = "" if v is None else str(v)
else:
champs[racine] = "" if valeurs is None else str(valeurs)
return urllib.parse.urlencode(champs).encode()
def __call__(self, chemin: str, corps: dict | None = None) -> dict:
data = self._formulaire(corps) if corps is not None else None
data = json.dumps(corps).encode() if corps is not None else None
h = {"Authorization": self.auth}
if data is not None:
h["Content-Type"] = "application/x-www-form-urlencoded"
if data:
h["Content-Type"] = "application/json"
req = urllib.request.Request(self.base + chemin, data=data, headers=h,
method="POST" if data is not None else "GET")
try:

View file

@ -796,24 +796,6 @@ def main(argv: list[str]) -> int:
if not tenants:
print("Aucune instance federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr)
return 1
# UNE FRONTIERE NE POLICE QUE LES TENANTS DE SON SITE. `decouvrir()` rend toute la
# federation ; l'hebergeur declare dans SON underlay ceux qu'il porte reellement.
# Sans ce filtre, la frontiere du second site recevait les regles et les routes du
# premier — acceptees par le boitier, jamais correspondantes, jamais signalees.
portes = underlay_mod.tenants_du_site(underlay_mod.charger())
if portes is not None:
connus = {nom for nom, _, _ in tenants}
for nom in portes:
if nom not in connus:
print(f"ATTENTION : `tenants` de l'underlay nomme « {nom} », "
f"qu'aucun dossier frere ne fournit.", file=sys.stderr)
tenants = [t for t in tenants if t[0] in set(portes)]
if not tenants:
print("Aucun tenant heberge sur ce site : `underlay.tenants` n'en retient "
"aucun de connu.", file=sys.stderr)
return 1
devis = construire(tenants)
if args.verifier:

View file

@ -131,29 +131,6 @@ def mtu_overlay(underlay: dict | None) -> int:
return int(v) if isinstance(v, int) else MTU_OVERLAY_DEFAUT
def tenants_du_site(underlay: dict | None) -> list[str] | None:
"""Noms de dossier des tenants HEBERGES sur ce site. None = non declare.
POURQUOI CETTE CLE EXISTE (mesure du 2026-08-13, sur le second site).
Les devis partaient de `devis_reseau.decouvrir()`, qui liste TOUTE la federation :
tout dossier frere portant un `plan/nomenclature.yml` 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 site, c'est faux et dangereux. Le devis de la frontiere du site B
voulait y poser les regles ET les routes du tenant du site A : trente regles et six
routes vers des sous-reseaux qui n'existent pas la-bas. Elles auraient ete acceptees
par le boitier, elles n'auraient jamais correspondu a un paquet, et rien ne l'aurait
signale — une politique qui a l'air complete et ne protege rien.
Absente, on garde l'ancien comportement (toute la federation) : un site unique n'a
rien a declarer. C'est le second site qui doit se nommer.
"""
v = (underlay or {}).get("underlay", underlay or {}).get("tenants")
if v is None:
return None
return [str(x) for x in v] if isinstance(v, list) else [str(v)]
def routage_tenants(underlay: dict | None) -> str:
"""Qui route entre les zones d'un tenant : `switch` (SVI) ou `sdn` (EVPN).