Tamper-Evident Audit Trail
An audit log is evidence only if a reader can tell whether it has been changed. The tamper-evident audit trail gives you and your auditors that check, and MITRITY makes it a property of the trail itself, rather than of the people who run it:
- every audit event carries a hash of its own content and the hash of the event recorded before it;
- about once a day, the platform signs a statement of where the chain stands.
Changing an event, removing one or inserting one out of order breaks a link that the next event still carries. Rewriting the whole chain consistently still cannot produce a signature the platform did not make.
You can ask the platform to verify any period, from the audit log page or the API. An auditor can recompute the trail with nothing but the exported events, the checkpoints and a public key.
How the chain works
Each organization has one chain. Every audit event written since the feature went live carries three fields:
| Field | Meaning |
|---|---|
chain_seq | The event's position on the chain: 1 for the first, then contiguous. |
prev_hash | The row_hash of the event at the position before it. For position 1, it is the organization's genesis hash. |
row_hash | SHA-256 over the event's canonical record, which includes chain_seq and prev_hash. |
The canonical record is the event's stored fields rendered as RFC 8785 canonical JSON: keys sorted, no whitespace, and one spelling for every number and string. That is what makes the hash reproducible. The metadata you sent and the metadata the database stored may differ in whitespace and key order, and both hash the same.
Events are chained in the order they were recorded, not the order they occurred. A connector that imports last week's activity today appends it at the head. A verification therefore walks positions, not timestamps. It covers every position between the earliest and the latest event of the period you ask about.
Events written before the chain went live carry none of the three fields. They are never chained retroactively, because a hash computed later over old content would prove nothing. A verification over a period that includes them says so.
What retention leaves behind
Your plan's retention window and the archival job remove old events. A removed event leaves a tombstone: its position and its two hashes, never its content. The next event's prev_hash still resolves, so the chain links through the gap, and a verification reports those positions as retired rather than missing. A position with neither an event nor a tombstone is missing, and missing is broken.
Checkpoints
A checkpoint is a signed statement that, at a given moment, the organization's chain ran from one position to another and ended in a particular hash. The platform seals one checkpoint per organization about once a day. Each checkpoint also records the previous checkpoint's root, so the checkpoints form a chain of their own.
Checkpoints are signed with an Ed25519 key that lives outside the database. Every signed-in user can fetch its public half, together with a key_id, at GET /api/v1/audit/checkpoints/public-key. Every checkpoint records the key_id that signed it, so a key rotation is visible per checkpoint instead of invalidating history.
Every compliance report's data-integrity section cites the latest checkpoint: when it was sealed, the positions it covers, its root hash and a prefix of its signature. The section also states whether the checkpoint covers the report's period.
Verifying a period
To verify a period from the dashboard:
- On the Audit log page, set a date range.
- Choose Verify period.
Managers and owners can run a verification. The result is itself recorded on the trail, as an audit.chain.verified event with who ran it and what it found.
The same check is available as POST /api/v1/audit/verify:
curl -X POST https://api.mitrity.com/api/v1/audit/verify \
-H "Authorization: Bearer $MITRITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from": "2026-09-01T00:00:00Z", "to": "2026-09-21T00:00:00Z"}'
{
"status": "valid",
"period_start": "2026-09-01T00:00:00Z",
"period_end": "2026-09-21T00:00:00Z",
"chain_started_at": "2026-08-14T09:12:41Z",
"events_in_period": 940,
"unchained_events": 0,
"first_seq": 1,
"last_seq": 940,
"events_verified": 931,
"events_failed": 0,
"retired_events": 9,
"missing_events": 0,
"checkpoints_checked": 2,
"checkpoints_failed": 0,
"truncated": false,
"notes": []
}
Both bounds are optional, and the default is the last 30 days. A verification:
- recomputes every position in the span and checks every link;
- checks each checkpoint whose root falls in the span against the recomputed hashes, and its signature against the public key;
- extends the walk to the first checkpoint sealed after the period, so that the events are anchored to a signature.
| Status | Meaning |
|---|---|
valid | Every event recomputed and linked, every checkpoint matched and verified, and the period is anchored to a checkpoint. |
broken | At least one event, link, position or checkpoint did not match. first_divergent_seq, first_divergent_event_id and failure locate the first one; the counts say how much is wrong. |
partial | Nothing failed, but the result is not the full guarantee: events predate the chain, the walk stopped at the 250,000-position cap, no checkpoint anchors the period yet, or a checkpoint was signed by a key the deployment no longer holds. notes says which. |
A partial result on a fresh organization is expected: the first checkpoint is sealed about a day after the first chained event. A broken result is something to act on. The response tells you where to look.
Listing checkpoints
To list the checkpoints sealed in a period:
curl "https://api.mitrity.com/api/v1/audit/checkpoints?from=2026-09-01T00:00:00Z&to=2026-10-01T00:00:00Z" \
-H "Authorization: Bearer $MITRITY_API_KEY"
Each checkpoint carries first_seq, last_seq, event_count, root_hash, prev_root_hash, key_id, signature, and the exact statement that the signature covers. You can therefore verify the signature over the bytes that were signed, rather than reassembling them. Managers and owners can list checkpoints.
Verifying offline
An auditor who does not want to trust the platform's own verifier needs three things:
- an export of the events (the list endpoint carries
chain_seq,prev_hashandrow_hash, and every field of the canonical record,sourceincluded); - the checkpoints;
- the public key.
Make the export as an Owner or a Manager. Changes to people, their access and API keys are shown to Owners and Managers only (see Who sees changes to people and API keys). An export made by a Member or a Viewer has gaps where those events sit.
The procedure:
- For each checkpoint, verify
signatureoverstatementwith the public key (Ed25519). - Start from the genesis hash: SHA-256 of
mitrity-audit-chain/v1:genesis:followed by the organization id. Inchain_seqorder, recompute each event's canonical record and hash. Check that each event'sprev_hashequals the previous hash, and that itsrow_hashequals the recomputed one. - Confirm that the hash at each checkpoint's
last_seqequals itsroot_hash.
The canonical record is the RFC 8785 rendering of an object with these keys: v (mitrity-audit-chain/v1), seq, prev_hash, id, tenant_id, environment_id, event_type, source, agent_id, user_id, policy_id, edge_node_id, action, mission_scope, decision, blocked_by, surface, traffic_type, enforcement_mode, behavioral_hash, drift_score, response_time_ms, metadata, occurred_at and created_at.
- Absent values are
null. - Timestamps are RFC 3339 in UTC, with six fractional digits.
metadatais the stored JSON value.
Any conforming RFC 8785 library produces the same bytes.
Who sees changes to people and API keys
Some events record changes to people, their access and API keys: user.*, access.* and api_key.*, listed under Role Change History and API Keys. These events name people by email address and role.
- Owners and Managers see them.
- Members and Viewers, Academy learners included, see the rest of the audit log without them. These events are left out of the list, its total and the audit summaries, and opening one by ID answers
404 Not Found. - Reports never include them.
These events stay on the trail. Verifying a period covers every event, these included, and the result names an event only by its position and ID. They are not forwarded to SIEM destinations.
What this does and does not protect against
| Situation | Result |
|---|---|
| An event's content is edited in place. | Its row_hash no longer matches: broken at that event. |
| An event is deleted outside retention. | Neither event nor tombstone at that position: broken. |
| Events are rewritten and every hash recomputed consistently. | The links verify; the checkpoint root or signature does not: broken. Before the first checkpoint over those events: partial, stated. |
| A checkpoint is edited. | Its signature no longer verifies: broken. |
| Events written before the chain went live are altered. | Not detectable; the verification reports how many such events the period contains. |
The signing key is held in the platform's secret store and read once at start-up. Rotating it does not affect earlier checkpoints.