Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/NavigationDocs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ export const docsNavigation = [
title: 'Generic HTTP',
href: '/manage/activity/event-streaming/generic-http',
},
{
title: 'Wazuh',
href: '/manage/activity/event-streaming/wazuh',
},
],
},
],
Expand Down
3 changes: 2 additions & 1 deletion src/pages/manage/activity/event-streaming/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
141 changes: 141 additions & 0 deletions src/pages/manage/activity/event-streaming/wazuh.mdx
Original file line number Diff line number Diff line change
@@ -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.
Loading