PostgreSQL-Auth-Fehler in Wazuh sauber dekodieren und als Rule alerten: Decoder-Design, Feldextraktion und Rule-Chaining

Einleitung

Fehlgeschlagene Datenbank-Authentifizierungen gehören zu den wichtigsten Signalen in einem SIEM: Sie sind häufiges Symptom für Credential-Stuffing, Brute-Force-Versuche, Fehlkonfigurationen oder ausgerollte Systeme ohne korrekt gesetzte Secrets. Damit Wazuh solche Ereignisse zuverlässig erkennt, braucht es zwei Bausteine: einen Decoder, der die relevanten Felder aus dem Log extrahiert (z. B. den betroffenen PostgreSQL-User), und passende Rules, die daraus Alerts mit sinnvollen Severity-Levels und Gruppierungen erzeugen.

Ausgangslage / Problemstellung

In der Praxis tauchen PostgreSQL-Logs oft nicht nur auf Englisch auf, sondern – je nach Locale – in anderen Sprachen, hier Französisch. Beispielhaft treten zwei zusammenhängende Zeilen auf:

  • FATAL: authentification par mot de passe échouée pour l'utilisateur « postgres »
  • DÉTAIL: L'utilisateur « postgres » n'a pas de mot de passe affecté. ... pg_hba.conf ... scram-sha-256

Die erste Herausforderung: Die ursprüngliche Decoder-Definition matcht nur teilweise, weil sie zu eng auf FATAL: bzw. DÉTAIL: und eine konkrete Struktur gebaut ist. Die zweite Herausforderung: Selbst wenn der Decoder korrekt funktioniert, braucht es Rules, damit Wazuh Alerts erzeugt, die im Dashboard sichtbar und filterbar sind.

Technische Analyse

1) Warum der ursprüngliche Decoder nicht zuverlässig greift

Ein prematch wie ^\d{4}-\d{2}-\d{2}.*FATAL: ist grundsätzlich ok, aber in realen PostgreSQL-Logs variieren häufig:

  • Zeitformat (Millisekunden, Zeitzone, unterschiedliche Separatoren)
  • Prefix-Struktur (postgres@postgres, PID, Client-IP, DB-Name, Locale-Strings wie [inconnu])
  • Unicode/Typografie (französische Anführungszeichen « », Akzente in DÉTAIL)

Wenn prematch zu eng ist, wird der Decoder nie “aktiv”, und nachgelagerte Child-Decoder greifen nicht.

2) Wazuh Decoder-Pipeline: Parent/Child richtig nutzen

Best Practice ist:

  • Parent-Decoder matcht nur das stabile “Gerüst” (z. B. PostgreSQL-Logprefix).
  • Child-Decoder extrahiert Felder (hier: user) aus konkreten Message-Varianten.

Damit bleibt die Lösung robust, auch wenn sich Details in FATAL/DÉTAIL ändern oder weitere PostgreSQL-Meldungen dazukommen.

3) Der Schritt von “Decoder funktioniert” zu “Alert erscheint”

Decoder allein erzeugen keine Alerts. Wazuh erzeugt Alerts über Rules, die u. a. auf Folgendem basieren können:

  • <decoded_as>: An welchen Decoder das Event gebunden ist
  • <match> / <regex>: Welche Message-Charakteristika ein Event haben muss
  • <if_sid>: Rule-Chaining (Parent/Child auf Rule-Ebene)
  • level: Relevanz/Severity – nur ab Level 3 wird typischerweise indexiert und im Dashboard sichtbar gemacht

Lösung / Best Practices

Schritt 1: Custom Decoder sauber ablegen

Custom Decoder gehören nach:
/var/ossec/etc/decoders/ (z. B. authentification_decoders.xml)

Ein robustes Beispiel, das sich an den gelieferten Logs orientiert (Parent + Feldextraktion für den User):

<decoder name="postgresql-base">
  <prematch>^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.\d+.*postgres@postgres</prematch>
</decoder>

<decoder name="postgresql-auth-user">
  <parent>postgresql-base</parent>
  <!-- Erfasst den User hinter "l'utilisateur" inkl. Unicode-Anführungszeichen -->
  <regex>l'utilisateur\s+«\s*([^»\s]+)\s*»</regex>
  <order>user</order>
</decoder>

Warum diese Variante in der Praxis stabil ist:

  • Parent matcht auf Datums-/Zeitstempel + typisches PostgreSQL-Prefix (nicht auf “FATAL” fixiert).
  • Child extrahiert den User anhand eines stabilen Textmusters (l'utilisateur « ... ») und ignoriert den Rest.

Wichtig: Wenn in eurer Umgebung statt « » normale Quotes oder gar keine Quotes vorkommen, erweitert man das Regex vorsichtig (z. B. alternative Delimiter), statt den Parent aufzuweichen.

Schritt 2: Manager neu starten und Decoder testen

Änderungen an Decodern werden nach Restart wirksam:

systemctl restart wazuh-manager

Für die Validierung ist wazuh-logtest (lokal auf dem Manager) das zentrale Werkzeug:

  • Prüfen, ob decoded_as korrekt gesetzt wird
  • Prüfen, ob das Feld user gefüllt ist
  • Prüfen, welche Rule am Ende triggert

Schritt 3: Custom Rule-Container anlegen (local_rules.xml)

Custom Rules gehören nach:
/var/ossec/etc/rules/local_rules.xml

Grundstruktur mit Group und einer Parent-Rule, die alle PostgreSQL-Events aus dem Parent-Decoder einsammelt:

<group name="postgresql_custom,authentication,">

  <rule id="100101" level="3">
    <decoded_as>postgresql-base</decoded_as>
    <description>PostgreSQL: Ereignis aus Custom-Decoder gruppiert.</description>
  </rule>

</group>

Warum das sinnvoll ist:

  • Du bekommst eine “sichere Klammer”, an die du weitere spezialisierte Rules per <if_sid> hängen kannst.
  • Du vermeidest Duplikate, weil nicht jede spezialisierte Rule erneut <decoded_as> matchen muss.

ID-Range: Für Custom Rules ist ein Bereich wie 100000–120000 gängig, um Konflikte mit Standard-Regeln zu vermeiden.

Schritt 4: Spezifische Rules für FATAL und DÉTAIL bauen

Nun hängst du konkrete Erkennungen an die Parent-Rule. Für deine Logs sind mindestens zwei Fälle interessant:

  1. Passwortauth fehlgeschlagen (Security-relevant, oft Brute-Force/Fehlkonfiguration)
  2. User ohne Passwort gesetzt (Konfigurations-/Härtungsthema)

Beispiel:

<group name="postgresql_custom,authentication,">

  <rule id="100101" level="3">
    <decoded_as>postgresql-base</decoded_as>
    <description>PostgreSQL: Ereignis aus Custom-Decoder gruppiert.</description>
  </rule>

  <rule id="100102" level="7">
    <if_sid>100101</if_sid>
    <match>FATAL:</match>
    <match>authentification par mot de passe échouée</match>
    <description>PostgreSQL: Passwort-Authentifizierung fehlgeschlagen (User: $(user)).</description>
    <group>authentication_failed,</group>
  </rule>

  <rule id="100103" level="5">
    <if_sid>100101</if_sid>
    <match>DÉTAIL:</match>
    <match>n'a pas de mot de passe</match>
    <description>PostgreSQL: User ohne gesetztes Passwort (User: $(user)).</description>
    <group>policy_violation,</group>
  </rule>

</group>

Hinweise zur Praxis:

  • Mehrere <match> wirken wie ein “UND”: alle müssen im Event vorkommen. Das reduziert False Positives.
  • $(user) nutzt das Feld aus dem Decoder. Wenn es leer ist, stimmt meist die Regex-Extraktion im Child-Decoder nicht oder der Child greift nicht.
  • level bewusst wählen: Auth-Fail eher höher (z. B. 7), “kein Passwort gesetzt” moderat (z. B. 5), je nach Policy.

Schritt 5: Optional – Kontext aus pg_hba.conf-Zeile extrahieren

Dein Beispiel enthält zusätzlich: La connexion correspond à la ligne ... du pg_hba.conf : « host ... scram-sha-256 »
Wenn du das später korrelieren willst (Policy-Audits, Fehlkonfigurationen), kannst du einen weiteren Child-Decoder ergänzen (z. B. Feld pg_hba_rule) und in einer Rule als Description/Tag nutzen. Best Practice ist: erst Auth-Fail stabil alerten, dann schrittweise mehr Felder extrahieren.

Lessons Learned / Best Practices

  • Parent-Decoder breit, Child-Decoder präzise: Parent nur für stabile Struktur; Feldextraktion nur in Children.
  • Unicode/Locale aktiv berücksichtigen: Akzente (DÉTAIL) und Typografie (« ») gehören ins Regex-Design – sonst scheitert Feldextraktion leise.
  • Rule-Chaining konsequent nutzen: Eine Parent-Rule mit <decoded_as> + Child-Rules mit <if_sid> hält die Rulebase wartbar.
  • Dashboard-Sichtbarkeit sicherstellen: Rules mit level < 3 sind häufig nicht indexiert – für operative Use-Cases mindestens Level 3 setzen.
  • Testgetrieben arbeiten: Nach jeder Änderung wazuh-logtest verwenden, um Decoder-Felder und Rule-Matches nachvollziehbar zu validieren.
  • Skalierung & Betrieb: Custom Decoder/Rules versionieren (Git), Änderungen via CI/Ansible ausrollen, und nach Updates prüfen, ob Standard-Decoder/Rules neue Überschneidungen erzeugen.

Fazit

Mit einem stabilen Parent/Child-Decoder-Design lassen sich lokalisierte PostgreSQL-Auth-Logs zuverlässig parsen und kritische Felder wie der Username extrahieren. Der entscheidende Schritt zum SIEM-Mehrwert ist anschließend eine klare Rule-Struktur: eine Parent-Rule zum Gruppieren (<decoded_as>) und darauf aufbauende spezialisierte Rules (<if_sid>) für konkrete Sicherheits- und Policy-Events. So entstehen saubere, sichtbare Alerts mit belastbarer Semantik – und eine Rulebase, die auch bei wechselnden Logformaten langfristig wartbar bleibt.

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