0007 - Tamper-evident audit via per-tenant Merkle chains
- Status: Accepted
- Date: 2026-05-24
- Deciders: nexus core team
- Tags: audit, cryptography, compliance, storage
Context and Problem Statement
Section titled “Context and Problem Statement”nexus records operational events in service-local staging tables and file sinks, but those records are mutable by any operator with database or filesystem access. Regulated deployments need audit evidence that can be independently verified after it leaves the hot database path. Request analytics cannot serve this purpose because analytics rows are optimized for tenant observability, enrichment, and product queries rather than evidentiary integrity.
The shared audit-event contract in vortex-common-crates/contracts/audit-event.md already defines canonical event encoding, BLAKE3 leaf and node hashing, batch envelopes, and verifier rules.
nexus needs a server-side pipeline that consumes those canonical events, sequences them per tenant, signs sealed batch roots, anchors batch roots to WORM object storage, and exposes offline verification.
Decision Drivers
Section titled “Decision Drivers”- Audit events must remain separate from tenant request analytics.
- Verification must work without trusting the mutable Postgres hot store.
- A tenant’s event sequence must be isolated from every other tenant’s sequence.
- The implementation must use dependencies permitted by the license policy.
- The design must use the same shared primitives as Aegis so both products can share verifier behavior.
Considered Options
Section titled “Considered Options”- Per-tenant Merkle chains with signed batch roots and WORM anchors. Events are consumed from tenant-scoped subjects, sequenced per tenant, batched into Merkle roots, signed with ed25519 keys, and anchored to S3-compatible Object Lock storage.
- Append-only Postgres tables with triggers only. This reduces accidental mutation but does not survive privileged database access or backup rewriting.
- HMAC over each audit row. This catches some application-level corruption, but a privileged operator with the HMAC key can rewrite rows and regenerate tags.
- Reuse request analytics as the audit log. This conflates product observability with compliance evidence and inherits analytics mutability.
Decision Outcome
Section titled “Decision Outcome”Chosen option: per-tenant Merkle chains with signed batch roots and WORM anchors.
Producers publish canonical CBOR audit events to nexus.audit.events.<tenant_id>.
The audit service owns the durable consumer, validates each event, assigns a monotonic per-tenant sequence number, stores the canonical hash in Postgres, seals batches with the shared Merkle construction, signs batch envelopes with ed25519, and writes anchored manifests and event archives to S3-compatible Object Lock storage.
The native verifier CLI validates archive contents by recomputing event hashes and Merkle roots, checking per-tenant chain continuity, and verifying ed25519 signatures against archived public keys. Read APIs expose events and inclusion proofs from the hot store with the same tenant authorization checks used by other read services.
Consequences
Section titled “Consequences”- Audit verification does not depend on trusting mutable request analytics or mutable Postgres rows.
- Operators must provision NATS JetStream, Postgres, and S3-compatible Object Lock for the audit service.
- The audit service becomes a security-sensitive service and must be covered by threat-model, runbook, and integration-test updates whenever its trust boundaries change.
- Signing keys need a rotation and retention policy; old public keys remain available for verification.
- The implementation has a larger operational surface than a database-only log, but produces evidence that can be checked offline.
More Information
Section titled “More Information”- Shared event and verifier contract:
vortex-common-crates/contracts/audit-event.md - Audit Proof Format
- Bifrost and Helicone audit lessons available in the GitLab repository
