#!/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@ 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())