Skip to content

Audit log & SIEM export

Every control-plane mutation in merido is recorded in a tamper-evident, hash-chained audit log. It answers "who changed what, when" — and lets you prove the trail hasn't been altered. The log can also be streamed to a SIEM (Splunk, a webhook, or files on disk) for long-term retention and alerting.

What gets logged

The audit log records control-plane changes, not inference traffic (for request-level observability see Monitoring). Each mutation appends one entry. The action names are namespaced, e.g.:

  • Providersprovider.add, provider.update, provider.delete
  • Gateway keyskey.create, key.update, key.rotate, key.revoke
  • Budgetsbudget.create, budget.update, budget.increase, budget.delete
  • Advisoradvisor.apply, advisor.confirm, advisor.rollback
  • Guardrailsguardrails.* (route policy, rules, moderation, PII, injection, secrets)
  • SSO / SCIMsso.config.update, sso.login, sso.scim_token.create, …
  • Organizationsorg.invite.create, org.member.role, org.member.remove, …
  • Vault / BYOKvault.byok, vault.rotate, vault.kms_failure
  • Settingssettings.update
  • Management tokensmgmt_token.create, mgmt_token.revoke

Each entry carries seq (a monotonic, 0-based sequence number), actor, action, an optional target, a JSON details blob, an RFC 3339 timestamp, and the chain hashes (below). Secrets are redacted before the entry is written — anything that looks like an API key, token, password, or a sk-… / Bearer … value is replaced with [REDACTED], so the audit log never stores credentials.

The hash chain

Entries are chained so any tampering is detectable. Each entry's hash is:

hash = SHA-256( prev_hash || canonical_json(entry) )

The genesis entry (seq = 0) uses a prev_hash of 64 zeros. canonical_json sorts keys deterministically so the hash is stable. Because every entry commits to the one before it, editing or deleting any past entry breaks the chain from that point forward.

Verifying integrity

GET /api/audit/verify

Recomputes the chain and reports whether it is intact. Response:

json
{
  "ok": true,
  "error": null,
  "count": 1240,
  "total": 1240,
  "complete": true
}
  • count — how many entries were verified.
  • total — the full chain length.
  • completetrue when count == total, i.e. the whole chain was checked.

By default the endpoint verifies the entire chain from genesis to head. You can pass ?limit=N to cap how many (oldest-first) entries are checked; in that case complete is false, signalling a partial verification. Treat anything other than ok: true and complete: true as "not fully verified" — a partial pass does not vouch for the most recent (and most security-relevant) entries.

On a mismatch, ok is false and error names the failure (a tampered entry at a given seq, or a gap in the sequence).

Listing entries

GET /api/audit?action=provider&since=2026-01-01&limit=200

Returns entries newest-first. action is a prefix filter (provider matches provider.add, provider.update, …), since bounds by timestamp, and limit defaults to 200 (max 1000). In multi-tenant mode, listing requires the ViewAudit capability.

SIEM export

merido can ship audit entries to an external sink on an interval. It's off by default; enable it with environment variables:

VariablePurposeDefault
MERIDO_AUDIT_EXPORT_SINKwebhook, hec (Splunk), file, or nonenone
MERIDO_AUDIT_EXPORT_URLDestination URL (webhook / Splunk HEC endpoint)
MERIDO_AUDIT_EXPORT_TOKENBearer token (webhook) or HEC token (Splunk)
MERIDO_AUDIT_EXPORT_DIROutput directory for the file sink
MERIDO_AUDIT_EXPORT_INTERVAL_SECSExport-loop interval300
MERIDO_AUDIT_EXPORT_BATCHMax entries shipped per tick500

Sinks:

  • webhookPOSTs NDJSON (one entry per line) to MERIDO_AUDIT_EXPORT_URL, optionally with an Authorization: Bearer header.
  • hec — Splunk HTTP Event Collector format, posted to <MERIDO_AUDIT_EXPORT_URL>/services/collector with an Authorization: Splunk <token> header.
  • file — appends NDJSON batches plus a manifest JSON into MERIDO_AUDIT_EXPORT_DIR.

The export cursor

Export is at-least-once and durable. A cursor (the seq of the last successfully exported entry) is persisted in settings. Each tick reads entries with seq > cursor, ships them, and only advances the cursor on success — so if the sink is unreachable, the same batch is retried next tick rather than skipped. Inspect it at:

GET /api/audit/export/status

which returns the configured sink, the current cursor, and the last export manifest (the from_seq/to_seq range, count, first/last hashes, and whether the exported slice was chain-verified). The cursor also acts as a safety floor: audit pruning must never delete entries that haven't yet been exported.

  • Monitoring — request-level observability (Live, Logs, Reliability).
  • Access control & teams — who can verify the audit log and read entries.
  • Guardrails — the moderation/PII/secret controls whose changes are audited.

MIT / Apache-2.0 licensed.