Skip to main content

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) and row_hash = SHA-256(canonical_bytes_of_this_row). Tampering with any row changes its row_hash, which breaks every subsequent prev_hash link. 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-free sequence_number values. A gap in sequence numbers indicates a deletion attempt.

Critical chain row schema

Canonical serialization

The row_hash is computed over a deterministic length-prefixed byte sequence:
Where 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:
The endpoint scans the entire tenant chain (every audit_logs row with a row_hash, ordered by id) and returns the verification result verbatim:
A failed verification sets 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:
Note that this endpoint verifies the standard 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.