# Logs and webhooks

> The tamper-evident audit log, network logs, webhooks for events, and streaming logs to your own tools.

## Audit log

**Logs → Audit log** records every change, sign-in and refused request in your workspace: who did it, when, from which address, and what changed. Owners and admins can see it.

- **Filters:** Machines, Access, Apps & services, Keys, People & sign-ins; by person; by time (24 hours, 7 days, 30 days, 90 days); and search.
- **Export CSV:** up to 10,000 events.
- **Kept:** 90 days.
- **Tamper-evident:** each entry includes a fingerprint (SHA-256) of the one before it, so editing, removing or reordering entries breaks the chain. Click **Verify integrity** to check it.

Event names look like `machine.approved`, `key.created`, `policy.updated`, `member.removed` and `account.signed_in`. When the API refuses a change, it's recorded as `<area>.failed`.

## Network logs

**Logs → Network logs** shows which machines talked to which, and how much. It's **off by default**; an owner or admin turns it on.

- **Recorded:** for each connection, the protocol, source, destination, packets and bytes each way, packets blocked by access rules, and whether it was between machines, to a subnet, or to the internet. Contents are **never** recorded.
- **Reported:** by the machines themselves, every half minute. Machines on the system WireGuard engine don't report.
- **Kept:** 30 days.
- **Filters:** 15 minutes to 7 days; machines, subnets or internet; search; auto-refresh; **Export CSV**.

Network logs need app version 0.5 or newer.

## Webhooks

**Logs → Webhooks** sends events to a URL as they happen.

| Setting | Options |
|---|---|
| Format | JSON, n8n, Slack, Discord, Google Chat |
| Events | Everything, or any of Machines, Keys, Members, Invites, Access controls, Apps, Services, Workspace settings, Webhooks, Log streams, Sign-ins |
| URL | `https` only; private and internal addresses are refused |

A JSON delivery looks like:

```json
{
  "id": "…",
  "type": "machine.approved",
  "time": "2026-10-28T10:42:00Z",
  "tailnet": "…",
  "actor": "amara@acme.com",
  "ip": "203.0.113.24",
  "target": { "type": "machine", "id": "…", "name": "db-prod" },
  "details": {},
  "summary": "…"
}
```

### Verify deliveries

JSON and n8n webhooks are signed with a secret (`whsec_…`), shown once and rotatable. Each request has:

- `X-Webhook-Event: <type>`
- `X-Webhook-Signature: t=<unix time>,v1=<hex>`, where `v1` is the HMAC-SHA256 of `"<t>.<raw body>"` with your secret

Check the signature, and reject requests older than 5 minutes:

```js
import crypto from "node:crypto";

function verify(secret, header, rawBody) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const a = Buffer.from(v1 ?? ""), b = Buffer.from(expected);
  return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

**Send test** sends a `test.ping` event.

### Delivery and retries

A failed delivery is retried after 2 seconds, 10 seconds, 1 minute and 5 minutes. After 20 failures in a row the webhook is paused (**Paused: failing**). The last 50 deliveries are kept, and you can redeliver any of them.

## Log streaming

**Logs → Log streaming** sends your **audit log** or **network logs** to a logging service.

| Destination | Authentication |
|---|---|
| Datadog | `DD-API-KEY` |
| Splunk HEC | `Authorization: Splunk <token>` |
| Elastic (bulk API) | `Authorization: ApiKey <key>` |
| Axiom | `Authorization: Bearer <token>` |
| HTTP (JSON) | `Authorization: Bearer <token>` |

Events are sent in batches of 100 (500 for network logs), when a batch fills or 5 seconds after its first event. Network log items have `type: "network.flow"`. Failed batches are retried after 2, 10 and 30 seconds.

## Limits

Up to 10 webhooks and 10 log streams per workspace.
