⚠️ Disclaimer

La méthode que je présente ici correspond à ma propre démarche d’apprentissage. Elle peut contenir des approximations ou des erreurs, car j’apprends et je progresse chaque jour un peu plus. Ne prenez donc pas ce que je fais comme une référence absolue, mais plutôt comme un retour d’expérience personnel.

Wazuh SIEM sur homelab : décodeurs custom, bug PCRE2 et logs pfSense RFC 5424

J’avais Wazuh qui tournait, des logs qui arrivaient, et un dashboard qui affichait surtout des alertes génériques. Clément Appercel ingénieur SIEM, ça sonne bien en entretien, mais si tu ne sais pas ce que ton SIEM capture vraiment, tu n’es pas loin du tableau Excel de monitoring.

Ce post raconte comment j’ai construit des décodeurs custom pour mes trois sources de logs (pfSense, Proxmox, Synology), les problèmes rencontrés en chemin, et surtout la méthode pour ne pas travailler à l’aveugle.

Les fichiers de config sont sur GitHub.


La situation de départ

Mon homelab tourne sur trois nœuds Proxmox, un pfSense en routeur/firewall, un NAS Synology DS723+, et un Mac Mini M4 Pro pour Docker. Tout est segmenté en VLANs depuis mars 2026.

Wazuh est déployé sur une VM Debian dans le VLAN LAB (192.168.30.x). Trois sources de logs :

  • pfSense : syslog UDP 514 vers Wazuh
  • Proxmox ASUS NUC : agent Wazuh installé directement sur l’hôte
  • Synology DS723+ : syslog UDP 514 vers Wazuh

Sur le papier, tout arrivait. En pratique, quand j’ouvrais le dashboard Wazuh, je voyais des centaines d’alertes pfSense filterlog (trafic bloqué/autorisé) et quelques events Synology. Les logs DHCP de pfSense ? Absents. Les alertes Suricata IDS ? Absentes. Et pourtant pfSense était configuré pour tout envoyer.


Avant de toucher aux décodeurs : cartographier ce qui arrive

Le réflexe naturel aurait été de commencer à écrire des décodeurs. J’aurais perdu du temps.

La bonne méthode, c’est d’abord de faire l’inventaire de ce qui arrive réellement dans Wazuh. Le fichier archives.log contient tout ce que Wazuh reçoit, décodé ou non, du moment que logall: yes est activé dans ossec.conf. À partir de là, une commande suffit pour avoir la cartographie complète :

1
cat /var/ossec/logs/archives/archives.log | grep -oP 'pfSense\.home\.arpa \K\S+' | sort | uniq -c | sort -rn | head -30

Résultat sur mon infrastructure sur une journée :

Type de logVolume%
filterlog46 44395,9%
cron1 7023,5%
dhcpd2250,5%
syslogd20~0%
suricata6~0%
php / nginx15~0%

96% de filterlog. Normal pour un firewall. Mais les 4% restants (DHCP, Suricata, nginx), c’est exactement ce qu’on veut dans un SIEM : traçabilité des devices, alertes IDS, accès admin à l’interface.

Tout arrivait dans archives.log. Rien n’apparaissait dans le dashboard. Diagnostic : pas de décodeur, pas d’alerte.


Pourquoi pfSense n’est pas décodé par défaut

pfSense envoie ses logs au format RFC 5424 :

1 2026-03-23T10:56:43.099591+01:00 pfSense.home.arpa dhcpd 44738 - - DHCPACK on 192.168.40.103 to 9c:9e:6e:65:72:a4 via igc1.40

Wazuh s’attend à du RFC 3164, le format syslog “historique” :

Mar 23 10:56:43 pfSense dhcpd[44738]: DHCPACK on 192.168.40.103 to...

La différence concrète : un 1 en tête (numéro de version du protocole), un timestamp ISO 8601 avec fuseau horaire et microsecondes, et le nom de domaine complet au lieu du hostname court. Le pré-décodeur natif de Wazuh ne reconnaît pas cette enveloppe. Il passe au log suivant.

J’aurais pu basculer pfSense en RFC 3164, l’option existe dans les paramètres syslog. Mais la RFC 5424 donne plus d’infos, notamment le timestamp précis utile en forensic. J’ai préféré garder le format et construire les décodeurs.


Un bug Wazuh 4.14.3 qui n’est pas dans la doc

En construisant les décodeurs Synology, j’ai rencontré un comportement bizarre : le premier décodeur enfant PCRE2 fonctionnait, les suivants étaient silencieux. Résultat : les logs d’authentification Synology matchaient, mais les logs système ne déclenchaient rien.

wazuh-logtest : le décodeur pfsense-suricata extrait tous les champs (Phase 2 et Phase 3, rule 100043 level 10)

Après plusieurs heures à tester des regex dans wazuh-logtest, le diagnostic : quand plusieurs enfants PCRE2 partagent le même parent, l’échec d’un enfant bloque l’évaluation des suivants. Ce n’est pas documenté. C’est reproductible.

Contournement : un parent par type de log, chacun avec un seul enfant. La structure devient plus verbeux mais stable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- À la place d'un parent avec 3 enfants PCRE2 : -->
<decoder name="synology-auth">
    <prematch>synolog@6574 event_id=</prematch>  <!-- prematch spécifique -->
</decoder>
<decoder name="synology-auth-ip">
    <parent>synology-auth</parent>
    <regex type="pcre2">event_id="(\S+)".*?arg_3="(\d+\.\d+\.\d+\.\d+)".*</regex>
    <order>id,srcip</order>
</decoder>

<decoder name="synology-dsm">
    <prematch>synolog@6574</prematch>  <!-- prematch générique — APRÈS le spécifique -->
</decoder>
<decoder name="synology-dsm-system">
    <parent>synology-dsm</parent>
    <regex type="pcre2">synotype="(\w+)" luser="([^"]+)" event="([^"]+)"</regex>
    <order>extra_data,srcuser,status</order>
</decoder>

L’ordre dans le fichier est critique : le prematch spécifique avant le générique, sinon le générique capture tout.


Les décodeurs pfSense construits

Avec la cartographie en main, j’ai identifié les types de logs prioritaires et construit un parent par type.

DHCP : trois variantes de messages (DHCPACK, DHCPREQUEST, bail dupliqué) :

1
2
3
4
5
6
7
8
<decoder name="pfsense-dhcp-ack">
    <prematch type="pcre2">pfSense\.home\.arpa dhcpd \d+ - - DHCPACK</prematch>
</decoder>
<decoder name="pfsense-dhcp-ack-fields">
    <parent>pfsense-dhcp-ack</parent>
    <regex type="pcre2">DHCPACK on (\d+\.\d+\.\d+\.\d+) to (\S+) via (\S+)</regex>
    <order>dstip,extra_data,status</order>
</decoder>

Suricata IDS : les alertes arrivent bien dans le syslog, mais uniquement si l’option “Send Alerts to System Log” est cochée dans la config de l’interface WAN dans pfSense. Elle ne l’est pas par défaut.

Dashboard Wazuh Threat Hunting — 117K alertes, évolution par level, agents actifs (wazuh-siem + proxmox)

1
2
3
4
5
6
7
8
<decoder name="pfsense-suricata">
    <prematch type="pcre2">pfSense\.home\.arpa suricata \d+ - -</prematch>
</decoder>
<decoder name="pfsense-suricata-alert">
    <parent>pfsense-suricata</parent>
    <regex type="pcre2">\[(\d+:\d+:\d+)\] (.+?) \[Classification: ([^\]]+)\] \[Priority: (\d+)\] \{(\w+)\} (\d+\.\d+\.\d+\.\d+):(\d+) -> (\d+\.\d+\.\d+\.\d+):(\d+)</regex>
    <order>id,url,extra_data,status,protocol,srcip,srcport,dstip,dstport</order>
</decoder>

Ce qui n’a pas marché comme prévu

Les logs nginx (accès à l’interface d’administration pfSense) : le décodeur natif Wazuh web-accesslog les intercepte avant mon décodeur custom, parce que les décodeurs built-in chargent avant local_decoder.xml. Il extrait les mauvais champs (le timestamp RFC 5424 au lieu de l’IP source). Ces logs restent en level 0 pour l’instant. C’est un angle mort.

La règle <field name="status"> : dans les règles Wazuh, cette syntaxe ne fonctionne qu’avec des champs dynamiques custom, pas avec les champs statiques built-in (status, srcip, protocol…). Pour matcher sur la priorité d’une alerte Suricata, j’ai utilisé <match> sur le texte brut :

1
2
3
4
5
<rule id="100042" level="8">
    <if_sid>100040</if_sid>
    <match>\[Priority: 2\]</match>
    <description>pfSense Suricata IDS haute (P2): ...</description>
</rule>

Le manager refusait de démarrer avec l’autre syntaxe. Pas de message d’erreur clair dans systemctl status, juste “Configuration error. Exiting”. C’est journalctl -xeu wazuh-manager.service qui donnait le détail.


Les règles en place

18 règles custom au total, réparties en trois groupes :

  • pfSense filterlog (100010-100014) : block/pass par protocole
  • pfSense DHCP (100030-100032) : bail accordé, demande, conflit IP (level 7)
  • pfSense Suricata IDS (100040-100043) : alerte générique (level 6), priorité 2 (level 8), hôte compromis ET (level 10), priorité 1 critique (level 12)
  • Synology DSM (100020-100025) : événements système, auth success/failure, brute force (level 10), énumération utilisateurs (level 12)

16 alertes Suricata IDS sur 24h — IPs hostiles ciblant SSH (ports 22 et 2222), rules 100040 et 100043


La suite

Ce qui reste à faire sur ce projet :

  • Décodeur nginx pfSense (accès WebUI admin) : contourner le conflit avec web-accesslog
  • Active Response : blocage automatique des IPs via l’API pfSense quand Suricata déclenche une alerte brute force
  • Vulnerability Detection sur l’agent Proxmox
  • Mapping MITRE ATT&CK sur les règles custom (T1110 pour le brute force, T1046 pour les scans…)

Ce que j’en retiens

La partie technique n’était pas la plus difficile. Ce qui a pris du temps, c’est d’accepter de ne pas commencer à écrire des décodeurs immédiatement. Faire l’inventaire des logs d’abord, même si ça semble ralentir les choses, change complètement la qualité du résultat. Tu sais ce que tu couvres et ce que tu ne couvres pas. C’est ce qu’on attend d’un ingénieur SIEM en entreprise aussi.

Le bug PCRE2 de Wazuh 4.14.3, je ne l’aurais pas trouvé si j’avais juste suivi les docs officielles. C’est le genre de chose qui se découvre en déboguant méthodiquement, en lisant les sorties de wazuh-logtest ligne par ligne, en posant des hypothèses et en les testant. Pas très glamour, mais c’est comme ça que ça marche.

Dernière chose : pfSense envoie du RFC 5424 par défaut, et beaucoup de ressources en ligne parlent de RFC 3164. Si tu déploies Wazuh avec pfSense et que tu ne vois pas ce que tu attends, commence par vérifier dans archives.log. Les logs sont probablement là, ils ne sont juste pas décodés.


Les décodeurs et règles complets sont disponibles sur GitHub.