voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.
DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.
Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.
UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.
CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.
DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.
COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.
Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Les voutes de cet ecosysteme, et la cle de chacune.
|
|
|
|
|
|
|
|
|
|
UNE VOUTE, UNE CLE (2026-08-28). Un seul mot de passe ouvrait les six voutes de la
|
|
|
|
|
flotte — celle de la fabric comprise. Tant que c'etait vrai, poser « la » cle sur chaque
|
|
|
|
|
runner aurait rendu chaque runner capable d'ouvrir les autres : compromettre le plus
|
|
|
|
|
petit locataire, c'etait obtenir les secrets de l'hebergeur. La separation precede donc
|
|
|
|
|
la distribution, et cet ordre n'est pas negociable.
|
|
|
|
|
|
|
|
|
|
CE FICHIER NE LIT AUCUN SECRET. Il ne fait que dire OU chaque cle se trouve, et laisse
|
|
|
|
|
Ansible l'ouvrir. Les chemins sont des pointeurs, jamais des valeurs.
|
|
|
|
|
|
|
|
|
|
DEUX VOUTES DANS LA MEME EXECUTION, ET C'EST LE CAS COURANT : `cloner_vm_debian.yml`
|
|
|
|
|
charge celle du tenant PUIS celle de l'underlay — les identifiants Proxmox appartiennent
|
|
|
|
|
a l'hebergeur, le reste au locataire. Un unique `ANSIBLE_VAULT_PASSWORD_FILE` ne peut
|
|
|
|
|
donc plus suffire. Ansible sait tenir plusieurs secrets a la fois
|
|
|
|
|
(`ANSIBLE_VAULT_IDENTITY_LIST`) et les essaie tous sur un bloc chiffre sans etiquette :
|
|
|
|
|
c'est exactement ce qu'il nous faut, et ca n'oblige a toucher aucun des 28 appels.
|
|
|
|
|
|
|
|
|
|
CONVENTION : ~/.config/setops-vault-<dossier-en-minuscules>
|
|
|
|
|
OPS-Chezlepro -> ~/.config/setops-vault-ops-chezlepro
|
|
|
|
|
SITE-Chezlepro -> ~/.config/setops-vault-site-chezlepro
|
|
|
|
|
|
|
|
|
|
Usage :
|
|
|
|
|
python3 scripts/voutes.py identites # la liste pour ANSIBLE_VAULT_IDENTITY_LIST
|
|
|
|
|
python3 scripts/voutes.py etat # ce qui est present, ce qui manque
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
|
|
|
|
|
|
from inventory_rules import instance_courante # noqa: E402
|
|
|
|
|
import underlay as underlay_mod # noqa: E402
|
|
|
|
|
|
|
|
|
|
PREFIXE = "setops-vault-"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _config() -> Path:
|
|
|
|
|
"""Le dossier des cles. `XDG_CONFIG_HOME` respecte, `~/.config` par defaut."""
|
|
|
|
|
return Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def slug(nom: str) -> str:
|
|
|
|
|
return nom.strip().lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def cle_de(nom_depot: str) -> Path:
|
|
|
|
|
return _config() / f"{PREFIXE}{slug(nom_depot)}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _freres() -> list[str]:
|
|
|
|
|
"""Les depots d'ecosysteme poses a cote du moteur — ceux qui portent un `plan/`.
|
|
|
|
|
|
|
|
|
|
MEME DECOUVERTE QUE LE RESTE DU DEPOT : des dossiers freres, pas de registre.
|
|
|
|
|
"""
|
|
|
|
|
racine = Path(__file__).resolve().parents[1]
|
|
|
|
|
return sorted(d.name for d in racine.parent.iterdir()
|
|
|
|
|
if d.is_dir() and (d / "plan").is_dir())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def voutes() -> list[tuple[str, str, Path]]:
|
|
|
|
|
"""[(role, nom du depot, chemin de la cle)] — tout ce que CETTE machine peut ouvrir.
|
|
|
|
|
|
|
|
|
|
L'ORDRE COMPTE PEU POUR ANSIBLE (il essaie tous les secrets) mais beaucoup pour un
|
|
|
|
|
humain qui lit la sortie : ce qu'on pilote, le terrain, puis le voisinage.
|
|
|
|
|
|
|
|
|
|
POURQUOI LE VOISINAGE Y FIGURE, ET POURQUOI CE N'EST PAS UNE FUITE. La liste nomme
|
|
|
|
|
des CHEMINS, pas des secrets : une cle absente du disque n'est pas listee, et la
|
|
|
|
|
machine ne peut ouvrir que ce qu'elle detient. C'est donc la PRESENCE des fichiers
|
|
|
|
|
qui borne le pouvoir, jamais cette fonction.
|
|
|
|
|
|
|
|
|
|
Sur le poste de l'exploitant, toutes les cles sont la, et c'est voulu : c'est
|
|
|
|
|
l'humain qui les detient toutes, et des preuves comme P03 lisent les inventaires de
|
|
|
|
|
TOUS les ecosystemes freres. Sur un runner, une seule cle existe — la sienne — et la
|
|
|
|
|
meme fonction n'en nomme qu'une. Le code est identique, le pouvoir ne l'est pas.
|
|
|
|
|
"""
|
|
|
|
|
out: list[tuple[str, str, Path]] = []
|
|
|
|
|
vus: set[str] = set()
|
|
|
|
|
|
|
|
|
|
def ajouter(role: str, nom: str) -> None:
|
|
|
|
|
if nom and nom not in vus:
|
|
|
|
|
vus.add(nom)
|
|
|
|
|
out.append((role, nom, cle_de(nom)))
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
inst = instance_courante()
|
|
|
|
|
if inst is not None:
|
|
|
|
|
ajouter("instance", Path(inst).resolve().name)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
c = underlay_mod.chemin()
|
|
|
|
|
if c is not None:
|
|
|
|
|
ajouter("hebergeur", c.resolve().parent.name)
|
|
|
|
|
for nom in _freres():
|
|
|
|
|
ajouter("voisin", nom)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def identites() -> str:
|
|
|
|
|
"""`etiquette@chemin,...` pour ANSIBLE_VAULT_IDENTITY_LIST — les cles PRESENTES.
|
|
|
|
|
|
|
|
|
|
Une cle absente est SAUTEE plutot que declaree : Ansible refuse de demarrer si un
|
|
|
|
|
fichier d'identite n'existe pas, et l'echec porterait alors sur une voute que
|
|
|
|
|
l'execution n'ouvre peut-etre meme pas. Ce qui manque se lit par `voutes.py etat`.
|
|
|
|
|
"""
|
|
|
|
|
return ",".join(f"{slug(nom)}@{chemin}" for _, nom, chemin in voutes() if chemin.is_file())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def etat() -> int:
|
2026-08-28 18:41:21 -04:00
|
|
|
"""Rapport lisible. Le CODE DE SORTIE ne juge que ce que CETTE machine doit pouvoir.
|
|
|
|
|
|
|
|
|
|
UNE CLE ABSENTE N'EST PAS UNE FAUTE — c'est souvent la bonne reponse (2026-08-28).
|
|
|
|
|
Sur le runner d'un tenant, la cle du SITE doit manquer : il porte la carte de la
|
|
|
|
|
fabric et ne doit jamais pouvoir l'ouvrir. Ma premiere version sortait en erreur des
|
|
|
|
|
qu'une cle manquait, ce qui faisait echouer une chaine parfaitement saine et
|
|
|
|
|
presentait une SEPARATION REUSSIE comme un defaut.
|
|
|
|
|
|
|
|
|
|
Seule l'absence de la cle de l'INSTANCE MONTEE empeche la machine de travailler :
|
|
|
|
|
c'est elle, et elle seule, qui decide du code de sortie. Le reste s'affiche.
|
|
|
|
|
"""
|
voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.
DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.
Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.
UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.
CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.
DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.
COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.
Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
|
|
|
v = voutes()
|
|
|
|
|
if not v:
|
|
|
|
|
print("Aucune voute a nommer : ni instance montee, ni underlay.")
|
|
|
|
|
return 0
|
2026-08-28 18:41:21 -04:00
|
|
|
bloquant = False
|
voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.
DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.
Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.
UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.
CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.
DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.
COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.
Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
|
|
|
for role, nom, chemin in v:
|
|
|
|
|
present = chemin.is_file()
|
2026-08-28 18:41:21 -04:00
|
|
|
if role == "instance" and not present:
|
|
|
|
|
bloquant = True
|
|
|
|
|
etiquette = "cle presente" if present else (
|
|
|
|
|
"CLE ABSENTE " if role == "instance" else "sans cle ")
|
|
|
|
|
print(f"{role:<11} {nom:<20} {etiquette} {chemin}")
|
|
|
|
|
if bloquant:
|
|
|
|
|
print("\nLa cle de l'instance montee manque — cette machine ne peut rien "
|
|
|
|
|
"configurer.\n umask 077; openssl rand -base64 48 | tr -d '\\n' > <chemin>")
|
|
|
|
|
return 1 if bloquant else 0
|
voutes : une voute, une cle — separer avant de distribuer
Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la
cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation
atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe
ouvrait les SIX voutes de la flotte, celle de la fabric comprise.
DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle
sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre
le plus petit locataire donnait les secrets de l'hebergeur.
Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-<depot>. Verification
croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle
maitresse n'ouvre plus aucune des six.
UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit :
`cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la
meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie
toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en
toucher un seul. La liste se derive dans scripts/voutes.py.
CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le
poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit
les inventaires de tous les freres. Sur un runner, une seule existe. Le code est
identique, le pouvoir ne l'est pas.
DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit
les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que
la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne
connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop
facilement comme une preuve passee.
COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les
runners recevront la leur. Le pari tient parce que le perimetre est borne.
Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste
sur le poste : la retirer est une decision, pas un nettoyage.
make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans
ANSIBLE_VAULT_PASSWORD_FILE.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
2026-08-28 14:23:33 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
|
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
|
|
|
ap.add_argument("commande", choices=("identites", "etat"))
|
|
|
|
|
args = ap.parse_args(argv)
|
|
|
|
|
if args.commande == "identites":
|
|
|
|
|
print(identites())
|
|
|
|
|
return 0
|
|
|
|
|
return etat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|