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
|
# 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
|
## 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
|
**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 "
|
print(f"\n {echecs} echec(s) — RIEN N'EST RECHARGE : le tunnel garde son etat "
|
||||||
f"precedent. Corriger, puis rejouer.")
|
f"precedent. Corriger, puis rejouer.")
|
||||||
return 1
|
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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -293,6 +296,16 @@ def cmd_plan(args) -> int:
|
||||||
if not tous:
|
if not tous:
|
||||||
print("Aucun tunnel declare — ni au plan du site, ni chez un locataire.")
|
print("Aucun tunnel declare — ni au plan du site, ni chez un locataire.")
|
||||||
return 0
|
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
|
plans, a_faire = [], False
|
||||||
for vpn in tous:
|
for vpn in tous:
|
||||||
p = rapprocher(api, vpn)
|
p = rapprocher(api, vpn)
|
||||||
|
|
@ -356,6 +369,33 @@ def cmd_pair(args) -> int:
|
||||||
return 0
|
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:
|
def cmd_config(args) -> int:
|
||||||
vpn = _tunnel_vise(getattr(args, "locataire", None))
|
vpn = _tunnel_vise(getattr(args, "locataire", None))
|
||||||
d = next((p for n, p in (vpn.get("pairs") or {}).items()
|
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 "
|
raise SystemExit(f"REFUS : l'instance « {nom} » n'existe pas encore sur la frontiere "
|
||||||
f"(CONFIRMER=true python3 scripts/vpn_admin.py appliquer).")
|
f"(CONFIRMER=true python3 scripts/vpn_admin.py appliquer).")
|
||||||
intr = devis_mod.intrants_frontiere()
|
intr = devis_mod.intrants_frontiere()
|
||||||
print(f"[Interface]\n# PrivateKey : celle affichee par `pair-nouveau` pour {args.nom}\n"
|
cle = (Path(args.cle).expanduser().read_text().strip() if getattr(args, "cle", None)
|
||||||
f"PrivateKey = <cle privee de l'appareil>\n"
|
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"Address = {d['adresse']}\n"
|
||||||
f"MTU = {vpn.get('mtu', 1412)}\n\n"
|
f"MTU = {vpn.get('mtu', 1412)}\n\n"
|
||||||
f"[Peer]\n"
|
f"[Peer]\n"
|
||||||
f"PublicKey = {s.get('pubkey')}\n"
|
f"PublicKey = {s.get('pubkey')}\n"
|
||||||
f"Endpoint = {intr.get('opnsense_wan_ip')}:{vpn['port']}\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"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
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -394,15 +443,23 @@ def main() -> int:
|
||||||
for nom_cmd in ("plan", "appliquer"):
|
for nom_cmd in ("plan", "appliquer"):
|
||||||
p = sous.add_parser(nom_cmd, help="rapproche le plan et la frontiere "
|
p = sous.add_parser(nom_cmd, help="rapproche le plan et la frontiere "
|
||||||
"(ecriture seulement avec CONFIRMER=true)")
|
"(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.set_defaults(fn=cmd_plan)
|
||||||
p = sous.add_parser("pair-nouveau", help="tire une paire de cles et rend la ligne de 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("--nom", required=True, help="personne-appareil, ex. daniel-portable")
|
||||||
p.add_argument("--locataire", help="le tunnel de cet ecosysteme (ex. OPS-Chezlepro) ; "
|
p.add_argument("--locataire", help="le tunnel de cet ecosysteme (ex. OPS-Chezlepro) ; "
|
||||||
"sans lui, celui du site")
|
"sans lui, celui du site")
|
||||||
p.set_defaults(fn=cmd_pair)
|
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 = sous.add_parser("config", help="la configuration a coller dans l'appareil")
|
||||||
p.add_argument("--nom", required=True)
|
p.add_argument("--nom", required=True)
|
||||||
p.add_argument("--locataire")
|
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)
|
p.set_defaults(fn=cmd_config)
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
return args.fn(args)
|
return args.fn(args)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue