Audit & SIEM
HMAC-chained audit log, RFC 5424 syslog forwarding, 30-day retention with SIEM export, and a replay-and-verify CLI.
What gets audited
Every state-changing API call, every job phase transition, every login, every approval decision. KEK lifecycle operations are the exception — they run from the CLI, never touch the audit log, and so are never forwarded to syslog; the operator who started a rotation is recorded on the rotation record itself and shown in the By column of Settings → KEK Versions. Reads are not audited, and there is no setting to enable it — the audit middleware returns before writing anything for GET, HEAD and OPTIONS. Only state-changing requests are recorded — plus the few read-only calls that decrypt a credential and dial one of your systems (target / module health checks, credential tests, and the live pickers' MODULE_BROWSE listings, recorded with the target, credential and listing kind), because who dialed what with which credential is an audit question even when nothing changed.
HMAC chain
Each audit entry includes:
{
"id": "65f1a...",
"timestamp": "2026-04-28T10:42:11.443Z",
"actor_name": "alex@example.com",
"action": "CERT_ISSUED",
"resource_type": "certificate",
"resource_id": "65f0...",
"old_value": { ... },
"new_value": { ... },
"prev_checksum": "9f1b...",
"checksum": "ce42..."
}
checksum = HMAC-SHA-256(audit_secret, prev_checksum || id || timestamp || action || resource_type).
The signature covers the entry's identity fields — id, timestamp, action and resource type — plus the
previous entry's checksum, so reordering, deleting or altering any of those breaks the chain at that
entry and every entry afterwards. The old_value/new_value/metadata payloads are not part of
the signature.
The audit_secret is audit.signing_key when set, otherwise the JWT secret. Using a dedicated
audit.signing_key separates the two trust domains so a JWT-forger cannot also forge the chain.
Setting or changing audit.signing_key invalidates verification of entries written under the
previous key (they will report as breaks until they age out of retention). To decouple the audit
chain from the JWT secret without breaking verification, first set audit.signing_key to your
current jwt.secret, then rotate the JWT secret.
Verifying the chain
POST /api/v1/audit-logs/verify (Owner role, rate-limited to one request per minute; optional
?since=<RFC3339> and ?limit= query parameters) walks the chain and returns
{ "verified": N, "intact": true|false, "breaks": [...], "next_since": "..." } — call it again with
next_since to continue through a long history. A break = either a bug or a tamper attempt —
investigate either way.
Entries written by releases before 1.5.45 were signed with a higher timestamp precision than the database stores, so they always report as breaks; they age out with the audit retention window (30 days). Entries written from 1.5.45 onward verify correctly, including the oldest surviving entry after retention has deleted its predecessor.
Syslog forwarding
Forward to a SIEM in real-time over syslog, encoded as RFC 5424, CEF (ArcSight/Splunk), or LEEF (IBM QRadar). Transports: UDP, TCP, TCP-with-TLS (TLS ≥ 1.2, custom CA and optional mTLS supported). See Syslog forwarding for the format details.
Settings → Syslog
Destination: siem.example.com:6514
Transport: TCP+TLS
TLS trust: <paste root>
One forwarder per organisation. Delivery is durable: events are queued in MongoDB before send, so a SIEM outage doesn't lose events — the queue drains with backoff once the SIEM is reachable again.
Full field mapping, settings reference, and troubleshooting: Syslog forwarding.
Retention
Audit entries are kept for 30 days — a MongoDB TTL index on the audit collection deletes older entries automatically. For longer regulatory windows, forward to a SIEM (syslog) and/or run periodic exports; the SIEM becomes the long-term store.
Storage
Audit entries live in their own MongoDB collection with a 30-day TTL index. Sized for ~1 KB per entry.
Export
Bulk export for legal-hold or DR goes through the API — there is no certautopilot audit CLI command:
# org-wide (admin)
GET /api/v1/audit-logs/export?since=2026-01-01T00:00:00Z&until=2026-12-31T23:59:59Z
# one project
GET /api/v1/projects/{projectId}/audit-logs/export?since=...&until=...
since and until are RFC3339 and are the only filters the export accepts; a malformed value returns 400. Each entry carries its prev_checksum and checksum, so the chain can be re-verified offline.