Provenance

Every action is permanently provable

A mutable audit log is a comfort, not a guarantee. We chain every event with the SHA-256 hash of the previous record, so altering a single entry invalidates every record that came after it. Tampering becomes a self-evident, mathematically demonstrable fact — not a thing you have to detect.

Executive summary

Every audit event — login, seal application, role change, document access — is recorded with a SHA-256 hash linking it to the previous event. Verification walks the chain in O(n) time; tampering with any single record breaks every record after it in a single deterministic check.

On top of the chain we layer 7 prebuilt compliance reports (activity summary, access audit, document lifecycle, credential status, security incidents, data retention, integrity verification), JSON and CSV export, and per-organization retention policies. Reports go through a draft → approved → immutable workflow so a finalized audit cannot be quietly edited.

Our commitments

Five rules for the audit log

01

Tampering is mathematically detectable

Every record's hash includes the previous record's hash. Modifying one event in the middle of history requires recomputing every event after it — visibly so, in any verification pass.

02

Verification is open code, not a trust statement

Chain integrity is checked by a deterministic Elixir function whose source is in this repository. Customers can run their own verifier against an exported chain — independent of our infrastructure.

03

Approved reports are immutable

Compliance reports go through draft → approved. Once approved, the report's content hash is signed and cannot be altered. Adding new content means a new report version with a new hash, never a silent edit.

04

The log captures who, what, when, and from where

Every event records actor, action, target, timestamp, source IP, user agent, and a full metadata payload. No "system" actor without explicit cause. No "automated" without the worker module name.

05

Retention is your decision, not ours

Each organization sets its own retention policy. We do not auto-prune below your policy minimum. We do not retain above your policy maximum. The default is "keep forever" — sealing is a professional record.

Implementation — the chain

How the chain is built and verified

Hash algorithm SHA-256 NIST FIPS 180-4
Chained surface hash(prev_hash || canonical_event) Canonical = sorted JSON encoding; deterministic across runtimes
Storage audit_logs table prev_hash, current_hash, sequence_number columns; indexed by org
Insertion ordering Serial within org Strict per-org sequence; verifies linear consistency without distributed coordination
Verifier Credo.Enterprise.Audit.AuditChain Pure Elixir + Erlang :crypto; sequential chain walk with parallel per-record hash via Task.async_stream
Verification cost O(n) Linear in event count; full org chain verifies in seconds
Independent verification Export + run verifier locally Customer can verify against an exported chain without contacting us
Tamper evidence First broken link Verifier returns the exact sequence number where divergence occurs

Implementation — events

What the log records

Sealing operations seal_applied, seal_revoked Per document version
Document access document_viewed, document_downloaded With actor + IP
Credential lifecycle credential_verified, expired, suspended Includes board verification source
Authentication login, login_new_device, mfa_passed, session_revoked From the security event log; folded into the chain
Authorization role_changed, permission_granted, permission_revoked With before/after state and the actor who made the change
Configuration webhook_created, idp_configured, kms_attached Settings changes that affect the security posture
Export operations audit_exported, report_generated, report_approved The audit log knows when it has been audited

Implementation — the mirror pattern

How security and credential events join the chain

Document and seal events write directly to audit_logs. Security events (MFA, SCIM, step-up at sealing) and credential-manifest events (license re-checks, status changes, suspensions, revocations) keep their canonical tables — and mirror into the same per-organization hash-chained audit_logs row-for-row at write time.

The mirror keeps the canonical security_events and credential manifest_events tables as the analytics-friendly stores, while every row also lands in audit_logs with the same chained-hash treatment as a sealing event. One verifier, one chain per org, covers all of them. Tampering with any mirrored row is detected by the same chain-linkage check that protects the document and seal events next to it.

Best-effort, never blocking. If a mirror write fails — advisory-lock timeout, transient DB error, a constraint we did not foresee — the canonical security_events or manifest_events row is still inserted, the user's action still succeeds, and the failure is logged plus reported to Sentry for the on-call to reconcile. The chain stays consistent with what it contains; we never silently substitute or partially write.

Mirror entry points Security.Event.record/3, Manifests.append_event/2 Every call site that records a security or manifest event triggers a mirror
Chain key — user events User's primary organization Security events use Organizations.get_primary_organization_for_user/1 to resolve the chain
Chain key — manifest events credential.organization_id Manifest events use the credential's org directly; no lookup needed
Mirror failure mode Best-effort Logged + reported to Sentry; canonical insert is unaffected; chain never silently diverges
Coverage caveat From deploy forward Pre-mirror rows in security_events and manifest_events stay in their canonical tables; only events written after this code shipped are in the chain
Out-of-scope events Telemetry, request logs, signups Not every system event is chained — only events that flow through Security.Event.record/3 or Manifests.append_event/2

Implementation — compliance reports

Seven prebuilt reports for your auditor

Activity summary Aggregated event counts Per actor, per action, per org, per time window
Access audit Who accessed what, when Document and credential access trail with IP and device
Document lifecycle Per-document timeline Upload → seal → views → revoke; with full metadata
Credential status Per-credential timeline Verification source, renewal events, status transitions
Security incidents Anomaly events New devices, geo changes, failed MFA, session revocations
Data retention What we still hold Per-record retention basis; pending deletions
Integrity verification Chain verification result Pass/fail with the first divergence sequence number if any

The full picture

What is built, what is being built, and what we chose not to build

Live today

SHA-256 hash chain

Live

O(n) verifier over the org's event log; sequential chain walk with parallel per-record hashing.

Security + manifest events mirrored into the chain

Live

Security.Event.record/3 (MFA, SCIM, step-up at sealing) and Manifests.append_event/2 (credential re-checks, status changes, suspensions, revocations) both mirror into the per-org hash-chained audit_logs table. Mirror is best-effort: failures are logged + Sentry-reported, never block the canonical insert.

Per-event metadata: actor, IP, UA, target, before/after state

Live

No silent system actors. Every event traces to a who and a why.

Audit log export (JSON + CSV)

Live

Self-service export for your SIEM or auditor. Includes the chain hashes for independent verification.

Seven prebuilt compliance reports

Live

Activity summary, access audit, lifecycle, credentials, incidents, retention, integrity.

Draft → approved report workflow

Live

Approved reports lock in their content hash; later edits create new versions, never silent revisions.

Per-organization retention policies

Live

Configurable per-event-type retention. Auto-archive on schedule; never auto-delete below policy minimum.

Building now

Periodic chain anchoring to an external TSA

Building now

Daily snapshot of the chain head signed by an RFC 3161 TSA. Anchors the chain to a third-party clock so we cannot rewrite history even if we wanted to.

Same TSA pipeline as document seals; reuses verified infrastructure.

SIEM streaming via signed webhooks

Building now

Real-time event push to Splunk, Datadog, Sumo. HMAC-signed, replay-protected. Same hardening as the API webhook surface.

Field-level encryption for high-sensitivity event metadata

Building now

Sensitive payloads in audit events (e.g., credential numbers in verification events) get a second AES-256-GCM wrap so partial database read does not leak them.

Roadmap

Custom report builder

Roadmap

Org admins define their own report shapes alongside the prebuilt seven. Same draft → approved → immutable lifecycle.

Cross-organization log federation

Roadmap

Enterprise customers with multiple EngineeringID orgs get a federated view across them, with per-org access controls preserved.

Continuous compliance dashboards

Roadmap

Real-time dashboards mapped to specific control IDs (SOC 2 / ISO / HIPAA) showing live evidence collection per control.

Considered & rejected

Blockchain-anchored audit log

Considered & rejected

A chained SHA-256 + periodic TSA anchor gives the same tamper-evidence at zero per-write cost.

Why we rejected it: blockchain solves the problem of "no trusted third party exists." We do have a trusted third party — public TSAs — and using them is the eIDAS / 21 CFR Part 11 / common-law-evidence answer to "prove this was unmodified at this time." Blockchain adds cost and latency to a problem that already has a published-standard solution.

Append-only file-based logs without a chain

Considered & rejected

Append-only filesystems are append-only until they are not. Chunked storage backends, S3 object replacement, restoring a snapshot — many ways to silently rewrite an "append-only" log.

Why we rejected it: filesystem-level append-only is a property of one storage system. A SHA-256 hash chain is a mathematical property of the data, independent of the storage system. We use the latter; storage hardening is defense in depth on top.

Compressing duplicate events

Considered & rejected

A "10x clicked the same button" rollup looks tidier and obscures attack signal.

Why we rejected it: failed-MFA events especially must be recorded individually. Aggregation is what the report layer does — it does not rewrite the underlying log. The log is dense on purpose.

Skipping the chain in dev environments to save cycles

Considered & rejected

Code that runs differently in dev than in prod is the source of every "but it worked locally" production incident.

Why we rejected it: the chain runs everywhere. Dev environments hash a few hundred events per session. The performance argument was "we save 50ms across an entire test suite," and the risk was "production-only code paths." Hard no.

Compliance mappings

Controls this surface satisfies

SOC 2 CC7.2

System Operations — Anomaly Detection

Hash-chained event log with first-divergence reporting

SOC 2 CC7.3

System Operations — Evaluation of Security Events

Seven prebuilt reports map directly to evaluation evidence

SOC 2 A1.2

Availability — Recovery

Tamper-evident operational records survive disaster recovery

ISO 27001 A.12.4.1

Event logging

Per-event actor, action, time, source — chained for integrity

ISO 27001 A.12.4.3

Administrator and operator logs

Privileged actions are first-class events with full payload

HIPAA §164.312(b)

Audit Controls

Hardware, software, and procedural mechanisms to record activity

21 CFR Part 11 11.10(e)

Audit trail — secure, computer-generated

Tamper-evident chain with attribution; export retains integrity

GDPR Art. 30

Records of processing activities

Per-record processing log; retention configurable per data category

For compliance teams

Questions you don't need to call to ask

Can an EngineeringID employee modify the audit log?
Not without breaking the chain. Database write access exists for operations, but any modification to a past record causes the verifier to fail at exactly that record's sequence number on the next pass. The chain is checked automatically on every report generation and on customer-initiated verification.
What's the smallest tamper that produces a detection?
A change to any chained field (organization, user, action, resource type, resource id, sequence number) of any record breaks the chain at that record. See Credo.Enterprise.Audit.AuditChain.compute_record_hash/7 for the exact field set. Cosmetic changes to resource_name, metadata, or change-state fields are recorded but not yet part of the chained hash — the v2 hash that covers all 14 content fields is implemented but not wired into live writes yet.
Which events are actually in the chain — everything in the system?
No. The chain lives on the audit_logs table. Three categories of writes land there: (1) document and seal events that write directly, (2) security events (MFA challenges, SCIM provisioning, step-up MFA at sealing) mirrored from Credo.Security.Event.record/3, (3) credential-manifest events (license re-checks, status changes, suspensions, revocations) mirrored from Credo.Credentials.Manifests.append_event/2. Out of scope: telemetry events, request logs, signup-form submissions, and other operational logs — those are not chained. The mirror covers events written after this code shipped; rows that existed in security_events or manifest_events before the mirror landed remain in their canonical tables but are not in the chain.
What happens if a mirror write fails?
The mirror is best-effort: a failure logs a warning, reports to Sentry, and returns. The canonical security_events or manifest_events row has already been inserted, so the user's action still completes and the analytics tables stay accurate. The chain does not silently diverge — it simply omits a record we know about, which the on-call investigates from the Sentry alert. Chain verification still passes because the chain stays consistent with what it contains.
Can we run the verifier ourselves against an exported log?
Yes. The verifier source is in the repository at `lib/credo/enterprise/audit/audit_chain.ex` and the canonical hash form is documented in its `@moduledoc` (pipe-delimited positional field order, SHA-256 per record, previous-hash linkage). Export produces a JSON file containing every event plus the hash chain. Reimplement the canonical form in your runtime of choice and walk the chain; the verifier returns either "valid" or the sequence number of the first divergence.
What's the retention default and can we change it?
The default is "keep forever" because sealing creates a professional record. Org admins can configure shorter retention per event type — for example, login events for 90 days, sealing events forever — within the limits set by your active compliance frameworks.
Are reports themselves tamper-evident?
Approved reports have their content hash signed and stored. A modified report becomes a new version with a new hash and a draft state — it cannot quietly replace an approved one. Auditors verify the report hash matches the approved hash they received.
What happens if the chain verification fails?
The verifier returns the first sequence number where divergence occurred. Org admins are notified. Operations are paused for sealing pending investigation; verification of existing seals continues since their integrity is independent of the audit chain.
Do you support real-time SIEM ingestion?
Webhook streaming to your SIEM is in active development; see the API & Webhook Security page for the delivery hardening (HMAC-SHA256 signing, replay protection, exponential-backoff retry). Today, polled export covers most SIEM ingestion patterns.

An audit log that stands up to its own audit

Run the verifier yourself, or have your auditor talk to ours.