une source vide n est pas « tout le monde »
Les journaux de la fabric, et le defaut de classe que leur mise en place a revele. 10 hotes dans Loki (7 VM du site + 3 hyperviseurs), site a 67 services tous OK contre 51 avant. METRIQUES TIREES, JOURNAUX POUSSES. Un scrape part du collecteur et doit REVENIR : il exige un chemin symetrique, que la route par defaut gelee interdit. Un push part de la source et n attend qu un accuse. Trois trous dans les generateurs. Le devis de la frontiere ne connaissait fabric qu en DESTINATION - un flux entrant tombait dans « rien d autre n entre » sans rien dire. La regle etait posee sur la patte de la destination alors que D-61 raisonne en ARRIVEE, et opt7 manquait a la table des zones. Et le plus grave : fabric etait un mot RECONNU mais NON RESOLU. Source vide, donc regle sans saddr - tcp dport 3100 accept, ouvert a tous. Un mot reconnu mais non resolu est pire qu un mot inconnu : celui-ci serait refuse a la validation, celui-la produit une porte grande ouverte qui a l air d un flux precis. LA CLASSE ENTIERE EST REFERMEE. Le defaut n etait pas propre a fabric : toute paire nommant un ensemble et ne resolvant rien ouvrait le port. Trouve en lisant les fichiers generes, l API de supervision d un tenant etait ouverte a tous parce que pair: serveur_backup ne resout rien - cet ecosysteme depose chez le site. expositions et externe, eux, veulent bien dire tout le monde : c est une intention. Ailleurs, pas de source, pas de regle, et le generateur le DIT. Trois regles se referment, chacune verifiee AVANT d appliquer. Deux etaient sans effet ; la troisieme portait Grafana, qui ecoute bien sur 3000. Sa declaration disait pair: edge - juste chez un tenant, faux au site qui n a pas d edge. Ajout de admin : ce qui etait accidentel devient explicite. Et la mesure a montre autre chose : Grafana etait DEJA injoignable depuis le poste, la frontiere ne laissant pas passer le 3000. Le site a une console deployee et sans chemin d acces. Ce n est pas corrige ici, mais c est dit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
parent
5abf20102d
commit
0778979b89
8 changed files with 231 additions and 10 deletions
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -1,5 +1,75 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-10 (15) — Une source vide n'est pas « tout le monde »
|
||||
|
||||
Les journaux de la fabric, et le defaut de classe que leur mise en place a revele.
|
||||
|
||||
journaux 10 hotes dans Loki (7 VM du site + 3 hyperviseurs)
|
||||
site 67 services, tous OK (51 avant)
|
||||
|
||||
### Metriques TIREES, journaux POUSSES — et ce n'est pas un caprice
|
||||
|
||||
Un scrape part du collecteur et doit REVENIR : il exige un chemin symetrique, que la route
|
||||
par defaut gelee (D-57) interdit. Un push part de la source et n'attend qu'un accuse : la
|
||||
route SPECIFIQUE vers les zones du site suffit — celle qu'on venait justement de completer.
|
||||
|
||||
### Trois trous dans les generateurs, du meme jour
|
||||
|
||||
**Le devis de la frontiere ne connaissait `fabric` qu'en DESTINATION.** Un flux entrant
|
||||
depuis la fabric tombait dans « rien d'autre n'entre » et n'emettait AUCUNE regle, sans
|
||||
rien dire. Meme forme que le trou des integrations universelles, trouve le matin meme.
|
||||
|
||||
**La regle etait sur la mauvaise patte.** D-61 fait raisonner le devis en ARRIVEE ; la
|
||||
regle etait posee sur l'interface de la DESTINATION. `opt7` — la patte face a la fabric,
|
||||
libellee PROXMOX — manquait a la table des zones, qui ne recensait que le site. Elle y est,
|
||||
et l'interface se derive desormais du reseau ou vivent les hyperviseurs.
|
||||
|
||||
**Et le plus grave : `fabric` etait un mot RECONNU mais NON RESOLU.** Le generateur de
|
||||
pare-feu d'hote l'acceptait comme valide, ne trouvait aucune adresse, et une source vide
|
||||
produit une regle sans `saddr` :
|
||||
|
||||
tcp dport 3100 accept # ouvert a tout le monde
|
||||
|
||||
*Un mot reconnu mais non resolu est pire qu'un mot inconnu* : celui-ci serait refuse a la
|
||||
validation, celui-la produit une porte grande ouverte qui a l'air d'un flux precis.
|
||||
|
||||
### La classe entiere, refermee
|
||||
|
||||
Le defaut n'etait pas propre a `fabric`. **Toute** paire nommant un ensemble et ne
|
||||
resolvant rien ouvrait le port. Trouve en lisant les fichiers generes :
|
||||
|
||||
tcp dport 5665 accept # serveur_icinga, sur le mon-01 d'un TENANT
|
||||
|
||||
L'API de supervision ouverte a tous, parce que `pair: serveur_backup` ne resout rien — cet
|
||||
ecosysteme depose son etat chez le site et n'a pas de depot a lui.
|
||||
|
||||
`expositions` et `externe`, EUX, veulent bien dire « tout le monde » : ce sont des services
|
||||
publies, et leur ouverture est une INTENTION. Ailleurs : **pas de source, pas de regle** —
|
||||
et le generateur le DIT, parce qu'un flux tu en silence est une porte qu'on croit fermee.
|
||||
|
||||
Trois regles se sont refermees. Chacune verifiee AVANT d'appliquer :
|
||||
|
||||
| regle | verdict |
|
||||
|---|---|
|
||||
| `site-forge-01:3000` | rien n'ecoute — la forge sert 443 |
|
||||
| `mon-01:5665` (tenant) | deux autres regles couvrent les pousseurs |
|
||||
| `site-mon-01:3000` | **Grafana ecoute** — a corrige avant de fermer |
|
||||
|
||||
### Grafana : `edge` ET `admin`
|
||||
|
||||
Chez un tenant, le nginx d'edge termine le TLS : `edge` resout, c'est le seul chemin. Le
|
||||
SITE n'a PAS d'edge — chaque service s'y sert lui-meme. `edge` n'y resolvait donc rien, et
|
||||
la console etait ouverte a l'Internet PAR ACCIDENT, sous un commentaire qui parlait d'un
|
||||
edge inexistant.
|
||||
|
||||
Ajouter `admin` rend explicite ce qui etait accidentel. Et la mesure a montre autre chose :
|
||||
**Grafana etait deja injoignable depuis le poste** — la frontiere ne laisse pas passer le
|
||||
3000. La regle ouverte ne servait a rien, sauf a rester ouverte si la frontiere s'ouvrait
|
||||
un jour.
|
||||
|
||||
*Le site a donc une console deployee et sans chemin d'acces.* Ce n'est pas corrige ici,
|
||||
mais c'est dit.
|
||||
|
||||
## 2026-09-10 (14) — Le site surveille enfin sa fabric
|
||||
|
||||
La supervision du site voyait ses sept VM et rien d'autre. Mesure de depart, depuis
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
|
||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 41 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 105 flux, schéma + matrice OK. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 106 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 13 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 49 groupe(s), 81 regle(s). |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 49 groupe(s), 85 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 34 VM placee(s), aucun nom ni VMID en collision. |
|
||||
|
|
@ -55,14 +55,14 @@
|
|||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 55 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 153 regle(s) du site. |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 154 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 89 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (123 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 215 regles `pass`), tous non consignes et tous motives. |
|
||||
| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (124 lignes). |
|
||||
| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 216 regles `pass`), tous non consignes et tous motives. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
| `serveur_forgejo` | egress | 25 | tcp | serveur_postfix | starttls | Notifications courriel (relais via le MTA Postfix). |
|
||||
| `serveur_forgejo` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC et jetons auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||
| `serveur_forgejo` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Forgejo (verify-full). |
|
||||
| `serveur_grafana` | ingress | 3000 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge). |
|
||||
| `serveur_grafana` | ingress | 3000 | tcp | edge, admin | clair | Interface web : servie via l'edge la ou il y en a un, joignable depuis le plan d'administration partout — sans quoi un deploiement sans edge n'a plus de console. |
|
||||
| `serveur_grafana` | egress | 443 | tcp | edge | tls-requis | Authentification OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||
| `serveur_icinga` | ingress | 5665 | tcp | localhost | clair | API Icinga 2 consommée en local par Icinga Web 2 co-localisé. |
|
||||
| `serveur_icinga` | ingress | 5665 | tcp | client_backup | tls-requis | Rapport passif de chaque detenteur d'etat sur SON depot distant : le depot du site heberge du chiffre et ne peut pas le juger. |
|
||||
|
|
@ -60,6 +60,7 @@
|
|||
| `serveur_keycloak` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Persistance Keycloak dans PostgreSQL (verify-full). |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | client_journal | tls | Réception des journaux poussés par Alloy (client_journal) en HTTPS (http_tls_config, cert step-ca). |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | localhost | clair | Requêtes de Grafana co-localisé (datasource Loki en localhost). |
|
||||
| `serveur_loki` | ingress | 3100 | tcp | fabric | tls | Journaux des hyperviseurs. La machine qui PORTE les VM ecrivait ses journaux nulle part ailleurs que sur son propre disque — invisibles le jour ou c'est elle qui casse. |
|
||||
| `serveur_nextcloud` | ingress | 80 | tcp | edge | clair | Interface web servie via l'edge (TLS terminé à l'edge). |
|
||||
| `serveur_nextcloud` | egress | 443 | tcp | edge | tls-requis | Découverte OIDC auprès de Keycloak (via son FQDN publié à l'edge). |
|
||||
| `serveur_nextcloud` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Base de données Nextcloud (verify-full). |
|
||||
|
|
@ -118,6 +119,6 @@
|
|||
- **n-a** : 6 flux
|
||||
- **ssh** : 8 flux
|
||||
- **starttls** : 6 flux
|
||||
- **tls** : 10 flux
|
||||
- **tls** : 11 flux
|
||||
- **tls-cible** : 2 flux
|
||||
- **tls-requis** : 35 flux
|
||||
|
|
|
|||
|
|
@ -2,12 +2,27 @@
|
|||
# Flux réseau de Grafana (tableaux de bord). Voir docs/flux-conception.md.
|
||||
# Prometheus et Loki sont co-localisés (datasources en localhost) : aucun flux inter-nœud.
|
||||
flux:
|
||||
# `edge` ET `admin` — PARCE QUE TOUS LES DEPLOIEMENTS N'ONT PAS D'EDGE (2026-09-10).
|
||||
#
|
||||
# Chez un tenant, le nginx d'edge termine le TLS et parle en clair a Grafana : `edge`
|
||||
# resout, et c'est le seul chemin. Le SITE, lui, n'a PAS d'edge — chaque service s'y
|
||||
# sert lui-meme. `edge` n'y resolvait donc rien, et une source vide produisait une regle
|
||||
# SANS `saddr` : la console ouverte a l'Internet, par accident, avec un commentaire qui
|
||||
# parlait d'un edge inexistant.
|
||||
#
|
||||
# Le jour ou le generateur a cesse d'ouvrir sur une source vide, la regle a disparu et
|
||||
# l'acces avec elle — mesure : `ss` ne montre que `*:3000`, rien sur 443. Ajouter `admin`
|
||||
# rend explicite ce qui etait accidentel : la console se joint depuis le plan
|
||||
# d'ADMINISTRATION, qui est d'ou l'exploitant la regarde. Chez un tenant, la ligne ne
|
||||
# retire rien — elle ajoute l'operateur a l'edge.
|
||||
- sens: ingress
|
||||
port: 3000
|
||||
protocole: tcp
|
||||
pair: edge
|
||||
pair: [edge, admin]
|
||||
chiffrement: clair
|
||||
raison: "Interface web servie via l'edge (TLS terminé à l'edge)."
|
||||
raison: >-
|
||||
Interface web : servie via l'edge la ou il y en a un, joignable depuis le plan
|
||||
d'administration partout — sans quoi un deploiement sans edge n'a plus de console.
|
||||
- sens: egress
|
||||
port: 443
|
||||
protocole: tcp
|
||||
|
|
|
|||
|
|
@ -13,3 +13,22 @@ flux:
|
|||
pair: localhost
|
||||
chiffrement: clair
|
||||
raison: "Requêtes de Grafana co-localisé (datasource Loki en localhost)."
|
||||
|
||||
# LA FABRIC JOURNALISE AUSSI, ET ELLE, ELLE PEUT POUSSER (2026-09-10).
|
||||
#
|
||||
# Les metriques des hyperviseurs sont TIREES : leur route par defaut est gelee (D-57) et
|
||||
# le porteur de sante y expirait. Les journaux, eux, passent — parce que les routes
|
||||
# SPECIFIQUES vers les zones du site ont ete completees le meme jour (`10.0.35.0/24` et
|
||||
# `10.0.36.0/24` manquaient). Alloy pousse donc vers Loki comme n'importe quelle machine.
|
||||
#
|
||||
# LA DIFFERENCE AVEC LES METRIQUES N'EST PAS UN CAPRICE : un scrape part du collecteur et
|
||||
# doit REVENIR, ce qui exige un chemin symetrique ; un push part de la source et n'attend
|
||||
# qu'un accuse. La route completee suffit au second, pas au premier.
|
||||
- sens: ingress
|
||||
port: 3100
|
||||
protocole: tcp
|
||||
pair: fabric
|
||||
chiffrement: tls
|
||||
raison: >-
|
||||
Journaux des hyperviseurs. La machine qui PORTE les VM ecrivait ses journaux nulle
|
||||
part ailleurs que sur son propre disque — invisibles le jour ou c'est elle qui casse.
|
||||
|
|
|
|||
|
|
@ -787,6 +787,38 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
# jamais la zone entiere.
|
||||
_portees = [("SETOPS_SITE", z) for z in
|
||||
sorted({_if_de(m.get("reseau")) for m in _machines_site})]
|
||||
elif _entrant and "fabric" in _pairs_fl:
|
||||
# LA FABRIC PARLE AUSSI AU SITE (2026-09-10).
|
||||
#
|
||||
# `fabric` n'etait connu qu'en DESTINATION — « le runner du site pilote
|
||||
# les hyperviseurs ». Un flux ENTRANT depuis la fabric tombait donc dans
|
||||
# la branche « rien d'autre n'entre » et n'emettait AUCUNE regle, sans
|
||||
# rien dire. Meme forme que le trou des integrations universelles,
|
||||
# trouve le matin meme : le generateur ne voyait qu'une moitie du monde.
|
||||
#
|
||||
# Le cas concret : les hyperviseurs POUSSENT leurs journaux vers Loki.
|
||||
# Ils ne peuvent pas etre scrutes (route par defaut gelee, D-57) mais ils
|
||||
# peuvent emettre — un push n'attend qu'un accuse, la ou un scrape exige
|
||||
# un chemin symetrique.
|
||||
#
|
||||
# Une regle par ZONE du site ou vit le role vise, comme pour `flotte` :
|
||||
# le paquet penetre le pare-feu par la patte de la zone DESTINATAIRE.
|
||||
# L'INTERFACE EST CELLE PAR OU LE PAQUET ARRIVE (D-61), donc celle
|
||||
# qui fait face a la FABRIC — pas celle de la destination. Derivee du
|
||||
# reseau ou vivent les hyperviseurs, jamais ecrite : le jour ou la
|
||||
# fabric change de patte, la regle suit.
|
||||
# SEULEMENT LES RESEAUX DONT ON CONNAIT LA PATTE. Un hyperviseur a
|
||||
# trois adresses (gestion, transport, transit) ; deux de ces reseaux ne
|
||||
# touchent pas la frontiere et n'ont pas d'interface declaree. Sans ce
|
||||
# filtre, `_if_de` retombe sur la patte par DEFAUT et pose une regle sur
|
||||
# une interface que ce trafic n'emprunte jamais — une regle qui a l'air
|
||||
# d'ouvrir un flux et ne correspond a rien.
|
||||
_res_fabric = sorted({str(h.get("reseau")) for h in underlay_mod.hotes(_u)
|
||||
if h.get("role") == "hyperviseur"
|
||||
and str(h.get("reseau")) in if_zones})
|
||||
_portees = [("SETOPS_FABRIC", _if_de(r)) for r in _res_fabric]
|
||||
if not _portees:
|
||||
continue
|
||||
elif _entrant and "22" in _ports(_fl) and _admin_site:
|
||||
# Le SSH de gestion arrive par la patte d'ADMINISTRATION de la
|
||||
# frontiere, pas par celle du site : c'est la que le paquet penetre
|
||||
|
|
|
|||
|
|
@ -223,6 +223,28 @@ def _ip_par_hote(data: dict) -> dict[str, str]:
|
|||
return table
|
||||
|
||||
|
||||
def _adresses_fabric() -> list[str]:
|
||||
"""Les adresses du materiel de l'hebergeur : hyperviseurs et frontiere.
|
||||
|
||||
MEME SOURCE QUE L'ALIAS `SETOPS_FABRIC` du devis de la frontiere — la carte de
|
||||
l'hebergeur. Ecrire ces adresses ailleurs les ferait mentir le jour ou un noeud
|
||||
change de patte, ce qui est deja arrive : la frontiere a porte `.1` puis `.17`, et
|
||||
les hyperviseurs ont continue de renvoyer vers une adresse que plus personne ne
|
||||
portait, « sans qu'aucun journal ne le dise ».
|
||||
|
||||
Rend [] quand aucun underlay n'est monte : degrader, jamais deviner.
|
||||
"""
|
||||
try:
|
||||
import underlay as _u
|
||||
carte = _u.charger()
|
||||
if not carte:
|
||||
return []
|
||||
return sorted({str(h["ip"]) for h in _u.hotes(carte)
|
||||
if h.get("role") in ("hyperviseur", "frontiere") and h.get("ip")})
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict[str, str],
|
||||
site: bool = False) -> list[str]:
|
||||
"""pair -> IP sources concretes (pour une regle ingress). [] = pas de regle inter-noeud."""
|
||||
|
|
@ -236,6 +258,22 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict
|
|||
# `nftables_admin_ssh` ; `voisins_site` des supernets des autres tenants
|
||||
# federes ; `runner_site` du plan du SITE. Traites a part, plus bas.
|
||||
continue
|
||||
if p == "fabric":
|
||||
# LA FABRIC N'EST PAS UN GROUPE D'HOTES, ELLE EST DANS LA CARTE (2026-09-10).
|
||||
#
|
||||
# `fabric` figurait dans `MOTS_PAIR` — donc accepte comme mot valide — sans
|
||||
# aucune branche pour le resoudre. Il tombait dans le cas general, ne trouvait
|
||||
# aucun hote, et une source VIDE produit une regle SANS `saddr` :
|
||||
#
|
||||
# tcp dport 3100 accept # ouvert a tout le monde
|
||||
#
|
||||
# Un mot reconnu mais non resolu est pire qu'un mot inconnu : celui-ci serait
|
||||
# refuse a la validation, celui-la produit une porte grande ouverte qui a
|
||||
# l'air d'un flux precis.
|
||||
#
|
||||
# Les adresses viennent de l'underlay, comme celles du devis de la frontiere :
|
||||
# hyperviseurs et frontiere, le materiel qu'aucun agent n'habite.
|
||||
continue
|
||||
if p == "flotte":
|
||||
noms |= actifs_noms
|
||||
else:
|
||||
|
|
@ -245,6 +283,8 @@ def _resoudre_sources(data: dict, pair, actifs_noms: set[str], ip_par_hote: dict
|
|||
demandes = pair if isinstance(pair, list) else [pair]
|
||||
if "admin" in demandes:
|
||||
srcs |= set(_sources_admin_ssh_site() if site else _sources_admin_ssh())
|
||||
if "fabric" in demandes:
|
||||
srcs |= set(_adresses_fabric())
|
||||
if "voisins_site" in demandes:
|
||||
# DU COTE DU SITE, « VOISIN » N'EXCLUT PERSONNE (mesure du 2026-09-02).
|
||||
#
|
||||
|
|
@ -572,7 +612,36 @@ def generer_nftables(flux_par_role: dict[str, list[dict]], site: bool = False) -
|
|||
ports = reels
|
||||
srcs = _resoudre_sources(data, fl.get("pair"), actifs_noms, ip_par_hote, site=site)
|
||||
saddr = "" if not srcs else "ip saddr { " + ", ".join(srcs) + " } "
|
||||
local = "localhost" in (fl.get("pair") if isinstance(fl.get("pair"), list) else [fl.get("pair")])
|
||||
_pairs_fl = fl.get("pair") if isinstance(fl.get("pair"), list) else [fl.get("pair")]
|
||||
local = "localhost" in _pairs_fl
|
||||
# UNE SOURCE VIDE N'EST PAS « TOUT LE MONDE » (mesure du 2026-09-10).
|
||||
#
|
||||
# Une paire qui NOMME un ensemble — un role, `flotte`, `fabric`, `edge` —
|
||||
# et qui ne resout rien decrivait jusqu'ici une regle SANS `saddr`, donc
|
||||
# ouverte a l'Internet. Le silence de la resolution devenait une porte.
|
||||
#
|
||||
# CE QUE CA A COUTE, trouve en lisant les fichiers generes : sur le
|
||||
# `mon-01` d'un tenant,
|
||||
#
|
||||
# tcp dport 5665 accept # serveur_icinga
|
||||
#
|
||||
# L'API de supervision ouverte a tous, parce que `pair: serveur_backup` ne
|
||||
# resout rien — cet ecosysteme depose son etat chez le site et n'a pas de
|
||||
# depot a lui. La regle avait l'air d'un flux precis.
|
||||
#
|
||||
# `expositions` et `externe`, EUX, veulent bien dire « tout le monde » :
|
||||
# ce sont des services publies, et leur ouverture est une intention, pas un
|
||||
# accident. On ne les touche pas.
|
||||
#
|
||||
# Ailleurs : pas de source, pas de regle — et on le DIT, parce qu'un flux
|
||||
# tu en silence est une porte qu'on croit ouverte (ou fermee).
|
||||
_public = {"expositions", "externe"} & set(_pairs_fl)
|
||||
if not srcs and not local and not _public:
|
||||
print(f"note : {g} declare un flux entrant depuis "
|
||||
f"`{'/'.join(str(x) for x in _pairs_fl)}` qui ne resout aucune "
|
||||
f"source — aucune regle emise pour {hote} (une source vide "
|
||||
f"ouvrirait le port a tous).", file=sys.stderr)
|
||||
continue
|
||||
for port in ports:
|
||||
if local and not srcs:
|
||||
continue # lo deja accepte
|
||||
|
|
|
|||
|
|
@ -539,6 +539,12 @@ def inventaire() -> dict:
|
|||
_ip_icinga = str(_s.get("ip") or "")
|
||||
break
|
||||
|
||||
_ip_loki = ""
|
||||
for _n_app, _a_app in applications.items():
|
||||
if str(_a_app.get("groupe")) == "serveur_loki":
|
||||
_ip_loki = str((serveurs.get(_a_app.get("hote")) or {}).get("ip") or "")
|
||||
break
|
||||
|
||||
_hyperviseurs = {}
|
||||
for _h in U.hotes(u):
|
||||
if str(_h.get("role")) != "hyperviseur":
|
||||
|
|
@ -571,6 +577,10 @@ def inventaire() -> dict:
|
|||
# de noeud du cluster. Mesure : `curl: (6) Could not resolve host` sur les trois.
|
||||
**({"client_sante_icinga_url": f"https://{_ip_icinga}:5665"}
|
||||
if _ip_icinga else {}),
|
||||
# LES JOURNAUX, EUX, PARTENT D'ICI — et par une ADRESSE, meme raison que
|
||||
# ci-dessus : pas de plancher `/etc/hosts` sur un hyperviseur.
|
||||
**({"client_journal_loki_url":
|
||||
f"http://{_ip_loki}:3100/loki/api/v1/push"} if _ip_loki else {}),
|
||||
**communes,
|
||||
}
|
||||
groupes.setdefault("hyperviseurs", []).append(_nom)
|
||||
|
|
@ -596,6 +606,11 @@ def inventaire() -> dict:
|
|||
# Prometheus le remplace par sa propre cible `down`, que la sonde `collecte`
|
||||
# rapporte deja.
|
||||
groupes.setdefault("client_metrique", []).append(_nom)
|
||||
# LES JOURNAUX SE POUSSENT, EUX. La route specifique vers les zones du site — celle
|
||||
# qui manquait pour 35 et 36 — suffit a un push : il part de la source et n'attend
|
||||
# qu'un accuse. Un scrape, lui, exige que la reponse revienne par le meme chemin,
|
||||
# ce que la route par defaut gelee empeche. D'ou metriques TIREES, journaux POUSSES.
|
||||
groupes.setdefault("client_journal", []).append(_nom)
|
||||
|
||||
out: dict = {g: {"hosts": sorted(set(h))} for g, h in groupes.items()}
|
||||
out["site"] = {"hosts": sorted(hostvars)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue