2026-06-24 20:17:46 -04:00
|
|
|
// Gere par Set-OPS (role client_journal). Ne pas editer a la main.
|
|
|
|
|
|
|
|
|
|
loki.write "loki" {
|
|
|
|
|
endpoint {
|
|
|
|
|
url = "{{ client_journal_loki_url }}"
|
2026-07-04 21:15:00 -04:00
|
|
|
{% if client_journal_loki_tls | default(false) %}
|
|
|
|
|
tls_config {
|
|
|
|
|
ca_file = "{{ client_journal_loki_ca }}"
|
|
|
|
|
}
|
|
|
|
|
{% endif %}
|
2026-06-24 20:17:46 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 17:58:47 -04:00
|
|
|
{% set sondeurs = client_journal_sondeurs | map('regex_escape') | join('|') | replace('\\', '\\\\') %}
|
|
|
|
|
{% if sondeurs %}
|
|
|
|
|
// --- LE BRUIT DE LA SONDE « connectivite » RESTE SUR LA MACHINE (2026-10-03) ---
|
|
|
|
|
//
|
|
|
|
|
// Chaque minute, chaque machine ouvre une connexion TCP vers les ports que le registre des
|
|
|
|
|
// flux lui promet, puis la referme sans rien dire (`nftables_baseline`,
|
|
|
|
|
// `sonde-connectivite.sh`). Ceux qui ecoutent le notent : `sshd` (« Connection closed by
|
|
|
|
|
// <pair> port <n> ») et les serveurs Go — Forgejo, step-ca — (« TLS handshake error from
|
|
|
|
|
// <pair>:<n>: EOF »). Mesure au site : le journal de chaque VM est passe de 2 000 a
|
|
|
|
|
// 14 000 lignes par jour le 2026-10-01, et ces deux phrases faisaient 98 % des « erreurs »
|
|
|
|
|
// de la forge et de l'autorite. Rien a corriger a la source : `sshd` journalise toute
|
|
|
|
|
// connexion fermee avant l'echange de cles, quel que soit le client.
|
|
|
|
|
//
|
|
|
|
|
// CE QUI EST JETE EST ETROIT : la phrase EXACTE, et seulement depuis une machine qui porte
|
|
|
|
|
// la sonde (`client_sante`, derive de l'inventaire). Le meme message venu d'ailleurs — un
|
|
|
|
|
// balayage, une machine inconnue — passe. Les lignes restent dans le journal local ; le
|
|
|
|
|
// compte de ce qui est jete est `loki_process_dropped_lines_total{reason="sonde_connectivite"}`,
|
|
|
|
|
// distinct des pertes que surveille la sonde « journaux ».
|
|
|
|
|
loki.process "sondes" {
|
|
|
|
|
forward_to = [loki.write.loki.receiver]
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^Connection closed by ({{ sondeurs }}) port [0-9]+$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "TLS handshake error from ({{ sondeurs }}):[0-9]+: EOF$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
2026-10-03 19:01:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LE COURRIEL PARLE PLUS QUE LES AUTRES (mesure chez les deux locataires, 2026-10-03).
|
|
|
|
|
// Postfix ecrit trois lignes par connexion de sonde — quatre sur le port chiffre — et
|
|
|
|
|
// faisait de `edge-mta-01` la machine la plus bavarde de chaque locataire (7 000 lignes
|
|
|
|
|
// par heure). Une VRAIE session venue de la flotte perd sa ligne `connect from`, rien
|
|
|
|
|
// d'autre : ses identifiants de file (`client=`), et son `disconnect` qui compte des
|
|
|
|
|
// commandes (`commands=N/M`, jamais `0/0`), restent.
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^connect from [^\\[ ]+\\[({{ sondeurs }})\\]$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^(NOQUEUE: )?lost connection after CONNECT from [^\\[ ]+\\[({{ sondeurs }})\\]$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^disconnect from [^\\[ ]+\\[({{ sondeurs }})\\] commands=0/0$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^SSL_accept error from [^\\[ ]+\\[({{ sondeurs }})\\]: lost connection$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dovecot LMTP, en TLS des l'accueil : la sonde ferme avant la poignee. Seule la forme
|
|
|
|
|
// « fermee avant de commencer » est jetee ; toute autre raison d'echec TLS passe, et une
|
|
|
|
|
// livraison reelle se lit toujours a `saved mail to`.
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^lmtp\\([0-9]+\\): Connect from ({{ sondeurs }})$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^lmtp\\([0-9]+\\): Error: lmtp-server: conn ({{ sondeurs }}):[0-9]+ \\[[0-9]+\\]: SSL handshake failed: Connection closed$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stage.drop {
|
|
|
|
|
expression = "^lmtp\\([0-9]+\\): Disconnect from ({{ sondeurs }}): SSL Initialization failed \\(state=GREETING\\)$"
|
|
|
|
|
drop_counter_reason = "sonde_connectivite"
|
2026-10-03 17:58:47 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{% endif %}
|
2026-06-24 20:17:46 -04:00
|
|
|
loki.source.journal "journal" {
|
2026-10-03 17:58:47 -04:00
|
|
|
forward_to = [{{ 'loki.process.sondes.receiver' if sondeurs else 'loki.write.loki.receiver' }}]
|
2026-06-24 20:17:46 -04:00
|
|
|
max_age = "12h"
|
|
|
|
|
labels = {
|
|
|
|
|
job = "systemd-journal",
|
|
|
|
|
host = "{{ inventory_hostname }}",
|
|
|
|
|
}
|
|
|
|
|
}
|
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
2026-09-10 19:38:19 -04:00
|
|
|
|
audit : la piste sort de la machine auditee, et quelqu un regarde
auditd tournait, onze regles etaient armees, et rien de tout cela ne servait
a grand-chose. Quatre manques, mesures avant d etre combles.
1. Rien ne sortait. Alloy ne contenait aucune reference a l audit et les cinq
greffons etaient inactifs. Il lit maintenant audit.log et le pousse vers Loki
— sans toucher une permission, alloy etant deja dans le groupe adm. On ecarte
le greffon syslog : journald limite le debit, et une rafale d evenements est
exactement le moment qui compte.
2. Aucune sonde. Elle mesure la COLLECTE, pas l armement : le nombre de
regles est precisement le chiffre qui restait bon pendant la panne du
2026-08-30. Il part en perfdata, jamais en verdict.
3. Retention non declaree. Mesuree a 4,6 Mo/jour au repos ; portee de 40 a
128 Mo. Depuis (1), le local n est plus l archive mais un tampon.
4. Les regles ignoraient la cle privee de l hote. -w /etc/step/ partout, et
-w /etc/step-ca/ sur la seule autorite : auditctl refuse un watch vers un
chemin absent et fait echouer le chargement entier.
Verifie sur l autorite seule d abord, puis 14/14 : regles armees, auditd
actif, sonde a 0, et les quatorze hotes presents dans Loki sous job=audit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
2026-09-11 09:00:28 -04:00
|
|
|
{% if client_journal_audit_actif %}
|
|
|
|
|
// --- LA PISTE D'AUDIT SORT DE LA MACHINE AUDITEE (2026-09-11) ---
|
|
|
|
|
//
|
|
|
|
|
// `auditd` ecrit dans `/var/log/audit/audit.log` et nulle part ailleurs. Tant que la
|
|
|
|
|
// piste reste sur la machine qu'elle surveille, qui compromet la machine possede la
|
|
|
|
|
// preuve de l'avoir fait — et le fichier est precisement ce qu'on efface en premier.
|
|
|
|
|
//
|
|
|
|
|
// POURQUOI PAS LE GREFFON `syslog.conf` D'AUDITD, qui ferait passer les evenements par
|
|
|
|
|
// journald (deja collecte) : journald applique une LIMITE DE DEBIT. Une rafale
|
|
|
|
|
// d'evenements d'audit — c'est-a-dire exactement le moment qui compte — serait tronquee
|
|
|
|
|
// en silence. On lit donc le fichier directement.
|
|
|
|
|
//
|
|
|
|
|
// AUCUNE PERMISSION A POSER : `/var/log/audit` est `root:adm` en 0750, et l'utilisateur
|
|
|
|
|
// `alloy` appartient DEJA au groupe `adm` (mesure le 2026-09-11). Si cela changeait,
|
|
|
|
|
// Alloy se tairait sans erreur visible — la sonde `audit` reste le filet cote machine.
|
|
|
|
|
loki.source.file "audit" {
|
|
|
|
|
targets = [{
|
|
|
|
|
__path__ = "{{ client_journal_audit_chemin }}",
|
|
|
|
|
job = "audit",
|
|
|
|
|
host = "{{ inventory_hostname }}",
|
|
|
|
|
}]
|
|
|
|
|
forward_to = [loki.write.loki.receiver]
|
|
|
|
|
}
|
|
|
|
|
{% endif %}
|
|
|
|
|
|
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
2026-09-10 19:38:19 -04:00
|
|
|
{% 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 %}
|