Hash-Chained Audit Logging
How iddio builds a tamper-evident audit trail using SHA-256 hash chaining. Every event links to its predecessor cryptographically — change one line, and verification fails.
Why Hash Chaining
A plain JSONL audit log records events sequentially. If someone edits a line, deletes an entry, or reorders events, the log looks normal — there’s no way to detect the tampering.
Hash chaining fixes this. Every audit event includes a SHA-256 hash of its own content, plus the hash of the previous event. This creates a cryptographic chain: change any single entry, and every subsequent hash becomes invalid. Verification is automated and instantaneous.
How It Works
Each audit event is a JSON object with two special fields:
{
"timestamp": "2026-02-08T10:15:30Z",
"agent": "claude-code",
"method": "DELETE",
"path": "/api/v1/namespaces/payments/pods/api-7d4b8f6c9",
"namespace": "payments",
"resource": "pods",
"tier": 3,
"decision": "escalate",
"latency_us": 180,
"hash": "a3f8c2d1e5b7...",
"prev_hash": "9e8d7c6b5a4f..."
}
The hash field is computed over ALL other fields in the event (including prev_hash). The prev_hash field is the hash of the previous event.
The Hash Function
func computeHash(event *AuditEvent, prevHash string) string {
event.PrevHash = prevHash
// Marshal without the hash field itself
event.Hash = ""
data, _ := json.Marshal(event)
h := sha256.Sum256(data)
return hex.EncodeToString(h[:])
}
The first event in the log has prev_hash: "genesis" — a well-known sentinel value that anchors the chain.
Plain JSONL vs Hash-Chained
| Plain JSONL | Hash-Chained | |
|---|---|---|
| Tamper detection | None — edits are invisible | Any change breaks the chain |
| Deletion detection | None — missing lines go unnoticed | Gap in prev_hash sequence |
| Insertion detection | None — fabricated entries blend in | Inserted hash won’t match neighbors |
| Reordering detection | Only if timestamps look wrong | Immediate — prev_hash breaks |
| Verification | Manual review | iddio audit verify (automated) |
Verification
The iddio audit verify command walks the entire log and checks every hash:
iddio audit verify
# Output:
# Verifying audit log: ~/.iddio/audit.jsonl
# Events: 14,892
# Chain integrity: VALID
# First event: 2026-01-15T10:00:12Z
# Last event: 2026-02-08T10:15:30Z
# Duration: 24 days
If tampering is detected:
iddio audit verify
# Output:
# Verifying audit log: ~/.iddio/audit.jsonl
# Events: 14,892
# Chain integrity: BROKEN at line 8,421
# Expected prev_hash: 9e8d7c6b5a4f...
# Actual prev_hash: a1b2c3d4e5f6...
# Event timestamp: 2026-02-01T14:30:00Z
# ERROR: Audit log has been tampered with
Append-Only File Operations
The audit log is written with append-only semantics:
func (a *AuditWriter) Write(event *AuditEvent) error {
event.Hash = computeHash(event, a.prevHash)
data, _ := json.Marshal(event)
data = append(data, '\n')
// Atomic append — single write(2) syscall
if _, err := a.file.Write(data); err != nil {
return err
}
a.prevHash = event.Hash
return nil
}
The file is opened with O_APPEND | O_WRONLY and permissions 0600. The single Write call is atomic on Linux for reasonably-sized events — the kernel guarantees that concurrent appends don’t interleave.
Integration with Compliance
The hash chain is the foundation of iddio’s compliance story. SOC 2 CC7.2 (system monitoring) requires evidence that monitoring is continuous and tamper-evident. The iddio audit verify command produces a verification report that maps directly to this criterion.
For enterprise deployments, audit events are also streamed to the control plane’s PostgreSQL database, where they’re stored with their hashes. Verification can run against either the local JSONL file or the database — both produce the same result.
Try It Yourself
Iddio is open source. Deploy a zero-trust command proxy for your AI agents in minutes.