flux : seulement_si — un flux peut dependre d'un reglage de l'ecosysteme
La vigie (8080) et la console (8090) n'ecoutent sur le reseau qu'en mode locale ; en SSO, nginx est lie a 127.0.0.1 et la regle ne menait a rien. Evaluee par ecosysteme dans les trois generateurs (nftables, Proxmox, frontiere). Locataires : 8 regles Proxmox et 4 nftables en moins. Site : inchange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
209730b62a
commit
34bfd43e06
6 changed files with 85 additions and 4 deletions
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (82 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ❌ NON CONFORME (81 OK · 1 echec · 1 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 44 groupe(s), 116 regle(s). |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 108 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. |
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 101 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (139 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 317 regles `pass`), tous non consignes et tous motives. |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 309 regles `pass`), tous non consignes et tous motives. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
|
|
@ -91,7 +91,8 @@
|
|||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 185 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 5 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, SITE-Chezlepro) : pattes, edges, certificats, rechargements, jumeaux d' |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ❌ ECHEC | 1 repli(s) silencieux — une derivation vide rend un succes :
|
||||
- flux : OPS-Chezlepro-lab — perimees par rapport au plan : ops-01.nft |
|
||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des voutes portees, source d'inventaire `instance/inventories/principal/hosts.yml`, et 15 route(s) POST exigent toutes un pouvoir. |
|
||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
||||
|
|
|
|||
|
|
@ -13,10 +13,15 @@ flux:
|
|||
# Les deux consoles de l'observabilite avaient la meme contrainte et une seule des deux
|
||||
# l'avait ecrite. En mode `locale`, nginx authentifie lui-meme : ce qui entre par le
|
||||
# plan d'administration frappe un vestibule, pas l'application.
|
||||
#
|
||||
# SEULEMENT SI NGINX ECOUTE SUR LE RESEAU (2026-09-30). En SSO, il est lie a 127.0.0.1
|
||||
# derriere la passerelle (`serveur_icingaweb2_nginx_bind: "127.0.0.1:"`) : ouvrir ce port
|
||||
# depuis l'edge ne menait a rien — la sonde `connectivite` le disait chaque minute.
|
||||
- sens: ingress
|
||||
port: 8080
|
||||
protocole: tcp
|
||||
pair: [edge, admin]
|
||||
seulement_si: {variable: serveur_icingaweb2_nginx_bind, egal: ""}
|
||||
chiffrement: clair
|
||||
raison: >-
|
||||
Interface web servie via l'edge (TLS terminé à l'edge ; SSO possible via
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@ flux:
|
|||
#
|
||||
# LA REGLE N'EST EMISE QUE SI LE PLAN L'EXPOSE : un runner qui ne publie pas sa console
|
||||
# n'a pas ce port ouvert, et `serveur_ops_gui_actif` reste faux.
|
||||
#
|
||||
# SEULEMENT EN MODE `locale` (2026-09-30) : c'est la que nginx EST le vestibule et ecoute
|
||||
# sur le reseau. En `oidc`, il est lie a 127.0.0.1 (voir `serveur_ops_gui_nginx_bind`) et
|
||||
# la regle etait « sans objet » — desormais elle n'est plus emise du tout.
|
||||
- sens: ingress
|
||||
port: 8090
|
||||
protocole: tcp
|
||||
pair: [edge, admin]
|
||||
seulement_si: {variable: serveur_ops_gui_auth, egal: locale}
|
||||
chiffrement: clair
|
||||
raison: >-
|
||||
Console d'exploitation servie par l'edge (TLS terminé à l'edge), et joignable
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ from inventory_rules import sous_reseau_de, supernet_de # noqa: E402
|
|||
# lecture, sinon le devis et les nftables divergeraient en silence.
|
||||
from resoudre_flux import ( # noqa: E402
|
||||
charger_flux,
|
||||
flux_applicable,
|
||||
_enfants,
|
||||
_hotes_du_groupe,
|
||||
_inventaire,
|
||||
|
|
@ -563,6 +564,9 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
tenants_sans_inventaire.append(nom)
|
||||
continue
|
||||
for role, fl in retenus:
|
||||
# Un flux conditionnel (`seulement_si`) ne vaut que la ou son reglage le dit.
|
||||
if not flux_applicable(fl, role, inv.parent.parent.parent):
|
||||
continue
|
||||
# AVANT L'ALIAS (2026-09-17). Le saut plus bas arrivait APRES la creation de
|
||||
# l'alias du role : un flux vers la frontiere declare par `serveur_prometheus`
|
||||
# posait `SETOPS_<tenant>_SRV_PROMETHEUS` chez chaque locataire, qu'aucune regle
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from resoudre_flux import ( # noqa: E402
|
|||
GROUPE_EDGE,
|
||||
charger_flux,
|
||||
est_groupe_operationnel,
|
||||
flux_applicable,
|
||||
_enfants,
|
||||
_hotes_du_groupe,
|
||||
_ip_par_hote,
|
||||
|
|
@ -204,6 +205,8 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
for fl in flux_par_role.get(groupe, []):
|
||||
if fl.get("sens") != "ingress":
|
||||
continue
|
||||
if not flux_applicable(fl, groupe, inv.parent.parent.parent):
|
||||
continue
|
||||
pairs = _pairs(fl)
|
||||
# `externe` est saute PAIR PAR PAIR, jamais le flux entier : le SSH du
|
||||
# socle est declare `[flotte, externe]`, et sauter tout le flux perdait
|
||||
|
|
|
|||
|
|
@ -100,6 +100,60 @@ def charger_flux() -> dict[str, list[dict]]:
|
|||
return resultat
|
||||
|
||||
|
||||
# UN FLUX PEUT DEPENDRE D'UN REGLAGE (2026-09-30). La console d'exploitation et Icinga Web
|
||||
# n'ecoutent sur le reseau qu'en mode `locale` (le SITE, sans annuaire) ; en SSO, nginx est
|
||||
# lie a 127.0.0.1 derriere la passerelle, et le port ouvert depuis l'edge ne mene a rien —
|
||||
# la sonde `connectivite` le montrait chaque minute. Le code le savait (« la regle devient
|
||||
# simplement sans objet ») ; le registre ne savait pas le dire.
|
||||
#
|
||||
# seulement_si: {variable: <nom>, egal: <valeur>}
|
||||
#
|
||||
# La valeur est lue la ou ce role la lit : variables de l'hote (inventaire du site), puis
|
||||
# `group_vars` de l'ecosysteme, puis defauts du role. INDETERMINABLE (une expression Jinja)
|
||||
# -> la regle est emise, comme avant, et on le dit : ouvrir sans savoir est le statu quo,
|
||||
# fermer sans savoir pourrait couper une console.
|
||||
def _valeur_variable(variable: str, groupe: str, dossier: Path | None,
|
||||
hostvars: dict | None) -> tuple[bool, object]:
|
||||
if hostvars and variable in hostvars:
|
||||
return True, hostvars[variable]
|
||||
if dossier is not None:
|
||||
gv = dossier / "inventories" / "principal" / "group_vars"
|
||||
candidats = [gv / f"{groupe}.yml", *sorted((gv / groupe).glob("*.yml"))]
|
||||
candidats += [f for f in sorted((gv / "all").glob("*.yml")) if "vault" not in f.name]
|
||||
for f in candidats:
|
||||
if not f.is_file():
|
||||
continue
|
||||
try:
|
||||
data = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
||||
except yaml.YAMLError:
|
||||
continue
|
||||
if isinstance(data, dict) and variable in data:
|
||||
return True, data[variable]
|
||||
defauts = ROLES / groupe / "defaults" / "main.yml"
|
||||
if defauts.is_file():
|
||||
data = yaml.safe_load(defauts.read_text(encoding="utf-8")) or {}
|
||||
if variable in data:
|
||||
v = data[variable]
|
||||
if isinstance(v, str) and "{{" in v:
|
||||
return False, None
|
||||
return True, v
|
||||
return False, None
|
||||
|
||||
|
||||
def flux_applicable(fl: dict, groupe: str, dossier: Path | None,
|
||||
hostvars: dict | None = None) -> bool:
|
||||
"""Le flux s'applique-t-il a CET ecosysteme (et a cet hote) ? Voir `seulement_si`."""
|
||||
cond = fl.get("seulement_si")
|
||||
if not cond:
|
||||
return True
|
||||
connu, valeur = _valeur_variable(str(cond.get("variable")), groupe, dossier, hostvars)
|
||||
if not connu:
|
||||
print(f"note : {groupe} — `{cond.get('variable')}` indeterminable ici : la regle "
|
||||
f"du port {fl.get('port')} est emise (statu quo).", file=sys.stderr)
|
||||
return True
|
||||
return str(valeur if valeur is not None else "") == str(cond.get("egal", ""))
|
||||
|
||||
|
||||
def _pairs(flux: dict) -> list:
|
||||
pair = flux.get("pair")
|
||||
return pair if isinstance(pair, list) else [pair]
|
||||
|
|
@ -125,6 +179,10 @@ def valider(flux_par_role: dict[str, list[dict]]) -> tuple[int, int]:
|
|||
erreurs.append(f"{ref} : protocole invalide {fl.get('protocole')!r}")
|
||||
if fl.get("chiffrement") not in CHIFFREMENT:
|
||||
erreurs.append(f"{ref} : chiffrement invalide {fl.get('chiffrement')!r}")
|
||||
cond = fl.get("seulement_si")
|
||||
if cond is not None and (not isinstance(cond, dict) or not cond.get("variable")
|
||||
or "egal" not in cond):
|
||||
erreurs.append(f"{ref} : seulement_si attend {{variable: <nom>, egal: <valeur>}}")
|
||||
for p in _pairs(fl):
|
||||
if p not in MOTS_PAIR and p not in roles_existants and not str(p).startswith(("serveur_", "client_")):
|
||||
erreurs.append(f"{ref} : pair inconnu {p!r}")
|
||||
|
|
@ -704,8 +762,10 @@ def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) -
|
|||
# chez le site, pas dans l'instance montee au moment ou on l'a genere.
|
||||
import underlay as underlay_mod
|
||||
sortie_dir = (underlay_mod.chemin().resolve().parent / "flux-genere")
|
||||
dossier_ecosysteme = None # le site porte ses reglages par hote (plan)
|
||||
else:
|
||||
sortie_dir = _inventaire().parent.parent.parent / "flux-genere"
|
||||
dossier_ecosysteme = _inventaire().parent.parent.parent
|
||||
sortie_dir.mkdir(parents=True, exist_ok=True)
|
||||
ecrits: list[Path] = []
|
||||
# CE QUE CHAQUE VM DOIT POUVOIR JOINDRE, ET DE QUI ELLE ACCEPTE (2026-09-30). La sonde
|
||||
|
|
@ -723,6 +783,9 @@ def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) -
|
|||
for fl in flux_par_role.get(g, []):
|
||||
if fl.get("sens") != "ingress":
|
||||
continue
|
||||
if not flux_applicable(fl, g, dossier_ecosysteme,
|
||||
(_enfants(data).get(g, {}).get("hosts") or {}).get(hote)):
|
||||
continue
|
||||
ports = fl["port"] if isinstance(fl["port"], list) else [fl["port"]]
|
||||
# `derive` se resout par le plan ; sans lui on n'emet RIEN plutot qu'une
|
||||
# regle que `nft` refusera — et l'on DIT ce qu'on ne pose pas. Un flux
|
||||
|
|
|
|||
Loading…
Reference in a new issue