diff --git a/CHANGELOG.md b/CHANGELOG.md index e09253b..beeb1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # CHANGELOG — Set-OPS +## 2026-09-10 (16) — La frontiere journalise a nouveau, et une garde veille cette fois + +Doctrine posee par l'exploitant : *des lors que le site a son Loki, la journalisation de la +frontiere et des hyperviseurs doit y etre dirigee.* + + 46 681 lignes recues de la frontiere en 30 min + site : 68 services, tous OK + +### Ce qui etait casse, et depuis quand + +La destination syslog d'OPNsense pointait `10.17.20.11:3100` — une adresse de **TENANT**, +sur le port de Loki, en **UDP**, ce que Loki ne sait pas lire. La VM a ete rasee ; une cible +morte a arrete TOUTE la journalisation d'OPNsense pendant dix jours. La destination a ete +ETEINTE pour reparer, et jamais remplacee. + +Deux fautes en une : la frontiere ne journalisait plus nulle part, **et** sa cible etait chez +un locataire — ce que D-87 refuse dans l'autre sens. + +L'intention, elle, etait juste : les bonnes facilites, `info` et au-dessus. On l'a REPRISE +telle quelle et change uniquement la destination — ces choix avaient ete faits, ce n'etait +pas a nous de les redecider. + +### Un traducteur, parce que Loki ne parle pas syslog + +`loki.source.syslog` dans l'Alloy qui tourne DEJA sur le collecteur — un second agent +n'aurait servi qu'a tenir deux configurations en phase. Port 1514 et non 514 : au-dessus de +1024, donc ecoute sans privilege. *Un collecteur qui aurait besoin des droits du systeme +pour entendre un equipement serait un mauvais echange.* + +### Une regle de relabel sans garde n'ignore pas : elle EFFACE + +Trois quarts d'heure sur un symptome absurde — la configuration deposee portait +`host = "bifrost-1"`, visible a l'oeil dans le fichier, et le flux arrivait sans etiquette. + + rule { source_labels = ["__syslog_connection_ip_address"] + target_label = "host" } # sans regex + +Sans `regex`, la regle correspond **toujours** — meme quand sa source n'existe pas — et pose +`host = ""`. Loki jette une etiquette vide, et celle de l'ecouteur disparaissait avec elle. +Les deux autres regles etaient gardees ; celle-la ne l'etait pas. + +Et la verification finale a corrige une seconde erreur, la mienne : `label/host/values` +rendait une liste en CACHE. La requete directe `{host="bifrost-1"}` rendait bien un flux. +*Un index qui ne montre pas une chose ne prouve pas qu'elle n'existe pas.* + +### La garde, qui manquait depuis l'incident + +`journaux-frontiere` demande a **Loki ce qu'il a RECU**, pas a la frontiere ce qu'elle croit +avoir envoye — la destination est le seul juge, et un emetteur qui parle a un trou noir se +porte tres bien. + +**La fenetre est DERIVEE du debit observe**, pas choisie : ~1 500 lignes/minute mesurees. +Une frontiere qui filtre ne se tait jamais dix minutes. + +Trois etats, tous eprouves sur une copie : + + frontiere muette rc=2 « LA FRONTIERE NE JOURNALISE PLUS » + Loki injoignable rc=3 « la sonde ne peut rien affirmer » + debit normal rc=0 « 973 ligne(s) recue(s) en 10m » + +Le `3` compte autant que le `2` : *« je ne peux rien affirmer » n'est pas « c'est casse »*. +Une sonde qui confondrait les deux accuserait la frontiere d'un silence qui serait le sien. + ## 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. diff --git a/docs/audit/preuve-2026-09-10.md b/docs/audit/preuve-2026-09-10.md index 84cdb37..7ababe9 100644 --- a/docs/audit/preuve-2026-09-10.md +++ b/docs/audit/preuve-2026-09-10.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 : 41 groupes classes, aucun cycle, aucune arete en arriere. | -| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 106 flux, schéma + matrice OK. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 39 rôles, 107 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), 85 regle(s). | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 49 groupe(s), 87 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. | @@ -45,7 +45,7 @@ | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 59 scripts expliques et atteignables, 116 cibles make documentees, 67 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 38 exigence(s) de role, toutes satisfaites (132 cle(s) declaree(s) par l'instance). | -| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 34 revendication(s) de port, aucune collision entre roles co-localises (36 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 44 document(s) declarent leur lecteur (36 genere(s) exempte(s)). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | @@ -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, 154 regle(s) du site. | +| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 7 machine(s) du plan retrouvees, 155 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 (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. | +| 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 (125 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 217 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 | @@ -76,7 +76,7 @@ | P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 47 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp | | P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:5, serveurs:3 champ(s) lus par validateur). | | P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s | -| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 23 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ | +| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 24 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ | | P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. | ## Couverture des affirmations ✅ du registre diff --git a/docs/registre-flux.md b/docs/registre-flux.md index 4743e98..90a0499 100644 --- a/docs/registre-flux.md +++ b/docs/registre-flux.md @@ -58,6 +58,7 @@ | `serveur_keycloak` | ingress | 8080 | tcp | edge | clair | Console et endpoints OIDC servis au navigateur et aux applications via l'edge (TLS terminé à l'edge). | | `serveur_keycloak` | egress | 636 | tcp | serveur_openldap | tls-requis | Fédération de l'annuaire OpenLDAP (LDAPS). | | `serveur_keycloak` | egress | 5432 | tcp | serveur_postgresql | tls-requis | Persistance Keycloak dans PostgreSQL (verify-full). | +| `serveur_loki` | ingress | 1514 | tcp | fabric | clair | Journaux de la frontiere. Elle n'accueille aucun agent et n'emet que du syslog ; sans ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. | | `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. | @@ -115,7 +116,7 @@ ## Synthèse chiffrement -- **clair** : 38 flux +- **clair** : 39 flux - **n-a** : 6 flux - **ssh** : 8 flux - **starttls** : 6 flux diff --git a/roles/client_journal/defaults/main.yml b/roles/client_journal/defaults/main.yml index 6dc5283..92fd5a8 100644 --- a/roles/client_journal/defaults/main.yml +++ b/roles/client_journal/defaults/main.yml @@ -57,3 +57,26 @@ client_journal_sonde_etat: "/var/lib/setops/journaux-jetees" # DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture. client_journal_depot_schema: >- {{ 'http' if (artefacts_amorcage | default('') | string | length > 0) else 'https' }} + +# --- RECEPTEUR SYSLOG (vide = pas de recepteur) --------------------------------------- +# +# CE QUI NE PEUT PAS PORTER D'AGENT DOIT QUAND MEME ETRE ENTENDU. La frontiere OPNsense +# n'accueille rien ; elle sait emettre du syslog. Le collecteur du site l'ecoute et le +# traduit vers Loki. +# +# CE QUE CA A COUTE DE NE PAS L'AVOIR : la destination syslog de la frontiere pointait une +# VM de TENANT, rasee depuis. Une cible morte a arrete TOUTE la journalisation d'OPNsense +# pendant dix jours, et rien ne l'a signale — c'est le meme silence qui a motive le `ttl` +# des sondes. La destination a ete eteinte pour reparer, et jamais remplacee : la frontiere +# ne journalisait plus nulle part. +# +# ET LA CIBLE ETAIT CHEZ UN LOCATAIRE, ce que D-87 refuse dans l'autre sens : les journaux +# du site ne vivent pas chez un tenant, pas plus que l'inverse. Le site a son Loki depuis +# le 2026-09-10 ; c'est la que ses equipements parlent. +# +# Vide par defaut : un tenant n'a pas de frontiere a ecouter. +client_journal_syslog_ecoute: "" +client_journal_syslog_protocole: "tcp" +# Le nom sous lequel les journaux recus apparaitront. Derive de la carte par l'inventaire +# du site — jamais fabrique ici. +client_journal_syslog_emetteur: "frontiere" diff --git a/roles/client_journal/templates/config.alloy.j2 b/roles/client_journal/templates/config.alloy.j2 index 8b0eebb..85a5cb0 100644 --- a/roles/client_journal/templates/config.alloy.j2 +++ b/roles/client_journal/templates/config.alloy.j2 @@ -19,3 +19,80 @@ loki.source.journal "journal" { host = "{{ inventory_hostname }}", } } + +{% if client_journal_syslog_ecoute %} +// --- RECEPTEUR SYSLOG : CE QUI NE PEUT PAS PORTER D'AGENT (2026-09-10) --- +// +// La frontiere OPNsense n'accueille aucun agent. Elle sait emettre du syslog, et rien +// d'autre. Loki ne parle pas syslog : il faut un traducteur, et Alloy en est un. +// +// POURQUOI CE RECEPTEUR VIT ICI plutot que dans un role a lui : c'est la meme machine qui +// pousse deja ses propres journaux vers le meme Loki. Un second agent ne servirait qu'a +// avoir deux configurations a garder en phase. +// +// L'ETIQUETTE `host` VIENT DE L'EMETTEUR, pas de nous : plusieurs equipements peuvent +// pointer ici, et les confondre sous le nom du recepteur rendrait les journaux +// inutilisables au moment ou l'on en a besoin. +loki.source.syslog "frontiere" { + listener { + address = "{{ client_journal_syslog_ecoute }}" + protocol = "{{ client_journal_syslog_protocole }}" + label_structured_data = true + labels = { job = "syslog", host = "{{ client_journal_syslog_emetteur }}" } + } + forward_to = [loki.relabel.syslog.receiver] +} + +// QUI A PARLE ? On veut la meme etiquette `host` que pour une machine qui pousse, sans +// quoi la requete change de forme selon la provenance et personne ne s'en souvient. +// +// DEUX SOURCES, DANS CET ORDRE, ET C'EST DELIBERE. +// +// L'en-tete RFC5424 porte un nom que l'emetteur se donne — pratique quand il est juste, +// et vide ou faux quand il ne l'est pas. Mesure du 2026-09-10 : ni les journaux reels de +// la frontiere, ni un message d'essai forge a la main avec un nom explicite n'ont produit +// d'etiquette. Le champ existe dans la specification ; il n'arrive pas ici. +// +// L'ADRESSE DE LA CONNEXION, ELLE, EST TOUJOURS LA — et elle ne s'invente pas : c'est le +// pare-feu qui l'observe, pas l'emetteur qui la declare. Un nom qu'on se donne peut +// mentir ; une adresse de connexion, non. +// +// CE QUI A FINALEMENT SERVI : l'etiquette posee sur l'ECOUTEUR, depuis la carte de +// l'hebergeur. Aucune des trois regles ci-dessous n'a produit quoi que ce soit — ni sur +// les journaux reels, ni sur un message forge a la main. On garde le relabel : le jour ou +// l'emetteur enverra un nom, il prendra le dessus. En attendant, le nom vient de la ou il +// est DECLARE, pas de la ou on l'espere. +// +// CE QUE CA SUPPOSE, ET QU'IL FAUT SAVOIR : un SEUL emetteur par ecouteur. Le site n'a +// qu'une frontiere ; le jour ou un second equipement pointe ici, il faudra un ecouteur par +// emetteur, ou faire enfin marcher le relabel. La supposition est ecrite pour qu'on la +// retrouve, plutot que decouverte devant des journaux melanges. +// +// La derniere regle qui correspond gagne : le nom l'emporte quand il existe. +loki.relabel "syslog" { + forward_to = [loki.write.loki.receiver] + // `regex` SUR LES TROIS, Y COMPRIS CELLE-CI (mesure du 2026-09-10). + // + // Sans elle, la regle correspond TOUJOURS — meme quand sa source n'existe pas — et pose + // `host = ""`. Loki jette une etiquette vide, et l'etiquette posee sur l'ecouteur juste + // au-dessus disparaissait avec elle. Le symptome : un flux sans `host`, alors que la + // configuration deposee en portait un, visible a l'oeil dans le fichier. + // + // Une regle de relabel sans garde ne « laisse pas passer » : elle EFFACE. + rule { + source_labels = ["__syslog_connection_ip_address"] + target_label = "host" + regex = "(.+)" + } + rule { + source_labels = ["__syslog_connection_hostname"] + target_label = "host" + regex = "(.+)" + } + rule { + source_labels = ["__syslog_message_hostname"] + target_label = "host" + regex = "(.+)" + } +} +{% endif %} diff --git a/roles/serveur_loki/defaults/main.yml b/roles/serveur_loki/defaults/main.yml index 55b111b..29db66f 100644 --- a/roles/serveur_loki/defaults/main.yml +++ b/roles/serveur_loki/defaults/main.yml @@ -49,3 +49,13 @@ serveur_loki_sonde_url: >- # DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture. serveur_loki_depot_schema: >- {{ 'http' if (artefacts_amorcage | default('') | string | length > 0) else 'https' }} + +# --- Sonde « journaux-frontiere » (voir meta/supervision.yml) --- +# L'etiquette sous laquelle la frontiere apparait — la meme que celle posee sur l'ecouteur +# syslog par `client_journal`, derivee de la carte. Vide = pas de sonde a poser. +serveur_loki_sonde_frontiere_etiquette: "" + +# LA FENETRE EST DERIVEE DU DEBIT OBSERVE, pas choisie. Mesure du 2026-09-10 : ~46 000 +# lignes en trente minutes, soit environ 1 500 par minute. Une frontiere qui filtre ne se +# tait jamais dix minutes ; si elle le fait, c'est qu'elle ne parle plus a nous. +serveur_loki_sonde_frontiere_fenetre: "10m" diff --git a/roles/serveur_loki/meta/flux.yml b/roles/serveur_loki/meta/flux.yml index d8ea5f8..864778e 100644 --- a/roles/serveur_loki/meta/flux.yml +++ b/roles/serveur_loki/meta/flux.yml @@ -32,3 +32,20 @@ flux: 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. + + # LE SYSLOG DE LA FRONTIERE (2026-09-10). + # + # Port 1514 et non 514 : au-dessus de 1024, donc Alloy l'ecoute sans privilege. Un + # collecteur qui aurait besoin des droits du systeme pour entendre un equipement serait + # un mauvais echange. + # + # `fabric` couvre la frontiere. Le flux est declare COTE LOKI parce que c'est lui qui + # ouvre un port : l'emetteur, lui, n'est pas dans la flotte et ne declare rien. + - sens: ingress + port: 1514 + protocole: tcp + pair: fabric + chiffrement: clair + raison: >- + Journaux de la frontiere. Elle n'accueille aucun agent et n'emet que du syslog ; sans + ce flux, le seul equipement qui voit passer TOUT le trafic n'ecrit nulle part. diff --git a/roles/serveur_loki/meta/supervision.yml b/roles/serveur_loki/meta/supervision.yml index b9c0864..544a32b 100644 --- a/roles/serveur_loki/meta/supervision.yml +++ b/roles/serveur_loki/meta/supervision.yml @@ -6,3 +6,11 @@ sondes: raison: Loki est-il PRET a ingerer ? Un Loki qui ecoute mais dont l'ingesteur n'est pas pret accepte les connexions et perd les journaux — la panne se voit des semaines plus tard, quand on cherche une trace qui n'a jamais ete ecrite. + + - nom: journaux-frontiere + ttl: 5400 + raison: >- + La frontiere journalise-t-elle encore ? Sa destination syslog a deja pointe une VM + rasee : toute la journalisation d'OPNsense s'est arretee dix jours sans que rien ne + le signale. Un equipement muet ne se plaint pas — on demande donc a Loki ce qu'il a + RECU, pas a la frontiere ce qu'elle croit avoir envoye. diff --git a/roles/serveur_loki/tasks/main.yml b/roles/serveur_loki/tasks/main.yml index 81b4d88..243c15e 100644 --- a/roles/serveur_loki/tasks/main.yml +++ b/roles/serveur_loki/tasks/main.yml @@ -262,3 +262,20 @@ owner: root group: root mode: "0750" + +# LA SONDE DE LA FRONTIERE N'EXISTE QUE LA OU ELLE EST ECOUTEE. Un tenant n'a pas de +# frontiere a lui : sans etiquette declaree, rien n'est depose et rien n'est attendu. +- name: Deposer la sonde des journaux de la frontiere + ansible.builtin.template: + src: sonde-frontiere.sh.j2 + dest: /usr/local/lib/setops/sondes/journaux-frontiere.sh + owner: root + group: root + mode: "0750" + when: serveur_loki_sonde_frontiere_etiquette | length > 0 + +- name: Retirer la sonde de frontiere la ou aucune n'est ecoutee + ansible.builtin.file: + path: /usr/local/lib/setops/sondes/journaux-frontiere.sh + state: absent + when: not (serveur_loki_sonde_frontiere_etiquette | length > 0) diff --git a/roles/serveur_loki/templates/sonde-frontiere.sh.j2 b/roles/serveur_loki/templates/sonde-frontiere.sh.j2 new file mode 100644 index 0000000..38e7232 --- /dev/null +++ b/roles/serveur_loki/templates/sonde-frontiere.sh.j2 @@ -0,0 +1,54 @@ +#!/bin/bash +# GENERE par Set-OPS (role serveur_loki). Ne pas editer a la main. +# +# SONDE « journaux-frontiere » — l'equipement qui voit TOUT parle-t-il encore ? +# +# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2. +# Mise en defaut PAR PARAMETRE : `serveur_loki_sonde_frontiere_etiquette` (nom inconnu). +# +# POURQUOI CETTE SONDE EXISTE, ET POURQUOI ELLE EST LA PLUS IMPORTANTE DU LOT. +# +# La destination syslog de la frontiere a pointe une VM de TENANT, rasee depuis. Une cible +# morte a arrete TOUTE la journalisation d'OPNsense pendant DIX JOURS, et rien ne l'a +# signale : le boitier fonctionnait, le pare-feu filtrait, les regles s'appliquaient — il +# n'ecrivait simplement plus nulle part. On ne l'a su qu'en allant chercher un evenement +# qui n'avait jamais ete ecrit. +# +# Remettre la destination sans poser cette garde, ce serait reconstruire la meme panne en +# esperant s'en souvenir. Un equipement muet ne se plaint pas. +set -uo pipefail + +URL={{ serveur_loki_sonde_url }} +ETIQ="{{ serveur_loki_sonde_frontiere_etiquette }}" +FENETRE={{ serveur_loki_sonde_frontiere_fenetre }} + +# ON DEMANDE A LOKI CE QU'IL A RECU, pas a la frontiere ce qu'elle croit avoir envoye. +# La destination est le seul juge : un emetteur qui « envoie » vers un trou noir se porte +# tres bien, et c'est exactement ce qui s'est passe. +rep=$(curl -sk --max-time 15 -G "${URL}/loki/api/v1/query" \ + --data-urlencode "query=count_over_time({host=\"${ETIQ}\", job=\"syslog\"}[${FENETRE}])" \ + 2>/dev/null) +[[ -n "${rep}" ]] || { echo "Loki ne repond pas sur ${URL} — la sonde ne peut rien affirmer."; exit 3; } + +n=$(printf '%s' "${rep}" | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + print("-1"); raise SystemExit +r = (d.get("data") or {}).get("result") or [] +print(int(sum(float(x.get("value", [0, 0])[1]) for x in r))) +' 2>/dev/null) +: "${n:=-1}" + +perf="lignes=${n} fenetre=${FENETRE}" +if (( n < 0 )); then + echo "Reponse illisible de Loki — impossible de dire si la frontiere journalise. | ${perf}" + exit 3 +fi +if (( n == 0 )); then + echo "LA FRONTIERE NE JOURNALISE PLUS : aucune ligne de « ${ETIQ} » depuis ${FENETRE}. | ${perf}" + exit 2 +fi +echo "Frontiere journalise : ${n} ligne(s) recue(s) en ${FENETRE}. | ${perf}" +exit 0 diff --git a/scripts/site_inventaire.py b/scripts/site_inventaire.py index b4bf3ce..ea6b079 100755 --- a/scripts/site_inventaire.py +++ b/scripts/site_inventaire.py @@ -349,6 +349,11 @@ def inventaire() -> dict: }) _fabric_surveillee.sort(key=lambda x: x["adresse"]) + # Le nom de la frontiere, tel que la carte le donne — pour etiqueter ses journaux. + _nom_frontiere = next((str(h.get("nom")) for h in U.hotes(u) + if h.get("role") == "frontiere" + and str(h.get("etat") or "actif") == "actif"), "frontiere") + hostvars: dict[str, dict] = {} groupes: dict[str, list[str]] = {} @@ -478,6 +483,16 @@ def inventaire() -> dict: # Le plan du site, pour les roles qui lisent des registres. "setops_plan_dir": str(plan_dir() or ""), "hosts_statiques_expositions": expositions, + # LE COLLECTEUR ECOUTE AUSSI CE QUI NE PEUT PAS POUSSER. La frontiere n'a pas + # d'agent : elle emet du syslog, et Alloy le traduit vers Loki. Active sur la + # machine qui PORTE Loki — un second recepteur ailleurs serait une seconde + # configuration a garder en phase. + **({"client_journal_syslog_ecoute": f"{srv.get('ip')}:1514", + "client_journal_syslog_emetteur": _nom_frontiere, + # La sonde interroge la MEME etiquette que celle posee sur l'ecouteur : + # une seule derivation, deux consommateurs. + "serveur_loki_sonde_frontiere_etiquette": _nom_frontiere} + if "serveur_loki" in _groupes_de(nom) else {}), # LA FABRIC SURVEILLEE PAR CONTROLE ACTIF (2026-09-10). # # Un pare-feu n'accueille aucun agent : Icinga doit aller VOIR. Une entree par