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.

CE QUI ETAIT CASSE. 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 pendant dix jours. La destination a ete eteinte
pour reparer et jamais remplacee. Deux fautes en une : plus de journaux, et
une cible chez un locataire, ce que D-87 refuse dans l autre sens.

L intention etait juste - 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. Port 1514 et non 514, 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 dans le fichier, et le flux arrivait sans
etiquette. 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.

Et la verification finale a corrige une seconde erreur, la mienne :
label/host/values rendait une liste en CACHE. La requete directe 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 - environ 1500
lignes par minute - pas choisie.

Trois etats eprouves sur une copie : frontiere muette rc=2, Loki
injoignable rc=3, debit normal rc=0. Le 3 compte autant que le 2 : « je ne
peux rien affirmer » n est pas « c est casse ».

Mesure : 46 681 lignes recues en 30 min, site a 68 services tous OK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-10 19:38:19 -04:00
parent 0778979b89
commit c733d2df1c
11 changed files with 293 additions and 8 deletions

View file

@ -1,5 +1,68 @@
# CHANGELOG — Set-OPS # 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 » ## 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. Les journaux de la fabric, et le defaut de classe que leur mise en place a revele.

View file

@ -21,7 +21,7 @@
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | 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. | | 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. | | 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). | | 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 | | 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. | | 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). | | 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. | | 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. | | 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). | | 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. | | 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. | | 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. | | 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. | | 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). | | 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)). | | 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). | | 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). | | 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. | | 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). | | 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 | | 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. | | 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). | | 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 | | 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). | | 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. | | 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). | | 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 216 regles `pass`), tous non consignes et tous motives. | | 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 | | 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. | | 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 | | 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 | | 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). | | 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 | | 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. | | 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 ## Couverture des affirmations ✅ du registre

View file

@ -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` | 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 | 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_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 | 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 | 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_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 ## Synthèse chiffrement
- **clair** : 38 flux - **clair** : 39 flux
- **n-a** : 6 flux - **n-a** : 6 flux
- **ssh** : 8 flux - **ssh** : 8 flux
- **starttls** : 6 flux - **starttls** : 6 flux

View file

@ -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. # DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture.
client_journal_depot_schema: >- client_journal_depot_schema: >-
{{ 'http' if (artefacts_amorcage | default('') | string | length > 0) else 'https' }} {{ '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"

View file

@ -19,3 +19,80 @@ loki.source.journal "journal" {
host = "{{ inventory_hostname }}", 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 %}

View file

@ -49,3 +49,13 @@ serveur_loki_sonde_url: >-
# DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture. # DEGRADE, JAMAIS DEVINE : pas de cache d'amorcage declare, pas de reecriture.
serveur_loki_depot_schema: >- serveur_loki_depot_schema: >-
{{ 'http' if (artefacts_amorcage | default('') | string | length > 0) else 'https' }} {{ '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"

View file

@ -32,3 +32,20 @@ flux:
raison: >- raison: >-
Journaux des hyperviseurs. La machine qui PORTE les VM ecrivait ses journaux nulle 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. 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.

View file

@ -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 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 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. 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.

View file

@ -262,3 +262,20 @@
owner: root owner: root
group: root group: root
mode: "0750" 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)

View file

@ -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

View file

@ -349,6 +349,11 @@ def inventaire() -> dict:
}) })
_fabric_surveillee.sort(key=lambda x: x["adresse"]) _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] = {} hostvars: dict[str, dict] = {}
groupes: dict[str, list[str]] = {} groupes: dict[str, list[str]] = {}
@ -478,6 +483,16 @@ def inventaire() -> dict:
# Le plan du site, pour les roles qui lisent des registres. # Le plan du site, pour les roles qui lisent des registres.
"setops_plan_dir": str(plan_dir() or ""), "setops_plan_dir": str(plan_dir() or ""),
"hosts_statiques_expositions": expositions, "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). # LA FABRIC SURVEILLEE PAR CONTROLE ACTIF (2026-09-10).
# #
# Un pare-feu n'accueille aucun agent : Icinga doit aller VOIR. Une entree par # Un pare-feu n'accueille aucun agent : Icinga doit aller VOIR. Une entree par