Runbooks/Siem Webhook

Audit-write webhook (HTTPS push)

Operator runbook for piping IdentityMesh audit events to a customer-supplied HTTPS endpoint in near-real-time. This is the push counterpart to the pull-style audit feeds described in siem-integration.md and the syslog stream shipped via siem-syslog.md. Use it when your SIEM (or detection-orchestration platform — Splunk SOAR, Tines, Torq, custom Logic Apps / Cloud Run) wants events delivered synchronously rather than polled.

The webhook sink is opt-in. Nothing is wired by default, and nothing the audit-row write path does depends on the webhook being healthy. The audit row is committed to IM_AdminAudit / IM_ObjectAudit and stamped into the audit hash chain before the webhook fires; if the receiver is unreachable, the durable record is still intact.

When to choose the webhook

If your receiver speaks syslog, prefer the syslog sink — it has fewer moving parts, no TLS cert juggling, and lower per-event overhead. If your receiver wants ArcSight / QRadar envelopes, see siem-cef-leef.md.

What gets shipped

The webhook ships audit-row events only — admin-surface mutations recorded to IM_AdminAudit, plus identity-data changes recorded to IM_ObjectAudit. Operational debug / info lines are filtered out so the receiver isn’t drowned in connector chatter.

The filter is expression-based and ships in appsettings.Webhook.example.json:

EndsWith(SourceContext, 'AuditStore') or @Properties['AuditRow'] = 'true'

Tighten or loosen this expression to your taste. The AuditRow property is reserved for a future release that tags audit-row events explicitly; the filter already allows for it.

Each event is delivered as a JSON object — by default the Compact JSON Format shape. Example:

{
  "@t": "2026-04-25T11:32:18.4210000Z",
  "@m": "Admin audit row recorded: POST /api/connectors -> 201",
  "@l": "Information",
  "SourceContext": "IdentityMesh.Infrastructure.Sql.SqlAdminAuditStore",
  "ActorUpn": "alice@corp",
  "Action": "POST /api/connectors",
  "TargetKind": "Connector",
  "TargetId": "8e6b8c44-...",
  "StatusCode": 201,
  "CorrelationId": "0HMV-...",
  "MachineName": "im-engine-01",
  "InstanceName": "IdentityMeshEngine"
}

Batches of up to 100 events (or 256 KB, whichever first) are POSTed to the configured requestUri every two seconds.

Enabling the webhook

The Sync Engine ships with a sample in its install folder (C:\Program Files\IdentityMesh\engine by default):

<install dir>\appsettings.Webhook.example.json

Copy the Serilog.Using array entry, the Serilog.WriteTo block, and the Serilog.Filter block into the active appsettings.json (or, preferred for a host-specific override, appsettings.Production.json). Restart the Sync Engine service (IdentityMeshEngine), then confirm the receiver sees a batch after the next admin action or sync run.

Minimal recipe

{
  "Serilog": {
    "Using": [
      "Serilog.Sinks.Console",
      "Serilog.Sinks.File",
      "Serilog.Sinks.EventLog",
      "Serilog.Sinks.Http"
    ],
    "WriteTo": [
      {
        "Name": "Http",
        "Args": {
          "requestUri": "https://siem.corp.example/identitymesh/webhook",
          "queueLimitBytes": 67108864,
          "logEventsInBatchLimit": 100,
          "batchSizeLimitBytes": 262144,
          "period": "00:00:02",
          "textFormatter": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact",
          "restrictedToMinimumLevel": "Information"
        }
      }
    ],
    "Filter": [
      {
        "Name": "ByIncludingOnly",
        "Args": {
          "expression": "EndsWith(SourceContext, 'AuditStore') or @Properties['AuditRow'] = 'true'"
        }
      }
    ]
  }
}

Key knobs:

FieldMeaning
requestUriHTTPS endpoint on the receiver. HTTP is permitted but not recommended; the sink does not disable TLS verification.
logEventsInBatchLimitMax events per POST. 50-200 is sane.
batchSizeLimitBytesMax batch payload size (262144 = 256 KB).
periodFlush interval (TimeSpan). 1-5 s is sane.
queueLimitBytesProcess-memory queue cap (67108864 = 64 MB). Sustained outage past this drops oldest events.
textFormatterPer-event JSON shape. CompactJsonFormatter is recommended.
restrictedToMinimumLevelSeverity floor for the webhook specifically (Information catches admin-audit + object-audit; Warning is the safe minimum on a noisy host).

Receiver expectations

The receiver should:

  1. Accept POST on the configured path with content-type application/json.
  2. Read the body as a JSON envelope with an events array (the sink’s batch shape):
    { "events": [ { ...event 1... }, { ...event 2... } ] }
  3. Respond with a 2xx status within ~10 seconds. Non-2xx is treated as a transient failure and the batch is retried.
  4. Be idempotent: under retry the same event may be delivered twice. Use @t + the audit-row primary key (AuditId if present in @mt / properties) as the dedup key.
  5. Verify the HMAC signature header (see “Signing” below) once it’s enabled.

Sample receiver — Node.js (Express)

const express = require('express');
const crypto  = require('crypto');

const HMAC_KEY = Buffer.from(process.env.IM_HMAC_KEY, 'base64');
const app = express();
app.use(express.raw({ type: 'application/json', limit: '1mb' }));

app.post('/identitymesh/webhook', (req, res) => {
  const sig  = req.header('X-IdentityMesh-Signature') || '';
  const body = req.body;
  const expected = crypto
    .createHmac('sha256', HMAC_KEY)
    .update(body)
    .digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).end();
  }
  const { events } = JSON.parse(body.toString('utf8'));
  for (const ev of events) {
    // ... persist / forward / alert ...
  }
  res.status(202).end();
});

app.listen(8443);

Sample receiver — Python (Flask)

import os, hmac, hashlib, json
from flask import Flask, request, abort

HMAC_KEY = bytes.fromhex(os.environ["IM_HMAC_KEY"])
app = Flask(__name__)

@app.post("/identitymesh/webhook")
def receive():
    raw = request.get_data()
    sig = request.headers.get("X-IdentityMesh-Signature", "")
    expected = hmac.new(HMAC_KEY, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)
    payload = json.loads(raw)
    for ev in payload.get("events", []):
        # persist / forward / alert
        pass
    return "", 202

Sample receiver — C# (IHostedService)

app.MapPost("/identitymesh/webhook", async (HttpRequest req, byte[] body) =>
{
    var sig = req.Headers["X-IdentityMesh-Signature"].ToString();
    var expected = Convert.ToHexString(
        HMACSHA256.HashData(hmacKey, body)).ToLowerInvariant();
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.ASCII.GetBytes(sig),
            Encoding.ASCII.GetBytes(expected)))
        return Results.Unauthorized();

    using var doc = JsonDocument.Parse(body);
    foreach (var ev in doc.RootElement.GetProperty("events").EnumerateArray())
    {
        // persist / forward / alert
    }
    return Results.Accepted();
});

Signing

Signed delivery uses HMAC-SHA256 origin verification on every batch. The shared key is configured under Webhook:HmacKey in appsettings.json; the receiver verifies the X-IdentityMesh-Signature header against the raw request body.

Status: signature emission ships as a follow-up in this release. The current Serilog.Sinks.Http configuration delivers batches over HTTPS but does not stamp the HMAC header. If you need signed delivery today, front the receiver with a reverse proxy (NGINX / HAProxy / Cloudflare) that computes the HMAC on the body and injects the header.

The receiver code samples above already verify the header so when signing flips on, no receiver changes are required.

HMAC key rotation

  1. Generate the new key on the receiver side (openssl rand -base64 32).
  2. Update the receiver to accept either the old key or the new key during the rotation window.
  3. Update Webhook:HmacKey in IdentityMesh’s appsettings.json (or its secrets backend per secrets-keyvault.md / secrets-vault.md).
  4. Restart the Sync Engine service (IdentityMeshEngine). The new key takes effect on first emit.
  5. Once the next batch is observed at the receiver verifying under the new key, drop the old key from the receiver.

Failure modes

To surface the sink’s own diagnostics (TLS handshake errors, 4xx drops), enable Serilog SelfLog once at startup or temporarily point SELF_LOG at a writable file:

Serilog.Debugging.SelfLog.Enable(msg =>
    File.AppendAllText("logs\\serilog-selflog.txt", msg + "\n"));

Operational notes

Configurable fields cheat-sheet

ConcernWhere to set it
Severity floorSerilog.WriteTo[Http].Args.restrictedToMinimumLevel
Per-event field shapetextFormatter arg — CompactJsonFormatter recommended
Filter (audit-only)Serilog.Filter[ByIncludingOnly].Args.expression
Receiver URLrequestUri
Batch size / periodlogEventsInBatchLimit / batchSizeLimitBytes / period
Memory capqueueLimitBytes (drops-oldest beyond this)
HMAC keyWebhook:HmacKey (secrets-backed; see signing follow-up)