SHA-256 Hash Chain — Critical Audit Chain
For sensitive operations — BYOK KMS lifecycle, secret store migrations, and tenant impersonation — each write goes into a separate, append-only table (audit_log_critical) that is distinct from the standard audit_logs table.
The critical chain is protected by:
- Application-layer SHA-256 hash chain — each row commits to the prior row via
prev_hash(32 bytes) androw_hash = SHA-256(canonical_bytes_of_this_row). Tampering with any row changes itsrow_hash, which breaks every subsequentprev_hashlink. A verifier can detect the exact row that was altered. - Append-only enforcement — database triggers prevent UPDATE and DELETE at the Postgres level. There is no retention pruning on this table; records exist indefinitely.
- Serialized writes — an advisory lock (
pg_advisory_xact_lock) on every write ensures monotonic, gap-freesequence_numbervalues. A gap in sequence numbers indicates a deletion attempt.
Critical chain row schema
Canonical serialization
Therow_hash is computed over a deterministic length-prefixed byte sequence:
details_canonical_json is JSON with keys recursively sorted so the hash is deterministic regardless of insertion order. The sentinel byte distinguishes null (\x00) from empty string (\x01).
The authoritative implementation is computeRowHash() in src/audit/critical.ts. An external verifier can re-derive any row’s hash from raw DB values and this spec.
Verifying the chains
The critical chain (audit_log_critical, described above) is verified by verifyChainSegment() in src/audit/critical.ts, which walks the chain in sequence_number order, recomputes each row_hash, and stops at the first break. It runs nightly via the verify-audit-chain cron job. There is currently no HTTP endpoint that exposes the critical-chain verifier.
A separate verifier covers the standard audit_logs table. It is exposed through the dashboard admin API:
audit_logs row with a row_hash, ordered by id) and returns the verification result verbatim:
valid to false and lists the offending row ids — brokenChainAt for rows whose prev_hash does not match the prior row’s row_hash, and invalidHashAt for rows whose stored row_hash does not match the recomputed value:
audit_logs chain, whose row_hash is SHA-256(prev_hash + tenant_id + user_id + action + resource_type + resource_id + JSON.stringify(details) + ip_address) — a plain string concatenation, distinct from the length-prefixed canonical serialization of the critical chain above. The endpoint takes no range parameters; it always scans the whole tenant chain.