inter-sites : l'administration du site pair, par le lien entre les sites
Une cle administration sur un lien de opnsense_liens_sites : - entrante : les reseaux d'administration du pair (lus dans son plan) entrent par l'interface du lien, en SSH et HTTPS, vers la patte de gestion de la frontiere seulement ; - sortante : la gestion et le tunnel des admins sortent vers le rebond du pair, et rien d'autre. Routes derivees, gardees avec celles du lien. vpn_admin.py route le rebond de chaque pair. frontiere-plan ne declare plus absentes du noyau les routes d'hote, que FreeBSD affiche sans masque. Sans la cle, le devis des deux sites est identique a l'ancien. Pas encore applique. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d152e32328
commit
e7d704fdcc
4 changed files with 157 additions and 4 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,3 +1,33 @@
|
|||
## 2026-10-14 (152) — L'administration du site de TechnoLibre, par le lien entre les sites
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `devis_opnsense.py --verifier` est
|
||||
CONFORME aux deux sites. Témoin : sans la clé `administration`, le devis des deux sites est
|
||||
identique au caractère près à celui de l'ancien code. `make frontiere-plan` n'annonce que des
|
||||
créations : 6 à `requin`, 8 à `bifrost-1`, aucun retrait. **Pas encore appliqué** aux frontières.
|
||||
|
||||
**Décision de Daniel (2026-10-14)** : administrer le site de TechnoLibre depuis le site de
|
||||
Chezlepro par le lien existant (`requin` opt10 ↔ `bifrost-1` opt11), sans VPN d'administration
|
||||
de plus. Elle remplace l'administration par le relais (2026-09-29), dont le chemin n'existait pas :
|
||||
SSH vers `requin` par le relais expire à l'échange de bannière.
|
||||
|
||||
**Une clé `administration` sur un lien de `opnsense_liens_sites`.**
|
||||
- `entrante` (site administré) : les réseaux d'administration du pair (sa gestion et son tunnel
|
||||
des admins, lus dans SON plan) entrent par l'interface du lien, en SSH et HTTPS, vers la patte
|
||||
de gestion de cette frontière seulement ; routes de retour vers le prochain saut.
|
||||
- `sortante` (site qui administre) : la gestion et le tunnel des admins sortent vers la patte de
|
||||
gestion du pair (le rebond), en SSH et HTTPS, et rien d'autre ; route d'hôte vers le prochain
|
||||
saut. Les règles du tunnel portent sur le groupe `wireguard`.
|
||||
- La garde des routes du lien admet ces alias (`SETOPS_ADMPAIR_*`, `SETOPS_REBOND_*`).
|
||||
- Les pairs WireGuard admettaient déjà ces adresses : il ne manquait que le filtre et les routes.
|
||||
- `vpn_admin.py` ajoute le rebond de chaque pair aux réseaux que le tunnel `admins` route.
|
||||
|
||||
**Une fausse alerte corrigée.** `frontiere-plan` déclarait « absentes du noyau » les routes
|
||||
d'hôte (`10.17.19.11/32`…) : FreeBSD les affiche sans masque. La lecture compare les deux
|
||||
formes ; une route vraiment absente reste signalée (témoin sur une table fictive).
|
||||
|
||||
**Reste** : appliquer `requin` puis `bifrost-1` ; la route `10.31.0.1/32` au poste ; l'essai de
|
||||
bout en bout (SSH vers `requin`, `maurice` par le rebond, l'API en HTTPS).
|
||||
|
||||
## 2026-10-14 (151) — Le second serveur de noms, désigné : `dns1.technolibre.ca`
|
||||
|
||||
**95 preuves.** `make verifier` passe (site de Chezlepro), `make test` passe. Témoin de bout en
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ Usage :
|
|||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
|
|
@ -355,8 +356,22 @@ def routes_du_noyau(api: Frontiere) -> set[str] | None:
|
|||
lignes = rep if isinstance(rep, list) else (rep.get("rows") or rep.get("items"))
|
||||
if not lignes:
|
||||
continue
|
||||
return {str(r.get("destination") or "").strip() for r in lignes
|
||||
if isinstance(r, dict)}
|
||||
# UNE ROUTE D'HOTE S'ECRIT SANS MASQUE (mesure du 2026-10-14). FreeBSD affiche
|
||||
# `10.17.19.11` (drapeau `H`), le devis demande `10.17.19.11/32` : les routes /32 du lien
|
||||
# entre les sites etaient dites « absentes du noyau » alors qu'il les portait. On garde
|
||||
# la forme lue ET sa forme masquee.
|
||||
out: set[str] = set()
|
||||
for r in lignes:
|
||||
if not isinstance(r, dict):
|
||||
continue
|
||||
d = str(r.get("destination") or "").strip()
|
||||
out.add(d)
|
||||
try:
|
||||
a = ipaddress.ip_address(d)
|
||||
out.add(f"{d}/{a.max_prefixlen}")
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1587,6 +1587,87 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"description": f"Primaire DNS d'un locataire de {_pair}, par le "
|
||||
f"tunnel (prime sur la route de reprise)"})
|
||||
|
||||
# L'ADMINISTRATION PAR LE LIEN (2026-10-14, decision de Daniel). Le site de TechnoLibre
|
||||
# n'a pas de tunnel d'administration a lui : depuis le site de Chezlepro, on entre par
|
||||
# le lien, jusqu'a la FRONTIERE du pair seulement — SSH (le rebond, d'ou tout le reste
|
||||
# comme sur place) et HTTPS (son API, pour le devis). Declare des deux cotes :
|
||||
# - `entrante` : les reseaux d'administration du pair (gestion + tunnel des admins,
|
||||
# lus dans SON plan) entrent par le tunnel jusqu'a notre patte de gestion ;
|
||||
# - `sortante` : les notres sortent vers la patte de gestion du pair.
|
||||
# Le chiffrement admettait deja ces adresses (pairs WireGuard, releve du 2026-10-14) ;
|
||||
# manquaient le filtre et les routes, les routes WireGuard etant desactivees.
|
||||
def _gestion_frontiere(_carte: dict) -> str:
|
||||
return next((str(h["ip"]) for h in underlay_mod.hotes(_carte)
|
||||
if str(h.get("role")) == "frontiere"
|
||||
and str(h.get("reseau")) == "management" and h.get("ip")), "")
|
||||
|
||||
def _reseaux_admin(_carte: dict) -> list[str]:
|
||||
_vpn_r = str(((underlay_mod.lire_plan_site("10-intrants.yml") or {})
|
||||
.get("acces_admin_vpn") or {}).get("reseau") or "")
|
||||
return sorted({str(r["sous_reseau"]) for r in underlay_mod.reseaux(_carte)
|
||||
if r.get("nom") == "management" and r.get("sous_reseau")}
|
||||
| ({_vpn_r} - {""}))
|
||||
|
||||
_adm = str(_lien.get("administration") or "")
|
||||
if _adm == "entrante":
|
||||
with underlay_mod.sur_le_site(_pair):
|
||||
_adm_pair = _reseaux_admin(underlay_mod.charger() or {})
|
||||
_ma_gestion = _gestion_frontiere(_u)
|
||||
if _adm_pair and _ma_gestion:
|
||||
alias[f"SETOPS_ADMPAIR_{_court}"] = {
|
||||
"type": "network", "contenu": _adm_pair,
|
||||
"description": f"Administration de {_pair} (gestion et tunnel des admins), "
|
||||
f"par le lien entre les sites"}
|
||||
alias["SETOPS_FRONTIERE_GESTION"] = {
|
||||
"type": "host", "contenu": [_ma_gestion],
|
||||
"description": "Patte de gestion de la frontiere — le rebond du site"}
|
||||
for _port, _chiffre, _quoi in (("22", "ssh", "SSH vers le rebond du site"),
|
||||
("443", "tls", "API et console de la frontiere")):
|
||||
regles.append({
|
||||
"sens": "in", "interface": _if_lien, "protocole": "tcp",
|
||||
"source": f"SETOPS_ADMPAIR_{_court}",
|
||||
"destination": "SETOPS_FRONTIERE_GESTION",
|
||||
"ports": [_port], "chiffrement": _chiffre, "role": "administration",
|
||||
"tenant": "INTERSITES",
|
||||
"raison": f"{_quoi}, depuis l'administration de {_pair}, par le tunnel. "
|
||||
f"Jusqu'a la frontiere seulement : la suite passe par le rebond.",
|
||||
})
|
||||
for _c in _adm_pair:
|
||||
routes.append({"reseau": _c, "prochain_saut": _saut, "tenant": "INTERSITES",
|
||||
"description": f"Administration de {_pair} : le retour par "
|
||||
f"le tunnel"})
|
||||
elif _adm == "sortante":
|
||||
with underlay_mod.sur_le_site(_pair):
|
||||
_rebond = _gestion_frontiere(underlay_mod.charger() or {})
|
||||
_gestion_ici = [c for c in _reseaux_admin(_u) if not _par_le_vpn(c)]
|
||||
_vpn_ici = [c for c in _reseaux_admin(_u) if _par_le_vpn(c)]
|
||||
if _rebond:
|
||||
alias[f"SETOPS_REBOND_{_court}"] = {
|
||||
"type": "host", "contenu": [_rebond],
|
||||
"description": f"Patte de gestion de la frontiere de {_pair} — son rebond"}
|
||||
for _itf, _src in ((if_gestion, _gestion_ici), (if_vpn, _vpn_ici)):
|
||||
if not _src:
|
||||
continue
|
||||
_a_src = f"SETOPS_ADMSORT_{'VPN' if _itf == if_vpn else 'GESTION'}"
|
||||
alias[_a_src] = {"type": "network", "contenu": _src,
|
||||
"description": "Administration de ce site, vers le rebond "
|
||||
"d'un site pair"}
|
||||
for _port, _chiffre, _quoi in (("22", "ssh", "SSH vers le rebond"),
|
||||
("443", "tls", "API et console de la frontiere")):
|
||||
regles.append({
|
||||
"sens": "in", "interface": _itf, "protocole": "tcp",
|
||||
"source": _a_src, "destination": f"SETOPS_REBOND_{_court}",
|
||||
"ports": [_port], "chiffrement": _chiffre, "role": "administration",
|
||||
"tenant": "INTERSITES",
|
||||
"raison": f"{_quoi} de {_pair}, par le lien entre les sites.",
|
||||
})
|
||||
routes.append({"reseau": f"{_rebond}/32", "prochain_saut": _saut,
|
||||
"tenant": "INTERSITES",
|
||||
"description": f"Rebond de {_pair}, par le tunnel"})
|
||||
elif _adm:
|
||||
print(f"note : lien vers {_pair} : `administration: {_adm}` inconnu (entrante, "
|
||||
f"sortante) — aucune regle emise.", file=sys.stderr)
|
||||
|
||||
# L'INTERIEUR REFUSE A VOIX HAUTE, LA FRONTIERE AUSSI (2026-10-01).
|
||||
#
|
||||
# Les machines refusaient deja en le disant (nftables : `reject` admin-prohibited ;
|
||||
|
|
@ -2187,11 +2268,15 @@ def verifier(devis: dict) -> tuple[bool, list[str]]:
|
|||
# alias du lien, et cet alias doit servir a une regle — rien de route que le filtre
|
||||
# refuserait.
|
||||
_lien = [r for r in devis.get("routes") or [] if r.get("tenant") == "INTERSITES"]
|
||||
_alias_lien = {n for n in devis["alias"] if n.startswith("SETOPS_DNSPUB_")
|
||||
# L'administration par le lien (2026-10-14) route des RESEAUX (ceux du pair) : un contenu
|
||||
# qui porte deja son masque se compare tel quel.
|
||||
_alias_lien = {n for n in devis["alias"] if n.startswith(("SETOPS_DNSPUB_", "SETOPS_ADMPAIR_",
|
||||
"SETOPS_REBOND_"))
|
||||
or n == "SETOPS_DNS_PRIMAIRES_DISTANTS"}
|
||||
_servis = {n for n in _alias_lien for r in devis["regles"]
|
||||
if n in (str(r["source"]).lstrip("!"), str(r["destination"]).lstrip("!"))}
|
||||
_adresses_lien = {f"{c}/32" for n in _servis for c in devis["alias"][n]["contenu"]}
|
||||
_adresses_lien = {c if "/" in str(c) else f"{c}/32"
|
||||
for n in _servis for c in devis["alias"][n]["contenu"]}
|
||||
for r in _lien:
|
||||
if str(r["reseau"]) not in _adresses_lien:
|
||||
erreurs.append(f"{r['reseau']} est route par le lien entre les sites, mais aucune "
|
||||
|
|
|
|||
|
|
@ -155,9 +155,32 @@ def reseaux_joignables() -> list[str]:
|
|||
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()}
|
||||
out |= set(rebonds_des_pairs())
|
||||
return sorted(out, key=lambda c: ipaddress.ip_network(c, strict=False))
|
||||
|
||||
|
||||
def rebonds_des_pairs() -> list[str]:
|
||||
"""Le rebond (patte de gestion de la frontiere) de chaque site pair que nos
|
||||
administrateurs administrent PAR LE LIEN (`opnsense_liens_sites`, `administration:
|
||||
sortante`, 2026-10-14) — en /32, comme le devis le route. Rien d'autre du pair.
|
||||
"""
|
||||
out: list[str] = []
|
||||
actifs = underlay_mod.sites_pairs_actifs()
|
||||
for lien in devis_mod.intrants_frontiere().get("opnsense_liens_sites") or []:
|
||||
pair = str(lien.get("site") or "")
|
||||
if str(lien.get("administration") or "") != "sortante" or pair not in actifs:
|
||||
continue
|
||||
try:
|
||||
with underlay_mod.sur_le_site(pair):
|
||||
carte = underlay_mod.charger() or {}
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
out += [f"{h['ip']}/32" for h in underlay_mod.hotes(carte)
|
||||
if str(h.get("role")) == "frontiere" and str(h.get("reseau")) == "management"
|
||||
and h.get("ip")]
|
||||
return sorted(set(out))
|
||||
|
||||
|
||||
def _supernets_locataires() -> list[str]:
|
||||
"""Les supernets des locataires heberges — decouverts, jamais listes."""
|
||||
import devis_reseau
|
||||
|
|
|
|||
Loading…
Reference in a new issue