Deux corrections de propriété, l'une dans le plan, l'autre dans les intrants. 1. Intégrations universelles (D-33/D-34, P26) Le plan portait 57 lignes d'intégration écrites à la main, dont 28 disaient oui à quelque chose de vrai pour tous les hôtes. Elles n'existaient que pour être oubliées — et elles l'avaient été : dans Chezlepro, backup-01 et infra-pki-01 n'étaient ni supervisés, ni journalisés, ni certifiés. Le rôle déclare désormais sa politique une fois, dans meta/integration.yml ; le plan ne garde que les vrais choix et refuse la recopie. Les exemptions se dérivent du service rendu (sauf_role), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. Une seule fonction de résolution — integrations_de() — lue par l'inventaire, la voûte et le panneau. Sans le passage par la voûte, les secrets des intégrations universelles auraient cessé d'être exigés et P18 serait passé au vert sur une voûte incomplète. Vérifié : diff vide sur Technolibre (la politique reproduit exactement les 41 lignes retirées) ; sur Chezlepro, exactement les groupes manquants, et pas client_pki sur infra-pki-01. 2. Vue Intégrations : la matrice La fiche montrait les intégrations d'UN serveur ; le trou de Chezlepro n'a pas été trouvé par le panneau mais par le devis de pare-feu. Matrice serveurs x intégrations : colonnes de politique en lecture seule, facultatives cochables sur place, ligne de couverture n/N qui rend le motif visible sans le juger. 3. Propriété des intrants (D-35/D-36, P27) Le cluster Proxmox appartient à l'hébergeur, comme sa fabric et sa frontière. Recopié chez chaque tenant, son inventaire avait déjà divergé : deux listes de stockages contradictoires pour le même matériel. API/nœuds/stockages/ponts vont dans proxmox-hebergeur.yml, à côté d'underlay.yml, dont le chemin se dérive — l'hébergeur reste non déclaré (D-17). Restent au tenant son golden template et ses défauts de placement. Le panneau nomme désormais le propriétaire de chaque section : éditer une section « hébergeur » vaut pour tous ses tenants, et l'écran ne le disait pas. 26 preuves OK, 0 échec. --syntax-check des deux playbooks Proxmox. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 lines
5.9 KiB
Python
163 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Confronte le GABARIT de voute d'une instance aux secrets que son plan exige.
|
|
|
|
Le gabarit `group_vars/all/vault.yml.example` est la seule liste que l'operateur
|
|
voit avant de creer sa voute. S'il est incomplet, le deploiement casse au milieu,
|
|
sur une variable indefinie ; s'il est bavard, l'operateur genere des secrets morts.
|
|
|
|
Les secrets EXIGES sont recenses a trois endroits, sans rien deviner :
|
|
1. `plan/bases-donnees.yml` -> le champ `secret` de chaque base ;
|
|
2. les roles des groupes actifs du plan -> toute reference `vault_*` ;
|
|
3. les `group_vars` de l'instance -> toute reference `vault_*`.
|
|
|
|
La voute REELLE (vault.yml) n'est jamais lue ni dechiffree : on ne compare que
|
|
des NOMS, et seulement ceux du gabarit en clair.
|
|
|
|
Usage :
|
|
python3 scripts/voute.py lister
|
|
python3 scripts/voute.py verifier # rc=2 si le gabarit est incomplet
|
|
python3 scripts/voute.py verifier --strict # rc=2 aussi si des cles sont inutiles
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
from inventory_rules import (charger_applications, charger_bases_donnees, charger_serveurs,
|
|
integrations_universelles)
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
|
ROLES = RACINE / "roles"
|
|
MOTIF_VAULT = re.compile(r"\b(vault_[a-z0-9_]+)")
|
|
|
|
# Secrets d'acces a l'hyperviseur : attendus au gabarit, mais hors motif vault_*.
|
|
SECRETS_HORS_MOTIF = {"proxmox_api_token_id", "proxmox_api_token_secret"}
|
|
|
|
|
|
def _inventaire_dir() -> Path | None:
|
|
for nom in ("principal", "production", "lab"):
|
|
d = INSTANCE / "inventories" / nom
|
|
if d.is_dir():
|
|
return d
|
|
return None
|
|
|
|
|
|
def gabarit() -> Path | None:
|
|
d = _inventaire_dir()
|
|
if not d:
|
|
return None
|
|
p = d / "group_vars" / "all" / "vault.yml.example"
|
|
return p if p.is_file() else None
|
|
|
|
|
|
def _refs_vault(chemin: Path) -> set[str]:
|
|
try:
|
|
return set(MOTIF_VAULT.findall(chemin.read_text(encoding="utf-8", errors="ignore")))
|
|
except OSError:
|
|
return set()
|
|
|
|
|
|
def groupes_du_plan() -> set[str]:
|
|
apps = charger_applications(INSTANCE / "plan/applications.yml")
|
|
srv = charger_serveurs(INSTANCE / "plan/serveurs.yml")
|
|
groupes = {a.get("groupe") for a in (apps.get("applications") or {}).values() if a.get("groupe")}
|
|
serveurs = (srv.get("serveurs") or {})
|
|
for s in serveurs.values():
|
|
groupes.update(s.get("integrations") or [])
|
|
# Les integrations universelles ne figurent plus dans le plan (D-33) : sans
|
|
# cette ligne, leurs secrets cesseraient d'etre exiges — et P18 passerait au
|
|
# vert sur une voute incomplete. L'exemption `sauf_role` ne retire le role qu'a
|
|
# UN hote : le groupe existe des qu'il reste un serveur, donc pas de filtrage.
|
|
if serveurs:
|
|
groupes.update(integrations_universelles())
|
|
return groupes
|
|
|
|
|
|
def secrets_exiges() -> dict[str, set[str]]:
|
|
"""{secret: {origines}} — d'ou vient chaque exigence, pour un message utile."""
|
|
exiges: dict[str, set[str]] = {}
|
|
|
|
def noter(nom: str, origine: str) -> None:
|
|
exiges.setdefault(nom, set()).add(origine)
|
|
|
|
bases = charger_bases_donnees(INSTANCE / "plan/bases-donnees.yml")
|
|
for cle, base in (bases.get("bases_donnees") or {}).items():
|
|
if base.get("secret"):
|
|
noter(str(base["secret"]), f"base '{cle}'")
|
|
|
|
for groupe in sorted(groupes_du_plan()):
|
|
dossier = ROLES / groupe
|
|
if not dossier.is_dir():
|
|
continue
|
|
for f in list(dossier.rglob("*.yml")) + list(dossier.rglob("*.j2")):
|
|
for nom in _refs_vault(f):
|
|
noter(nom, f"role {groupe}")
|
|
|
|
d = _inventaire_dir()
|
|
if d and (d / "group_vars").is_dir():
|
|
for f in (d / "group_vars").rglob("*.yml"):
|
|
if f.name.endswith(".example"):
|
|
continue
|
|
for nom in _refs_vault(f):
|
|
noter(nom, f"group_vars/{f.name}")
|
|
|
|
return exiges
|
|
|
|
|
|
def cles_gabarit(path: Path) -> set[str]:
|
|
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
return set(data) if isinstance(data, dict) else set()
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Couverture du gabarit de voute Set-OPS.")
|
|
sub = parser.add_subparsers(dest="commande", required=True)
|
|
sub.add_parser("lister", help="Liste les secrets exiges et leur origine.")
|
|
pv = sub.add_parser("verifier", help="Le gabarit couvre-t-il les secrets exiges ?")
|
|
pv.add_argument("--strict", action="store_true",
|
|
help="Echouer aussi si le gabarit porte des cles inutiles.")
|
|
args = parser.parse_args()
|
|
|
|
g = gabarit()
|
|
if not g:
|
|
print("Aucun gabarit vault.yml.example : rien a verifier.")
|
|
return 0
|
|
|
|
exiges = secrets_exiges()
|
|
presentes = cles_gabarit(g)
|
|
|
|
if args.commande == "lister":
|
|
for nom in sorted(exiges):
|
|
etat = "au gabarit" if nom in presentes else "ABSENT du gabarit"
|
|
print(f"{nom:<34} {etat:<18} <- {', '.join(sorted(exiges[nom]))}")
|
|
return 0
|
|
|
|
manquants = sorted(set(exiges) - presentes)
|
|
inutiles = sorted(presentes - set(exiges) - SECRETS_HORS_MOTIF)
|
|
|
|
if manquants:
|
|
print(f"erreur: {len(manquants)} secret(s) exige(s) par le plan mais ABSENT(s) "
|
|
f"du gabarit {g.relative_to(RACINE) if g.is_relative_to(RACINE) else g} :",
|
|
file=sys.stderr)
|
|
for nom in manquants:
|
|
print(f" - {nom} <- {', '.join(sorted(exiges[nom]))}", file=sys.stderr)
|
|
if inutiles:
|
|
flux = sys.stderr if args.strict else sys.stdout
|
|
print(f"{'erreur' if args.strict else 'note'}: {len(inutiles)} cle(s) du gabarit "
|
|
f"que le plan n'exige plus : {', '.join(inutiles)}", file=flux)
|
|
|
|
if manquants or (inutiles and args.strict):
|
|
return 2
|
|
print(f"Gabarit de voute complet : {len(exiges)} secret(s) exige(s), tous presents.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|