Wazuh

## Overview

This post walks through building a log pipeline that takes CEF-formatted security events from UniFi network devices, routes them through a Vector syslog pipeline, and delivers them to Wazuh for security alerting. By the end, Wazuh is decoding UniFi firewall blocks, WiFi disconnects, and IDS detections into structured alert fields — with custom rules firing on each event type.

## Architecture

“`

UniFi Device

    │

    │  UDP Syslog (CEF)

    ▼

Vector (collectors stack, port 514)

    │  parse_cef transform → cef_only filter

    │

    ├──▶ VictoriaLogs  (all logs)

    ├──▶ VictoriaMetrics  (disconnect metrics)

    └──▶ Wazuh Manager  (CEF events only, UDP 1514)

                │

                ▼

         Wazuh Indexer → Wazuh Dashboard

“`

UniFi devices send syslog UDP to Vector. Vector parses the CEF format, normalizes severity, and fans out to multiple destinations. Wazuh receives only the CEF branch — security-relevant events — keeping noise low in the SIEM.

## Step 1 — Add the Wazuh Sink to Vector

The Vector config already had a `cef_only` filter that isolates CEF events from UniFi devices. We added a `socket` sink pointing at the Wazuh manager’s syslog listener.

**`vector.yaml` addition:**

“`yaml

sinks:

  wazuh:

    type: socket

    inputs: [“cef_only”]

    mode: udp

    address: “172.17.0.1:1514”

    encoding:

      codec: syslog

“`

`172.17.0.1` is the Docker bridge gateway — the Wazuh stack runs in a separate Compose project, so Vector reaches it via the host’s mapped port rather than a shared Docker network. The `syslog` codec outputs RFC 5424 format, which Wazuh’s remote listener understands.

After restarting Vector:

“`bash

docker compose restart vector

docker compose logs vector

“`

Vector started cleanly with all healthchecks passing and no errors related to the new sink.

## Step 2 — Verify Wazuh is Ready to Receive

Checking the Wazuh manager’s `ossec.conf` confirmed it was already configured for remote syslog on 1514:

“`xml

<remote>

    <connection>syslog</connection>

    <port>1514</port>

    <protocol>udp</protocol>

    <allowed-ips>0.0.0.0/0</allowed-ips>

    <local_ip>0.0.0.0</local_ip>

</remote>

<remote>

    <connection>syslog</connection>

    <port>1514</port>

    <protocol>tcp</protocol>

    <allowed-ips>0.0.0.0/0</allowed-ips>

    <local_ip>0.0.0.0</local_ip>

</remote>

“`

Both UDP and TCP are configured, with a comment referencing Vector and UniFi — this was set up intentionally.

## Step 3 — End-to-End Verification

To confirm delivery without waiting for live UniFi traffic, we injected a test CEF syslog event directly:

“`bash

echo ‘<134>May  8 10:00:00 ubnt CEF:0|Ubiquiti|UniFi|1.0|wifi_disconnect|WiFi Disconnect|5|src=192.168.1.100 dst=10.0.0.1 cs1=TestAP cs2=TestClient’ \

  | nc -u -w1 127.0.0.1 514

“`

We temporarily enabled `logall` in Wazuh to capture all events (not just matched alerts):

“`bash

docker exec wazuh-wazuh-manager-1 sed -i \

  ‘s/<logall>no<\/logall>/<logall>yes<\/logall>/’ \

  /var/ossec/etc/ossec.conf

docker exec wazuh-wazuh-manager-1 /var/ossec/bin/wazuh-control reload

“`

The archive log confirmed receipt:

“`

2026 May 08 11:06:29 wazuh-manager->172.22.0.1  1 2026-05-08T11:06:29.285408Z ubnt CEF – – – \

  0|Ubiquiti|UniFi|1.0|wifi_disconnect|WiFi Disconnect|5|src=192.168.1.100 dst=10.0.0.1 \

  cs1=TestAP cs2=TestClient

“`

The event arrived from `172.22.0.1` (the Vector container IP). Pipeline confirmed working end-to-end. `logall` was reverted to `no` afterward.

## Step 4 — Write the UniFi CEF Decoder

With the pipeline working, Wazuh was receiving events but had no decoder for UniFi CEF format. Events passed through as generic syslog with no field extraction.

### What Wazuh Sees

Vector’s `syslog` codec outputs RFC 5424. Wazuh receives the full raw line:

“`

1 2026-05-08T11:06:29.285408Z ubnt CEF – – – 0|Ubiquiti|UniFi|1.0|wifi_disconnect|WiFi Disconnect|5|src=192.168.1.100 dst=10.0.0.1 spt=54321 dpt=80 proto=udp

“`

### Wazuh OS_Regex — Important Quirks

Writing this decoder required working through several non-obvious behaviors in Wazuh’s regex engine (OS_Regex):

| Pattern | Behavior |

|—|—|

| `\p` | Matches any **printable** character (including `\|`) |

| `\.` | Matches **any single character** (like `.` in standard regex) |

| `.` | Matches a **literal dot** |

| `\w` | Word chars `[a-zA-Z0-9_]` — stops at `\|` naturally |

| `\|` in `<regex>` | **Alternation operator** — cannot be used as literal pipe |

| `[^…]` | **Not supported** — negated character classes cause syntax errors |

| Nested groups + quantifiers | **Not supported** — e.g. `(\w+(\s\w+)*)` fails |

The key insight: use `\p` in `<regex>` where you need to match a literal `|`, and use `\w+` for CEF header fields (it stops at `|` naturally since `|` is not a word character).

For the version field (`1.0`), unescaped `.` is a literal dot, so `\d+.\d+` matches `1.0` exactly and stops before the next pipe — without consuming it.

### The Decoder

“`xml

<!– Parent: identifies all UniFi CEF events –>

<decoder name=”unifi-cef”>

    <prematch>CEF – – – \d+\pUbiquiti\pUniFi\p</prematch>

</decoder>

<!– signatureId: anchor on UniFi\p, skip version with literal-dot, capture \w+ –>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>UniFi\p\d+.\d+\p(\w+)\p</regex>

    <order>id</order>

</decoder>

<!– event name: may contain spaces; \d+\p anchors end to severity field –>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>UniFi\p\d+.\d+\p\w+\p(\.+)\p\d+\p</regex>

    <order>extra_data</order>

</decoder>

<!– Extension key=value fields –>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>src=(\S+)</regex>

    <order>srcip</order>

</decoder>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>dst=(\S+)</regex>

    <order>dstip</order>

</decoder>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>spt=(\d+)</regex>

    <order>srcport</order>

</decoder>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>dpt=(\d+)</regex>

    <order>dstport</order>

</decoder>

<decoder name=”unifi-cef-fields”>

    <parent>unifi-cef</parent>

    <regex>proto=(\S+)</regex>

    <order>protocol</order>

</decoder>

“`

Multiple child decoders share the same name (`unifi-cef-fields`) — this is Wazuh’s accumulator pattern, where each one extracts a different field from the same log line.

### Enabling the Custom Decoder Directory

By default this Wazuh Docker setup only loaded `ruleset/decoders`. The `etc/decoders` directory (where custom decoders live) needed to be explicitly added to `ossec.conf`:

“`xml

<ruleset>

    <decoder_dir>ruleset/decoders</decoder_dir>

    <decoder_dir>etc/decoders</decoder_dir>

    <rule_dir>ruleset/rules</rule_dir>

    <rule_dir>etc/rules</rule_dir>

    …

</ruleset>

“`

## Step 5 — Write the Alert Rules

“`xml

<group name=”unifi,cef,network,”>

    <!– Base rule: fires for every UniFi CEF event –>

    <rule id=”100100″ level=”3″>

        <decoded_as>unifi-cef</decoded_as>

        <description>UniFi: $(extra_data)</description>

        <group>unifi_cef,</group>

    </rule>

    <!– WiFi events –>

    <rule id=”100101″ level=”3″>

        <if_sid>100100</if_sid>

        <id>wifi_disconnect</id>

        <description>UniFi: WiFi client disconnected</description>

        <group>wireless,</group>

    </rule>

    <rule id=”100102″ level=”3″>

        <if_sid>100100</if_sid>

        <id>wifi_roam</id>

        <description>UniFi: WiFi client roamed to new AP</description>

        <group>wireless,</group>

    </rule>

    <!– Firewall blocks –>

    <rule id=”100110″ level=”5″>

        <if_sid>100100</if_sid>

        <id>^firewall</id>

        <description>UniFi firewall: Connection blocked from $(srcip) to $(dstip)</description>

        <group>firewall,pci_dss_1.4,</group>

    </rule>

    <!– IDS/IPS detections –>

    <rule id=”100120″ level=”8″>

        <if_sid>100100</if_sid>

        <id>^IDS</id>

        <description>UniFi IDS: Threat detected from $(srcip)</description>

        <group>ids,intrusion_detection,gdpr_IV_35.7.d,pci_dss_11.4,</group>

    </rule>

    <rule id=”100121″ level=”12″>

        <if_sid>100120</if_sid>

        <extra_data>^Critical</extra_data>

        <description>UniFi IDS: Critical threat detected from $(srcip)</description>

        <group>ids,intrusion_detection,gdpr_IV_35.7.d,pci_dss_11.4,</group>

    </rule>

</group>

“`

## Step 6 — Verify with wazuh-logtest

`wazuh-logtest` is the right tool to validate decoders and rules without waiting for live traffic:

“`bash

echo ‘1 2026-05-08T11:06:29.285408Z ubnt CEF – – – 0|Ubiquiti|UniFi|1.0|wifi_disconnect|WiFi Disconnect|5|src=192.168.1.100 dst=10.0.0.1 spt=54321 dpt=80 proto=udp’ \

  | docker exec -i wazuh-wazuh-manager-1 /var/ossec/bin/wazuh-logtest

“`

**Output:**

“`

**Phase 2: Completed decoding.

    name: ‘unifi-cef’

    dstip: ‘10.0.0.1’

    dstport: ’80’

    extra_data: ‘WiFi Disconnect’

    id: ‘wifi_disconnect’

    protocol: ‘udp’

    srcip: ‘192.168.1.100’

    srcport: ‘54321’

**Phase 3: Completed filtering (rules).

    id: ‘100101’

    level: ‘3’

    description: ‘UniFi: WiFi client disconnected’

    groups: [‘unifi’, ‘cef’, ‘network’, ‘wireless’]

**Alert to be generated.

“`

Testing a firewall event:

“`

**Phase 2: Completed decoding.

    name: ‘unifi-cef’

    id: ‘firewall_block’

    extra_data: ‘Firewall Block’

    srcip: ‘203.0.113.5’

    dstip: ‘10.0.0.1’

    srcport: ‘4444’

    dstport: ‘443’

    protocol: ‘tcp’

**Phase 3: Completed filtering (rules).

    id: ‘100110’

    level: ‘5’

    description: ‘UniFi firewall: Connection blocked from 203.0.113.5 to 10.0.0.1’

    groups: [‘unifi’, ‘cef’, ‘network’, ‘firewall’]

    pci_dss: [‘1.4’]

**Alert to be generated.

“`

## Step 7 — Persist Configuration

The decoder and rules files were added as bind mounts in `docker-compose.yml` so they survive a full stack rebuild:

“`yaml

volumes:

  – ./config/wazuh-manager/ossec.conf:/wazuh-config-mount/etc/ossec.conf

  – ./config/wazuh-manager/decoders/local_decoder.xml:/var/ossec/etc/decoders/local_decoder.xml

  – ./config/wazuh-manager/rules/local_rules.xml:/var/ossec/etc/rules/local_rules.xml

“`

**File layout:**

“`

docker/wazuh/

├── docker-compose.yml

└── config/

    └── wazuh-manager/

        ├── ossec.conf

        ├── decoders/

        │   └── local_decoder.xml

        └── rules/

            └── local_rules.xml

“`

All three files are version-controlled with the project. Future changes: edit the host-side files, then run `docker exec wazuh-wazuh-manager-1 /var/ossec/bin/wazuh-control reload`.

## Summary

| What | How |

|—|—|

| CEF events forwarded to Wazuh | Vector `socket` sink → UDP 1514 |

| Wazuh remote listener | `ossec.conf` `<remote>` stanzas on 1514 UDP+TCP |

| Field extraction | `unifi-cef` parent + `unifi-cef-fields` accumulator children |

| Fields decoded | signatureId, event name, srcip, dstip, srcport, dstport, protocol |

| Alert rules | 6 rules covering WiFi, firewall, and IDS event types |

| Config persistence | Bind mounts in `docker-compose.yml` |

UniFi CEF events now flow from the network edge through Vector into Wazuh as structured, actionable alerts. The decoder can be extended with additional `unifi-cef-fields` child decoders as new extension fields are encountered, and new rules can reference any decoded field by name.