123 lines
5.3 KiB
Python
123 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())
|