docs(event-streaming): add Wazuh integration guide (#734)

* docs(event-streaming): add Wazuh integration guide

Documents how to wire NetBird's Generic HTTP event-streaming integration
to a self-hosted Wazuh manager: NetBird-side configuration, KQL queries
for audit and traffic events in the Wazuh dashboard, and a schema
reference for both event shapes via the receiver wrapper.

Adds the page to the Stream Activity Events sidebar and the
event-streaming index.

* docs(event-streaming): replace Wazuh Generic HTTP screenshot
This commit is contained in:
Jack Carter
2026-05-12 15:52:25 +02:00
committed by GitHub
parent b968695737
commit 6d0ad7d60d
4 changed files with 147 additions and 1 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@@ -436,6 +436,10 @@ export const docsNavigation = [
title: 'Generic HTTP',
href: '/manage/activity/event-streaming/generic-http',
},
{
title: 'Wazuh',
href: '/manage/activity/event-streaming/wazuh',
},
],
},
],

View File

@@ -22,4 +22,5 @@ supported third-party platforms. To get started, select one of the following int
- [Amazon S3](/manage/activity/event-streaming/amazon-s3)
- [Amazon Data Firehose](/manage/activity/event-streaming/amazon-firehose)
- [SentinelOne Data Lake](/manage/activity/event-streaming/sentinelone-data-lake)
- [Generic HTTP](/manage/activity/event-streaming/generic-http)
- [Generic HTTP](/manage/activity/event-streaming/generic-http)
- [Wazuh](/manage/activity/event-streaming/wazuh)

View File

@@ -0,0 +1,141 @@
# Stream Activity Events to Wazuh
Wire NetBird's [Event Streaming → Generic HTTP](/manage/activity/event-streaming/generic-http) integration to a Wazuh manager so every [audit event](/manage/activity/audit-events) and [traffic event](/manage/activity/traffic-events-logging) lands in Wazuh as a searchable alert.
## Prerequisites
- A running Wazuh stack (4.14+) with a public, TLS-terminated webhook endpoint that NetBird Cloud can POST to. If you don't have one, see the [Wazuh receiver reference architecture](./wazuh-receiver) for a starter deployment built around `wazuh/wazuh-docker` + Caddy.
- The shared secret your receiver expects in the `X-API-Key` HTTP header. If you're deploying the receiver from scratch, you'll generate this as part of its `.env`. If a receiver is already running, use the value of `NB_WEBHOOK_SECRET` from its `.env`.
- The receiver writes each NetBird event as a single JSON line to a file the Wazuh manager tails via a `<localfile>` block with `<log_format>json</log_format>`, and ships with a starter ruleset under custom rule IDs `100100``100106` (audit) and `100200``100202` (traffic). The verification steps below assume that wiring.
## Step 1 — Configure NetBird's Generic HTTP integration
In **https://app.netbird.io**:
1. **Integrations → Event Streaming**, find **Generic HTTP**, click **Connect**.
2. Fill the **General** tab:
- **Endpoint URL**: `https://YOUR-HOSTNAME/events`
- **Authentication**: **Custom Authentication**
- **Header name**: `X-API-Key`
- **Header value**: the secret from the prerequisites
3. Leave **Headers** and **Body Template** at their defaults.
4. **Save Changes**.
<p>
<img src="/docs-static/img/manage/activity/event-streaming/wazuh/netbird-generic-http-config.png" alt="NetBird Generic HTTP integration configuration" className="imagewrapper-big"/>
</p>
<Note>
Don't use a custom body template. In testing, NetBird's body-template engine HTML-escapes string values (so `+` becomes `&#43;`, mangling ISO-8601 timestamps and URLs with special characters), and there's no JSON-encoding helper for `meta`. The default body is the right choice for Wazuh.
</Note>
On save NetBird POSTs a synthetic test event to your endpoint and rejects the save with a `412` if the receiver doesn't return `2xx` (observed in NetBird Cloud, May 2026). A successful save also generates an "integration created" audit event that streams *back* through the same integration; that's NetBird logging its own configuration change. Subsequent edits produce "integration updated" the same way.
## Step 2 — Verify in the Wazuh dashboard
Open the Wazuh dashboard, log in, then ☰ → **Explore → Discover**.
### Audit events
Set the index pattern to `wazuh-alerts-*` and run this KQL:
```text
rule.groups : "netbird" and not data.nb.Message : TYPE_*
```
Set the time picker to **Last 15 minutes** and add these columns from the field selector: `rule.id`, `rule.level`, `rule.description`, `data.nb.InitiatorID`, `data.nb.target_id`.
You should see one row per audit event (peer add/remove, policy change, group create, etc.) with `rule.description` reading like `NetBird group event: group created`.
If `data.nb.*` doesn't appear in the field selector, the index pattern's field cache is stale. Go to ☰ → **Stack Management → Index Patterns** → `wazuh-alerts-*` and click the **↻ Refresh fields** icon at the top right, then return to Discover; all `data.nb.*` fields will appear.
### Traffic events
If you've also enabled [Traffic Events Logging](/manage/activity/traffic-events-logging) for one or more groups, switch the KQL to:
```text
rule.groups : "traffic"
```
Useful columns: `rule.id`, `rule.level`, `rule.description`, `data.nb.meta.source_name`, `data.nb.meta.destination_addr`, `data.nb.meta.tx_bytes`.
`TYPE_DROP` events (rule `100202`) escalate to level 7 to make policy denials stand out from normal flow telemetry.
## Schema reference
NetBird emits **two distinct event shapes** through the same integration. Both arrive wrapped in a small envelope so they don't collide with Wazuh's index template:
```json
{
"_stream": "netbird-events",
"received_at": "<receiver-side ISO 8601 timestamp>",
"nb": { "the NetBird payload": "..." }
}
```
`received_at` is the receiver's local timestamp at the moment the POST was accepted. Compare it against `nb.Timestamp` (set by NetBird Cloud) to spot clock skew or buffering delays in the streaming path.
### Audit events
```json
{
"ID": 22456950,
"Timestamp": "2026-05-08T12:57:57.00483949Z",
"Message": "integration created",
"InitiatorID": "oauth2|azure-oauth2|<user-uuid>",
"target_id": "event_streaming:1172",
"meta": { "platform": "generic_http" },
"reference": "https://app.netbird.io/activity?id=22456950"
}
```
- **Casing is mixed on the wire**: `ID`, `Timestamp`, `Message`, `InitiatorID` are PascalCase; `target_id`, `meta`, `reference` are snake_case. KQL queries against the indexer are case-sensitive, so match the on-the-wire form exactly: `data.nb.Message`, `data.nb.target_id`, etc. The same applies to Wazuh rule `<field name="...">` references.
- **`ID` may be a number or a string** (test events use a string identifier; real audit events use a numeric ID). Wazuh normalises both to strings in the indexed `data.nb.ID`, so KQL queries don't need to handle the mixed type.
- **`Message`** is the most reliable field to pivot rules on; it's a stable English phrase like "peer added" or "policy created".
- **`reference`** is the canonical URL into NetBird's activity log; useful as a click-through pivot from a Wazuh alert back to the source event.
- **`meta` is event-type-specific.** Peer events carry `meta.name`, setup-key events carry `meta.type`, integration events carry `meta.platform`. Treat unknown keys as opaque.
### Traffic events
```json
{
"ID": "<flow uuid>",
"Timestamp": "2026-05-08T10:33:41.723079Z",
"Message": "TYPE_START",
"InitiatorID": "",
"target_id": "",
"reference": "",
"meta": {
"flow_id": "<uuid>",
"direction": "EGRESS",
"source_addr": "100.121.42.206:0",
"source_name": "Jacks-MacBook-Air.local",
"source_type": "PEER",
"destination_addr": "100.121.255.254:0",
"destination_name": "",
"destination_type": "PEER",
"protocol": 6,
"tx_bytes": 56, "tx_packets": 1,
"rx_bytes": 0, "rx_packets": 0,
"policy_name": "",
"reporter_id": "<peer-id-of-reporter>",
"user_id": "oauth2|azure-oauth2|<user-uuid>"
}
}
```
Traffic events keep the same envelope as audit events: `ID` and `Timestamp` are populated; `InitiatorID`, `target_id`, and `reference` are intentionally empty (the actor lives in `meta.user_id`, source/destination identity in `meta.source_*` / `meta.destination_*`).
- `destination_name` is usually empty; `destination_addr` (overlay IP) is always set.
- `protocol` is an IANA number (`1`=ICMP, `6`=TCP, `17`=UDP, `58`=ICMPv6).
- A single flow can produce both a `TYPE_START` and a `TYPE_END` event sharing `meta.flow_id`. Filter on `data.nb.Message : "TYPE_START"` to count distinct flow starts and avoid double-counting lifecycle pairs.
- Volume is an order of magnitude higher than audit events on a busy mesh; size your retention and rate limits accordingly.
<Note>
Wazuh's JSON decoder strips empty-string and empty-object fields when it extracts events into `data.nb.*`. Fields shown as `""` or `{}` in the schema examples above (such as `destination_name`, `policy_name`, an empty `meta` on some audit events) won't appear in indexed alerts, so KQL queries like `data.nb.meta.destination_name : *` return zero hits. The original wire payload is preserved in `full_log` if you ever need the raw form; for queryable fields, filter on a populated one instead.
</Note>
## Next steps
- Build out a starter deployment with a webhook receiver, Caddy in front, and the rules referenced here: [Wazuh receiver reference architecture](./wazuh-receiver).
- Set up a freshness watchdog by pointing an external uptime monitor (UptimeRobot, Prometheus blackbox-exporter, etc.) at the receiver's `/healthz` endpoint. NetBird's retry behaviour on 5xx is undocumented, so external monitoring is the best way to catch a downed receiver quickly.