Audit trail internals
The audit trail is our single most load-bearing piece of compliance evidence. This page explains the internals so you can debug it, extend it, and defend its integrity to auditors.
Three layers of integrity
Section titled “Three layers of integrity”| Layer | Mechanism | Protects against |
|---|---|---|
| Hash chain | Each event includes SHA-256 of the previous event’s hash | Insertion, deletion, reordering |
| Event signatures | Ed25519 signature on each event’s canonical hash | Forged events (without key) |
| Checkpoint anchoring | Signed checkpoints written every N events | Full chain replacement |
Event shape
Section titled “Event shape”{ "audit_id": "ae_01JC...", "ts": "2026-04-18T14:23:00.123Z", "prev_hash": "abc123...", "event_kind": "chat", "agent_id": "support-bot", "session_id": "sess_01JC...", "provider": "openai", "model": "gpt-4o-mini", "detections": [...], "policy_result": {"verdict": "allow", "rule": "default"}, "cost": {"prompt_tokens": 123, "completion_tokens": 45, "usd": 0.0012}, "_hash": "def456...", // SHA-256 of canonical-JSON(event minus _hash, _sig) "_sig": "base64..." // Ed25519.sign(_hash)}Canonical JSON = sorted keys, no whitespace, no non-ASCII escaping. Used to ensure two servers produce the same hash for the same event.
Chain construction
Section titled “Chain construction”Every event carries prev_hash = <previous event's _hash>. The very first event has prev_hash = null.
evt_1 _hash=H1, prev_hash=nullevt_2 _hash=H2, prev_hash=H1evt_3 _hash=H3, prev_hash=H2...Break any event and the chain after it no longer validates.
Checkpoints
Section titled “Checkpoints”Every N events (default 1000), the server writes a checkpoint row containing:
- The current chain head hash
- Signed by the audit signing key
- Timestamp and event count
If an attacker replaces the whole chain (e.g., by taking over Postgres), they’d have to re-sign every checkpoint too — which requires the signing key, which lives in Secret Manager and is not accessible from the DB.
Signing keys
Section titled “Signing keys”- Algorithm: Ed25519 (fast, compact, IETF-standard)
- Generation:
tappass keys generate --purpose audit— written to Secret Manager - Access: Only the core server has
secretmanager.versions.accessfor it - Public half: exposed at
/audit/signing-keyfor independent verification
Never rotate without an operational plan — see Rotate API keys.
/audit/integrity endpoint
Section titled “/audit/integrity endpoint”Walks the entire chain, verifies hashes and signatures. Returns:
{ "status": "intact", "chain_length": 15432, "current_head": "a1b2c3...", "events_verified": 15432, "broken_events": []}Runs in O(n); on a 10M-event chain it takes ~45s. We cache the result for 5 min per env.
Cold-storage archive
Section titled “Cold-storage archive”Every day, the server dumps all events for that day to:
gs://tappass-audit-archive/YYYY-MM-DD.jsonl.zstBucket is WORM (write-once, 7-year retention). Archives are the source of truth for compliance evidence older than 90 days.
Incident: “chain break” alert
Section titled “Incident: “chain break” alert”See Incident response. A chain break is always SEV1 — customer-visible disclosure may be required.