pfSense WebGUI- und Konfigurations-Events in Wazuh zuverlässig erkennen: Decoder-Design, Rule-Chain und typische Stolperfallen

Einleitung

pfSense ist in vielen Umgebungen nicht nur ein Firewall-Gateway, sondern auch ein zentraler Audit-Punkt: WebGUI-Logins, Konfigurationsänderungen und das Anwenden von Filter-Regeln sind sicherheitsrelevant und gehören in ein SIEM. Wazuh eignet sich dafür hervorragend – allerdings stehen viele Integrationen und “funktioniert nur teilweise”-Symptome nicht im Zeichen falscher Regex, sondern in der falschen Kopplung zwischen Decoder-Ausgabe und Ruleset-Logik. Dieser Beitrag zeigt, wie pfSense-WebGUI-Events über php-fpm sauber dekodiert und per Rule-Chain zuverlässig alarmiert werden.

Ausgangslage / Problemstellung

Ziel der Integration:

  • WebGUI-Logins erkennen
  • Konfigurationsänderungen erfassen (z. B. Rule Delete/Edit)
  • Firewall-Regeln “Applied/Reloaded” (Filter-Rebuild) detektieren

Ist-Zustand:

  • Custom Decoder für php-fpm-Logs (Login + Configuration Change)
  • Custom Rules für Login, Config Change und Filter Apply

Symptome:

  • Nur WebGUI-Login-Alerts triggern
  • Config-Change-Events sind in archives.log vorhanden, lösen aber keine Alerts aus
  • Root Causes im Setup:
    • Verwechslung von Rule-Tags (<decoded_as> vs. <if_matched_sid> / <if_sid>)
    • Fehlerhafte <group>-Syntax (z. B. trailing commas)
    • Rule-Matches auf Texte, die in pfSense so nicht vorkommen
    • Firewall-Apply-Events loggen tatsächlich als system.filter.configure
    • Login/Config Rules waren nicht konsistent an Decoder/Parent-Rule gebunden
    • Zusätzlich: Decoder-Reihenfolge/Matching-Logik sorgte dafür, dass ein Child-Decoder nicht greift

Technische Analyse

1) Decoder: Parent/Child ist korrekt – aber Matching-Reihenfolge entscheidet

Wazuh verarbeitet Decoder nicht “parallel”, sondern sequenziell. Für eine Logzeile wird anhand von program_name erst der Parent-Decoder gewählt, danach werden passende Child-Decoder unter diesem Parent gesucht. Zwei Punkte sind in der Praxis entscheidend:

  • Der erste passende Decoder “gewinnt”: Wenn ein Child-Decoder sehr generisch ist oder unglücklich platziert wurde, kann er andere Decoder “ausstechen”.
  • Mehrere Regex-Varianten für dasselbe Event-Format sollten als Decoder-Varianten modelliert werden, nicht als lose Decoder-Familie, die man später per Rules “adressieren” will.

Ein gängiges Muster ist daher: ein logischer Decoder-Name mit mehreren Regex-Definitionen (same name), sodass Wazuh nacheinander mehrere Muster versucht, bis eines passt. Das verhindert, dass ein nicht passender “erster Child-Decoder” das Event ohne Feldextraktion durchrutschen lässt.

2) Rules: <decoded_as> ist nicht “decoder-spezifische Rule-Verkettung”

Hier passieren die häufigsten Fehler:

  • <decoded_as> referenziert den Decoder-Namen, also “wie Wazuh das Event klassifiziert”.
  • <if_sid> referenziert eine Rule-ID und baut darauf eine Rule-Chain.
  • <if_matched_sid> wird typischerweise dann relevant, wenn man auf eine vorherige Rule matcht, oft im Kontext von Frequency/Timeframe oder korrelierenden Regeln – nicht als Standardmechanismus, um “Child-Decoder” zu verbinden.

Der robuste Ansatz: Eine Parent-Rule mit <decoded_as> auf den Parent-Decoder, danach Child-Rules mit <if_sid> für spezifische Matches (Login, Configuration Change, Filter Apply).

3) pfSense-Logrealität: Texte matchen, die wirklich vorkommen

Viele Integrationen scheitern, weil auf “naheliegende” Texte gematcht wird, die pfSense nicht schreibt. Im Beispiel ist das positive Gegenstück klar:

  • Config Change enthält: Configuration Change: ...
  • Filter Apply/Rebuild enthält: system.filter.configure

Wer stattdessen auf “Firewall rule changed” matcht, wird nie triggern.

4) Regex-Qualität: IP- und Prefix-Anteile robust dekodieren

Die Logzeile enthält häufig einen Prefix wie /firewall_rules.php: vor dem eigentlichen Text. Wenn Regex nur auf den nackten Satz abzielte, kann das – je nach ^-Anker und Pattern – den Match verhindern.

Beispiel aus dem Log:
/firewall_rules.php: Configuration Change: admin@10.10.1.52 (Local Database): Firewall: Rules - deleted selected firewall rules.

Die Decoder-Regex sollte daher:

  • optional den Page-Pfad mit erfassen (für Kontext)
  • Username und srcip sauber extrahieren
  • die Message als Rest aufnehmen

Lösung / Best Practices

1) Decoder konsolidieren: ein logischer Child-Decoder mit mehreren Regex-Varianten

Stabiler Aufbau:

<decoder name="pfsense-php">
  <program_name>^php-fpm</program_name>
</decoder>

<!-- Variante 1: Configuration Change -->
<decoder name="pfsense-php-event">
  <parent>pfsense-php</parent>
  <regex type="pcre2">^\s*(/\S+\.php):\s*Configuration Change:\s*(\S+)@(\d{1,3}(?:\.\d{1,3}){3})\s*\(([^)]*)\):\s*(.*)$</regex>
  <order>page,username,srcip,authdatabase,message</order>
</decoder>

<!-- Variante 2: Successful login -->
<decoder name="pfsense-php-event">
  <parent>pfsense-php</parent>
  <regex type="pcre2">^\s*(/\S+\.php):\s*Successful login for user '(\S+)'\s*from:\s*(\d{1,3}(?:\.\d{1,3}){3})\s*\(([^)]*)\)\s*$</regex>
  <order>page,username,srcip,authdatabase</order>
</decoder>

Wichtig:

  • Gleicher Decoder-Name pfsense-php-event für beide Varianten, damit Wazuh diese Varianten als zusammengehörig nacheinander prüft.
  • <order> mit Kommas (Wazuh erwartet kommagetrennte Felder).
  • authdatabase wird aus dem Klammeranteil ((Local Database)) gezogen – das ist später im Alert-Kontext nützlich.
  • page erlaubt Auswertung nach WebGUI-Seite (/firewall_rules.php, /system_*.php etc.).

2) Rules sauber ketten: Parent-Rule gruppiert, Child-Rules matchen spezifisch

Empfohlenes Regel-Design:

<group name="pfsense,web_gui,audit">

  <!-- Parent: alle php-fpm Events für pfSense gruppieren -->
  <rule id="100049" level="3">
    <decoded_as>pfsense-php</decoded_as>
    <description>pfSense: php-fpm WebGUI events grouped</description>
    <group>pfsense,web_gui</group>
  </rule>

  <!-- Login -->
  <rule id="100050" level="8">
    <if_sid>100049</if_sid>
    <match>Successful login for user</match>
    <description>pfSense: WebGUI login by $(username) from $(srcip) via $(page)</description>
    <group>authentication_success,pfsense</group>
  </rule>

  <!-- Configuration Change -->
  <rule id="100051" level="12">
    <if_sid>100049</if_sid>
    <match>Configuration Change:</match>
    <description>pfSense: Configuration change by $(username) from $(srcip) via $(page) - $(message)</description>
    <group>config_change,audit,pfsense</group>
  </rule>

  <!-- Firewall rules applied / reload -->
  <rule id="100055" level="12">
    <if_sid>100049</if_sid>
    <match>system.filter.configure</match>
    <description>pfSense: Firewall rules applied / filter reloaded</description>
    <group>config_change,audit,pfsense</group>
  </rule>

</group>

Warum das stabil ist:

  • <decoded_as> wird nur einmal genutzt, um die Logfamilie zu definieren.
  • Alle inhaltlichen Entscheidungen passieren in Child-Rules über <if_sid>.
  • Das vermeidet, dass einzelne Child-Rules “in der Luft hängen” (z. B. weil sie zwar <decoded_as> haben, aber nicht sauber in der Rule-Pipeline verankert sind).

3) “Nur Login triggert” diagnostizieren: Logtest und Feldprüfung

Wenn Config Change nicht triggert, sind die drei häufigsten Ursachen:

  1. Decoder matcht nicht (Regex greift nicht wegen Prefix/Anchors)
  2. Rule matcht nicht (Match-String kommt nicht vor oder falsche Rule-Chain)
  3. Gruppen/Ruleset-Syntaxfehler (z. B. trailing commas, doppelte IDs, XML-Fehler – Wazuh lädt dann Teile nicht)

Praxis-Workflow:

  • Sample-Log mit wazuh-logtest prüfen (Decoder-Match, extrahierte Felder, getroffene Regeln)
  • Danach archives.json kontrollieren: Sind username, srcip, page, message wirklich gefüllt?
  • Erst dann die Alert-Levels feinjustieren.

4) Nebenwirkungen und Betrieb

  • Nach Änderungen an Decodern/Rules: Wazuh Manager neu starten oder Ruleset reload (je nach Betriebsmodell) – sonst laufen Tests gegen alte Definitionen.
  • Bei pfSense-Updates können Logtexte leicht variieren; daher Match eher auf stabile Marker (Configuration Change:) als auf vollständige Sätze.

Lessons Learned / Best Practices

  • Rule-Chain immer über <if_sid> modellieren, <decoded_as> nur für die “Event-Familie” (Parent-Rule).
  • Mehrere Eventtypen unter einem Parent-Decoder am besten als mehrere Regex-Varianten unter demselben Decoder-Namen abbilden, damit Wazuh die Patterns sequenziell versucht.
  • Regex für pfSense-WebGUI-Logs prefix-aware bauen (/page.php: ist Teil des Formats).
  • Auf reale pfSense-Tokens matchen, z. B. system.filter.configure statt erfundener Phrasen.
  • XML-Syntax ist ein häufiger “Silent Killer”: trailing commas in <group>, doppelte Rule-IDs, doppelte Decoder-Namen an falscher Stelle – das führt zu nicht geladenen Regeln ohne sofort offensichtlichen Alert.

Fazit

Eine pfSense-Integration in Wazuh scheitert selten an “pfSense ist schwierig”, sondern an zwei wiederkehrenden Designfehlern: Decoder-Varianten werden nicht als Varianten modelliert (Matching-Reihenfolge), und Rules werden nicht sauber über eine Parent-Rule verkettet. Mit einem konsolidierten Decoder (pfsense-php-event mit mehreren Regex-Varianten) und einer klaren Rule-Chain (decoded_asif_sid + match) werden WebGUI-Logins, Configuration Changes und system.filter.configure-Events zuverlässig erkannt und SIEM-tauglich alarmiert.

Mehr zu Wazuh …
https://wazuh.com/?utm_source=ambassadors&utm_medium=referral&utm_campaign=ambassadors+program

Mehr zum Wazuh Ambassador Program …
https://wazuh.com/ambassadors-program/?utm_source=ambassadors&utm_medium=referral&utm_campaign=ambassadors+program

https://wazuh.slack.com/archives/C07CCCCGHHP/p1769354291717739