diff --git a/CHANGELOG.md b/CHANGELOG.md index c00e766..7eaeca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # CHANGELOG — Set-OPS +## 2026-08-03 (suite 9) — le SSH inter-nœud était perdu + +En éclatant les règles par rôle source, un défaut de ma première version est apparu : je +**sautais le flux entier** dès qu'un de ses pairs valait `externe`. + +Or le SSH du socle est déclaré `pair: [flotte, externe]`. La moitié `externe` relève bien de +la frontière — mais la moitié **`flotte`**, le SSH entre hôtes, celui d'Ansible, était perdue. +Sous une politique `DROP`, **plus aucun hôte n'aurait été joignable en SSH depuis l'intérieur**. +Même piège pour le SMTP interne de Postfix, déclaré `[externe, client_smtp]`. + +`externe` est désormais sauté **pair par pair**, jamais le flux entier. 36 groupes, 56 règles. + +### Ajouté — ce qui n'a aucune règle entrante, et pourquoi +Onze rôles portés n'ont aucune règle entrante : sous `DROP`, ils sont injoignables. C'est +voulu dans les onze cas — la boucle locale pour Prometheus, Redis, rspamd, Icinga et Unbound, +la frontière seule pour nginx, aucun service pour `serveur_durci` et les clients. + +Le devis les **nomme avec leur motif** au lieu de laisser un lecteur le vérifier lui-même. Et +un douzième motif existe, marqué `/!\` : « flux entrants déclarés mais aucune source résolue +ici » — celui-là serait un vrai trou. + ## 2026-08-03 (suite 8) — le pare-feu Proxmox, troisième lecture du même registre `make devis-proxmox-fw` (**preuve P25**). Le filtrage est-ouest intra-tenant est désormais diff --git a/docs/audit/preuve-2026-08-03.md b/docs/audit/preuve-2026-08-03.md index 3036912..3ebdf2e 100644 --- a/docs/audit/preuve-2026-08-03.md +++ b/docs/audit/preuve-2026-08-03.md @@ -13,7 +13,7 @@ | # | Preuve | Affirmations | Statut | Detail | |---|---|---|---|---| -| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | | +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | :1: SyntaxWarning: invalid decimal literal | | P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. | | P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. | | P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | @@ -37,7 +37,7 @@ | P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (19 sections). | | P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 5 reseau(x), aucune collision avec la plage tenant. | | P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 26 regles, 2 routes, admin=192.168.254.2/32,192.168.255.0/24,192.168.255.2/32. | -| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 34 groupe(s), 52 regle(s). | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 36 groupe(s), 56 regle(s). | ## Couverture des affirmations ✅ du registre diff --git a/scripts/devis_proxmox_fw.py b/scripts/devis_proxmox_fw.py index 4d060d0..91a2ae1 100644 --- a/scripts/devis_proxmox_fw.py +++ b/scripts/devis_proxmox_fw.py @@ -141,8 +141,14 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: if fl.get("sens") != "ingress": continue pairs = _pairs(fl) - if "externe" in pairs: - continue # la frontiere s'en charge + # `externe` est saute PAIR PAR PAIR, jamais le flux entier : le SSH du + # socle est declare `[flotte, externe]`, et sauter tout le flux perdait + # la moitie `flotte` — plus aucun hote joignable en SSH depuis + # l'interieur, Ansible compris. Meme piege pour le SMTP interne de + # Postfix, declare `[externe, client_smtp]`. + pairs = [p for p in pairs if p != "externe"] + if not pairs: + continue # purement externe : la frontiere s'en charge if pairs == ["localhost"]: continue # boucle locale : aucune regle inter-noeud if not _resoudre_sources(data, fl.get("pair"), tous, ips): @@ -198,7 +204,29 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: if porte: affect.append({"hote": hote, "vmid": vmid, "groupes": porte}) + # Roles portes mais SANS regle entrante : sous une politique DROP ils sont + # injoignables. C'est souvent voulu (ils n'ecoutent que sur la boucle locale, ou + # seule la frontiere les atteint) — mais l'absence doit se lire, pas se deviner. + avec = {g["role"] for g in groupes} + muets: list[dict] = [] + for groupe in sorted(_enfants(data)): + if not est_groupe_operationnel(groupe) or groupe in avec: + continue + if not _hotes_du_groupe(data, groupe): + continue + ing = [f for f in flux_par_role.get(groupe, []) if f.get("sens") == "ingress"] + if not ing: + motif = "aucun service ecoute" + elif all(set(_pairs(f)) <= {"localhost"} for f in ing): + motif = "n'ecoute que sur la boucle locale" + elif all(set(_pairs(f)) <= {"externe"} for f in ing): + motif = "joignable seulement par la frontiere" + else: + motif = "A VERIFIER — flux entrants declares mais aucune source resolue ici" + muets.append({"role": groupe, "motif": motif}) + blocs.append({ + "muets": muets, "tenant": nom, "etiquette": etiquette, "ipsets": {k: v for k, v in ipsets.items() if k in utilises}, "groupes": groupes, @@ -251,6 +279,11 @@ def rendre(devis: dict) -> str: for a in b["affectations"]: out.append(f" vmid {a['vmid'] or '(non materialise)'} {a['hote']:<18} " f"-> {', '.join(a['groupes'])}") + if b["muets"]: + out += ["", "### Sans regle entrante — injoignables sous la politique DROP"] + for m in b["muets"]: + marque = " /!\\" if m["motif"].startswith("A VERIFIER") else "" + out.append(f" {m['role']:<22} {m['motif']}{marque}") if b["sans_source"]: out += ["", "# /!\\ Flux ingress sans source resolue dans ce tenant (aucune regle emise) :"] out += [f"# {x}" for x in b["sans_source"]]