voutes : elles sortent du poste, dans leur propre archive, relues
exporter_voutes.py et make voutes-exporter : les six voutes (gitignorees, sur aucune forge) sont sur la cle USB, a cote des cles ; restauration eprouvee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2bd761b85f
commit
4855cda294
6 changed files with 179 additions and 3 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,5 +1,28 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-09-28 (6) — Les voûtes sortent du poste, à côté des clés qui les ouvrent
|
||||||
|
|
||||||
|
**Ce qui manquait.** La clé USB portait les CLÉS des voûtes, pas les voûtes. Or elles sont
|
||||||
|
gitignorées : sur aucune forge, seulement sur le poste et sur le runner de chaque
|
||||||
|
écosystème. Poste et runner perdus ensemble, les clés restaurées n'ouvraient rien, et
|
||||||
|
chaque secret était à régénérer. La runbook `cles` affirmait pourtant « les voûtes sortent
|
||||||
|
chiffrées ».
|
||||||
|
|
||||||
|
**`scripts/exporter_voutes.py`** (`make voutes-recenser`, `make voutes-exporter VERS=…`).
|
||||||
|
Une archive à part, `setops-voutes-<date>-<poste>.tar` : `restaurer_cles.py` range chaque
|
||||||
|
clé d'après son seul nom, et les voûtes s'appellent presque toutes `vault.yml` — elles s'y
|
||||||
|
seraient écrasées. Chaque voûte garde donc son chemin relatif aux dépôts ; restauration :
|
||||||
|
`tar -xf … -C <dossier des dépôts>`. Pas de seconde couche : elles sont déjà chiffrées par
|
||||||
|
une clé qui n'est pas dans cette archive, et le script REFUSE tout fichier dont l'en-tête
|
||||||
|
n'est pas `$ANSIBLE_VAULT`. Il refuse aussi une destination dans l'infrastructure et
|
||||||
|
l'écrasement d'une archive existante, puis relit empreinte par empreinte.
|
||||||
|
|
||||||
|
**Fait et éprouvé** : six voûtes (Chezlepro, Chezlepro-lab ×2, Technolibre, les deux
|
||||||
|
sites) sur la clé USB, 80 Ko ; extraites dans un répertoire temporaire, chacune rouverte
|
||||||
|
avec les clés (33, 25, 2, 37, 20 et 14 entrées), puis détruites. Le mode d'emploi de la
|
||||||
|
clé, la runbook `cles` et `sortir-les-cles-du-poste.md` le disent désormais. À refaire
|
||||||
|
après toute écriture dans une voûte.
|
||||||
|
|
||||||
## 2026-09-28 (5) — L'exportateur PostgreSQL de Chezlepro, et où vivent vraiment les voûtes
|
## 2026-09-28 (5) — L'exportateur PostgreSQL de Chezlepro, et où vivent vraiment les voûtes
|
||||||
|
|
||||||
**Le dernier critique de Chezlepro.** `obs-01!collecte` : Prometheus scrutait
|
**Le dernier critique de Chezlepro.** `obs-01!collecte` : Prometheus scrutait
|
||||||
|
|
|
||||||
9
Makefile
9
Makefile
|
|
@ -590,6 +590,13 @@ site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declaree
|
||||||
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
|
cles-recenser: ## Montre ce qui n'existe QUE sur ce poste (sans rien ecrire)
|
||||||
python3 scripts/exporter_cles.py --recenser
|
python3 scripts/exporter_cles.py --recenser
|
||||||
|
|
||||||
|
voutes-recenser: ## Montre les voutes de ce poste (gitignorees : sur aucune forge), sans rien ecrire
|
||||||
|
python3 scripts/exporter_voutes.py --recenser
|
||||||
|
|
||||||
|
voutes-exporter: ## Sort les voutes (deja chiffrees) vers un support, relues — VERS=<repertoire> NOM=<base>
|
||||||
|
@if [ -z "$(VERS)" ]; then printf '%s\n' 'Refus: VERS=<repertoire du support> requis.'; exit 2; fi
|
||||||
|
python3 scripts/exporter_voutes.py --vers "$(VERS)" $(if $(NOM),--nom "$(NOM)")
|
||||||
|
|
||||||
cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS=<repertoire>
|
cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS=<repertoire>
|
||||||
@# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne
|
@# A LANCER SOI-MEME, PAS PAR UN AGENT. `gpg` demande une phrase de passe : elle ne
|
||||||
@# doit passer ni par un journal, ni par le contexte d'un assistant. Taper la commande
|
@# doit passer ni par un journal, ni par le contexte d'un assistant. Taper la commande
|
||||||
|
|
@ -599,7 +606,7 @@ cles-exporter: ## Sort les cles du poste, chiffrees et relues — VERS=<repertoi
|
||||||
printf 'Ex. make cles-exporter VERS=/media/danallaire/CLE\n'; exit 2; fi
|
printf 'Ex. make cles-exporter VERS=/media/danallaire/CLE\n'; exit 2; fi
|
||||||
python3 scripts/exporter_cles.py --vers "$(VERS)"
|
python3 scripts/exporter_cles.py --vers "$(VERS)"
|
||||||
|
|
||||||
.PHONY: cles-recenser cles-exporter
|
.PHONY: cles-recenser cles-exporter voutes-recenser voutes-exporter
|
||||||
depot-hors-site: ## Copie le depot de sauvegarde du site HORS du site — VERS=<repertoire>
|
depot-hors-site: ## Copie le depot de sauvegarde du site HORS du site — VERS=<repertoire>
|
||||||
@# Le depot heberge la racine de l'AC, la forge du genome, et l'etat de CHAQUE
|
@# Le depot heberge la racine de l'AC, la forge du genome, et l'etat de CHAQUE
|
||||||
@# locataire — sur une seule machine, dans un seul batiment. La copie est opaque
|
@# locataire — sur une seule machine, dans un seul batiment. La copie est opaque
|
||||||
|
|
|
||||||
|
|
@ -704,8 +704,9 @@ runbooks:
|
||||||
portee: poste
|
portee: poste
|
||||||
doc: docs/sortir-les-cles-du-poste.md
|
doc: docs/sortir-les-cles-du-poste.md
|
||||||
but: >-
|
but: >-
|
||||||
Ce qui n'existe QUE sur le poste meurt avec lui. Les voutes sortent chiffrees, sur un
|
Ce qui n'existe QUE sur le poste meurt avec lui : les CLES, et les VOUTES qu'elles
|
||||||
support qu'on relit — et on refait l'operation a chaque voute nouvelle.
|
ouvrent (gitignorees, donc sur aucune forge). Les deux sortent, sur un support qu'on
|
||||||
|
relit — et on refait l'operation a chaque voute nouvelle ou modifiee.
|
||||||
etapes:
|
etapes:
|
||||||
- cible: cles-recenser
|
- cible: cles-recenser
|
||||||
nature: mesure
|
nature: mesure
|
||||||
|
|
@ -714,6 +715,15 @@ runbooks:
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
variables: [VERS]
|
variables: [VERS]
|
||||||
pourquoi: "Sortir les cles, chiffrees, et les RELIRE apres ecriture."
|
pourquoi: "Sortir les cles, chiffrees, et les RELIRE apres ecriture."
|
||||||
|
- cible: voutes-recenser
|
||||||
|
nature: mesure
|
||||||
|
pourquoi: "Les voutes de ce poste, et leur empreinte. Une voute en clair est refusee avant tout."
|
||||||
|
- cible: voutes-exporter
|
||||||
|
nature: ecriture
|
||||||
|
variables: [VERS]
|
||||||
|
pourquoi: >-
|
||||||
|
Sortir les voutes, deja chiffrees, dans leur propre archive, et la RELIRE. Sans
|
||||||
|
elles, les cles restaurees n'ouvrent rien (2026-09-28).
|
||||||
- cible: cles-compagnons
|
- cible: cles-compagnons
|
||||||
nature: ecriture
|
nature: ecriture
|
||||||
variables: [VERS]
|
variables: [VERS]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,14 @@ Le **code** de Set-OPS est répliqué **deux fois** : `eregion` et la forge du s
|
||||||
ce poste et sur le runner de son écosystème, qui en reçoit une copie chiffrée par
|
ce poste et sur le runner de son écosystème, qui en reçoit une copie chiffrée par
|
||||||
`serveur_ops_tenant`. *(Corrigé le 2026-09-28 : cette page les disait sur les forges.)*
|
`serveur_ops_tenant`. *(Corrigé le 2026-09-28 : cette page les disait sur les forges.)*
|
||||||
|
|
||||||
|
**Elles sortent donc aussi, dans une archive à part** (`make voutes-exporter VERS=<support>`,
|
||||||
|
`scripts/exporter_voutes.py`) : déjà chiffrées par `ansible-vault`, elles n'ont pas besoin
|
||||||
|
d'une seconde couche ; le script refuse tout fichier dont l'en-tête n'est pas
|
||||||
|
`$ANSIBLE_VAULT`, garde le chemin de chaque voûte (elles s'appellent presque toutes
|
||||||
|
`vault.yml`) et relit l'archive empreinte par empreinte. Restauration :
|
||||||
|
`tar -xf setops-voutes-*.tar -C <dossier des dépôts>`. **À refaire après toute écriture
|
||||||
|
dans une voûte**, comme les clés après toute voûte nouvelle.
|
||||||
|
|
||||||
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
|
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
|
||||||
> solide — mais c'est la **redondance du génome** qui a diminué, pas le chiffrement, et
|
> solide — mais c'est la **redondance du génome** qui a diminué, pas le chiffrement, et
|
||||||
> c'est exactement ce que la page *Filiation, signatures & témoins* appelle la vraie mesure
|
> c'est exactement ce que la page *Filiation, signatures & témoins* appelle la vraie mesure
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,15 @@ Trois commandes suffisent. C'est le vrai filet :
|
||||||
cp /tmp/setops-vault-* ~/.config/ ; cp /tmp/id_* ~/.ssh/
|
cp /tmp/setops-vault-* ~/.config/ ; cp /tmp/id_* ~/.ssh/
|
||||||
chmod 700 ~/.ssh ~/.config ; chmod 600 ~/.ssh/id_* ~/.config/setops-vault-*
|
chmod 700 ~/.ssh ~/.config ; chmod 600 ~/.ssh/id_* ~/.config/setops-vault-*
|
||||||
|
|
||||||
|
LES VOUTES, DANS UNE AUTRE ARCHIVE
|
||||||
|
---------------------------------
|
||||||
|
Les cles ci-dessus OUVRENT les voutes ; les voutes elles-memes (chiffrees, gitignorees,
|
||||||
|
donc sur aucune forge) sont dans `setops-voutes-*.tar`. Elles gardent leur chemin :
|
||||||
|
|
||||||
|
tar -xf setops-voutes-*.tar -C "<dossier ou sont clones les depots>"
|
||||||
|
|
||||||
|
Sans elles, les cles n'ouvrent rien : chaque secret des ecosystemes serait a refaire.
|
||||||
|
|
||||||
ENSUITE, ESSAYER — PAS SUPPOSER
|
ENSUITE, ESSAYER — PAS SUPPOSER
|
||||||
-------------------------------
|
-------------------------------
|
||||||
ssh -o BatchMode=yes ansible@<un hyperviseur> hostname
|
ssh -o BatchMode=yes ansible@<un hyperviseur> hostname
|
||||||
|
|
|
||||||
119
scripts/exporter_voutes.py
Normal file
119
scripts/exporter_voutes.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sort les VOUTES du poste — la moitie du coffre que `exporter_cles.py` n'emporte pas.
|
||||||
|
|
||||||
|
CE QUI MANQUAIT (mesure du 2026-09-28). La cle USB portait les CLES qui ouvrent les voutes,
|
||||||
|
pas les voutes elles-memes. Or les voutes sont gitignorees : elles ne sont sur AUCUNE forge.
|
||||||
|
Chacune vit sur ce poste et sur le runner de son ecosysteme, et nulle part ailleurs. Poste
|
||||||
|
et runner perdus ensemble, les cles restauraient l'acces a... rien : tous les secrets de
|
||||||
|
l'ecosysteme etaient a regenerer.
|
||||||
|
|
||||||
|
UNE ARCHIVE A PART, PAS DANS CELLE DES CLES. `restaurer_cles.py` range chaque cle d'apres
|
||||||
|
son seul NOM (`~/.config` ou `~/.ssh`). Les voutes s'appellent presque toutes `vault.yml` :
|
||||||
|
elles s'y ecraseraient. Ici, chaque voute garde son chemin relatif aux depots —
|
||||||
|
`OPS-Chezlepro/inventories/principal/group_vars/all/vault.yml` — et se restaure par :
|
||||||
|
|
||||||
|
tar -xf setops-voutes-<date>-<poste>.tar -C "<dossier des depots>"
|
||||||
|
|
||||||
|
PAS DE SECONDE COUCHE DE CHIFFREMENT, et c'est fonde : chaque voute est deja chiffree
|
||||||
|
(AES256, `ansible-vault`) par une cle qui n'est PAS dans cette archive. Le script REFUSE
|
||||||
|
donc tout fichier dont l'en-tete n'est pas `$ANSIBLE_VAULT` : une voute dechiffree par
|
||||||
|
accident ne sortira jamais du poste par ce chemin.
|
||||||
|
|
||||||
|
ECRIRE PUIS RELIRE (D-68) : l'archive est rouverte et l'empreinte de CHAQUE voute comparee
|
||||||
|
a l'original. Au moindre ecart, elle est supprimee.
|
||||||
|
|
||||||
|
python3 scripts/exporter_voutes.py --recenser
|
||||||
|
python3 scripts/exporter_voutes.py --vers "/media/<support>" --nom setops-voutes-<date>
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
from exporter_cles import _refuser_destination_interne # noqa: E402
|
||||||
|
|
||||||
|
DEPOTS = Path(__file__).resolve().parents[2]
|
||||||
|
MOTIFS = ("*/inventories/*/group_vars/**/*vault.yml", "*/*.vault.yml")
|
||||||
|
ENTETE = b"$ANSIBLE_VAULT"
|
||||||
|
|
||||||
|
|
||||||
|
def _empreinte(chemin: Path) -> str:
|
||||||
|
return hashlib.sha256(chemin.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def recenser() -> list[Path]:
|
||||||
|
trouves = {f for m in MOTIFS for f in DEPOTS.glob(m) if f.is_file()}
|
||||||
|
return sorted(trouves)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
|
ap.add_argument("--recenser", action="store_true", help="montrer, sans rien ecrire")
|
||||||
|
ap.add_argument("--vers", help="repertoire de destination (support amovible)")
|
||||||
|
ap.add_argument("--nom", default="setops-voutes", help="nom de base de l'archive")
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
|
voutes = recenser()
|
||||||
|
en_clair = [f for f in voutes if not f.read_bytes().startswith(ENTETE)]
|
||||||
|
print(f"\n LES VOUTES DE CE POSTE ({DEPOTS}) :\n")
|
||||||
|
for f in voutes:
|
||||||
|
etat = "chiffree" if f not in en_clair else "EN CLAIR — REFUSEE"
|
||||||
|
print(f" {str(f.relative_to(DEPOTS)):<66} {f.stat().st_size:>6} o "
|
||||||
|
f"{_empreinte(f)[:16]}… {etat}")
|
||||||
|
print(f"\n {len(voutes)} voute(s).")
|
||||||
|
if en_clair:
|
||||||
|
raise SystemExit("\n REFUS : une voute n'est pas chiffree. Rien ne sort du poste tant "
|
||||||
|
"qu'elle ne l'est pas.")
|
||||||
|
if not voutes:
|
||||||
|
raise SystemExit(" Aucune voute trouvee : rien a sortir.")
|
||||||
|
if args.recenser:
|
||||||
|
print("\n RECENSEMENT SEUL — rien n'a ete ecrit.\n")
|
||||||
|
return 0
|
||||||
|
if not args.vers:
|
||||||
|
raise SystemExit(" --vers est requis pour ecrire.")
|
||||||
|
|
||||||
|
dest = Path(args.vers).expanduser()
|
||||||
|
_refuser_destination_interne(dest)
|
||||||
|
if not dest.is_dir():
|
||||||
|
raise SystemExit(f" REFUS : {dest} n'est pas un repertoire (support monte ?).")
|
||||||
|
archive = dest / f"{args.nom}-{os.uname().nodename}.tar"
|
||||||
|
if archive.exists():
|
||||||
|
raise SystemExit(f" REFUS : {archive} existe deja. On n'ecrase pas une sauvegarde.")
|
||||||
|
|
||||||
|
attendu = {str(f.relative_to(DEPOTS)): _empreinte(f) for f in voutes}
|
||||||
|
with tarfile.open(archive, "w") as tar:
|
||||||
|
for f in voutes:
|
||||||
|
tar.add(f, arcname=str(f.relative_to(DEPOTS)))
|
||||||
|
archive.chmod(0o600)
|
||||||
|
|
||||||
|
ecarts = []
|
||||||
|
with tarfile.open(archive, "r") as tar:
|
||||||
|
noms = tar.getnames()
|
||||||
|
for nom, emp in attendu.items():
|
||||||
|
membre = tar.extractfile(nom) if nom in noms else None
|
||||||
|
if membre is None:
|
||||||
|
ecarts.append(f"{nom} : ABSENT")
|
||||||
|
elif hashlib.sha256(membre.read()).hexdigest() != emp:
|
||||||
|
ecarts.append(f"{nom} : empreinte differente")
|
||||||
|
ecarts += [f"{n} : EN TROP" for n in noms if n not in attendu]
|
||||||
|
if ecarts:
|
||||||
|
archive.unlink(missing_ok=True)
|
||||||
|
print("\n ECARTS — l'archive a ete SUPPRIMEE :", file=sys.stderr)
|
||||||
|
for e in ecarts:
|
||||||
|
print(f" ! {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"\n Ecrit : {archive}")
|
||||||
|
print(f" {len(attendu)} voute(s) relue(s), empreintes identiques a l'original.")
|
||||||
|
print(f" Restaurer : tar -xf \"{archive.name}\" -C \"<dossier des depots>\"")
|
||||||
|
print(" Les CLES qui les ouvrent sont dans l'archive `setops-cles-*` — pas dans celle-ci.\n")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:]))
|
||||||
Loading…
Reference in a new issue