cle-appareil et config --cle/--vers ecrivent les secrets en 0600 ; --portee restreint appliquer au tunnel vise. Nouvelles cles en service, anciennes retirees, Technolibre intact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
469 lines
22 KiB
Python
469 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""L'acces d'administration par WireGuard — le plan declare, la frontiere suit.
|
|
|
|
POURQUOI CETTE VOIE (2026-09-17). L'exploitant entrait par les pattes de la frontiere, une
|
|
regle par zone. La question posee etait : « le runner ne devrait-il pas etre le rebond ? »
|
|
Non — il detient la cle de la voute du site et les cles SSH de toutes les machines : une
|
|
session humaine compromise deviendrait le plan de controle, et reconstruire le runner
|
|
couperait l'acces. Un tunnel nominatif donne la meme porte unique sans reunir ces pouvoirs.
|
|
|
|
CE QUE CE SCRIPT RECONCILIE, et rien d'autre :
|
|
- UNE instance WireGuard (celle que le plan nomme), distincte du tunnel site-a-site ;
|
|
- les PAIRS qui lui sont attaches : un par personne ET par appareil.
|
|
|
|
PERIMETRE STRICT : un pair attache a une AUTRE instance n'est jamais touche — le tunnel vers
|
|
le site pair vit a cote et ne doit rien craindre d'ici.
|
|
|
|
AUCUNE CLE PRIVEE D'APPAREIL N'ENTRE DANS LE DEPOT. Le plan ne porte que des cles PUBLIQUES.
|
|
`pair-nouveau` tire une paire, affiche la privee UNE fois (a coller dans l'appareil) et rend
|
|
la ligne de plan a ajouter.
|
|
|
|
Usage :
|
|
python3 scripts/vpn_admin.py plan # aucune ecriture
|
|
CONFIRMER=true python3 scripts/vpn_admin.py appliquer
|
|
python3 scripts/vpn_admin.py pair-nouveau --nom daniel-portable
|
|
python3 scripts/vpn_admin.py config --nom daniel-portable
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import base64
|
|
import ipaddress
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
import appliquer_opnsense as appl # noqa: E402
|
|
import devis_opnsense as devis_mod # noqa: E402
|
|
import underlay as underlay_mod # noqa: E402
|
|
|
|
|
|
def tunnels(inclure_vides: bool = False) -> list[dict]:
|
|
"""Tous les tunnels a tenir : celui du SITE, puis un par locataire qui declare des pairs.
|
|
|
|
DEUX DECLARANTS, DEUX PORTEES. Le site declare le sien dans son plan (`acces_admin_vpn`).
|
|
Un LOCATAIRE declare les siens dans SON plan (`plan/acces.yml`) : c'est lui qui decide qui
|
|
entre chez lui, et le site ne porte que la route — meme partage que les zones DNS
|
|
publiques. Son reseau, son port et son numero d'instance se DERIVENT de son index : rien
|
|
a allouer, aucune collision possible (P21 garde deja l'unicite des index).
|
|
"""
|
|
import devis_reseau
|
|
from inventory_rules import (charger_acces, charger_nomenclature, port_vpn_locataire,
|
|
reseau_vpn_locataire, valider_acces)
|
|
out: list[dict] = []
|
|
site = (underlay_mod.lire_plan_site("10-intrants.yml") or {}).get("acces_admin_vpn") or {}
|
|
if site.get("reseau") and site.get("port"):
|
|
out.append({**site,
|
|
"nom": str(site.get("nom") or "admins"),
|
|
"portee": "site",
|
|
"pairs": {str(p["nom"]): p for p in (site.get("pairs") or [])}})
|
|
for nom_depot, _prefixe, _n in devis_reseau.decouvrir_du_site():
|
|
base = RACINE.parent / nom_depot
|
|
registre = charger_acces(base / "plan" / "acces.yml")
|
|
index = charger_nomenclature(base / "plan" / "nomenclature.yml").get("index")
|
|
# UN LOCATAIRE SANS PAIR N'A PAS DE TUNNEL — sauf quand on vient justement lui en
|
|
# proposer un (`pair-nouveau`) : refuser la` aurait fait de la premiere cle un
|
|
# probleme d'oeuf et de poule, et c'est ce que le premier essai a montre.
|
|
if index is None or (not (registre.get("acces_admin_vpn") or {}) and not inclure_vides):
|
|
continue
|
|
valider_acces(registre, index)
|
|
slug = nom_depot.lower().removeprefix("ops-")
|
|
out.append({
|
|
"nom": f"admins-{slug}",
|
|
"portee": nom_depot,
|
|
# L'INSTANCE EST L'INDEX : `wg17`, `wg23`. Un rang (2, 3, 4...) changerait de
|
|
# numero le jour ou un locataire part, et renommerait l'interface des autres.
|
|
"instance": int(index),
|
|
"port": port_vpn_locataire(index),
|
|
"reseau": reseau_vpn_locataire(index),
|
|
"adresse_frontiere": f"{next(ipaddress.ip_network(reseau_vpn_locataire(index)).hosts())}/24",
|
|
"mtu": int(site.get("mtu", 1412)),
|
|
# Le nom du pair sur le boitier porte le locataire : deux ecosystemes peuvent
|
|
# avoir chacun un `daniel-portable` sans se marcher dessus.
|
|
"pairs": {f"{slug}-{n}": {"nom": f"{slug}-{n}", **c}
|
|
for n, c in (registre.get("acces_admin_vpn") or {}).items()},
|
|
})
|
|
return out
|
|
|
|
|
|
def plan_vpn() -> dict:
|
|
d = (underlay_mod.lire_plan_site("10-intrants.yml") or {}).get("acces_admin_vpn") or {}
|
|
if not d.get("reseau") or not d.get("port"):
|
|
raise SystemExit("REFUS : le plan du site ne declare pas `acces_admin_vpn` "
|
|
"(reseau, port). Rien a reconcilier.")
|
|
return d
|
|
|
|
|
|
def api_frontiere() -> appl.Frontiere:
|
|
base = devis_mod.depot_hebergeur()
|
|
if base is None:
|
|
raise SystemExit("Aucun underlay ne designe d'hebergeur : pas de frontiere a piloter.")
|
|
intr = devis_mod.intrants_frontiere()
|
|
v = appl._voute(base)
|
|
return appl.Frontiere(str(intr.get("opnsense_api_url") or ""),
|
|
v["vault_opnsense_api_key"], v["vault_opnsense_api_secret"],
|
|
bool(intr.get("opnsense_api_verifier_certs")))
|
|
|
|
|
|
def _table(rep: dict, *chemin: str) -> dict:
|
|
"""OPNsense emboite ses listes : {'server': {'servers': {'server': {uuid: {...}}}}}."""
|
|
noeud = rep
|
|
for c in chemin:
|
|
noeud = (noeud or {}).get(c) or {}
|
|
return noeud if isinstance(noeud, dict) else {}
|
|
|
|
|
|
def _choisi(champ) -> list[str]:
|
|
"""Un champ a choix d'OPNsense : rend les valeurs selectionnees."""
|
|
if isinstance(champ, dict):
|
|
return [k for k, v in champ.items() if isinstance(v, dict) and v.get("selected")]
|
|
return [str(champ)] if champ else []
|
|
|
|
|
|
def etat(api: appl.Frontiere) -> tuple[dict, dict]:
|
|
"""(serveurs par uuid, pairs par uuid) tels que la frontiere les porte."""
|
|
srv = _table(appl._lire(api, "/api/wireguard/server/get"), "server", "servers", "server")
|
|
pairs = _table(appl._lire(api, "/api/wireguard/client/get"), "client", "clients", "client")
|
|
return srv, pairs
|
|
|
|
|
|
def reseaux_joignables() -> list[str]:
|
|
"""Ce que le tunnel doit router : les zones du site, la fabric, les supernets locataires.
|
|
|
|
DERIVE, JAMAIS ECRIT. Une liste recopiee ici prendrait du retard sur la carte a la
|
|
premiere zone ajoutee — et l'administrateur decouvrirait le trou au pire moment.
|
|
"""
|
|
u = underlay_mod.charger() or {}
|
|
out = {str(r["sous_reseau"]) for r in underlay_mod.reseaux(u)
|
|
if r.get("sous_reseau")
|
|
and (str(r.get("nom", "")) in ("management", "grappe-controle", "transit-frontiere")
|
|
or str(r.get("nom", "")).startswith("site-"))}
|
|
out |= {str(s) for s in _supernets_locataires()}
|
|
return sorted(out, key=lambda c: ipaddress.ip_network(c, strict=False))
|
|
|
|
|
|
def _supernets_locataires() -> list[str]:
|
|
"""Les supernets des locataires heberges — decouverts, jamais listes."""
|
|
import devis_reseau
|
|
out = []
|
|
for nom_depot, _prefixe, _n in devis_reseau.decouvrir_du_site():
|
|
try:
|
|
nomencl = (RACINE.parent / nom_depot / "plan" / "nomenclature.yml")
|
|
import yaml
|
|
d = yaml.safe_load(nomencl.read_text(encoding="utf-8")) or {}
|
|
idx = d.get("index")
|
|
if idx is not None:
|
|
out.append(f"10.{int(idx)}.0.0/16")
|
|
except (OSError, ValueError, TypeError):
|
|
continue
|
|
return sorted(out)
|
|
|
|
|
|
def rapprocher(api: appl.Frontiere, vpn: dict) -> dict:
|
|
srv, pairs = etat(api)
|
|
nom = str(vpn.get("nom") or "admins")
|
|
uuid_srv = next((u for u, s in srv.items() if str(s.get("name")) == nom), None)
|
|
declares = {n: p for n, p in (vpn.get("pairs") or {}).items()
|
|
if str(p.get("etat", "present")) == "present"}
|
|
retires = {n for n, p in (vpn.get("pairs") or {}).items()
|
|
if str(p.get("etat", "present")) == "absent"}
|
|
|
|
poses = {}
|
|
if uuid_srv:
|
|
for u, p in pairs.items():
|
|
if uuid_srv in _choisi(p.get("servers")):
|
|
poses[str(p.get("name"))] = (u, p)
|
|
|
|
def different(nom_p: str) -> bool:
|
|
_u, p = poses[nom_p]
|
|
d = declares[nom_p]
|
|
return (str(p.get("pubkey")) != str(d.get("cle_publique"))
|
|
or _choisi(p.get("tunneladdress")) != [str(d.get("adresse"))])
|
|
|
|
return {
|
|
"nom": nom,
|
|
"uuid_serveur": uuid_srv,
|
|
"serveur_a_creer": uuid_srv is None,
|
|
"serveur": srv.get(uuid_srv or "", {}),
|
|
"pairs_creer": {n: d for n, d in declares.items() if n not in poses},
|
|
"pairs_majer": {n: d for n, d in declares.items() if n in poses and different(n)},
|
|
"pairs_garder": {n for n in declares if n in poses and not different(n)},
|
|
# UN PAIR ATTACHE A NOTRE INSTANCE QUE LE PLAN NE DECLARE PLUS : c'est un acces qui
|
|
# survit a la decision de le retirer. On le nomme, et `appliquer` le retire.
|
|
"pairs_retirer": {n: u for n, (u, _p) in poses.items()
|
|
if n not in declares or n in retires},
|
|
}
|
|
|
|
|
|
def afficher(p: dict, vpn: dict) -> bool:
|
|
if p["serveur_a_creer"]:
|
|
print(f" + instance WireGuard {p['nom']} (port {vpn['port']}, "
|
|
f"{vpn['adresse_frontiere']})")
|
|
else:
|
|
s = p["serveur"]
|
|
print(f" = instance WireGuard {p['nom']} — port {s.get('port')}, "
|
|
f"cle publique {str(s.get('pubkey'))[:12]}…")
|
|
for n, d in sorted(p["pairs_creer"].items()):
|
|
print(f" + pair {n:<24} {d.get('adresse')}")
|
|
for n, d in sorted(p["pairs_majer"].items()):
|
|
print(f" ~ pair MODIFIE {n:<24} {d.get('adresse')}")
|
|
for n in sorted(p["pairs_retirer"]):
|
|
print(f" - pair RETIRE {n}")
|
|
print(f"\n a creer : {len(p['pairs_creer']) + (1 if p['serveur_a_creer'] else 0)}"
|
|
f" | a retirer : {len(p['pairs_retirer'])}"
|
|
f" | inchange : {len(p['pairs_garder'])}")
|
|
return bool(p["serveur_a_creer"] or p["pairs_creer"] or p["pairs_majer"] or p["pairs_retirer"])
|
|
|
|
|
|
def _cle_privee() -> tuple[str, str]:
|
|
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
|
|
from cryptography.hazmat.primitives import serialization
|
|
k = X25519PrivateKey.generate()
|
|
priv = k.private_bytes(encoding=serialization.Encoding.Raw,
|
|
format=serialization.PrivateFormat.Raw,
|
|
encryption_algorithm=serialization.NoEncryption())
|
|
pub = k.public_key().public_bytes(encoding=serialization.Encoding.Raw,
|
|
format=serialization.PublicFormat.Raw)
|
|
return base64.b64encode(priv).decode(), base64.b64encode(pub).decode()
|
|
|
|
|
|
def appliquer(api: appl.Frontiere, p: dict, vpn: dict) -> int:
|
|
echecs = 0
|
|
|
|
def fait(rep, quoi):
|
|
nonlocal echecs
|
|
if rep.get("result") in ("saved", "deleted") or rep.get("status") == "ok":
|
|
return rep
|
|
echecs += 1
|
|
print(f" ! ECHEC {quoi} : {str(rep)[:200]}")
|
|
return {}
|
|
|
|
uuid_srv = p["uuid_serveur"]
|
|
if p["serveur_a_creer"]:
|
|
# LA CLE DU SERVEUR NAIT SUR LE BOITIER ET Y RESTE. Elle n'ouvre rien toute seule :
|
|
# c'est la cle de la FRONTIERE, pas celle d'un administrateur. Le jour ou le boitier
|
|
# est refait, les appareils recoivent la nouvelle cle publique — et c'est tout.
|
|
priv, pub = _cle_privee()
|
|
rep = fait(api("/api/wireguard/server/add_server", {"server": {
|
|
"enabled": "1", "name": p["nom"], "instance": str(vpn.get("instance", 1)),
|
|
"pubkey": pub, "privkey": priv, "port": str(vpn["port"]),
|
|
"mtu": str(vpn.get("mtu", 1412)),
|
|
"tunneladdress": str(vpn["adresse_frontiere"]),
|
|
"disableroutes": "0", "peers": "",
|
|
}}), f"instance {p['nom']}")
|
|
uuid_srv = rep.get("uuid") or None
|
|
if uuid_srv is None:
|
|
srv, _ = etat(api)
|
|
uuid_srv = next((u for u, s in srv.items() if str(s.get("name")) == p["nom"]), None)
|
|
|
|
for nom_p, d in sorted({**p["pairs_creer"], **p["pairs_majer"]}.items()):
|
|
corps = {"client": {
|
|
"enabled": "1", "name": nom_p, "pubkey": str(d["cle_publique"]), "psk": "",
|
|
"tunneladdress": str(d["adresse"]), "keepalive": "25",
|
|
"servers": uuid_srv or "",
|
|
}}
|
|
if nom_p in p["pairs_majer"]:
|
|
fait(api(f"/api/wireguard/client/set_client/{_uuid_pair(api, nom_p)}", corps),
|
|
f"pair {nom_p}")
|
|
else:
|
|
fait(api("/api/wireguard/client/add_client", corps), f"pair {nom_p}")
|
|
|
|
for nom_p, u in sorted(p["pairs_retirer"].items()):
|
|
fait(api(f"/api/wireguard/client/del_client/{u}", {}), f"retrait du pair {nom_p}")
|
|
|
|
if echecs:
|
|
print(f"\n {echecs} echec(s) — RIEN N'EST RECHARGE : le tunnel garde son etat "
|
|
f"precedent. Corriger, puis rejouer.")
|
|
return 1
|
|
# OPNsense repond `{"result": "ok"}` : lire `status` seul affichait « ? » apres un
|
|
# rechargement reussi (constate le 2026-09-28, nouvelles cles bien en service).
|
|
rep = api("/api/wireguard/service/reconfigure", {})
|
|
print(" service :", rep.get("status") or rep.get("result") or rep)
|
|
return 0
|
|
|
|
|
|
def _uuid_pair(api: appl.Frontiere, nom_p: str) -> str:
|
|
_srv, pairs = etat(api)
|
|
return next((u for u, x in pairs.items() if str(x.get("name")) == nom_p), "")
|
|
|
|
|
|
def cmd_plan(args) -> int:
|
|
api = api_frontiere()
|
|
tous = tunnels()
|
|
if not tous:
|
|
print("Aucun tunnel declare — ni au plan du site, ni chez un locataire.")
|
|
return 0
|
|
# N'APPLIQUER QUE CE QU'ON VISE (2026-09-28). Sans filtre, `appliquer` reconcilie TOUS
|
|
# les tunnels : une rotation de cle chez l'un aurait emporte l'ecart en attente d'un
|
|
# autre — un pair retire que personne n'avait encore decide de retirer.
|
|
if getattr(args, "portee", None):
|
|
voulues = {x.lower() for x in args.portee}
|
|
connues = {t["portee"].lower() for t in tous}
|
|
if voulues - connues:
|
|
raise SystemExit(f"REFUS : portee inconnue {sorted(voulues - connues)} "
|
|
f"(connues : {sorted(connues)}).")
|
|
tous = [t for t in tous if t["portee"].lower() in voulues]
|
|
plans, a_faire = [], False
|
|
for vpn in tous:
|
|
p = rapprocher(api, vpn)
|
|
plans.append((p, vpn))
|
|
print(f"Tunnel « {p['nom']} » ({vpn['portee']}) — reseau {vpn['reseau']}, "
|
|
f"port {vpn['port']}\n")
|
|
a_faire = afficher(p, vpn) or a_faire
|
|
print()
|
|
if not a_faire:
|
|
print(" Rien a faire.")
|
|
return 0
|
|
if os.environ.get("CONFIRMER") != "true":
|
|
print(" PLAN SEUL — aucune ecriture. Rejouer avec CONFIRMER=true pour appliquer.")
|
|
return 0
|
|
code = 0
|
|
for p, vpn in plans:
|
|
code = appliquer(api, p, vpn) or code
|
|
return code
|
|
|
|
|
|
def _tunnel_vise(portee: str | None) -> dict:
|
|
"""Le tunnel du site par defaut ; celui d'un locataire avec `--locataire OPS-X`."""
|
|
tous = tunnels(inclure_vides=True)
|
|
if portee:
|
|
for t in tous:
|
|
if t["portee"].lower() == portee.lower():
|
|
return t
|
|
raise SystemExit(f"REFUS : aucun tunnel declare pour « {portee} » "
|
|
f"(connus : {', '.join(t['portee'] for t in tous) or 'aucun'}).")
|
|
for t in tous:
|
|
if t["portee"] == "site":
|
|
return t
|
|
raise SystemExit("REFUS : le plan du site ne declare aucun tunnel.")
|
|
|
|
|
|
def cmd_pair(args) -> int:
|
|
vpn = _tunnel_vise(getattr(args, "locataire", None))
|
|
priv, pub = _cle_privee()
|
|
reseau = ipaddress.ip_network(str(vpn["reseau"]), strict=False)
|
|
prises = {str(p.get("adresse", "")).split("/")[0] for p in (vpn.get("pairs") or {}).values()}
|
|
prises.add(str(vpn["adresse_frontiere"]).split("/")[0])
|
|
libre = next((str(h) for h in reseau.hosts() if str(h) not in prises), None)
|
|
if vpn["portee"] == "site":
|
|
print(f"# Pair « {args.nom} » — a AJOUTER au plan du site (plan/10-intrants.yml) :\n")
|
|
print(" pairs:")
|
|
print(f" - nom: {args.nom}")
|
|
print(f" cle_publique: \"{pub}\"")
|
|
print(f" adresse: {libre}/32")
|
|
print(" etat: present\n")
|
|
else:
|
|
print(f"# Pair « {args.nom} » — a AJOUTER au plan de {vpn['portee']} (plan/acces.yml) :\n")
|
|
print("acces_admin_vpn:")
|
|
print(f" {args.nom}:")
|
|
print(f" cle_publique: \"{pub}\"")
|
|
print(f" adresse: {libre}/32")
|
|
print(" etat: present\n")
|
|
print("# La cle PRIVEE ci-dessous ne s'affiche qu'une fois : elle appartient a l'appareil,")
|
|
print("# elle n'entre ni dans le depot ni dans une voute.\n")
|
|
print(f"PrivateKey = {priv}\n")
|
|
print(f"# Puis : python3 scripts/vpn_admin.py config --nom {args.nom}")
|
|
return 0
|
|
|
|
|
|
def _ecrire_secret(chemin: Path, contenu: str) -> None:
|
|
"""Ecrire un fichier que seul son proprietaire lit, sans jamais en ecraser un."""
|
|
chemin = chemin.expanduser()
|
|
if chemin.exists():
|
|
raise SystemExit(f"REFUS : {chemin} existe deja. On n'ecrase pas une cle.")
|
|
chemin.parent.mkdir(parents=True, exist_ok=True)
|
|
fd = os.open(chemin, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
with os.fdopen(fd, "w") as fh:
|
|
fh.write(contenu)
|
|
|
|
|
|
def cmd_cle_appareil(args) -> int:
|
|
"""LA CLE PRIVEE NE PASSE PAR AUCUN ECRAN (2026-09-28).
|
|
|
|
`pair-nouveau` affiche la privee pour qu'un humain la colle. Lance par un assistant,
|
|
ou dans un terminal journalise, cet affichage la depose dans un historique — c'est
|
|
ainsi que deux cles d'administration ont du etre changees. Ici, la privee va droit
|
|
dans un fichier en 0600 ; seule la PUBLIQUE s'affiche, pour le plan. Sert aussi a une
|
|
ROTATION : on remplace `cle_publique` du pair existant, nom et adresse inchanges.
|
|
"""
|
|
priv, pub = _cle_privee()
|
|
_ecrire_secret(Path(args.vers), priv + "\n")
|
|
print(f"Cle privee ecrite dans {Path(args.vers).expanduser()} (0600, jamais affichee).")
|
|
print(f"cle_publique: \"{pub}\"")
|
|
return 0
|
|
|
|
|
|
def cmd_config(args) -> int:
|
|
vpn = _tunnel_vise(getattr(args, "locataire", None))
|
|
d = next((p for n, p in (vpn.get("pairs") or {}).items()
|
|
if n == args.nom or str(p.get("nom")) == args.nom
|
|
or n.endswith(f"-{args.nom}")), None)
|
|
if d is None:
|
|
raise SystemExit(f"REFUS : aucun pair « {args.nom} » au plan de {vpn['portee']}.")
|
|
api = api_frontiere()
|
|
srv, _pairs = etat(api)
|
|
nom = str(vpn["nom"])
|
|
s = next((x for x in srv.values() if str(x.get("name")) == nom), None)
|
|
if s is None:
|
|
raise SystemExit(f"REFUS : l'instance « {nom} » n'existe pas encore sur la frontiere "
|
|
f"(CONFIRMER=true python3 scripts/vpn_admin.py appliquer).")
|
|
intr = devis_mod.intrants_frontiere()
|
|
cle = (Path(args.cle).expanduser().read_text().strip() if getattr(args, "cle", None)
|
|
else "<cle privee de l'appareil>")
|
|
texte = (f"[Interface]\n# PrivateKey : celle affichee par `pair-nouveau` pour {args.nom}\n"
|
|
f"PrivateKey = {cle}\n"
|
|
f"Address = {d['adresse']}\n"
|
|
f"MTU = {vpn.get('mtu', 1412)}\n\n"
|
|
f"[Peer]\n"
|
|
f"PublicKey = {s.get('pubkey')}\n"
|
|
f"Endpoint = {intr.get('opnsense_wan_ip')}:{vpn['port']}\n"
|
|
f"AllowedIPs = {', '.join(reseaux_joignables() if vpn['portee'] == 'site' else [_supernet_de(vpn)])}\n"
|
|
f"PersistentKeepalive = 25\n")
|
|
if getattr(args, "vers", None):
|
|
if not getattr(args, "cle", None):
|
|
raise SystemExit("REFUS : --vers exige --cle (sinon le fichier porterait un gabarit).")
|
|
_ecrire_secret(Path(args.vers), texte)
|
|
print(f"Configuration complete ecrite dans {Path(args.vers).expanduser()} (0600).")
|
|
else:
|
|
print(texte, end="")
|
|
return 0
|
|
|
|
|
|
def _supernet_de(vpn: dict) -> str:
|
|
"""CE QU'UN TUNNEL DE LOCATAIRE ATTEINT : son ecosysteme, et rien d'autre."""
|
|
return f"10.{ipaddress.ip_network(str(vpn['reseau']), strict=False).network_address.packed[1]}.0.0/16"
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
sous = ap.add_subparsers(dest="commande", required=True)
|
|
for nom_cmd in ("plan", "appliquer"):
|
|
p = sous.add_parser(nom_cmd, help="rapproche le plan et la frontiere "
|
|
"(ecriture seulement avec CONFIRMER=true)")
|
|
p.add_argument("--portee", action="append",
|
|
help="restreindre a un tunnel : `site` ou `OPS-X` (repetable)")
|
|
p.set_defaults(fn=cmd_plan)
|
|
p = sous.add_parser("pair-nouveau", help="tire une paire de cles et rend la ligne de plan")
|
|
p.add_argument("--nom", required=True, help="personne-appareil, ex. daniel-portable")
|
|
p.add_argument("--locataire", help="le tunnel de cet ecosysteme (ex. OPS-Chezlepro) ; "
|
|
"sans lui, celui du site")
|
|
p.set_defaults(fn=cmd_pair)
|
|
p = sous.add_parser("cle-appareil", help="tire une paire : privee dans un FICHIER, "
|
|
"publique a l'ecran (creation ou rotation)")
|
|
p.add_argument("--vers", required=True, help="fichier de la cle privee (cree en 0600)")
|
|
p.set_defaults(fn=cmd_cle_appareil)
|
|
p = sous.add_parser("config", help="la configuration a coller dans l'appareil")
|
|
p.add_argument("--nom", required=True)
|
|
p.add_argument("--locataire")
|
|
p.add_argument("--cle", help="fichier de la cle privee (sinon un gabarit a completer)")
|
|
p.add_argument("--vers", help="ecrire la configuration complete dans ce fichier (0600)")
|
|
p.set_defaults(fn=cmd_config)
|
|
args = ap.parse_args()
|
|
return args.fn(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|