# Stream Activity Events to Wazuh

Source: https://docs.netbird.io/manage/activity/event-streaming/wazuh

---

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

    

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

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.

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