frontiere : l'administration (P89) ; serveur_ops ne declare plus le 8006
La face reseau dit quelles entrees publiques sont ouvertes au poste ; verifier_administration confronte gestion, VPN et tunnel. La sortie 8006 de serveur_ops vers externe ne laissait passer aucun paquet (hyperviseurs en plages privees) et affirmait un pouvoir qu aucun locataire n a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
56033afd00
commit
ce84d0e1ea
10 changed files with 230 additions and 18 deletions
|
|
@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente
|
||||||
## Écrire, puis relire (D-68)
|
## Écrire, puis relire (D-68)
|
||||||
|
|
||||||
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
`--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**.
|
||||||
C'est aussi ce que font les 88 preuves de `make prouver` : elles lisent le dépôt, sans le
|
C'est aussi ce que font les 89 preuves de `make prouver` : elles lisent le dépôt, sans le
|
||||||
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le
|
||||||
dépôt annonce.**
|
dépôt annonce.**
|
||||||
|
|
||||||
|
|
|
||||||
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -1,5 +1,69 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-10-05 (87) — Une règle qui disait qu'un locataire pouvait créer des VM
|
||||||
|
|
||||||
|
**La question de l'exploitant** : « un flux est permis entre une machine d'un tenant et l'API
|
||||||
|
Proxmox ? » Déclaré : oui. `roles/serveur_ops/meta/flux.yml` portait une sortie 8006 vers
|
||||||
|
`externe`, « API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant », et la
|
||||||
|
frontière en tirait une règle pour le runner de chaque locataire.
|
||||||
|
|
||||||
|
**Mesuré** (2026-10-05) : **non**. Depuis trois machines de deux locataires, vers les neuf
|
||||||
|
adresses des trois hyperviseurs et vers leur passerelle de zone, avec la forge du site pour
|
||||||
|
témoin (200) :
|
||||||
|
- `192.168.11.4x:8006` et `10.0.4.4x:8006`, et leur 22 : `ECONNREFUSED`. La frontière refuse ;
|
||||||
|
son journal porte les tentatives en `block`, par le refus audible du lien de transit. La
|
||||||
|
règle déclarée visait « tout sauf les plages privées », et les hyperviseurs y sont : elle
|
||||||
|
ne laissait passer **aucun** paquet ;
|
||||||
|
- la passerelle de zone (`10.23.19.1`, l'hyperviseur lui-même dans le VRF du locataire) :
|
||||||
|
`ECONNREFUSED`, sans passer par la frontière ; c'est l'hyperviseur qui refuse. Cause probable,
|
||||||
|
**non vérifiée** (poste hors site, sans accès SSH aux hyperviseurs) : pveproxy écoute dans le
|
||||||
|
VRF par défaut et `net.ipv4.tcp_l3mdev_accept` vaut 0.
|
||||||
|
|
||||||
|
**Fait** : la sortie est retirée de `serveur_ops`, avec la mesure en trace. Une règle morte qui
|
||||||
|
affirme un pouvoir qu'aucun locataire ne doit avoir. Matérialiser reste au **site**
|
||||||
|
(`serveur_ops_site` → `SETOPS_FABRIC`, 8006, inchangé). Registre des flux régénéré ; flux des
|
||||||
|
locataires inchangés.
|
||||||
|
|
||||||
|
**Simulé** (`make frontiere-plan`) : 5 retraits — la règle 8006 des deux locataires et celle de
|
||||||
|
la console du site (`SETOPS_SITE_SERVEUR_OPS`, morte pour la même raison), et les deux alias
|
||||||
|
`SERVEUR_OPS` des locataires, devenus orphelins. Rien d'autre sur 335 règles et 12 routes.
|
||||||
|
|
||||||
|
**Reste ouvert** : le chemin par la passerelle de zone contourne la frontière. Il est fermé
|
||||||
|
aujourd'hui ; la garde qui le surveillera est à décider avec l'exploitant.
|
||||||
|
|
||||||
|
**Validation** : `make verifier` conforme, 89/89.
|
||||||
|
|
||||||
|
## 2026-10-05 (86) — Étape 2 : la frontière, troisième temps (l'administration)
|
||||||
|
|
||||||
|
**Le chemin** (`docs/conception-contextes.md` §6), étape 2, frontière, troisième temps,
|
||||||
|
première moitié : ce que l'administration atteint chez un locataire. Les sorties suivent.
|
||||||
|
|
||||||
|
**Fait** (`scripts/contexte.py`) :
|
||||||
|
- la face réseau dit, pour chaque entrée ouverte à tous, si son flux l'ouvre **aussi au
|
||||||
|
poste** (`poste`, vrai par défaut). Elle le lit dans le catalogue des flux du moteur, que le
|
||||||
|
runner du locataire possède aussi : aucun changement chez le locataire. Une seule entrée est
|
||||||
|
fermée au poste, le 25 de Postfix (un flux de serveur à serveur) ;
|
||||||
|
- `verifier_administration(site, locataire)` confronte, pour **chaque classe
|
||||||
|
d'administration** que la frontière range par interface (gestion, VPN), ce qu'elle atteint
|
||||||
|
machine par machine avec ce que la face lui ouvre : le SSH, les entrées qui nomment
|
||||||
|
l'administration parmi leurs sources, les entrées publiques ouvertes au poste. Le **tunnel**
|
||||||
|
du locataire doit exister si la face en a un, et viser toute sa flotte ; ses ports (22 et
|
||||||
|
443) sont une politique du site, écrite dans le devis, pas une donnée du locataire.
|
||||||
|
|
||||||
|
**Relevé, non modifié** : le devis justifie `poste: false` sur le 25 par « deux couches qui ne
|
||||||
|
déclarent pas la même politique » (mesure du 2026-08-09 : la bordure autorisait, nftables
|
||||||
|
refusait). Depuis le 2026-09-29, nftables accepte le 25 de partout (un port public ouvert à
|
||||||
|
tous) : l'administration y accède donc par la machine, pas par la bordure. La bordure est la
|
||||||
|
plus stricte, sans danger ; mais les deux couches divergent de nouveau, dans l'autre sens.
|
||||||
|
|
||||||
|
**Éprouvé** :
|
||||||
|
- **P89**, « Ce que l'administration atteint chez un locataire, la frontière le tient de lui » :
|
||||||
|
aucun écart chez les deux locataires.
|
||||||
|
- `test_contexte.py`, 86 contrôles : une administration altérée de **quatre façons** (le 25
|
||||||
|
ouvert au poste, les sources d'administration retirées d'une machine, un SSH d'administration
|
||||||
|
retiré, le tunnel retiré) est vue à la frontière.
|
||||||
|
- `make verifier` conforme, **89/89**. Les trois documents comptent 89 preuves.
|
||||||
|
|
||||||
## 2026-10-05 (85) — Un passage entre locataires que rien ne justifiait
|
## 2026-10-05 (85) — Un passage entre locataires que rien ne justifiait
|
||||||
|
|
||||||
**Trouvé en confrontant la frontière à la face réseau** (sous-étape 3 de la frontière,
|
**Trouvé en confrontant la frontière à la face réseau** (sous-étape 3 de la frontière,
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ croisée, plus aucun recalcul.
|
||||||
croisées produisent. La première mesure des flux a trouvé une information que le locataire
|
croisées produisent. La première mesure des flux a trouvé une information que le locataire
|
||||||
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
|
jetait : les clients nommés d'un port aussi public, que Proxmox doit admettre nommément. Il
|
||||||
les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités
|
les publie désormais (`sources_declarees`). La frontière, en trois temps : les identités
|
||||||
(P87) et les entrées publiques (P88) sont faites ; restent les sorties et l'administration.
|
(P87), les entrées publiques (P88) et l'administration (P89) sont faites ; restent les sorties.
|
||||||
|
|
||||||
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
|
Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe
|
||||||
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.
|
n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met
|
||||||
|
|
||||||
## Le trou qu'il comble
|
## Le trou qu'il comble
|
||||||
|
|
||||||
`scripts/prouver.py` porte 88 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
`scripts/prouver.py` porte 89 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
|
||||||
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est
|
||||||
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
cohérent **avec lui-même** — que les handlers existent, que les intrants ont un
|
||||||
propriétaire, que rien n'est codé en dur.
|
propriétaire, que rien n'est codé en dur.
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,6 @@
|
||||||
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
|
| `serveur_ops` | ingress | 8090 | tcp | edge, admin | clair | Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable depuis le plan d'administration là où il n'y a pas d'edge. Le GUI lui-même reste sur la boucle locale : c'est nginx qui authentifie devant. |
|
||||||
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
|
| `serveur_ops` | egress | 22 | tcp | flotte | ssh | Piloter la flotte — c'est la raison d'être du poste. |
|
||||||
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
|
| `serveur_ops` | egress | 443 | tcp | edge | tls-requis | Cloner et resynchroniser le génome depuis la forge de l'écosystème. |
|
||||||
| `serveur_ops` | egress | 8006 | tcp | externe | tls-requis | API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant. |
|
|
||||||
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
|
| `serveur_ops_site` | egress | 22 | tcp | fabric | ssh | Shell des hyperviseurs : ce que l'API ne couvre pas — configuration reseau, ponts, deplacement de disques. Un pouvoir distinct de l'API, donc declare a part. |
|
||||||
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
|
| `serveur_ops_site` | egress | 22 | tcp | serveur_ops_tenant | ssh | Insemination : amorcer le runner d'un tenant — socle, moteur, plan, plancher de resolution — pour qu'il prenne ensuite le relais sur ses propres machines. Ne transporte aucun secret : la voute est remise par un humain. |
|
||||||
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
|
| `serveur_ops_site` | egress | 443 | tcp | fabric | tls-requis | API de la frontiere OPNsense : poser les alias et les regles qui ouvrent les flux du tenant qu'on materialise. Preparer le terrain sans cela laisserait un terrain injoignable. |
|
||||||
|
|
@ -135,4 +134,4 @@
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
- **tls** : 10 flux
|
- **tls** : 10 flux
|
||||||
- **tls-cible** : 2 flux
|
- **tls-cible** : 2 flux
|
||||||
- **tls-requis** : 35 flux
|
- **tls-requis** : 34 flux
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri
|
||||||
Aucune de ces responsabilités n'est laissée à la parole :
|
Aucune de ces responsabilités n'est laissée à la parole :
|
||||||
|
|
||||||
```
|
```
|
||||||
make prouver le dépôt est-il cohérent avec lui-même (88 preuves, zéro réseau)
|
make prouver le dépôt est-il cohérent avec lui-même (89 preuves, zéro réseau)
|
||||||
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80)
|
||||||
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
make certificats-plan ce que le disque porte contre ce que la mémoire sert
|
||||||
make expositions-plan chaque service publié répond-il, et depuis où
|
make expositions-plan chaque service publié répond-il, et depuis où
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,17 @@ flux:
|
||||||
pair: flotte
|
pair: flotte
|
||||||
chiffrement: ssh
|
chiffrement: ssh
|
||||||
raison: "Piloter la flotte — c'est la raison d'être du poste."
|
raison: "Piloter la flotte — c'est la raison d'être du poste."
|
||||||
# L'HYPERVISEUR N'EST PAS DANS L'ÉCOSYSTÈME : il appartient au monde physique, de
|
# UN LOCATAIRE NE PARLE PAS A L'HYPERVISEUR — ET CE N'EST PLUS DECLARE ICI (2026-10-05).
|
||||||
# l'autre côté de la frontière. C'est donc un pair `externe`, et ce flux est le seul
|
#
|
||||||
# par lequel un écosystème peut en engendrer un autre.
|
# Ce fichier portait une sortie 8006 vers `externe` : « API de l'hyperviseur : créer et
|
||||||
- sens: egress
|
# cloner les VM d'un écosystème descendant ». Mesure du 2026-10-05, depuis trois machines de
|
||||||
port: 8006
|
# deux locataires : AUCUN paquet ne passait. La frontiere rendait cette sortie vers « tout
|
||||||
protocole: tcp
|
# sauf les plages privees », et les hyperviseurs sont en plages privees ; elle refusait donc
|
||||||
pair: externe
|
# (journal : `block`, refus audible du lien de transit). La regle etait morte, et sa lecture
|
||||||
chiffrement: tls-requis
|
# mentait : elle disait qu'un locataire pouvait creer des VM.
|
||||||
raison: "API de l'hyperviseur : créer et cloner les VM d'un écosystème descendant."
|
#
|
||||||
|
# Materialiser est un pouvoir du SITE (`serveur_ops_site`, sa voute porte l'API Proxmox).
|
||||||
|
# Une emancipation qui en aurait besoin se declarera le jour ou elle existera.
|
||||||
# LE GENOME SE CLONE A LA FORGE DU SITE — ET CE N'EST PLUS DECLARE ICI (2026-10-05).
|
# LE GENOME SE CLONE A LA FORGE DU SITE — ET CE N'EST PLUS DECLARE ICI (2026-10-05).
|
||||||
#
|
#
|
||||||
# Ce fichier portait une sortie 443 vers `voisins_site`, ecrite quand la forge du genome
|
# Ce fichier portait une sortie 443 vers `voisins_site`, ecrite quand la forge du genome
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import contextlib
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -423,12 +424,28 @@ class Locataire(Ecosysteme):
|
||||||
# LES FLUX DE CHAQUE MACHINE, tels que le locataire les a deja resolus pour ses propres
|
# LES FLUX DE CHAQUE MACHINE, tels que le locataire les a deja resolus pour ses propres
|
||||||
# pare-feux (`make flux` -> `flux-genere/<hote>.connectivite.json`) : chaque entree
|
# pare-feux (`make flux` -> `flux-genere/<hote>.connectivite.json`) : chaque entree
|
||||||
# admise (protocole, port, sources), plus ses reseaux d'administration.
|
# admise (protocole, port, sources), plus ses reseaux d'administration.
|
||||||
|
#
|
||||||
|
# UNE ENTREE OUVERTE A TOUS L'EST-ELLE AUSSI AU POSTE ? Le flux le dit (`poste`, vrai par
|
||||||
|
# defaut ; faux pour le 25 de Postfix, un flux de serveur a serveur). La frontiere en
|
||||||
|
# tire ses regles d'administration ; la face le lit dans le meme catalogue du moteur.
|
||||||
|
from devis_opnsense import _ports as ports_du_flux
|
||||||
|
from resoudre_flux import _pairs, charger_flux
|
||||||
|
catalogue = charger_flux()
|
||||||
|
|
||||||
|
def ouvert_au_poste(e: dict) -> bool:
|
||||||
|
semblables = [fl for fl in catalogue.get(str(e.get("role")), [])
|
||||||
|
if fl.get("sens") == "ingress" and "externe" in _pairs(fl)
|
||||||
|
and str(fl.get("protocole", "tcp")).lower() == str(e.get("proto"))
|
||||||
|
and str(e.get("port")) in ports_du_flux(fl)]
|
||||||
|
return all(fl.get("poste", True) for fl in semblables)
|
||||||
flux = {}
|
flux = {}
|
||||||
for h in sorted(ips):
|
for h in sorted(ips):
|
||||||
c = self.depot / "flux-genere" / f"{h}.connectivite.json"
|
c = self.depot / "flux-genere" / f"{h}.connectivite.json"
|
||||||
if c.is_file():
|
if c.is_file():
|
||||||
d = json.loads(c.read_text(encoding="utf-8"))
|
d = json.loads(c.read_text(encoding="utf-8"))
|
||||||
flux[h] = {"entrees": d.get("entrees") or [], "administration": d.get("admin_ssh") or []}
|
entrees = [dict(e, poste=ouvert_au_poste(e)) if "0.0.0.0/0" in (e.get("sources") or [])
|
||||||
|
else dict(e) for e in d.get("entrees") or []]
|
||||||
|
flux[h] = {"entrees": entrees, "administration": d.get("admin_ssh") or []}
|
||||||
sources = {str(Path(*f.relative_to(self.depot).parts)): _empreinte(f) for f in (
|
sources = {str(Path(*f.relative_to(self.depot).parts)): _empreinte(f) for f in (
|
||||||
self.plan / "nomenclature.yml", self.inventaire(), self.plan / "acces.yml",
|
self.plan / "nomenclature.yml", self.inventaire(), self.plan / "acces.yml",
|
||||||
self.plan / "domaines.yml", self.plan / "applications.yml",
|
self.plan / "domaines.yml", self.plan / "applications.yml",
|
||||||
|
|
@ -817,6 +834,66 @@ def verifier_entrees_publiques(site: "Site", locataire: "Locataire", face: dict
|
||||||
return ecarts
|
return ecarts
|
||||||
|
|
||||||
|
|
||||||
|
def verifier_administration(site: "Site", locataire: "Locataire", face: dict | None = None) -> list[str]:
|
||||||
|
"""La frontiere, troisieme temps (premiere moitie) : ce que l'ADMINISTRATION atteint.
|
||||||
|
|
||||||
|
Par la gestion et par le VPN du site, chaque classe d'administration doit atteindre, machine
|
||||||
|
par machine, exactement ce que la face reseau lui ouvre : le SSH, les entrees qui nomment
|
||||||
|
l'administration parmi leurs sources, et les entrees ouvertes a tous que leur flux ouvre
|
||||||
|
aussi au poste. Le tunnel du locataire, lui, atteint toute sa flotte sur les ports que la
|
||||||
|
POLITIQUE DU SITE lui accorde (22 et 443, ecrits dans le devis) : on verifie sa presence et
|
||||||
|
sa destination, pas ses ports, qui ne sont pas une donnee du locataire.
|
||||||
|
"""
|
||||||
|
import ipaddress
|
||||||
|
f = face if face is not None else locataire.face_reseau()
|
||||||
|
etq, devis = _devis_frontiere(site, locataire)
|
||||||
|
if not etq:
|
||||||
|
return ["la frontiere ne decouvre pas ce locataire"]
|
||||||
|
alias = devis.get("alias") or {}
|
||||||
|
ips = sorted({m["ip"] for m in f["machines"].values()})
|
||||||
|
|
||||||
|
def adresses(nom: str) -> set[str]:
|
||||||
|
a = alias.get(nom) or {}
|
||||||
|
if a.get("type") == "network":
|
||||||
|
reseaux = [ipaddress.ip_network(str(c), strict=False) for c in a.get("contenu") or []]
|
||||||
|
return {ip for ip in ips if any(ipaddress.ip_address(ip) in r for r in reseaux)}
|
||||||
|
return {str(c) for c in a.get("contenu") or []}
|
||||||
|
|
||||||
|
# Ce que la face ouvre a l'administration.
|
||||||
|
intrant = set(f["administration"]["intrant"])
|
||||||
|
attendu: set[tuple] = set()
|
||||||
|
for h, fl in (f.get("flux") or {}).items():
|
||||||
|
ip = f["machines"][h]["ip"]
|
||||||
|
for e in fl.get("entrees") or []:
|
||||||
|
sources = set(e.get("sources") or [])
|
||||||
|
if (sources & intrant) or ("0.0.0.0/0" in sources and e.get("poste", True)):
|
||||||
|
attendu.add((ip, str(e.get("proto")), _ICMP.get(str(e.get("port")), str(e.get("port")))))
|
||||||
|
if fl.get("administration"):
|
||||||
|
attendu.add((ip, "tcp", "22"))
|
||||||
|
|
||||||
|
ecarts: list[str] = []
|
||||||
|
classes = sorted(k for k in alias if re.fullmatch(rf"SETOPS_ADMIN_{etq}_(?!TUNNEL)\w+", k))
|
||||||
|
for classe in classes:
|
||||||
|
vu = {(ip, str(r.get("protocole")), _ICMP.get(str(p), str(p)))
|
||||||
|
for r in devis.get("regles") or []
|
||||||
|
if r.get("tenant") == locataire.nom and r.get("sens") == "in" and r.get("source") == classe
|
||||||
|
for ip in adresses(str(r.get("destination"))) for p in r.get("ports") or []}
|
||||||
|
for x in sorted(attendu - vu):
|
||||||
|
ecarts.append(f"{classe} : la face ouvre {x[0]} {x[1]}/{x[2]} a l'administration, la frontiere non")
|
||||||
|
for x in sorted(vu - attendu):
|
||||||
|
ecarts.append(f"{classe} : la frontiere ouvre {x[0]} {x[1]}/{x[2]}, la face ne l'ouvre pas a l'administration")
|
||||||
|
|
||||||
|
tunnel = [r for r in devis.get("regles") or []
|
||||||
|
if r.get("tenant") == locataire.nom and r.get("source") == f"SETOPS_ADMIN_{etq}_TUNNEL"]
|
||||||
|
if bool(tunnel) != bool(f["administration"]["tunnel"]):
|
||||||
|
ecarts.append(f"tunnel : {len(tunnel)} regle(s) a la frontiere, la face "
|
||||||
|
f"{'en a un' if f['administration']['tunnel'] else 'n en a aucun'}")
|
||||||
|
for r in tunnel:
|
||||||
|
if adresses(str(r.get("destination"))) != set(ips):
|
||||||
|
ecarts.append(f"tunnel : la regle {r.get('ports')} ne vise pas toute la flotte du locataire")
|
||||||
|
return ecarts
|
||||||
|
|
||||||
|
|
||||||
def verifier_frontiere(site: "Site", locataire: "Locataire", face: dict | None = None,
|
def verifier_frontiere(site: "Site", locataire: "Locataire", face: dict | None = None,
|
||||||
fiche: dict | None = None) -> list[str]:
|
fiche: dict | None = None) -> list[str]:
|
||||||
"""La frontiere, premier temps : les IDENTITES par lesquelles elle designe ce locataire.
|
"""La frontiere, premier temps : les IDENTITES par lesquelles elle designe ce locataire.
|
||||||
|
|
@ -974,7 +1051,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
for quoi, e in (("faits", verifier_face(site, loc, face)),
|
for quoi, e in (("faits", verifier_face(site, loc, face)),
|
||||||
("flux ", verifier_flux(site, loc, face)),
|
("flux ", verifier_flux(site, loc, face)),
|
||||||
("frontiere (identites)", verifier_frontiere(site, loc, face)),
|
("frontiere (identites)", verifier_frontiere(site, loc, face)),
|
||||||
("frontiere (entrees publiques)", verifier_entrees_publiques(site, loc, face))):
|
("frontiere (entrees publiques)", verifier_entrees_publiques(site, loc, face)),
|
||||||
|
("frontiere (administration)", verifier_administration(site, loc, face))):
|
||||||
total += len(e)
|
total += len(e)
|
||||||
print(f"{'OK ' if not e else 'ECART'} {loc.nom} -> {site.nom} ({quoi})"
|
print(f"{'OK ' if not e else 'ECART'} {loc.nom} -> {site.nom} ({quoi})"
|
||||||
+ "".join(f"\n - {x}" for x in e))
|
+ "".join(f"\n - {x}" for x in e))
|
||||||
|
|
|
||||||
|
|
@ -4786,6 +4786,28 @@ def preuve_frontiere_entrees_publiques() -> tuple[bool, str]:
|
||||||
f"ce que leur face reseau ouvre a tous.")
|
f"ce que leur face reseau ouvre a tous.")
|
||||||
|
|
||||||
|
|
||||||
|
def preuve_frontiere_administration() -> tuple[bool, str]:
|
||||||
|
"""Par la gestion, le VPN et le tunnel, l'administration atteint-elle chez chaque locataire
|
||||||
|
exactement ce que sa face reseau lui ouvre ?
|
||||||
|
|
||||||
|
POURQUOI (2026-10-05, `docs/conception-contextes.md` §2.4). Troisieme temps de la frontiere,
|
||||||
|
premiere moitie. Le SSH, les entrees qui nomment l'administration parmi leurs sources, et
|
||||||
|
les entrees publiques que leur flux ouvre au poste (`poste`, faux pour le 25 de Postfix).
|
||||||
|
Le tunnel atteint toute la flotte, sur les ports que la politique du site lui accorde.
|
||||||
|
"""
|
||||||
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
import contexte as _ctx
|
||||||
|
couples = _ctx.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
return True, "Aucun site frere : aucune administration a confronter."
|
||||||
|
ecarts = [f"{l.nom} : {e}" for s, l in couples for e in _ctx.verifier_administration(s, l)]
|
||||||
|
if ecarts:
|
||||||
|
return False, (f"{len(ecarts)} ecart(s) entre la frontiere et ce que la face ouvre a "
|
||||||
|
f"l'administration : " + " | ".join(ecarts[:4]))
|
||||||
|
return True, (f"{len(couples)} locataire(s) : par la gestion, le VPN et le tunnel, "
|
||||||
|
f"l'administration atteint exactement ce que leur face reseau lui ouvre.")
|
||||||
|
|
||||||
|
|
||||||
PREUVES: list[dict] = [
|
PREUVES: list[dict] = [
|
||||||
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
|
||||||
"cmds": [["ansible-lint", "-q"]]},
|
"cmds": [["ansible-lint", "-q"]]},
|
||||||
|
|
@ -4982,6 +5004,8 @@ PREUVES: list[dict] = [
|
||||||
"refs": [], "func": preuve_frontiere_identites},
|
"refs": [], "func": preuve_frontiere_identites},
|
||||||
{"id": "P88", "titre": "Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui",
|
{"id": "P88", "titre": "Ce qui entre depuis l'Internet chez un locataire, la frontiere le tient de lui",
|
||||||
"refs": [], "func": preuve_frontiere_entrees_publiques},
|
"refs": [], "func": preuve_frontiere_entrees_publiques},
|
||||||
|
{"id": "P89", "titre": "Ce que l'administration atteint chez un locataire, la frontiere le tient de lui",
|
||||||
|
"refs": [], "func": preuve_frontiere_administration},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ reseau de chaque locataire dit ce que les consommateurs du site en tirent, et un
|
||||||
Proxmox admettent les memes entrees, et des flux alteres (quatre facons) sont vus en ecart ;
|
Proxmox admettent les memes entrees, et des flux alteres (quatre facons) sont vus en ecart ;
|
||||||
la frontiere designe chaque locataire par les memes identites, et une identite alteree (cinq
|
la frontiere designe chaque locataire par les memes identites, et une identite alteree (cinq
|
||||||
facons) se voit ; ce qui entre depuis l'Internet concorde, et une entree alteree (quatre facons)
|
facons) se voit ; ce qui entre depuis l'Internet concorde, et une entree alteree (quatre facons)
|
||||||
se voit.
|
se voit ; ce que l'administration atteint concorde, et une administration alteree (quatre
|
||||||
|
facons) se voit.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
@ -351,6 +352,49 @@ def entrees_publiques() -> None:
|
||||||
f"une entree publique alteree ({nom}) est vue a la frontiere")
|
f"une entree publique alteree ({nom}) est vue a la frontiere")
|
||||||
|
|
||||||
|
|
||||||
|
def administration() -> None:
|
||||||
|
"""Ce que l'administration atteint par la gestion, le VPN et le tunnel : la frontiere dit ce
|
||||||
|
que la face ouvre — et chaque alteration se voit."""
|
||||||
|
import copy
|
||||||
|
couples = C.couples_hebergeur()
|
||||||
|
if not couples:
|
||||||
|
print("SAUTE aucun site frere : aucune administration a confronter")
|
||||||
|
return
|
||||||
|
for site, loc in couples:
|
||||||
|
e = C.verifier_administration(site, loc)
|
||||||
|
verifier(not e, f"administration {loc.nom} : gestion, VPN et tunnel concordent"
|
||||||
|
+ (f" ({e[0]})" if e else ""))
|
||||||
|
site, loc = couples[0]
|
||||||
|
face = loc.face_reseau()
|
||||||
|
ferme = [(h, e) for h, f in face["flux"].items() for e in f["entrees"] if e.get("poste") is False]
|
||||||
|
verifier(bool(ferme), f"la face porte les entrees publiques fermees au poste ({[(h, e['port']) for h, e in ferme]})")
|
||||||
|
hote_adm = next(h for h, f in face["flux"].items()
|
||||||
|
if any(set(e.get("sources") or []) & set(face["administration"]["intrant"])
|
||||||
|
and "0.0.0.0/0" not in (e.get("sources") or []) for e in f["entrees"]))
|
||||||
|
|
||||||
|
def ouvrir_au_poste(f):
|
||||||
|
h, e0 = ferme[0]
|
||||||
|
for e in f["flux"][h]["entrees"]:
|
||||||
|
if e.get("port") == e0.get("port"):
|
||||||
|
e["poste"] = True
|
||||||
|
|
||||||
|
def retirer_sources_admin(f):
|
||||||
|
for e in f["flux"][hote_adm]["entrees"]:
|
||||||
|
e["sources"] = [s for s in e.get("sources") or [] if s not in face["administration"]["intrant"]]
|
||||||
|
|
||||||
|
alterations = {
|
||||||
|
"une entree fermee au poste, ouverte": ouvrir_au_poste,
|
||||||
|
f"sources d'administration retirees sur {hote_adm}": retirer_sources_admin,
|
||||||
|
"SSH d'administration retire": lambda f: f["flux"][hote_adm].__setitem__("administration", []),
|
||||||
|
"tunnel retire": lambda f: f["administration"].__setitem__("tunnel", []),
|
||||||
|
}
|
||||||
|
for nom, alterer in alterations.items():
|
||||||
|
f = copy.deepcopy(face)
|
||||||
|
alterer(f)
|
||||||
|
verifier(bool(C.verifier_administration(site, loc, f)),
|
||||||
|
f"une administration alteree ({nom}) est vue a la frontiere")
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
d = Path(tmp)
|
d = Path(tmp)
|
||||||
|
|
@ -364,6 +408,7 @@ def main() -> int:
|
||||||
flux()
|
flux()
|
||||||
frontiere()
|
frontiere()
|
||||||
entrees_publiques()
|
entrees_publiques()
|
||||||
|
administration()
|
||||||
if ECHECS:
|
if ECHECS:
|
||||||
print(f"\n{len(ECHECS)} echec(s).")
|
print(f"\n{len(ECHECS)} echec(s).")
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue