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:
Daniel Allaire 2026-10-05 03:14:14 -04:00
parent 56033afd00
commit ce84d0e1ea
10 changed files with 230 additions and 18 deletions

View file

@ -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.**

View file

@ -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,

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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ù

View file

@ -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

View file

@ -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))

View file

@ -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},
] ]

View file

@ -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