From ee386dca002e71a6e151b7e006472fcb7d2c609b Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Thu, 17 Sep 2026 10:20:46 -0400 Subject: [PATCH] materiel : la frontiere expose ses temperatures, seul le Prometheus du site les lit os-node_exporter sur la patte de supervision, declare dans la carte du boitier : cible et hote Icinga en derivent. Les flux vers la frontiere ne s'ouvrent plus a tous pour un role qui n'est pas le socle. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 21 +++++++++++++++++++++ docs/audit/preuve-2026-09-17.md | 10 +++++----- docs/carte-set-ops.md | 4 ++-- docs/registre-flux.md | 3 ++- roles/serveur_prometheus/meta/flux.yml | 11 +++++++++++ scripts/devis_opnsense.py | 26 ++++++++++++++++++++++++-- scripts/site_inventaire.py | 24 ++++++++++++++++++++++-- 7 files changed, 87 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa063b..0be842f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # CHANGELOG — Set-OPS +## 2026-09-17 (2) — La frontiere rejoint la supervision du materiel + +- **Exportateur** : greffon officiel `os-node_exporter` installe et configure par l'API + d'OPNsense, n'ecoutant que sur la patte de supervision (`10.37.36.1:9100`), collecteurs + utiles seulement. Declare dans `opnsense.yml` (`opnsense_exportateur_metriques`) : c'est ce + fait du boitier qui fait deriver la cible Prometheus (`job="frontiere"`, `hote="bifrost-1"`) + et l'hote Icinga — sans lui, rien n'est derive. +- **Flux** `serveur_prometheus -> frontiere:9100`. Le bloc des flux vers la frontiere ouvrait + TOUT flux a chaque locataire et chaque zone : juste pour l'heure du socle, beaucoup trop + large pour une scrutation. Un role autre que le socle ne part desormais que de SES machines + du site. Et le saut des flux `frontiere` chez les locataires arrive AVANT la creation de + l'alias du role : le plan montrait deux alias `SETOPS_*_SRV_PROMETHEUS` que rien ne + referencait. +- Plan de la frontiere : 1 regle (opt9, tcp/9100, `SETOPS_SITE_SERVEUR_PROMETHEUS -> + SETOPS_FRONTIERE`), appliquee et chargee. +- **Carte d'orientation** : les equipements ne sont plus « sans supervision » (hyperviseurs et + frontiere) ; les commutateurs le restent. + +Mesure : `up{job="frontiere"}` = 1 ; `check_materiel.py --hote bifrost-1` : OK, 4 coeurs a +41 °C. `make prouver` : CONFORME, 82 OK. + ## 2026-09-17 (1) — Le materiel se regarde et se juge : hyperviseurs Les hyperviseurs exposaient deja 68 temperatures, leurs ventilateurs, SMART et l'usure NVMe, diff --git a/docs/audit/preuve-2026-09-17.md b/docs/audit/preuve-2026-09-17.md index d052e20..6e1c0f7 100644 --- a/docs/audit/preuve-2026-09-17.md +++ b/docs/audit/preuve-2026-09-17.md @@ -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 : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 flux, schéma + matrice OK. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 120 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. | @@ -46,7 +46,7 @@ | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 71 scripts expliques et atteignables, 131 cibles make documentees, 69 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (147 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 42 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). | -| P34 | Chaque document declare son lecteur | — | ✅ OK | 48 document(s) declarent leur lecteur (42 genere(s) exempte(s)). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 48 document(s) declarent leur lecteur (43 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 8 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | @@ -60,9 +60,9 @@ | 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 97 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 (137 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 282 regles `pass`), tous non consignes et tous motives. | +| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 99 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 (138 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 283 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 — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 8dcfab8..70d6646 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous. | rôles | 69 | `roles/*/` | | README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | documents | 44 | `docs/*.md` | -| pièces d'audit | 47 | `docs/audit/*` | +| pièces d'audit | 48 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | | décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` | | décisions renversées | 3 | lignes `\| **D-nn** —` du même document | @@ -72,7 +72,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis | Voûte au déploiement | secret jamais en clair ; **une voûte, une clé** depuis le 2026-08-28 | `ANSIBLE_VAULT_IDENTITY_LIST` construit par `scripts/voutes.py` (clé nommée `~/.config/setops-vault-`) ; déréférencé par `lookup('vars', )` | `autorisation.md` §6.1 | | Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` | | Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 | -| Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* — hyperviseurs, commutateurs, frontière — n'ont toujours ni inventaire, ni sauvegarde de configuration, ni supervision | `hebergeur-exploitation.md` | +| Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* : hyperviseurs et frontière **supervisés** depuis le 2026-09-17 — températures, ventilateurs, SMART, usure NVMe, un catalogue (`scripts/materiel.py`) lu par le tableau Grafana « Matériel » et le service Icinga `materiel` (`supervision-conception.md`). Les commutateurs restent sans supervision ; aucun équipement n'a de sauvegarde de configuration | `hebergeur-exploitation.md` | | Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` | | Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) | | **DNS public** | le locataire **écrit** ses zones `autorite: primaire-cache`, le site les **sert** en secondaire, un site pair les réplique. Transfert ouvert **à la clé TSIG seule** (`allow-axfr-ips` et TSIG sont alternatifs dans PowerDNS) ; instance `pdns@public` à part pour que le site ne puisse jamais interroger la zone `.internal` | `roles/serveur_dns_public/`, `roles/serveur_powerdns/tasks/zones-publiques.yml` ; relations dérivées dans `scripts/site_inventaire.py` ; mots de flux `dns_public_site` / `primaires_dns_locataires` ; garde **P82** | `dns-interne.md` §Zones publiques | diff --git a/docs/registre-flux.md b/docs/registre-flux.md index b3ed0f8..46a331b 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -111,6 +111,7 @@ | `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). | | `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). | | `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. | +| `serveur_prometheus` | egress | 9100 | tcp | frontiere | clair | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. | | `serveur_prometheus` | egress | 9100 | tcp | fabric | clair | Scrape des hyperviseurs : la machine qui PORTE les VM etait invisible de la supervision qui les surveille. Elle ne peut pas pousser (route par defaut gelee, D-57), donc on la tire. | | `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. | | `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. | @@ -128,7 +129,7 @@ ## Synthèse chiffrement -- **clair** : 49 flux +- **clair** : 50 flux - **n-a** : 8 flux - **ssh** : 8 flux - **starttls** : 6 flux diff --git a/roles/serveur_prometheus/meta/flux.yml b/roles/serveur_prometheus/meta/flux.yml index ed6a7f0..158bd39 100644 --- a/roles/serveur_prometheus/meta/flux.yml +++ b/roles/serveur_prometheus/meta/flux.yml @@ -24,6 +24,17 @@ flux: # Prometheus tire donc, dans le sens que la frontiere route deja pour `serveur_ops_site`. # 7887 lignes de metriques sur `asgard`, dont 1005 unites systemd avec leur etat — soit # davantage que ce qu'un ping ou un porteur de sante auraient dit. + # LA FRONTIERE ELLE-MEME (2026-09-17) : ses temperatures, sa charge, ses interfaces. Le + # greffon `os-node_exporter` n'ecoute que sur sa patte de supervision. + - sens: egress + port: 9100 + protocole: tcp + pair: frontiere + chiffrement: clair + raison: >- + Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de + supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni + charge dans la supervision. - sens: egress port: 9100 protocole: tcp diff --git a/scripts/devis_opnsense.py b/scripts/devis_opnsense.py index ea84119..2f119bd 100644 --- a/scripts/devis_opnsense.py +++ b/scripts/devis_opnsense.py @@ -535,6 +535,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: tenants_sans_inventaire.append(nom) continue for role, fl in retenus: + # AVANT L'ALIAS (2026-09-17). Le saut plus bas arrivait APRES la creation de + # l'alias du role : un flux vers la frontiere declare par `serveur_prometheus` + # posait `SETOPS__SRV_PROMETHEUS` chez chaque locataire, qu'aucune regle + # ne referencait. Le plan l'a montre avant toute ecriture. + if "frontiere" in _pairs(fl): + continue if role == ROLE_SOCLE: destination = alias_tenant else: @@ -1339,7 +1345,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: alias["SETOPS_FRONTIERE"] = { "type": "host", "contenu": _pattes_frontiere, - "description": "Pattes de la frontiere active (autorite de temps)", + "description": "Pattes de la frontiere active (heure, metriques)", } # LES PATTES D'ARRIVEE. Un locataire entre par le lien de transit — ses # passerelles de zone sont tenues par le SDN, pas par la frontiere. Une @@ -1350,7 +1356,23 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict: _portees_f += [("SETOPS_SITE", z) for z in sorted({_if_de(m.get("reseau")) for m in _machines_site})] for _role_f, _fl_f in _flux_frontiere: - for _src_f, _if_f in _portees_f: + # LE SOCLE PARLE A LA FRONTIERE DEPUIS PARTOUT ; UN ROLE, DEPUIS SES MACHINES + # (2026-09-17). L'heure est demandee par toutes les machines : les portees + # ci-dessus sont justes pour elle. La scrutation des metriques de la + # frontiere, elle, n'est l'affaire que du Prometheus du SITE — l'ouvrir a + # chaque locataire et a chaque zone aurait donne la carte de charge du + # boitier a qui la demande. + if _role_f == "serveur_debian": + _portees_role = _portees_f + else: + _portees_role = [(nom_alias(_role_f, "SITE"), z) for z in + sorted({_if_de(m.get("reseau")) for m in _machines_site + if _role_f in _services_site.get(m["nom"], [])})] + if not _portees_role: + print(f"note : {_role_f} declare un flux vers la frontiere et aucune " + f"machine du site ne le porte — aucune regle emise.", + file=sys.stderr) + for _src_f, _if_f in _portees_role: regles.append({ "sens": "in", "interface": _if_f, diff --git a/scripts/site_inventaire.py b/scripts/site_inventaire.py index fff7620..5dd96ec 100755 --- a/scripts/site_inventaire.py +++ b/scripts/site_inventaire.py @@ -536,6 +536,21 @@ def inventaire() -> dict: if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11.")} _fabric_metriques = sorted(_fabric_nommes) + # LA FRONTIERE, QUAND SON EXPORTATEUR EST DECLARE (2026-09-17). L'adresse est celle de sa + # patte sur la zone declaree, lue dans l'underlay ; le nom est celui du boitier. + _frontiere_metriques: dict[str, str] = {} + try: + _carte_o = U.chemin().resolve().parent / "opnsense.yml" if U.chemin() else None + _expo_f = ((yaml.safe_load(_carte_o.read_text(encoding="utf-8")) or {}) + .get("opnsense_exportateur_metriques") or {}) if _carte_o and _carte_o.is_file() else {} + except (OSError, yaml.YAMLError): + _expo_f = {} + if _expo_f: + for _h in U.hotes(u): + if (str(_h.get("role")) == "frontiere" and str(_h.get("etat") or "actif") == "actif" + and str(_h.get("reseau")) == str(_expo_f.get("patte")) and _h.get("ip")): + _frontiere_metriques[f"{_h['ip']}:{_expo_f.get('port', 9100)}"] = str(_h.get("nom")) + # CE QUE LA SUPERVISION IRA VOIR : chaque patte de la frontiere, nommee par la zone # qu'elle sert. Les commutateurs restent dehors — D-48 les tient hors flotte, et rien # ne les rend interrogeables sans leur ouvrir un acces qu'on ne veut pas leur ouvrir. @@ -827,7 +842,10 @@ def inventaire() -> dict: "setops_materiel_hotes": [ {"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "fabric", "role": "hyperviseur"} - for _c, _n in sorted(_fabric_nommes.items(), key=lambda x: x[1])]} + for _c, _n in sorted(_fabric_nommes.items(), key=lambda x: x[1])] + + [{"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "frontiere", + "role": "frontiere"} + for _c, _n in sorted(_frontiere_metriques.items(), key=lambda x: x[1])]} if (_fabric_nommes and ({"serveur_grafana", "serveur_icinga"} & set(_groupes_de(nom)))) else {}), # La fabric, en job separe — voir `_fabric_metriques` plus haut. @@ -835,7 +853,9 @@ def inventaire() -> dict: # `hotes` : chaque serie porte `hote="asgard"`. Sans lui, un tableau du # materiel legende « 192.168.11.43:9100 » — et c'est a l'exploitant de se # souvenir quelle machine chauffe. - [{"job": "fabric", "cibles": _fabric_metriques, "hotes": _fabric_nommes}]} + [{"job": "fabric", "cibles": _fabric_metriques, "hotes": _fabric_nommes}] + + ([{"job": "frontiere", "cibles": sorted(_frontiere_metriques), + "hotes": _frontiere_metriques}] if _frontiere_metriques else [])} if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}), **communes, # En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le