Audit Chain
Every security-relevant action, cryptographically chained. Tamper-evident by design.
HMAC-SHA256 row chaining, append-only database triggers, monotonic per-tenant sequence. Three independent tamper signals built into the schema, not bolted on.
WHY THIS IS NOT A FEATURE
Our audit log is cryptographically chained at the row level
An attacker with full database write access still cannot forge or delete a row without leaving evidence. Same hash-chain primitive used in blockchain ledgers, applied at the row level, with an HMAC secret instead of a consensus protocol.
7 years. 2,555 days. Every security-relevant action by every user, captured and verifiable.
HOW IT WORKS
Three fields turn each row into evidence
Every row in audit_logs carries three protective fields. The table is unremarkable; the discipline lives in what gets computed and signed for each insert.
sequence
Monotonically increasing integer, per tenant. A gap in the sequence means a row was deleted, and the verifier flags the first missing number.
prev_hash
Points back to the previous row's row_hash. A broken chain means a row was reordered or inserted out of band.
row_hash
HMAC-SHA256 over the canonical serialization of {tenant_id, sequence, prev_hash, actor_id, action, resource_type, resource_id, payload, created_at}, signed with AUDIT_HMAC_KEY held by the application, not the database.
THREE TAMPER SIGNALS
What we detect, and how
| Attack | What we detect | How |
|---|---|---|
| Row mutation | The stored row_hash no longer matches the recomputed HMAC. | Verifier walks the chain and recomputes each row from its canonical payload. |
| Row deletion | A sequence gap appears, and the surviving row's prev_hash no longer matches its actual predecessor. | Sequence check plus chain check; either alone is sufficient to detect. |
| Row insertion or reordering | The inserted row cannot reference a real prev_hash, so the chain fails at the inserted row or the one after it. | Chain walk catches the discontinuity at the first invalid hash. |
DEFENCES IN DEPTH
Three layers, separated on purpose
Append-only enforcement
A PostgreSQL trigger blocks every DELETE against audit_logs. Deletion is only possible inside a transaction that has SET LOCAL app.audit_retention_cleanup = on, a flag held by the retention sweep. SET LOCAL is transaction-scoped, so the flag auto-clears on commit or rollback and cannot leak to later statements.
Key separation
The HMAC secret lives in the application configuration, not the database. A read-only database compromise cannot forge valid rows. A write-level database compromise is caught by the next verifier run, which uses the HMAC secret to recompute every row and surfaces failures as their own audit events.
Verifier independence
The verifier walks each tenant's chain end to end using the application-held HMAC secret. Its outcomes are themselves audit-chain entries, so a disabled or skipped run is visible the moment it happens.
WHAT GETS LOGGED
Every event that touches the operational record
- 01 All authentication: login, logout, MFA enrolment, password change, password reset
- 02 All authorization: role changes, permission grants, API key creation and revocation
- 03 All sensitive admin actions: tenant settings, retention changes, RLS context overrides
- 04 All work-order signoffs, with the SHA-256 of the signature payload
- 05 All subscription and billing events: webhook receipt, plan change, customer-portal session
- 06 All ingest-source configuration changes
- 07 All SSO configuration changes
- 08 All exports: CSV, PDF, scheduled report deliveries
- 09 All bulk deletes and data-subject right-of-erasure exercises
RETENTION
2,555 days. Independent of operational retention.
RetentionConfig.AUDIT_LOG_DAYS keeps the chain for seven years. Tenant.data_retention_days is designed to control operational data separately, so machine telemetry can be purged on a configurable window while the audit chain continues to span the SOC 2 CC2.2 evidence window and the financial-adjacent retention norms.
COMPLIANCE MAPPING
What the chain answers for
SOC 2 CC6.1 & CC7.2
Tamper-evident logging and the change-management evidence trail. The chain is what your SOC 2 auditor walks through during the observation period to satisfy CC6.1 (logical access) and CC7.2 (system monitoring).
ISO 27001 A.12.4
Event logging and operator log integrity. Every administrator action and exception lands on the chain with timestamp, attribution, and the canonical payload that was acted on.
FDA 21 CFR Part 11 §11.10(e)
Secure, computer-generated, time-stamped audit trails for electronic records. Each signed event is attributable, contemporaneous, original, accurate, and time-stamped, supporting customer 21 CFR Part 11 validation efforts.
GDPR Article 30
Records of processing activities. Your DPO can pull a tenant-scoped audit slice on demand. Personal data inside audit_logs is limited to user identifiers and IP addresses; richer payloads carry only the minimum context needed to reconstruct the event.
EU NIS2 Directive
Incident investigation evidence. Member-state CSIRTs and your own forensic team can reconstruct exactly who did what and when, with a chain whose integrity does not depend on database administrator trust.
THE VERIFIER
Audit-logged audit verification
Chain verification recomputes each row from its canonical payload using the tenant's HMAC secret, returning PASS with the row count or FAIL at the first broken sequence number. Verifier outcomes are themselves audit-chain entries.
Cadence
On-demand from the admin verifier endpoint today. Scheduled daily verification ships with the platform GA release.
Outcome
PASS with the row count, or FAIL with the first sequence number where the chain broke. The row itself is preserved as evidence.
Source
Verifier source code is available for your auditor to walk through. We will sit in the room while they do.
WHY THIS BEATS "WE HAVE AUDIT LOGS"
Standard audit logs do not survive a determined operator
Plain audit table
Append-friendly but mutable. Anyone with database write access can change a row, drop a row, or insert one. No mathematical signal that it happened.
WORM storage
Helps against deletion, less helpful against insertion or reordering. Storage-level integrity does not catch a row that should never have been there.
Hash-chained log (this)
Mathematically tamper-evident. Detects mutation, deletion, and insertion at the row level, with the parent hash as the integrity anchor.
FREQUENTLY ASKED
What happens to the chain if a hash mismatches?
The verifier returns FAIL with the first sequence number where the chain broke. The row itself is not deleted, we keep the evidence. The failure is recorded as its own chain entry and surfaces through the platform's incident path.
Can the database administrator tamper with the chain?
Not without (a) breaking the append-only trigger, (b) obtaining the HMAC secret that lives outside the database, and (c) rewriting every subsequent row for the affected tenant. All three actions would themselves leave forensic evidence.
Can you delete a row for GDPR right-to-erasure?
Personal data inside audit_logs is already minimised to a user_id and IP. Right-to-erasure on the user record does not remove audit history, since that processing rests on Article 6(1)(c) legal obligation and (f) legitimate interest. We can pseudonymise the user_id reference if your DPO requires it.
Does the chain slow down writes?
Each insert computes one HMAC and reads one previous row, both indexed. Negligible cost at typical workloads, and the verifier runs out of band.