# Audit logs are immutable

- **Pattern:** `ab-002293` (`saas-logging.audit-trail.audit-logs-immutable`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002293
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002293)

## Why it matters

PCI-DSS Req 10.3.2 explicitly requires that audit log files be protected to prevent unauthorized modifications. NIST AU-9 requires protection of audit information from unauthorized access, modification, and deletion. An audit log a privileged user or a compromised service account can modify is not a trustworthy audit trail — an attacker who can delete audit events covering their tracks has neutralized your forensic capability entirely. SOC 2 CC7.2 requires integrity controls on security-relevant records. The most common failure mode is an `audit_events` table created with the same full-CRUD RLS policy as application tables, making its records as mutable as any other data in the system.

## Severity rationale

Medium because a mutable audit log weakens forensic integrity and fails PCI-DSS Req 10.3.2 and NIST AU-9, but exploitation requires privileged access already compromised.

## Remediation

Restrict the `audit_events` table to INSERT-only at the database level so no application code path — including a compromised service account — can modify or delete records.

For Supabase, create an INSERT-only RLS policy and explicitly omit UPDATE/DELETE:
```sql
-- Allow authenticated users to insert their own audit events
CREATE POLICY "audit_insert_only" ON audit_events
  FOR INSERT TO authenticated
  WITH CHECK (actor_id = auth.uid());

-- No UPDATE or DELETE policy — Postgres denies by default under RLS
-- Service role inserts via server-side API (bypasses RLS as expected)
```

Alternatively, route audit events to an external append-only log service (Better Stack Logs, Axiom, AWS CloudWatch) — these are immutable by design and require no additional database policy work.

## Detection

- **ID:** `audit-logs-immutable`
- **Severity:** `medium`
- **What to look for:** Enumerate all relevant files and Examine the audit log implementation (if it exists). Check: does the database schema include UPDATE or DELETE permissions on the audit log table? Does the application code include any update or delete operations on audit log records? Are Row-Level Security policies (for Supabase) or database permissions configured to prevent modification? Look for `REVOKE UPDATE ON audit_events FROM app_user` patterns in migrations, RLS policies that only allow INSERT, or use of an append-only log service (Axiom, Papertrail, CloudWatch, Better Stack Logs).
- **Pass criteria:** At least 1 conforming pattern must exist. Audit log records cannot be modified or deleted by the application. This is satisfied by: RLS policy allowing only INSERT (not UPDATE/DELETE) on the audit log table, database permission GRANT INSERT ONLY, no UPDATE/DELETE code paths for audit records in the application, OR use of an external append-only log service.
- **Fail criteria:** The audit log table has no write protections — standard RLS is disabled or allows full CRUD, and application code has no explicit prevention of audit record modification.
- **Skip (N/A) when:** No audit logging mechanism exists (if `audit-log-sensitive-ops` is skipped or failed with "no mechanism found"). Skip result should include a note referencing the failed prerequisite.
- **Detail on fail:** `"audit_events table has no INSERT-only RLS policy — records can be modified or deleted by the application service role"` or `"No immutability controls found on audit log implementation"`
- **Remediation:** An audit log someone can modify is not a reliable audit log. Immutability is what makes audit trails trustworthy.

  For Supabase, add an RLS policy that prevents update and delete:
  ```sql
  -- Allow authenticated users to insert their own audit events
  CREATE POLICY "audit_insert_only" ON audit_events
    FOR INSERT TO authenticated
    WITH CHECK (actor_id = auth.uid());

  -- No UPDATE or DELETE policy — deny by default
  -- Service role can still write via server-side API
  ```

  Alternatively, use an external log service (Better Stack Logs, Axiom, AWS CloudWatch) that is append-only by nature — no application code can delete log entries.

## External references

- cwe CWE-778
- nist AU-9
- pci-dss Req 10.3.2
- soc2 CC7.2

Taxons: observability, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-002293
