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.:
- Providers —
provider.add,provider.update,provider.delete - Gateway keys —
key.create,key.update,key.rotate,key.revoke - Budgets —
budget.create,budget.update,budget.increase,budget.delete - Advisor —
advisor.apply,advisor.confirm,advisor.rollback - Guardrails —
guardrails.*(route policy, rules, moderation, PII, injection, secrets) - SSO / SCIM —
sso.config.update,sso.login,sso.scim_token.create, … - Organizations —
org.invite.create,org.member.role,org.member.remove, … - Vault / BYOK —
vault.byok,vault.rotate,vault.kms_failure - Settings —
settings.update - Management tokens —
mgmt_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/verifyRecomputes the chain and reports whether it is intact. Response:
{
"ok": true,
"error": null,
"count": 1240,
"total": 1240,
"complete": true
}count— how many entries were verified.total— the full chain length.complete—truewhencount == 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=200Returns 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:
| Variable | Purpose | Default |
|---|---|---|
MERIDO_AUDIT_EXPORT_SINK | webhook, hec (Splunk), file, or none | none |
MERIDO_AUDIT_EXPORT_URL | Destination URL (webhook / Splunk HEC endpoint) | — |
MERIDO_AUDIT_EXPORT_TOKEN | Bearer token (webhook) or HEC token (Splunk) | — |
MERIDO_AUDIT_EXPORT_DIR | Output directory for the file sink | — |
MERIDO_AUDIT_EXPORT_INTERVAL_SECS | Export-loop interval | 300 |
MERIDO_AUDIT_EXPORT_BATCH | Max entries shipped per tick | 500 |
Sinks:
webhook—POSTs NDJSON (one entry per line) toMERIDO_AUDIT_EXPORT_URL, optionally with anAuthorization: Bearerheader.hec— Splunk HTTP Event Collector format, posted to<MERIDO_AUDIT_EXPORT_URL>/services/collectorwith anAuthorization: Splunk <token>header.file— appends NDJSON batches plus a manifest JSON intoMERIDO_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/statuswhich 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.
Related
- 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.