wireguard : rotation des cles de daniel-portable, sans afficher de privee
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>
This commit is contained in:
parent
4855cda294
commit
5f52dcb2b7
2 changed files with 84 additions and 4 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
|
@ -1,5 +1,28 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-28 (7) — Rotation des clés WireGuard de l'exploitant, sans qu'aucune privée ne s'affiche
|
||||
|
||||
**Pourquoi.** Les deux clés privées de `daniel-portable` (tunnel du site, tunnel de
|
||||
Chezlepro) ont fui dans l'historique d'une session d'assistance. On les change.
|
||||
|
||||
**Ce que `vpn_admin.py` ne savait pas faire.** `pair-nouveau` AFFICHE la privée pour qu'on
|
||||
la colle ; lancé par un assistant ou dans un terminal journalisé, cet affichage la dépose
|
||||
dans un historique — exactement la fuite qu'on répare. Trois ajouts :
|
||||
|
||||
- `cle-appareil --vers <fichier>` : la privée va droit dans un fichier en 0600 (refus
|
||||
d'écraser), seule la publique s'affiche. Sert à la création comme à la rotation ;
|
||||
- `config --cle <fichier> --vers <fichier>` : la configuration COMPLÈTE, écrite en 0600 ;
|
||||
- `plan|appliquer --portee site --portee OPS-X` : sans filtre, `appliquer` réconcilie TOUS
|
||||
les tunnels — la rotation aurait emporté l'écart en attente de Technolibre (un pair
|
||||
retiré que personne n'a encore décidé de retirer).
|
||||
|
||||
Et `service : ?` après un rechargement réussi : OPNsense répond `result`, pas `status`.
|
||||
|
||||
**Fait.** Deux paires tirées dans `~/wireguard-rotation/` (hors dépôt), `cle_publique`
|
||||
remplacée au plan du site et de Chezlepro — nom et adresse inchangés —, appliqué sur ces
|
||||
deux seuls tunnels. Relu EN SERVICE sur la frontière : les deux nouvelles clés actives,
|
||||
les deux anciennes absentes. Technolibre intact.
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -278,7 +278,10 @@ def appliquer(api: appl.Frontiere, p: dict, vpn: dict) -> int:
|
|||
print(f"\n {echecs} echec(s) — RIEN N'EST RECHARGE : le tunnel garde son etat "
|
||||
f"precedent. Corriger, puis rejouer.")
|
||||
return 1
|
||||
print(" service :", api("/api/wireguard/service/reconfigure", {}).get("status", "?"))
|
||||
# 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
|
||||
|
||||
|
||||
|
|
@ -293,6 +296,16 @@ def cmd_plan(args) -> int:
|
|||
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)
|
||||
|
|
@ -356,6 +369,33 @@ def cmd_pair(args) -> int:
|
|||
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()
|
||||
|
|
@ -371,15 +411,24 @@ def cmd_config(args) -> int:
|
|||
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()
|
||||
print(f"[Interface]\n# PrivateKey : celle affichee par `pair-nouveau` pour {args.nom}\n"
|
||||
f"PrivateKey = <cle privee de l'appareil>\n"
|
||||
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")
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -394,15 +443,23 @@ def main() -> int:
|
|||
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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue