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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-01 12:01:46 -04:00
parent 0318412a9e
commit 25fd4fc6f1
3 changed files with 33 additions and 10 deletions

View file

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

View file

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

View file

@ -107,15 +107,17 @@ def charger_flux() -> dict[str, list[dict]]:
# simplement sans objet ») ; le registre ne savait pas le dire.
#
# seulement_si: {variable: <nom>, egal: <valeur>}
# seulement_si: {variable: <nom>, 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: <nom>, egal: <valeur>}}")
or ("egal" not in cond) == ("non_vide" not in cond)):
erreurs.append(f"{ref} : seulement_si attend {{variable: <nom>, egal: <valeur>}} "
f"ou {{variable: <nom>, 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}")