Realtime-Attack-Map
SURICATA / LIVE FEED

BunkerWeb, syslog-ng, Wazuh und die KI-Auswertung

Decoder, Regeln und Active Response: wie man eine WAF-Kette sauber verdrahtet und testet.

BunkerWeb, syslog-ng, Wazuh und die KI-Auswertung

Im ersten Teil ging es darum, BunkerWeb vor die Dienste zu setzen. Im zweiten Teil habe ich die WAF mit Ausnahmen so abgestimmt, dass sie den Blog-Betrieb nicht mehr stört. Dieser dritte Teil beschreibt, wie die Ereignisse danach bei Wazuh landen, wie die Regeln sie bewerten und was passiert, wenn daraus ein Alarm wird.

Inhaltsverzeichnis
    BunkerWeb: Mehr Sicherheit für Web-Dienste
    Eine Web Application Firewall blockt bösartigen Traffic direkt an der Haustür.
    Mehr Schutz, weniger Fehlalarme: Mein Weg mit BunkerWeb
    Ausnahmen so eng wie möglich, Alarme so gezielt wie nötig. Ein Blick hinter die Kulissen von BunkerWeb

    Die Logs müssen ankommen, bevor man sie auswerten kann

    BunkerWeb schreibt drei Arten von Meldungen: Access-Zeilen, ModSecurity-Meldungen und Sperr-Meldungen. Die Access-Zeilen gehen über ACCESS_LOG_1 an syslog-ng, die Fehlermeldungen über ERROR_LOG_1. Die Blockierungen stehen in den Error-Logs, deshalb hatte ich lange Zeit nur die Hälfte bei Wazuh. Die Konfiguration in der BunkerWeb-Umgebung sieht so aus:

    environment:
      ACCESS_LOG_1: "syslog:server=syslog-ng:514,tag=bunkerweb,facility=local0"
      ERROR_LOG_1: "syslog:server=syslog-ng:514,tag=bunkerweb_error,facility=local0"
    

    Alle Meldungen werden an den Syslog-Filter geschickt

    Die Tags sind wichtig, weil die Decoder in Wazuh den Programmnamen als Startpunkt nutzen. bunkerweb für Access, bunkerweb_error für Fehler.

    Der alte Filter bei syslog-ng hatte einen Fehler, den ich lange übersehen habe. Das Muster für HTTP-Fehler verlangte ein Leerzeichen nach dem öffnenden Anführungszeichen. Access-Zeilen von BunkerWeb haben dort aber keines, deshalb fielen alle 403-Antworten durch den Filter, und Wazuh sah nur 200er. Die korrigierte Version:

    filter f_security_and_bans {
      match('\[(BUNKERNET|BLACKLIST|GREYLIST|ANTIBOT|LIMIT)\]' value("MESSAGE"))
      or match('(banned|blocked|Access denied)' value("MESSAGE"))
      or match('"[A-Z]+ [^"]* HTTP/[0-9.]+" [45][0-9][0-9] ' value("MESSAGE"))
    };
    

    Die empfangenen Logs werden markiert und werden an wazuh weitergeleitet. Dies ist der Pfad, der die Reaktionen von Bunkerweb verarbeitet.

    Die Prüfung dafür ist simpel: Man schickt einen Testfehler auf den eigenen Dienst und schaut im Archiv nach, ob die Zeile ankommt. Das Archiv (archives.json) zeigt alle eingehenden Zeilen, unabhängig davon, ob eine Regel feuert. Das ist die Stelle, an der man zuerst nachsieht, wenn ein Alarm fehlt.

    Decoder: Drei Zeilentypen, eine feste Reihenfolge

    Wazuh nimmt den ersten Decoder, dessen Prematch passt. Die Reihenfolge ist deshalb keine Stilfrage. Mein erster Versuch scheiterte daran: Der Access-Decoder hatte ein Prematch, das auch auf Fehlerzeilen passte, weil es nur nach dem fünften Feld ein [ verlangte. Die Blockzeile wurde also vom Access-Decoder abgefangen, dessen Regex sie nicht verarbeiten konnte. Die Lösung war ein Prematch, das das Zeitformat des Access-Logs verlangt:

    <decoder name="bunkerweb-access">
      <parent>bunkerweb</parent>
      <prematch type="pcre2">^\S+ \S+ \S+ \S+ \S+ \[\d{2}/\w{3}/\d{4}:</prematch>
      <regex type="pcre2">^(\S+) (\S+) (\S+) (\S+) (\S+) \[([^\]]+)\] "(\S+) ([^"]*) (HTTP/[\d.]+)" (\d{3}) (\d+|-) "([^"]*)" "([^"]*)"$</regex>
      <order>vhost, srcip, ident, session_id, remote_user, access_time, http_method, url, protocol, http_status_code, bytes_sent, http_referer, http_user_agent</order>
    </decoder>
    

    Normale Webseiten-Aufrufe

    Der Blockier-Decoder steht danach und extrahiert die Felder, die für die Korrelation nötig sind:

    <decoder name="bunkerweb-modsec-block">
      <parent>bunkerweb</parent>
      <prematch type="pcre2">ModSecurity: Access denied with code</prematch>
      <regex type="pcre2">with code (\d{3}) \(phase (\d)\).*?\[id "(\d+)"\].*?\[msg "([^"]*)"\].*?\[hostname "([^"]+)"\].*?\[uri "([^"]*)"\].*?\[unique_id "([^"]+)"\].*?client: ([^,\s]+)</regex>
      <order>modsec_status, modsec_phase, modsec_id, modsec_msg, vhost, url, unique_id, srcip</order>
    </decoder>
    

    Von der WAF geblockte Anfragen

    Die unique_id ist das wichtigste Feld. Warnungen und Blockierung derselben Anfrage teilen sie sich. Der Warn-Decoder liest dieselbe ID aus der Warnung, bekommt aber keine IP, weil die Warnungszeile keinen client:-Anhang hat. Das ist kein Fehler, sondern ein Unterschied im Logformat.

    Die Sperr-Meldung schließlich wird mit einem eigenen Decoder erfasst, der vor dem AuthBasic-Decoder steht. Der verwendet ebenfalls [ACCESS] im Prematch:

    <decoder name="bunkerweb-ban">
      <parent>bunkerweb</parent>
      <prematch type="pcre2">\[ACCESS\] IP \S+ is banned</prematch>
      <regex type="pcre2">\[ACCESS\] IP (\S+) is banned with reason ([^(]+?) \((\S+?)s remaining\)</regex>
      <order>srcip, ban_reason, ban_remaining_s</order>
    </decoder>
    

    IPs, die von Bunkerweb auf die Bannliste gesetzt wurden.

    Die Reihenfolge habe ich nicht vorab bedacht, sondern erst beim Test gelernt. Wer mit wazuh-logtest prüft, sollte jede Zeile mit dem Syslog-Kopf testen, also so, wie Wazuh sie empfängt. Ohne Kopf greift der Parent-Decoder nicht, und die Probe zeigt irreführende Ergebnisse. Mit docker logs allein bekommt man nur die Nachricht ohne Kopf. Beim Testen in Dateien hilft es, den Kopf vorab mit printf zu setzen.

    Regeln: Vom Ereignis zum Alarm

    Die Regeln bauen aufeinander auf. Die Basisregel erfasst alles, was der Decoder erkennt, die weiteren Regeln bewerten es:

    <rule id="100407" level="3">
      <if_sid>100401</if_sid>
      <field name="http_status_code">^403$</field>
      <description>BunkerWeb: 403 an $(srcip) auf $(vhost)$(url)</description>
      <group>web,access_denied,</group>
    </rule>
    
    <rule id="100408" level="12" frequency="xx" timeframe="60">
      <if_matched_sid>100407</if_matched_sid>
      <same_source_ip />
      <description>BunkerWeb: Scan-Muster von $(srcip), wiederholte 403-Antworten</description>
      <mitre><id>T1595</id></mitre>
      <group>web,scan,active_response_trigger,</group>
    </rule>
    

    Die Einzel-403 sind Level 3 und erzeugen keinen Alarm. Erst die Angegebene Menge von derselben Quelle innerhalb von z.B. 60 Sekunden ergeben einen Scan-Alarm. Diese Schwelle habe ich gewählt, weil ein Browser mit Ghost oder Nextcloud solche Serien praktisch nicht erzeugt. Mit der Blockierung verhält es sich ähnlich:

    <rule id="100421" level="12">
      <if_sid>100400</if_sid>
      <match>ModSecurity: Access denied with code</match>
      <description>BunkerWeb ModSecurity: Angriff wurde mit HTTP 403 blockiert</description>
      <mitre><id>T1190</id></mitre>
      <group>web,attack,active_response_trigger,pci_dss_10.2.4,</group>
    </rule>
    

    Warnungen sind das Gegenstück. Eine Anfrage kann viele Regeln gleichzeitig auslösen, und jede davon schreibt eine Warnung. Die Warnungen verschaffen Einblick, sind aber für sich genommen kein Alarm. Deshalb steht die Regel auf Level 3 mit no_email_alert:

    <rule id="100420" level="3">
      <if_sid>100400</if_sid>
      <field name="modsec_id" type="pcre2">^\d+$</field>
      <field name="modsec_msg" type="pcre2">.+</field>
      <description>BunkerWeb ModSecurity Warnung: $(modsec_msg) (Rule: $(modsec_id)) on $(vhost)</description>
      <options>no_email_alert</options>
      <group>web,attack,waf_warning,</group>
    </rule>
    

    Die spätere Sperr-Regel 100431 steht auf Level 12. Sie löst aus, sobald BunkerWeb eine IP sperrt. Ich habe sie bewusst nicht an die Frequenzregel gekoppelt, denn eine Sperre kann aus mehreren Gründen entstehen.

    Die Sperre ohne eigene Logzeile

    Hier liegt die wichtigste Erkenntnis des Teils. Ich war lange überzeugt, BunkerWeb schreibe bei jeder Sperre eine BADBEHAVIOR-Zeile. Das stimmt nicht. Die Sperren liegen in Redis, und die Zeile [ACCESS] IP ... is banned with reason entsteht nur beim Anlegen einer neuen Sperre. Wer auf eine Zeile wartet, die bei jeder Anfrage erscheint, wartet vergebens.

    Die AI-Analytics-Kette

    Hier hängt die Auswertung am Active-Response-Mechanismus. In der ossec.conf ist die Kette so definiert:

    <command>
      <name>ai-analytics</name>
      <executable>ai-analytics.sh</executable>
      <expect>json</expect>
      <timeout_allowed>no</timeout_allowed>
    </command>
    
    <active-response>
      <command>ai-analytics</command>
      <location>local</location>
      <level>11</level>
    </active-response>
    

    Zwei Dinge sind dabei wichtig. Der Dateiname im <executable>-Element muss exakt zum Skript in /var/ossec/active-response/bin/ passen. Die zweite Sache ist location local. Es bedeutet, dass das Skript auf dem Wazuh-Server lokal läuft und nicht auf dem Webserver. Eine Sperre aus Wazuh heraus braucht deshalb eine eigene Schnittstelle zu BunkerWeb.

    Die Auslösung prüft man am besten direkt im Active-Response-Log:

    tail -n 6 /var/ossec/logs/active-responses.log | cut -c1-200
    

    Eine durch active-response getriggerte Analyse sieht z.B. dann so aus:

    Automatisierter Reconnaissance-Angriff auf Web-DMZ

    Incident ID: 1791900690.30399010 | Datum: 13. Oktober 2026 | Status: Abgewehrt

    Management Summary

    Am 13.10.2026 zwischen 16:11:30 und 16:11:44 Uhr wurde ein automatisierter Reconnaissance-Angriff gegen den Webserver honigtopf.my-blog-domain.net (IP: 192.168.314.159) in der DMZ erkannt. Der Angreifer mit der IP 35.215.26.114 (Google Cloud, Montréal/CA) versuchte, über 50 verschiedene Pfade für sensible Dateien wie phpinfo.php, .env, credentials.json und Backup-Dateien abzurufen.

    Die Sicherheitsmaßnahmen (BunkerWeb WAF, ModSecurity/OWASP CRS) blockierten alle Anfragen erfolgreich mit HTTP Status 403. Die IP wurde automatisch als "bad behavior" gebannt. Es kam zu keiner Kompromittierung des Systems.

    Angriff erfolgreich abgewehrt

    Beteiligte Komponenten

    Komponente IP / Adresse Funktion im Angriffspfad
    Externe Firewall (Fritzbox) 31.99.213.95 Empfang des Traffics, Weiterleitung an interne Firewall
    Interne Firewall (OPNsense/Firewall) 192.168.321.254 NAT und Routing in die DMZ (192.168.314.0/24)
    Webserver (Nginx/BunkerWeb) 192.168.314.159 Ziel des Angriffs. BunkerWeb WAF hat die Anfragen gefiltert und blockiert.
    SIEM (Wazuh) Interner Server Erkennung des Ban-Events, Auslösung der Active-Response und Benachrichtigung.

    Täterprofil

    Quell-IP 35.215.26.114
    Geolocation Montréal, Québec, Kanada
    ASN / Organisation AS19527 Google LLC
    AbuseIPDB Score 100 / 100 (Kritisch)
    VirusTotal Reputation 1 Engine meldet Verdacht
    DNSBL Blacklists Gelistet auf 6 Listen (u.a. Spamhaus, Barracuda)
    Analyse: Die IP gehört zu Google Cloud Infrastructure. Ein AbuseIPDB-Score von 100 mit 48 Reports in 90 Tagen deutet stark darauf hin, dass diese IP Teil eines Botnetzes oder einer automatisierten Scan-Infrastruktur ist, die häufig für Missbrauch gemeldet wird.

    Erfolgsanalyse

    FAZIT: Angriff erfolgreich abgewehrt

    • HTTP Statuscodes: Alle erfassten Anfragen endeten mit 403 Forbidden. Es gab keine 200 OK Antworten für sensible Dateien.
    • WAF Blockade: BunkerWeb identifizierte das Verhalten als "bad behavior" und bannte die IP nach wenigen Sekunden. Die verbleibende Ban-Zeit wurde in den Logs dokumentiert (z.B. "587.73s remaining").
    • ModSecurity Treffer: Die OWASP Core Rule Set (CRS) Regeln wurden mehrfach ausgelöst, insbesondere:
      • 930130: Restricted File Access Attempt (LFI)
      • 920440: URL file extension is restricted by policy (.bak, .old)
      • 920500: Attempt to access a backup or working file
    • Keine Datenlecks: Da keine Datei erfolgreich geladen wurde, sind keine sensiblen Informationen (API Keys, DB Credentials, PHP Config) an den Angreifer gelangt.

    Angriffs- & Payload-Analyse

    Der Angreifer nutzte einen automatisierten Scanner, der eine Liste von bekannten sensiblen Dateien und Verzeichnissen durchsuchte. Die folgenden Kategorien wurden angegriffen:

    1. PHP Information & Debugging

    Versuche, die PHP-Umgebung und Konfiguration auszulesen:

    GET /phpinfo.php
    GET /info.php
    GET /debug.php
    GET /server-info.php
    GET /server-status.php

    2. Sensible Konfigurationsdateien & Secrets

    Versuche, Umgebungsvariablen, API-Keys und Cloud-Credentials zu stehlen:

    GET /.env
    GET /notifications/.env
    GET /service-account.json
    GET /gcp-key.json
    GET /credentials.json
    GET /google-credentials.json
    GET /.config/gcloud/application_default_credentials.json
    GET /firebase-adminsdk.json

    3. Backup & Working Files (LFI/Info Leak)

    Versuche, alte Versionen oder temporäre Dateien zu finden, die oft schwächer geschützt sind:

    GET /phpinfo.php.bak
    GET /phpinfo.php.old
    GET /phpinfo.php~
    GET /phpinfo.php.save
    GET /info.php.bak

    4. Verzeichnis-Enumeration

    Der Scanner probierte die obigen Dateien in vielen gängigen Hosting-Verzeichnissen:

    /admin/, /test/, /dev/, /tmp/, /public/, /mail/, /cpanel/, 
    /hosting/, /webmail/, /smtp/, /staging/, /beta/, /uat/, /qa/, 
    /preview/, /www/, /htdocs/, /public_html/, /site/, /docs/, 
    /wp-admin/, /administrator/, /core/, /includes/

    Chronologischer Ablauf (Timeline)

    Zeit (CEST) Komponente Ereignis Beschreibung
    16:11:30 Wazuh SIEM Alert Auslösung Erkennung des BunkerWeb Bans für IP 35.215.26.114. Active-Response gestartet.
    16:11:36 BunkerWeb Blockade Request GET /phpinfo.php blockiert (403). IP bereits gebannt.
    16:11:37 BunkerWeb Blockade Serie Mehrere Requests (/info.php, /php.php, /i.php) blockiert.
    16:11:38 ModSecurity Rule Match Treffer auf 930130 (Restricted File Access) bei /admin/phpinfo.php.
    16:11:39 BunkerWeb Blockade Serie Requests nach /_environment, /mail/phpinfo.php blockiert.
    16:11:40 ModSecurity Rule Match Treffer auf 920440 (Restricted Extension .bak) bei /phpinfo.php.bak.
    16:11:42 BunkerWeb Blockade Serie Requests nach /public_html/phpinfo.php, /wp-admin/phpinfo.php blockiert.
    16:11:43 ModSecurity Rule Match Treffer auf 930130 bei /gcp-credentials.json und /credentials.json.
    16:11:44 Firewall (filterlog) Traffic Log Log-Eintrag zeigt TCP SYN Packet von 35.215.26.114 zu 192.168.314.159:443 (pass).
    16:11:44 BunkerWeb Letzter Block Request GET / blockiert. Angriff beendet durch Ban.
    16:34 - 17:16 ntfy.sh / SIEM Benachrichtigung Wiederholte Push-Benachrichtigungen an das SOC-Team mit dem Incident-Report.

    Relevante Log-Auszüge (Beweismittel)

    # Beispiel ModSecurity Alert (LFI Versuch)
    Oct 13 16:11:38 bunkerweb bunkerweb_error: ... ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `restricted-files.data' against variable `REQUEST_FILENAME' (Value: `/admin/phpinfo.php' ) [id "930130"] [msg "Restricted File Access Attempt"] ... client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET /admin/phpinfo.php HTTP/1.1"
    
    # Beispiel BunkerWeb Ban Log
    Oct 13 16:11:36 bunkerweb bunkerweb_error: ... [ACCESS] IP 35.215.26.114 is banned with reason bad behavior (581.138s remaining), client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET /info.php HTTP/1.1"
    
    # Beispiel Firewall Log (Pass in DMZ)
    Oct 13 16:11:44 opnsense.meister.lan filterlog[39962]: ... vtnet1,match,pass,in,... tcp,60,35.215.26.114,192.168.314.159,47210,443 ...

    Risikobewertung & Handlungsempfehlung

    Kritikalität Mittel
    Erfolg des Angreifers Kein Erfolg

    Begründung

    Obwohl der Angriff abgewehrt wurde, ist die Intensität und die Art der gesuchten Dateien (Cloud Credentials, .env) hoch relevant. Ein erfolgreicher Zugriff hätte zu einem vollständigen Compromise der Cloud-Umgebung oder Datenbank führen können. Die IP hat einen maximalen Abuse-Score, was auf eine professionelle oder zumindest sehr aggressive Botnetz-Infrastruktur hindeutet.

    Empfohlene Maßnahmen

    1. IP-Blockade prüfen: Sicherstellen, dass die IP 35.215.26.114 auch auf der externen Firewall (Fritzbox/OPNsense) dauerhaft blockiert ist, nicht nur im WAF.
    2. Log-Rotation & Archivierung: Die Logs dieses Incidents für forensische Zwecke archivieren.
    3. WAF-Regeln überprüfen: Prüfen, ob die OWASP CRS Regeln optimal konfiguriert sind. Die aktuellen Einstellungen haben gut funktioniert.
    4. Monitoring: Auf weitere Versuche aus dem Google Cloud ASN (AS19527) achten, da diese oft für Scans genutzt werden. Ggf. Rate-Limiting für anonyme Requests erhöhen.
    5. Keine weiteren Aktionen nötig: Da keine Daten geleakt wurden, ist kein Incident Response Level 1 (z.B. Passwort-Reset, System-Rebuild) erforderlich.

    Logs bezüglich aller Angreifer-IPs: 35.215.26.114

    --- Logs für IP: 35.215.26.114 ---
    Oct 13 16:11:10 bunkerweb bunkerweb_error: 2026/10/13 16:11:10 [warn] 444423#444423: *1930738 ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `restricted-files.data' against variable `REQUEST_FILENAME' (Value: `/.git/config' ) [file "/usr/share/bunkerweb/core/modsecurity/files/coreruleset-v4/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "131"] [id "930130"] [rev ""] [msg "Restricted File Access Attempt"] [data "Matched Data: .git/ found within REQUEST_FILENAME: /.git/config"] [severity "2"] [ver "OWASP_CRS/4.29.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "honigtopf.my-blog-domain.net"] [uri "/.git/config"] [unique_id "17919006707.271155"] [ref "o1,5v4,12t:utf8toUnicode,t:urlDecodeUni,t:normalizePathWin"], client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET /.git/config HTTP/1.1", host: "honigtopf.my-blog-domain.net"
    Oct 13 16:11:10 bunkerweb bunkerweb: honigtopf.my-blog-domain.net 35.215.26.114 - 16647110bb4b7cb2b58b426dd16b0547 - [13/Oct/2026:16:11:10 +0200] "GET /.git/config HTTP/1.1" 404 69301 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    Oct 13 16:11:10 opnsense.meister.lan filterlog[39962]: 73,,,7539f185-43a5-4550-9e9c-c488adfddd4a,vtnet1,match,pass,in,4,0x0,,59,29187,0,DF,6,tcp,60,35.215.26.114,192.168.314.159,49094,443,0,S,3008651233,,65320,,mss;sackOK;TS;nop;wscale
    Oct 13 16:11:11 bunkerweb bunkerweb_error: 2026/10/13 16:11:11 [warn] 444423#444423: *1930738 ModSecurity: Warning. Matched "Operator `PmFromFile' with parameter `restricted-files.data' against variable `REQUEST_FILENAME' (Value: `/.env' ) [file "/usr/share/bunkerweb/core/modsecurity/files/coreruleset-v4/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf"] [line "131"] [id "930130"] [rev ""] [msg "Restricted File Access Attempt"] [data "Matched Data: .env found within REQUEST_FILENAME: /.env"] [severity "2"] [ver "OWASP_CRS/4.29.0"] [maturity "0"] [accuracy "0"] [tag "application-multi"] [tag "language-multi"] [tag "platform-multi"] [tag "attack-lfi"] [tag "paranoia-level/1"] [tag "OWASP_CRS"] [tag "OWASP_CRS/ATTACK-LFI"] [tag "capec/1000/255/153/126"] [hostname "honigtopf.my-blog-domain.net"] [uri "/.env"] [unique_id "179190067140.924289"] [ref "o1,4v4,5t:utf8toUnicode,t:urlDecodeUni,t:normalizePathWin"], client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET /.env HTTP/1.1", host: "honigtopf.my-blog-domain.net"
    Oct 13 16:11:11 bunkerweb bunkerweb: honigtopf.my-blog-domain.net 35.215.26.114 - ec6f32b01aa14bcba5ca3d69c00d9f64 - [13/Oct/2026:16:11:11 +0200] "GET /.env HTTP/1.1" 404 69301 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    Oct 13 16:11:18 bunkerweb bunkerweb_error: 2026/10/13 16:11:18 [warn] 444416#444416: *1930845 [BADBEHAVIOR] IP 35.215.26.114 is banned for 600s (30/30) on server honigtopf.my-blog-domain.net with scope service, context: ngx.timer
    Oct 13 16:11:18 bunkerweb bunkerweb_error: 2026/10/13 16:11:18 [warn] 444423#444423: *1930738 [ACCESS] IP 35.215.26.114 is banned with reason bad behavior (599.886s remaining), client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET /build/.env HTTP/1.1", host: "honigtopf.my-blog-domain.net"
    Oct 13 16:11:44 bunkerweb bunkerweb_error: 2026/10/13 16:11:44 [warn] 444425#444425: *1931085 [ACCESS] IP 35.215.26.114 is banned with reason bad behavior (573.204s remaining), client: 35.215.26.114, server: honigtopf.my-blog-domain.net, request: "GET / HTTP/1.1", host: "honigtopf.my-blog-domain.net"
    Oct 13 16:11:44 bunkerweb bunkerweb: honigtopf.my-blog-domain.net 35.215.26.114 - db6b641206deddd5e51da39b388f31cf - [13/Oct/2026:16:11:44 +0200] "GET / HTTP/1.1" 403 69300 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
    [... gekürzt: Vollständige Logs können bei Bedarf nachgeladen werden ...]

    Wie man sieht, verbessern sich die bereits voranalysierten Alarme. Da der Reverse-Proxy und die Webservices jeweils auf Hosts leben, auf denen auch ein wazuh-agent installiert ist, wüsste mein SIEM natürlich darüber bescheid, wenn Befehle auf dem Server ausgeführt worden wäre.

    Ich hatte Nachtschicht... und habe das auf die Probe gestellt ;-)
    So sieht es aus, wenn unattended-updates gelaufen sind. Das wird nicht als Malware identifiziert - aber dennoch geloggt.

    Auch wenn Dateien angelegt worden wären. All diese Informationen sind in den Logs vorhanden und würden in der KI-Analyse berücksichtigt werden.

    Testen, ohne sich selbst auszusperren

    Für Live-Tests braucht man Anfragen, die echte Serien erzeugen. Dafür nutze ich einen Dienst, der nur für Tests dient, und zähle die Ergebnisse, statt blind zu schicken. Die Serie von 40 Anfragen auf nicht existierende Pfade, gefolgt von wenigen Anfragen mit 403, ist die Vorlage:

    HOST="testsiem.meister-security.de"; MARK="ar-$(date +%s)"
    for i in $(seq 1 40); do curl -sk -o /dev/null "https://${HOST}/${MARK}/n-${i}"; done
    sleep 5
    for i in 1 2 3; do curl -sk -o /dev/null -w "%{http_code} " "https://${HOST}/${MARK}/s-${i}"; done; echo
    

    Zwei Regeln gelten dabei. Die Test-SourceIP sollte nicht die eigene sein, sonst löst man den Scan-Alarm gegen sich selbst aus. Und nach dem Test muss die Sperre in der Oberfläche wieder entfernt werden, sonst bleibt man ausgesperrt. Auf dem Testdienst habe ich das einmal vergessen, und die Sperre lief einige Minuten, bevor ich weitermachen konnte.

    Die Auswertung erfolgt über das Archiv, das alles zeigt, was ankommt:

    grep -a "$MARK" /var/ossec/logs/archives/archives.json | grep -a '" 403 ' | wc -l
    grep -a "$MARK" /var/ossec/logs/alerts/alerts.json | grep -c '"id":"100408"'
    

    Erst wenn beide Zahlen plausibel sind, gilt die Kette als funktionsfähig. Die Frequenzregel hat bei mir im Live-Test gefeuert, und zwar mit der Quell-IP des Tests.

    Fazit

    Die Kette von BunkerWeb über syslog-ng bis zu Wazuh funktioniert heute, und sie bewertet Ereignisse nach Wirkung statt nach Lärm. Die größten Fehler lagen nicht in den Regeln, sondern in der Reihenfolge der Decoder, in einem Filter mit einem Leerzeichen zu viel und in einem Skript mit falschem Namen. Wer so eine Kette aufbaut, sollte jeden Schritt mit einem echten Ereignis prüfen und nicht mit einer Vermutung.