## 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.