Security 3 min read

Audit Logging That Auditors Actually Accept: SOC 2 in Practice

Table of contents

    The first time an auditor asks for evidence, most teams discover that what they have is application logs, and what the auditor wants is an audit trail. These are different artifacts. Application logs answer "why did the system misbehave"; audit logs answer "who did what, to which resource, when, and with what outcome" - and they must answer it months later, verbatim, to a skeptical third party.

    We have built audit logging for our own products and helped clients through SOC 2 Type II preparation. Here is what separates an audit trail that passes from one that generates findings.

    The event schema is the whole game

    Every auditable event in our systems is a structured record with the same mandatory fields:

    {
      "event_id": "01JH2K8Q9R3W...",
      "occurred_at": "2026-01-08T14:32:07Z",
      "actor": { "type": "user", "id": "u_8842", "session": "s_1f9c" },
      "action": "role.grant",
      "target": { "type": "user", "id": "u_9013" },
      "outcome": "success",
      "context": { "ip": "203.0.113.40", "granted_role": "admin" }
    }

    The non-obvious rules that came from real audit sessions:

    • Failures are events too. Denied permission checks and failed logins are exactly what an auditor samples when testing access control. Logging only successes is the single most common gap we find.
    • The actor is sometimes the system. Scheduled jobs and support tooling perform privileged actions. "actor: cron" with a job identity is acceptable; an action with no actor is a finding.
    • Record the state that made the action privileged. "role.grant" is useless in a review if you cannot see which role. Auditors read these records without your data model in their heads.

    Immutability you can demonstrate, not just claim

    "Our engineers cannot modify logs" is a claim. Auditors want a mechanism. Ours has three layers:

    1. Audit events are written to a dedicated store, not the application database - a compromised or buggy app cannot rewrite its own history.
    2. The store ships batches to object storage under a retention lock, where objects cannot be deleted or overwritten by anyone, including administrators, until the retention period expires.
    3. Each batch carries a hash chained to the previous batch, so a gap or alteration is detectable, not just forbidden.

    This costs little to build and transforms the audit conversation. Instead of describing policy, you demonstrate a property.

    Separate the audit stream from the observability stream

    Teams resist running two logging pipelines, and we sympathize, but the requirements genuinely conflict. Observability logs want short retention, aggressive sampling, and free-form structure; audit logs want multi-year retention, zero sampling, and a frozen schema. Forcing both through one pipeline gives you an expensive observability stack and a non-compliant audit trail. We run a thin, boring audit pipeline next to the observability stack and let each be good at its job.

    Two operational details auditors checked: clock discipline (every host on NTP, timestamps in UTC - correlation across systems fails on skewed clocks) and monitoring on the audit pipeline itself, because an audit trail that silently stopped three months ago is worse than none at all.

    Prepare for the evidence request, not the certificate

    A Type II audit samples your history: "show me all admin role grants between March and May, with approval evidence for each." If answering that requires an engineer writing ad-hoc queries for a week, the audit becomes an engineering outage.

    We build the query interface at the same time as the trail - filter by actor, action, target, and time range, with export. The auditor gets a scoped read-only view, evidence requests become self-service, and the same interface turns out to be what your own security team wanted anyway.

    The mental shift that makes all of this fall into place: the audit trail is not a log. It is a product, whose user is a person paid to distrust you.

    Copied