Appearance
Audit Hash Chain
HMAC-SHA256 hash chain on odp_activity records for tamper detection. If any record is modified or deleted, the chain breaks and verification detects it.
How It Works
Chain Structure
Record 1: previous_hash = "GENESIS" hash = HMAC(GENESIS + data₁)
Record 2: previous_hash = hash₁ hash = HMAC(hash₁ + data₂)
Record 3: previous_hash = hash₂ hash = HMAC(hash₂ + data₃)
...Existing records (before the feature was enabled) have hash = NULL and are not part of the chain. The first record inserted after enabling the feature becomes the genesis point.
Hash Computation
Input:
previous_hash: hash of the previous record (or "GENESIS")
record: canonical JSON of { action, user, timestamp, ip, user_agent, collection, item, origin }
Algorithm:
HMAC-SHA256(previous_hash + canonical_JSON, AUDIT_HMAC_KEY)
Output:
64-character hex stringCanonical JSON uses sorted keys and excludes undefined values for deterministic serialization.
Scheduled Verification
A background job verifies the entire chain periodically:
- Read records in ID order (batch size: 1000)
- Recompute each record's hash from its data + previous record's hash
- Compare computed hash with stored hash
- If mismatch: emit
AUDIT_CHAIN_BROKENsecurity event (severity:critical)
The job uses scheduleSynchronizedJob — only one instance runs verification in a cluster.
Concurrency Safety
| Database | Mechanism |
|---|---|
| PostgreSQL | pg_advisory_xact_lock(9999) — serializes hash chain writes within transactions |
| SQLite | Single-connection pool provides natural serialization |
Overhead: ~2-3ms per activity insert (advisory lock + 1 SELECT for previous hash).
Key Rotation
The hash_version column supports key rotation:
- Deploy with new
AUDIT_HMAC_KEYand incrementHASH_VERSION - New records use the new key and version
- Verification reads the correct key based on each record's
hash_version - Old records remain verifiable with the previous key
Database Changes
sql
-- Migration: 062-audit-hash-chain.ts
ALTER TABLE odp_activity
ADD COLUMN previous_hash VARCHAR(64),
ADD COLUMN hash VARCHAR(64),
ADD COLUMN hash_version SMALLINT;Configuration
| Variable | Type | Default | Description |
|---|---|---|---|
AUDIT_HASH_ENABLED | boolean | false | Enable hash chain on activity inserts |
AUDIT_HMAC_KEY | string | '' | HMAC signing key (must differ from SECRET) |
AUDIT_VERIFY_SCHEDULE | string | '0 * * * *' | Cron schedule for chain verification (default: hourly) |
WARNING
AUDIT_HMAC_KEY must be a strong random string (32+ characters) and must not be the same as SECRET. If AUDIT_HASH_ENABLED=true but AUDIT_HMAC_KEY is empty, hash computation is silently skipped.
Source Files
| File | Purpose |
|---|---|
src/security/audit/hash-chain.ts | computeActivityHash(), GENESIS_HASH, HASH_VERSION |
src/security/audit/integrity.ts | verifyChain() — batch verification |
src/security/audit/scheduler.ts | Background verification job |
src/services/activity.ts | Hash chain integration in createActivity() |