Some checks are pending
verifier / verifier (push) Waiting to run
Le jour ou l on s en sert, le poste est mort — et cloner le depot demande la cle SSH qui est dans l archive qu on essaie d ouvrir. Une procedure rangee dans le depot serait inaccessible exactement quand elle sert. L export depose donc restaurer_cles.py et un LISEZ-MOI a cote de l archive. La cle ne demande plus que gpg, python3 et la phrase de passe. Le script remet chaque fichier a sa place selon son NOM (machine neuve, parfois autre compte), repose les droits a 0600 — ssh refuse une cle privee lisible par d autres, et son message ne dit pas qu il s agit d un droit — et refuse d ecraser une cle presente, en regardant AVANT d ecrire. Le LISEZ-MOI porte les trois commandes manuelles. Un outil peut avoir un defaut ; gpg et tar seront la. EPROUVE : export vers une cle, poste neuf vide, restauration depuis la cle SEULE, empreintes comparees — identiques 4 sur 4, droits 700/600, et le refus d ecraser tire. Le filet manuel passe au meme test separement, identiques 4 sur 4. make cles-restaurer refusera puisque les cles sont en place — et ce refus est la preuve que l archive s ouvre et que la phrase de passe est la bonne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
122 lines
5.3 KiB
Python
122 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Remet en place les cles sorties par `exporter_cles.py`. Autonome, sans le depot.
|
|
|
|
POURQUOI CE SCRIPT DOIT POUVOIR VIVRE SUR LA CLE USB.
|
|
|
|
Le jour ou l'on s'en sert, le poste est mort. Le depot Set-OPS est replique — eregion, la
|
|
forge du site, patient 0 — mais le CLONER demande la cle SSH, qui est justement dans
|
|
l'archive qu'on essaie d'ouvrir. Une procedure de restauration qui vit dans le depot
|
|
serait donc inaccessible exactement quand elle sert.
|
|
|
|
`exporter_cles.py` en depose donc une copie A COTE DE L'ARCHIVE, avec un mode d'emploi.
|
|
La cle USB se suffit a elle-meme : gpg, python3, et les trois commandes manuelles ecrites
|
|
dans le LISEZ-MOI si meme ce script refuse de tourner.
|
|
|
|
CE QU'IL FAIT DE PLUS QU'UN `tar -x` :
|
|
|
|
- il remet CHAQUE fichier a sa place (`~/.config` ou `~/.ssh`, selon son nom) ;
|
|
- il repose les permissions a 0600. Sans ca, `ssh` REFUSE une cle privee lisible par
|
|
d'autres — « UNPROTECTED PRIVATE KEY FILE » — et le message n'aide pas a comprendre
|
|
qu'il s'agit d'un droit, pas d'une cle invalide. Une archive extraite depuis un
|
|
support FAT arrive systematiquement dans cet etat ;
|
|
- il REFUSE d'ecraser une cle existante. Lance par erreur sur un poste qui fonctionne,
|
|
il ne doit rien detruire.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
MAISON = Path.home()
|
|
|
|
|
|
def _destination(nom: str) -> Path:
|
|
"""Ou remettre un fichier — deduit de son nom, pas d'un chemin enregistre.
|
|
|
|
L'archive ne garde que les noms de base : un chemin absolu la rendrait dependante du
|
|
compte qui l'a creee, et on restaure souvent sur une machine neuve, parfois sous un
|
|
autre nom d'utilisateur.
|
|
"""
|
|
if nom.startswith("setops-vault"):
|
|
return MAISON / ".config" / nom
|
|
if nom.startswith("id_"):
|
|
return MAISON / ".ssh" / nom
|
|
raise SystemExit(f"REFUS : {nom} n'est ni une cle de voute ni une cle SSH connue.")
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("archive", help="Chemin de l'archive .tar.gpg")
|
|
ap.add_argument("--lister", action="store_true",
|
|
help="Montrer ce que l'archive contient, sans rien ecrire")
|
|
ap.add_argument("--forcer", action="store_true",
|
|
help="Ecraser une cle deja presente (a n'utiliser qu'en connaissance)")
|
|
args = ap.parse_args()
|
|
|
|
arch = Path(args.archive).expanduser()
|
|
if not arch.is_file():
|
|
raise SystemExit(f"REFUS : {arch} introuvable.")
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
clair = Path(tmp) / "cles.tar"
|
|
print(" La phrase de passe de l'archive va etre demandee.\n")
|
|
r = subprocess.run(["gpg", "--quiet", "--output", str(clair),
|
|
"--decrypt", str(arch)])
|
|
if r.returncode != 0:
|
|
raise SystemExit(
|
|
"\nECHEC du dechiffrement.\n"
|
|
" Phrase de passe erronee, ou archive abimee. gpg verifie l'integrite\n"
|
|
" (AES256 + MDC) : s'il refuse, le contenu n'est pas fiable.")
|
|
|
|
with tarfile.open(clair) as tar:
|
|
noms = [n for n in tar.getnames() if "/" not in n and not n.startswith("..")]
|
|
if args.lister:
|
|
print(f" {len(noms)} fichier(s) dans l'archive :\n")
|
|
for n in sorted(noms):
|
|
m = tar.getmember(n)
|
|
print(f" {n:<44} {m.size:>5} o -> {_destination(n)}")
|
|
print("\n LISTE SEULE — rien n'a ete ecrit.")
|
|
return 0
|
|
|
|
# ON REGARDE AVANT D'ECRIRE. Refuser a mi-parcours laisserait la moitie des
|
|
# cles en place et l'autre non — un etat que personne ne sait diagnostiquer.
|
|
deja = [n for n in noms if _destination(n).exists()]
|
|
if deja and not args.forcer:
|
|
print(" REFUS : ces cles existent deja ici —", file=sys.stderr)
|
|
for n in sorted(deja):
|
|
print(f" ! {_destination(n)}", file=sys.stderr)
|
|
print("\n Rien n'a ete ecrit. Ce poste a peut-etre deja ses cles.\n"
|
|
" Si l'ecrasement est VOULU, relancer avec --forcer.", file=sys.stderr)
|
|
return 1
|
|
|
|
poses = []
|
|
for n in sorted(noms):
|
|
dest = _destination(n)
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
dest.parent.chmod(0o700)
|
|
membre = tar.extractfile(n)
|
|
if membre is None:
|
|
continue
|
|
dest.write_bytes(membre.read())
|
|
# 0600 SANS CONDITION : `ssh` refuse une cle privee que d'autres peuvent
|
|
# lire, et `ansible-vault` n'est pas plus tolerant. Une archive extraite
|
|
# depuis un support FAT arrive toujours avec de mauvais droits.
|
|
dest.chmod(0o600)
|
|
poses.append(dest)
|
|
|
|
print(f" {len(poses)} cle(s) remises en place, en 0600 :\n")
|
|
for d in poses:
|
|
print(f" {d}")
|
|
print("\n VERIFIER MAINTENANT, PAS PLUS TARD :")
|
|
print(" ssh -o BatchMode=yes ansible@<un hyperviseur> hostname")
|
|
print(" python3 scripts/voutes.py etat")
|
|
print("\n Une cle remise en place qu'on n'a pas essayee n'est pas une cle restauree.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|