Back to Blog
Audit 2026-02-08

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 JSONLHash-Chained
Tamper detectionNone — edits are invisibleAny change breaks the chain
Deletion detectionNone — missing lines go unnoticedGap in prev_hash sequence
Insertion detectionNone — fabricated entries blend inInserted hash won’t match neighbors
Reordering detectionOnly if timestamps look wrongImmediate — prev_hash breaks
VerificationManual reviewiddio 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.