Tamper-Evident Audit Logs: What SOC 2 and FedRAMP Auditors Actually Check
Every security platform claims to keep an audit log. What most of them actually keep is an application log with a timestamp column. The distinction matters when an auditor asks: "Prove that this record wasn't modified after it was written." For most systems, the honest answer is we can't. For any system that stores records in a mutable database without integrity protection, that answer is unavoidable.
SOC 2 Common Criteria 7.2 and 7.3, ISO 27001 A.12.4.2 and A.12.4.3, PCI DSS 10.5, HIPAA 164.312(b), and FedRAMP AU-9 all require audit trail integrity controls. They use different language and different checklists, but they converge on the same question: can you detect whether an audit record has been modified, deleted, or inserted after the fact? This post walks through the mechanism that gives you a "yes" answer, and what auditors actually verify when they review your controls.
The difference between an audit log and an application log
Application logs are operational telemetry: "request X took 40ms, returned 200, user Y." They're voluminous, noisy, and optimized for debugging. If one gets rewritten during a disk hiccup or a log rotation, nobody notices.
Audit logs are evidence. They record security-relevant events — who authenticated, who accessed which secret, who granted permission, who revoked access, who logged in to which host. They're queried during incident response, during compliance audits, during subpoena response. If one record is subtly altered — a timestamp changed, an actor swapped, a "denied" flipped to "allowed" — downstream decisions are wrong and nobody can tell.
The technical test for whether your audit log is actually an audit log: can you detect a one-character change in any historical record? If the answer is no, it's an application log.
Hash-chained audit logs
The standard mechanism for tamper-evidence is hash chaining, borrowed from blockchain without the blockchain. Each audit event carries a hash field computed over its own content plus the hash of the previous event:
event[N].hash = SHA256(event[N].fields || event[N-1].hash)
With this structure, flipping a single byte in event N invalidates not only N's hash but every subsequent event's hash. An attacker who modifies one record has to rewrite every record after it, recompute every hash, and then smuggle the rewritten tail back into the database. Verifying the chain — recomputing each hash and comparing — catches any change.
A few things have to be right for this to work:
- The hash input must be deterministic. Map iteration order in Go is random; JSON serialization without sorted keys produces different bytes for the same logical record. If the hash input isn't reproducible, verification fails even when nothing changed.
- The chain must be continuous. Concurrency: if two goroutines write events simultaneously, both reading the "last hash" at the same moment will chain to the same predecessor and break the chain's ordering guarantee. The fix is a serializable transaction around read-last-hash-then-insert.
- The tenant boundary matters. If the chain is per-tenant, you get tenant-scoped integrity. If it's global across all tenants, a burst of events from tenant A can delay event-writes from tenant B waiting for the serializable lock. CoreLink chose per-tenant chaining.
- Verification has to be periodic. A correct chain that nobody ever verifies provides the same evidence as no chain at all. Schedule a verify-chain job at least daily, alert on any mismatch.
What auditors actually check
Having a hash chain is necessary. It's not sufficient. Auditors ask for specific artifacts:
1. The hash algorithm and its properties
Is it SHA-256 or stronger? SHA-1 is a red flag — collisions are practical and auditors know it. MD5 is a fail. The answer should be SHA-256 minimum; SHA3-256 or BLAKE3 earns extra credit.
2. The input to the hash
A hash of "just the timestamp and action" is weaker than a hash of every field. Auditors want to see the full canonical serialization of the event as hash input, including actor, resource, IP, user agent, status, and details payload. Anything not in the hash input is a field an attacker can silently modify.
3. The chain verification schedule
"We could verify the chain" isn't the answer. The answer is a scheduled job, its last run time, its alert destination, and its runbook when it fails. For SOC 2, auditors will ask for evidence that the job ran during the audit period and that failures were investigated.
4. The write path's atomicity
How does the system ensure the previous-hash read and the new-event write happen atomically? If there's a race window, an attacker with DB access can insert an event between read and write and break the chain's ordering. Auditors will ask to see the transaction isolation level and the locking strategy.
5. The retention policy
Audit records have to be retained for the full audit period (SOC 2: 12 months minimum, often 24; FedRAMP: 3 years for High baseline; HIPAA: 6 years). Deletion before expiry is a finding. "Archived to cold storage" counts as retention only if the archive is also tamper-evident.
6. The access controls on the audit log
Who can read it? Who can write to it? The answer should be: the application writes via a tightly-scoped DB user, and only platform admins can read through the UI. Nobody should have direct DB write access to the audit table — if a DBA can silently insert rows, the hash chain just moves the trust problem from "the application is honest" to "the DBA is honest."
7. The tampering-response procedure
When chain verification fails, what happens? A mature control has: alerting (PagerDuty, Slack, email), an incident response runbook, a forensic capture mechanism (snapshot the DB, isolate affected tenant), and a documented communication path to affected customers if the failure is confirmed. "We'd figure it out" is not an answer.
What CoreLink does
CoreLink's audit subsystem (full compliance surface) writes every security event — 216+ event types covering authentication, secret access, permission grants, session starts, policy changes, and NHI attestations — through a hash-chained path:
audit_eventstable hashashandprevious_hashcolumns.- Hash is SHA-256 over a canonical serialization of the event (sorted keys, explicit field order).
- Writes go through a
CreateChainedmethod that reads the last event's hash inside aSERIALIZABLEtransaction, computes the new event's hash, and inserts — atomically. - Chain is scoped per tenant; cross-tenant reads at the repo layer are prevented by tenant-scoped query filters.
- A
VerifyChain(from, to)function walks the range, recomputes each hash, and returns true/false plus the first divergent event if it fails. - The compliance service runs chain verification on a schedule and surfaces results in the compliance dashboard.
The design is deliberately boring. No Merkle tree, no distributed ledger, no PQ crypto. Just SHA-256 chained events in a relational database, verified on a schedule. Auditors have seen this pattern before — it's the same mechanism used by regulated financial systems for decades — and they know how to review it.
What a failing chain actually tells you
When VerifyChain returns false, one of three things happened:
- Someone modified the database directly. An admin ran an UPDATE against
audit_events. This is the attack case; escalate immediately. - A row was deleted. Either malicious or a well-intentioned "cleanup" that wasn't actually safe. Same escalation path.
- There's a bug in the write path. The chain broke because a concurrent write used the wrong predecessor hash. This is less serious but still a production bug; the chain needs to be re-anchored (usually by inserting a new "chain recovery" event and carrying on).
Which one is which is answered by the forensic snapshot: look at the divergent event, compare against the expected hash, cross-reference against application logs for that time window. If the application never wrote the event that's in the database, it's case 1 or 2. If it did, it's case 3.
What about external attestation?
For the highest-assurance environments (FedRAMP High, DoD IL5, some financial regulators), in-system hash chains aren't enough. Auditors want external attestation: periodic submission of the chain's current head hash to a third-party timestamping service (RFC 3161) or a public blockchain so the platform can't alter history even with full root access.
This is a reasonable feature to add if you're chasing those compliance regimes. It's overkill for SOC 2 Type II and ISO 27001. The internal hash chain is the floor; external attestation is the ceiling, and you only need it when your regulator asks for it.
Where to start
If you're evaluating or building an audit log:
- Confirm the hash algorithm (SHA-256+) and the input fields.
- Verify the write path is atomic — serialize isolation or better.
- Run the verification job and check it actually detects a planted tamper (insert a test event, modify it, confirm the verify fails).
- Document the schedule and the escalation path.
- Restrict DB write access to the audit table to the application's service role only.
The payoff isn't just the audit. It's the operational peace of mind that comes from knowing that if something bad happened to your security state, you can prove exactly what, when, and by whom — and prove that the record of it hasn't been rewritten.
The compliance use-case page has the full scan / report / export surface; the platform overview covers the audit-adjacent features (session recording, SIEM forwarding) that compound the evidence story.