Metriques et journaux (CHANGELOG 142). Le site pose les quatre interrupteurs TLS que chaque locataire pose dans ses group_vars : node_exporter et Loki servaient en HTTP sur les neuf VM, le certificat etait la, inutilise. test_parite_tls_site.py compare desormais le site aux locataires. Les hyperviseurs poussent leurs journaux en HTTPS verifie, avec la racine du site comme simple fichier. L'exportateur PostgreSQL sert en HTTPS partout, sur l'interrupteur de PostgreSQL lui-meme, que Prometheus lit pour choisir son schema. La frontiere (CHANGELOG 143). Une autorite subordonnee signee par la racine du site, bornee (pathlen 0, noms sous le domaine et adresses du site, contrainte critique), cle nee sur la boite (scripts/frontiere_pki.py). Syslog vers Loki en TLS mutuel (seule la frontiere peut ecrire), exportateur en HTTPS, interface web sur le certificat de services ; les clients d'API verifient la frontiere contre la racine du site. La lecture du magasin passe par config.xml : l'API de recherche renvoie les cles privees. Deploye et mesure au site, chez les deux locataires et sur la frontiere ; 95 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Ce qu'un locataire chiffre par ses group_vars, le site le chiffre aussi.
|
|
|
|
POURQUOI (2026-10-12). Trois fois le meme ecart : un interrupteur TLS que chaque locataire
|
|
pose dans ses `group_vars`, et que l'inventaire du site — dynamique, sans group_vars — ne
|
|
posait nulle part. PostgreSQL servait le certificat auto-signe de Debian (2026-09-12),
|
|
l'edge aussi (meme jour), et node_exporter et Loki servaient en HTTP (2026-10-12) alors
|
|
que le registre des flux les dit `tls`. Chaque fois, le certificat etait la, inutilise.
|
|
|
|
Le test lit les `group_vars/<groupe>.yml` des locataires montes a cote du depot, retient
|
|
chaque cle qui contient `tls` et vaut vrai, et exige la meme valeur dans les hostvars des
|
|
machines du site qui portent ce groupe.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
RACINE = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
import site_inventaire as si # noqa: E402
|
|
|
|
# CE QUI RESTE A FAIRE, NOMME — pas une tolerance muette. Chaque entree dit pourquoi le
|
|
# site differe encore, et disparait le jour ou l'ecart se ferme.
|
|
RESTE = {
|
|
("client_metrique", "client_metrique_tls_actif", "hyperviseurs"):
|
|
"node_exporter des hyperviseurs : il leur faudrait un certificat a eux "
|
|
"(pas de client_pki sur la fabric) — collecte encore en HTTP, chantier suivant.",
|
|
}
|
|
|
|
# CE QUE LE SITE OBTIENT AUTREMENT, avec la CONDITION qui le prouve dans ses hostvars.
|
|
# Pas une dispense : si la condition tombe, l'ecart redevient un echec.
|
|
AUTREMENT = {
|
|
("serveur_icinga", "serveur_icinga_db_tls"): (
|
|
lambda v: v.get("serveur_postgresql_tls_force") is True,
|
|
"derive par le role de `serveur_postgresql_tls_force` (resoudre_base), vrai au site"),
|
|
("serveur_postfix", "serveur_postfix_lmtp_tls_verify"): (
|
|
lambda v: v.get("serveur_postfix_mode") == "relais",
|
|
"sans objet : le relais du site ne remet rien par LMTP (`serveur_postfix_mode: relais`)"),
|
|
}
|
|
|
|
|
|
def interrupteurs_des_locataires() -> dict[tuple[str, str], list[str]]:
|
|
"""{(groupe, cle): [locataires]} pour chaque cle *tls* vraie d'un group_vars."""
|
|
vus: dict[tuple[str, str], list[str]] = {}
|
|
for gv in sorted(RACINE.parent.glob("OPS-*/inventories/principal/group_vars/*.yml")):
|
|
locataire = gv.parents[3].name
|
|
if locataire.endswith("-lab"):
|
|
continue
|
|
try:
|
|
donnees = yaml.safe_load(gv.read_text(encoding="utf-8")) or {}
|
|
except yaml.YAMLError:
|
|
continue
|
|
if not isinstance(donnees, dict):
|
|
continue
|
|
for cle, val in donnees.items():
|
|
if "tls" in str(cle) and val is True:
|
|
vus.setdefault((gv.stem, str(cle)), []).append(locataire)
|
|
return vus
|
|
|
|
|
|
def main() -> int:
|
|
attendus = interrupteurs_des_locataires()
|
|
if not attendus:
|
|
print("OK : aucun locataire monte a cote du depot — rien a comparer.")
|
|
return 0
|
|
try:
|
|
inv = si.inventaire()
|
|
except SystemExit as e: # plan du site refuse ou absent
|
|
print(f"OK : aucun site monte ({e}) — rien a comparer.")
|
|
return 0
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"OK : aucun site monte ({type(e).__name__}) — rien a comparer.")
|
|
return 0
|
|
|
|
hv = inv["_meta"]["hostvars"]
|
|
hyperviseurs = set((inv.get("hyperviseurs") or {}).get("hosts") or [])
|
|
echecs, restes = [], []
|
|
for (groupe, cle), qui in sorted(attendus.items()):
|
|
for hote in (inv.get(groupe) or {}).get("hosts") or []:
|
|
if hv.get(hote, {}).get(cle) is True:
|
|
continue
|
|
autre = AUTREMENT.get((groupe, cle))
|
|
if autre and autre[0](hv.get(hote, {})):
|
|
continue
|
|
classe = "hyperviseurs" if hote in hyperviseurs else "vm"
|
|
raison = RESTE.get((groupe, cle, classe))
|
|
if raison:
|
|
restes.append(f"{hote} : {cle} — {raison}")
|
|
else:
|
|
echecs.append(f"{hote} ({groupe}) : `{cle}` vaut {hv.get(hote, {}).get(cle)!r} au site, "
|
|
f"true chez {', '.join(qui)}")
|
|
|
|
for r in sorted(set(restes)):
|
|
print(f"RESTE : {r}")
|
|
for e in echecs:
|
|
print(f"ECHEC : {e}")
|
|
if not echecs:
|
|
print(f"OK : les {len(attendus)} interrupteurs TLS des locataires valent aussi au site.")
|
|
return 1 if echecs else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|