From 25fd4fc6f1c2050a8cef6f434e7bc19f807c4289 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 1 Oct 2026 12:01:46 -0400 Subject: [PATCH] flux : seulement_si non_vide ; forge 3000 et PowerDNS 5300 conditionnels La forge du site sert son propre TLS (443) : le flux 3000 derriere un edge ne vaut que si serveur_forgejo_tls est faux. L'AXFR 5300 ne vaut que la ou l'instance publique existe (dns_public_site non vide) : pas au site. Absente partout = vide ; une expression Jinja garde le statu quo. Co-Authored-By: Claude Opus 5.5 --- roles/serveur_forgejo/meta/flux.yml | 6 ++++++ roles/serveur_powerdns/meta/flux.yml | 7 +++++++ scripts/resoudre_flux.py | 30 ++++++++++++++++++---------- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/roles/serveur_forgejo/meta/flux.yml b/roles/serveur_forgejo/meta/flux.yml index d2ca39f..5cbc893 100644 --- a/roles/serveur_forgejo/meta/flux.yml +++ b/roles/serveur_forgejo/meta/flux.yml @@ -19,11 +19,17 @@ flux: # dépend ». Le vocabulaire des flux n'a pas de mot pour l'indécision, et c'est tant # mieux : un flux se lit pour savoir ce qui circule et comment. Une valeur ambiguë # rendrait l'audit impossible sans aller lire le déploiement. + # + # ET LA SITUATION SE LIT DANS LE REGLAGE (2026-09-30) : la premiere seulement si la forge ne + # sert pas son propre TLS. Au site (`serveur_forgejo_tls: true`), elle ecoute 443 et rien + # sur 3000 ; la regle restait posee parce que le site a un edge — et la sonde + # `connectivite` la montrait coupee chaque minute. - sens: ingress port: 3000 protocole: tcp pair: edge chiffrement: clair + seulement_si: {variable: serveur_forgejo_tls, egal: false} raison: >- Interface web + Git HTTP derrière un edge : le nginx termine le TLS et parle en clair à la forge. C'est le cas de tout tenant. diff --git a/roles/serveur_powerdns/meta/flux.yml b/roles/serveur_powerdns/meta/flux.yml index 5625f93..90a7c52 100644 --- a/roles/serveur_powerdns/meta/flux.yml +++ b/roles/serveur_powerdns/meta/flux.yml @@ -47,15 +47,22 @@ flux: # Sur l'instance `pdns@public` seulement, qui ne sert que les zones `primaire-cache`. # UDP pour le SOA que le secondaire compare avant de tirer, TCP pour le transfert. La # regle limite qui peut FRAPPER ; TSIG decide qui peut LIRE. + # + # SEULEMENT LA OU LE SITE A UN SERVEUR PUBLIC A SERVIR (2026-09-30) — la ou l'instance + # `pdns@public` existe (`zones-publiques.yml` ne se joue que sous la meme condition). Le + # PowerDNS du SITE n'en a pas : il ecoute sur la boucle locale, et la regle restait posee, + # coupee — la sonde `connectivite` le montrait chaque minute. - sens: ingress port: 5300 protocole: tcp pair: dns_public_site + seulement_si: {variable: dns_public_site, non_vide: true} chiffrement: clair raison: "AXFR signe TSIG par le serveur DNS public du site, vers l'instance publique uniquement." - sens: ingress port: 5300 protocole: udp pair: dns_public_site + seulement_si: {variable: dns_public_site, non_vide: true} chiffrement: clair raison: "Interrogation du SOA par le secondaire du site avant chaque transfert." diff --git a/scripts/resoudre_flux.py b/scripts/resoudre_flux.py index a1d5ad1..7845fd8 100644 --- a/scripts/resoudre_flux.py +++ b/scripts/resoudre_flux.py @@ -107,15 +107,17 @@ def charger_flux() -> dict[str, list[dict]]: # simplement sans objet ») ; le registre ne savait pas le dire. # # seulement_si: {variable: , egal: } +# seulement_si: {variable: , non_vide: true} (absente partout = vide) # # 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]: + hostvars: dict | None) -> tuple[str, object]: + """("connu", valeur) | ("absent", None) | ("indetermine", None) — une expression Jinja.""" if hostvars and variable in hostvars: - return True, hostvars[variable] + return "connu", hostvars[variable] if dossier is not None: gv = dossier / "inventories" / "principal" / "group_vars" candidats = [gv / f"{groupe}.yml", *sorted((gv / groupe).glob("*.yml"))] @@ -128,16 +130,16 @@ def _valeur_variable(variable: str, groupe: str, dossier: Path | None, except yaml.YAMLError: continue if isinstance(data, dict) and variable in data: - return True, data[variable] + return "connu", 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 + return "indetermine", None + return "connu", v + return "absent", None def flux_applicable(fl: dict, groupe: str, dossier: Path | None, @@ -146,8 +148,15 @@ def flux_applicable(fl: dict, groupe: str, dossier: Path | None, cond = fl.get("seulement_si") if not cond: return True - connu, valeur = _valeur_variable(str(cond.get("variable")), groupe, dossier, hostvars) - if not connu: + etat, valeur = _valeur_variable(str(cond.get("variable")), groupe, dossier, hostvars) + if "non_vide" in cond: + # Absente partout, elle est vide : c'est ce que le role lit (`| default('')`). + if etat == "indetermine": + 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 bool(valeur) == bool(cond.get("non_vide")) + if etat != "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 @@ -181,8 +190,9 @@ def valider(flux_par_role: dict[str, list[dict]]) -> tuple[int, int]: 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: , egal: }}") + or ("egal" not in cond) == ("non_vide" not in cond)): + erreurs.append(f"{ref} : seulement_si attend {{variable: , egal: }} " + f"ou {{variable: , non_vide: true}}") 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}")