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.logvorhanden, 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
- Verwechslung von Rule-Tags (
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-eventfür beide Varianten, damit Wazuh diese Varianten als zusammengehörig nacheinander prüft. <order>mit Kommas (Wazuh erwartet kommagetrennte Felder).authdatabasewird aus dem Klammeranteil ((Local Database)) gezogen – das ist später im Alert-Kontext nützlich.pageerlaubt Auswertung nach WebGUI-Seite (/firewall_rules.php,/system_*.phpetc.).
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:
- Decoder matcht nicht (Regex greift nicht wegen Prefix/Anchors)
- Rule matcht nicht (Match-String kommt nicht vor oder falsche Rule-Chain)
- Gruppen/Ruleset-Syntaxfehler (z. B. trailing commas, doppelte IDs, XML-Fehler – Wazuh lädt dann Teile nicht)
Praxis-Workflow:
- Sample-Log mit
wazuh-logtestprüfen (Decoder-Match, extrahierte Felder, getroffene Regeln) - Danach
archives.jsonkontrollieren: Sindusername,srcip,page,messagewirklich 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.configurestatt 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_as → if_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