Stream Activity Events to Wazuh

Wire NetBird's Event Streaming → Generic HTTP integration to a Wazuh manager so every audit event and traffic event 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 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 100100100106 (audit) and 100200100202 (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.

NetBird Generic HTTP integration configuration

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:

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 Patternswazuh-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 for one or more groups, switch the KQL to:

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:

{
    "_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

{
    "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

{
    "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.

Next steps

  • Build out a starter deployment with a webhook receiver, Caddy in front, and the rules referenced here: Wazuh receiver reference architecture.
  • 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.