Skip to main content
Private preview. fremforge is in private preview — invited customers only. Content is still subject to change. Request access →
SIEM forwarding

SIEM forwarding

fremforge writes every state-changing admin action to a per-tenant audit log with a hash-chained integrity guarantee (see Audit chain integrity). The same events can be fanned out to your SIEM as soon as they’re written, so your detection rules see them in seconds rather than at the next CSV export.

This page covers the forwarder shape, the wire contract for verification, and per-target setup for the receivers we test against.

Architecture in one line

audit-emit writes the chained row, then schedules a non-blocking fan-out. Each enabled endpoint gets one HMAC-signed POST. Five consecutive failures flip the endpoint to failing; after 30 minutes the next emit retries it.

The customer-visible knobs live under Org admin → Integration → SIEM forwarding (/<org>/_admin/siem).

Configuring an endpoint

  1. Go to Org admin → SIEM forwarding → Add endpoint.
  2. Enter a Name (free text, unique within the org) and the HTTPS URL of your receiver. http://, localhost, RFC1918, and metadata endpoints are rejected at submit time, pick a publicly reachable HTTPS sink.
  3. Optionally narrow the Action prefixes allowlist (e.g. forgejo.push, billing.). Empty = forward everything. Each prefix is matched with action.startsWith(prefix).
  4. Click Add endpoint. The HMAC shared secret is shown once on the next screen, copy it into your receiver config; fremforge never re-displays it. (Lost the secret? Delete the endpoint and re-add it.)

The endpoint goes live immediately. Click Send test event to fire a synthetic fremforge.siem.test payload through the same code path; the response status and the first 200 bytes of the receiver body come back in the UI.

Wire contract, what arrives at your receiver

Every delivery is a POST with JSON body and three headers:

HeaderValue
Content-Typeapplication/json
User-Agentfremforge-siem-forwarder/1
X-Fremforge-TimestampUnix seconds (string) when fremforge built the body
X-Fremforge-Signaturesha256=<hex> over <timestamp>.<body>, keyed by your endpoint’s HMAC secret

The JSON body has this shape (top-level keys are emitted in this order; fields.* keys are sorted alphabetically so the bytes are stable):

{
  "id": "00000000-0000-0000-0000-000000000000",
  "tenant_id": "11111111-1111-1111-1111-111111111111",
  "actor": "user:alice@example.com",
  "action": "policy.update.api",
  "fields": { "...": "..." },
  "created_at": "2026-05-15T13:42:11.123Z",
  "hash_chain": {
    "prev_hash": "ab12...",
    "this_hash": "cd34..."
  }
}

hash_chain.this_hash is the same hash anchored to OBS Object Lock, your SIEM can prove an event predates a known anchor.

Verifying the signature

The HMAC is computed over the exact bytes in the body, in the order they arrive. Don’t reparse + reserialise before signing; the deterministic-byte contract is the load-bearing guarantee.

Node.js receiver:

import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(req, body) {
  const sig = req.headers['x-fremforge-signature'];
  const ts = req.headers['x-fremforge-timestamp'];
  if (!sig?.startsWith('sha256=') || !ts) throw new Error('missing headers');

  // 5-minute replay window. Adjust to your tolerance.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) throw new Error('stale');

  const expected = createHmac('sha256', SHARED_SECRET)
    .update(`${ts}.${body}`)
    .digest('hex');
  const got = sig.slice(7); // strip "sha256="
  if (expected.length !== got.length) throw new Error('bad sig');
  if (!timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(got, 'hex'))) {
    throw new Error('bad sig');
  }
}

Python receiver:

import hmac, hashlib, time

def verify(headers, raw_body: bytes) -> None:
    sig = headers.get('X-Fremforge-Signature', '')
    ts = headers.get('X-Fremforge-Timestamp', '')
    if not sig.startswith('sha256=') or not ts:
        raise ValueError('missing headers')
    if abs(time.time() - int(ts)) > 300:
        raise ValueError('stale')
    expected = hmac.new(
        SHARED_SECRET.encode(),
        f'{ts}.'.encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(expected, sig[7:]):
        raise ValueError('bad sig')

Failure handling

  • Each delivery has a 5 second timeout. Slow receivers time out and count as a failure.
  • After 5 consecutive failures the endpoint flips to status=failing and is skipped on subsequent fan-outs. The admin UI shows the last failure reason verbatim.
  • After 30 minutes in failing state, the next emit retries the endpoint once. A success flips it back to healthy; a fresh failure restarts the 30-minute window. No manual intervention is needed for transient receiver outages.
  • Per-process concurrent in-flight POSTs are capped at 32. Beyond that, overflow events are dropped (logged operator-side as siem_fanout_dropped_overflow), surfaced in the operator SIEM-health view; rare in practice.
  • Delivery is best-effort, not at-least-once. Use the audit log itself (CLI export, REST API) as the system of record; SIEM is a real-time sink for detection.

Receiver recipes

Splunk HEC

  1. In Splunk: Settings → Data inputs → HTTP Event Collector → New Token. Source type _json; assign an index.
  2. The HEC URL is https://<your-splunk-host>:8088/services/collector/event. Paste this as the URL.
  3. Splunk HEC expects the token in Authorization: Splunk <token>, NOT HMAC. Use a thin proxy (Lambda, Cloudflare Worker) to translate: verify our HMAC, wrap the body in {"event": <body>}, forward to HEC with the Splunk token. We don’t ship a reference proxy; the recipe is ~30 lines of code, email compliance@frem.sh if you want a starting point or example HMAC-verification snippet.

Microsoft Sentinel, HTTP Data Collector API

  1. In Sentinel: Settings → Workspace settings → Agents → Data collection rules → New (or legacy HTTP Data Collector, both work, DCR is the recommended path).
  2. Sentinel signs requests with a workspace-id + shared-key SHA-256. Again, a thin proxy is the simplest path: verify the fremforge HMAC, recompute the Azure signature, forward.
  3. We don’t ship a reference proxy; the recipe is similar in shape to the Splunk one above (~30 lines: verify HMAC, recompute Azure SHA-256 signature, forward). Email compliance@frem.sh for a starting point.

Elastic Cloud (Filebeat HTTP input)

  1. Stand up a Filebeat HTTP input listener on a public endpoint with TLS terminated. Filebeat 8.12+ supports http_endpoint natively.
  2. Point fremforge directly at that URL. Filebeat doesn’t verify HMAC out of the box; use the Elastic script processor to verify against the shared secret before indexing.
  3. Index template: type keyword for actor, action, tenant_id; text for fields.* (dynamic).

Generic webhook

Any HTTPS endpoint that returns 2xx on success. Bring-your-own verification using the Node/Python snippets above. This is what we recommend for self-hosted SIEMs (Wazuh, Graylog, …) and for routing into Logflare/Datadog via their HTTP intake.

Operator-side health

Beyond the per-org admin view, fremverk operators monitor the fleet-wide state at the operator portal SIEM health page. Endpoints that flap, time-out, or drop into the overflow bucket are surfaced there. If your org’s endpoint goes silent for >24 hours we’ll proactively reach out, but you should also alert on the absence of fremforge events in your SIEM, as a backstop.

What’s not (yet) on the wire

  • Batched delivery. Today every event is one POST. A 250 ms batching window is the obvious next move when throughput demands it.
  • Replay-from-history. If your receiver was offline, the events that fired during the gap are not redelivered. Use the audit-log REST API (/_app/api/v1/orgs/<org>/audit?since=<ts>) to backfill.
  • Cross-tenant fan-out. Each endpoint is bound to one tenant. There is no operator-side mirror, events from your org never leave the per-tenant chain.

Related

  • Audit chain integrity, what’s actually in the events, and how to verify the chain end-to-end.
  • Webhooks, same wire-shape semantics for repo + org events, signed and delivered the same way.
  • Hosted runner model, where runner-emitted events come from in the chain.