Files
atelier/examples/bad/compliance-audit-log.md
Jon Chery 9ebc9c8868 docs(milestone): complete v0.3 — GitOps+Operators/AI-ML/i18n/Compliance
---ci---
project: atelier
phase: 6
milestone: v0.3
status: complete
requirements:
  covered: [ATELIER-60..91]
  partial: []
---/ci---
2026-08-05 03:45:38 +00:00

9.7 KiB

Bad Example: Compliance Audit Log (Two Breaches)

An audit logging implementation that violates two Atelier compliance principles in one example (per IDEATE-26, D-044): P1 (Audit Logs are Append-Only) — a mutable audit log with routine DELETE/UPDATE "cleanup" — and P9 (Secrets and Sensitive Data are Redacted in Audit) — a database password leaked into an audit record. Each violation is cited, then fixed.

The Code

# audit_log.py — the audit sink, stored in a mutable Postgres table
import psycopg2, datetime

# P1 VIOLATION: the audit log is a regular mutable table. There is no
# write-once protection, no immutable bucket, no hash-chaining.
# Any DB user with UPDATE/DELETE can rewrite history.
CREATE_TABLE = """
CREATE TABLE audit_log (
    id           BIGSERIAL PRIMARY KEY,
    timestamp    TIMESTAMPTZ NOT NULL,
    event        TEXT NOT NULL,
    actor        TEXT NOT NULL,
    target       TEXT,
    payload      JSONB,
    request_id   TEXT
);
-- No row-level immutability. No trigger preventing UPDATE/DELETE.
"""

def write_event(event, actor, target=None, payload=None, request_id=None):
    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    conn.execute(
        "INSERT INTO audit_log (timestamp, event, actor, target, payload, request_id) "
        "VALUES (%s, %s, %s, %s, %s, %s)",
        (datetime.datetime.utcnow(), event, actor, target,
         json.dumps(payload), request_id),
    )

# P1 VIOLATION (continued): "cleanup" that mutates the audit log.
# A routine job deletes records older than 30 days to "save space"
# and updates records to "fix typos in the actor field."
def cleanup_audit_log():
    conn = psycopg2.connect(os.environ["DATABASE_URL"])
    # DELETE — an audit record is destroyed. This is tampering,
    # dressed as housekeeping.
    conn.execute("DELETE FROM audit_log WHERE timestamp < NOW() - INTERVAL '30 days'")
    # UPDATE — an audit record is rewritten. The "fix" is the
    # violation; the original actor is lost.
    conn.execute("UPDATE audit_log SET actor = 'admin' WHERE actor LIKE 'svc-%'")
# The call site that leaks a secret into the audit log.
def read_config(key):
    # ... fetches a secret from the secrets manager ...
    value = secrets_manager.get(key)   # e.g. the raw DB password
    # P9 VIOLATION: the raw secret value is written into the audit
    # payload. The append-only log is now a secret store.
    write_event(
        event="config.read",
        actor="api-server",
        target={"kind": "secret", "id": key},
        payload={"value": value},            # <- the secret, in plaintext
        request_id=req.id,
    )
    return value

The resulting audit record:

{
  "id": 48213,
  "timestamp": "2026-08-05T09:12:03Z",
  "event": "config.read",
  "actor": "api-server",
  "target": {"kind": "secret", "id": "db-password"},
  "payload": {"value": "p@ssw0rd-sup3r-s3cr3t-plaintext"},
  "request_id": "req_91c2"
}

A week later, the cleanup_audit_log job DELETEs this record (it is older than 30 days in the team's "retention" — which is actually a storage-economy decision, not a policy), and UPDATEs every svc-* actor to admin. The secret was in the log for a week, readable by anyone with SELECT on the table; now the record of it having been there is gone.

What Makes It Bad

Breach 1 — Mutable Audit Log (Compliance P1 Audit Logs are Append-Only)

  • The audit log is a regular mutable Postgres table. DELETE FROM audit_log WHERE timestamp < ... and UPDATE audit_log SET actor = ... both succeed. The log is a draft, not a record.

  • Routine DELETE as "cleanup" is the cardinal P1 violation: the deletion of an audit record is itself an auditable incident, not a housekeeping task. "We deleted old records to save space" is a P3 (Retention is Policy, Not Storage) violation and a P1 violation — the retention decision is driven by storage cost, and the mechanism is tampering.

  • The UPDATE that rewrites svc-deployadmin destroys attribution (a P7 violation stacked on the P1 violation): the original actor is lost, and the replacement (admin) is a shared identity that could be any of ten engineers.

  • Fix: the audit sink is append-only by construction, not by policy. Write-once storage (WORM bucket, immutable log stream, hash-chained ledger) enforces immutability at the substrate. Retention is a declared policy with a meta-audit of deletions; a human does not run ad-hoc DELETE jobs.

    # Fix: write to an append-only sink (illustrative — S3 Object Lock,
    # WORM bucket, or a hash-chained ledger). The API has no update /
    # delete path; the storage refuses mutation.
    def write_event(event, actor, target=None, payload=None, request_id=None):
        record = {
            "timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
            "event": event,
            "actor": actor,            # the authenticated principal, not "admin"
            "target": target,
            "payload": redact(payload), # see Breach 2 fix
            "request_id": request_id,
            "prev_hash": last_hash(),   # hash-chaining: tampering breaks the chain
        }
        record["hash"] = sha256(canonical_json(record))
        append_only_sink.write(record)  # WORM storage; no update/delete API exists
    
    # Fix: retention is a declared, reviewed policy — not an ad-hoc DELETE.
    # When an audit segment ages out, the deletion is itself meta-audited
    # in a higher-tier log with the rule that authorized it.
    # (See domains/compliance/data-retention.md and audit-logs.md.)
    
  • See domains/compliance/audit-logs.md (Audit Logs are Append-Only) and domains/compliance/first-principles.md P1.

Breach 2 — Secret Leaked in Audit Log (Compliance P9 Secrets and Sensitive Data are Redacted in Audit)

  • payload={"value": value} writes the raw DB password into the audit record. The append-only log is now a secret store: anyone with SELECT on audit_log can read production credentials. The log is harder to secure than the secrets manager it read from.

  • Once the secret is in an append-only log, the remediation is expensive — rotate the secret and rewrite the log's access scope (you cannot edit the record; it is append-only). Redaction must happen at the logging boundary, before the record is written, not by opportunistic scrubbing after the fact.

  • The redaction policy here is "nothing" — there is no rule for which fields are redacted, by what mechanism, in which event type. A redaction rule that lives in no one's head and no code is a P9 violation waiting to happen (and it happened).

  • Fix: redaction is structural, applied at the logging boundary before the record reaches the append-only sink. The policy is itself auditable (which fields, by what rule, in which event).

    # Fix: redaction at the boundary. Log the FACT of the action
    # (a secret was read), never the CONTENT of the secret.
    REDACTED_FIELDS = {"value", "token", "password", "authorization", "secret"}
    
    def redact(payload):
        if not isinstance(payload, dict):
            return "[REDACTED:non-object]"
        out = {}
        for k, v in payload.items():
            if k.lower() in REDACTED_FIELDS or "secret" in k.lower():
                out[k] = "[REDACTED:secret]"
            else:
                out[k] = v
        out["_redaction"] = "secret-value-policy/v1"  # the rule is auditable
        return out
    
    # The fixed audit record:
    # {
    #   "event": "config.read",
    #   "actor": "api-server",            # the authenticated principal
    #   "target": {"kind": "secret", "id": "db-password"},
    #   "payload": {"value": "[REDACTED:secret]"},
    #   "_redaction": "secret-value-policy/v1",
    #   "request_id": "req_91c2"
    # }
    # The fact of the read is logged; the secret never enters the log.
    
  • See domains/compliance/audit-logs.md (Redaction at the Boundary) and domains/compliance/first-principles.md P9. Cross domains/security/secrets.md — the audit-side redaction is the complement of secret management.

The Cascade (Two Breaches Compound)

The two violations compound destructively. The secret enters the mutable log (P9 breach), where it sits readable by any SELECT-holder for a week. Then the cleanup job DELETEs the record (P1 breach) — destroying the evidence that the secret was ever logged, while the secret itself has already been exposed to every reader of the table. The UPDATE that rewrites svc-deployadmin (a P7 attribution breach stacked on the P1 breach) means that even if a copy of the record survived, the actor who triggered the secret read is no longer identifiable. The team cannot answer "who read the DB password and when" — the log that would answer it was mutated, and the secret it leaked is now in the wild. This is the worst-case interaction of P1 and P9: a secret leak with no attributable actor and no surviving record.

  • domains/compliance/audit-logs.md — the append-only guarantee and the redaction-at-boundary rule this code violates.
  • domains/compliance/first-principles.md — P1 (Append-Only) and P9 (Redacted) are the two breached principles; P7 (Attributable) is breached by the UPDATE rewrite.
  • domains/compliance/evidence.md — an audit log that can be DELETEd is not admissible evidence; the append-only guarantee is what makes it admissible.
  • domains/compliance/data-retention.md — retention is a declared policy with meta-audited deletions, not an ad-hoc DELETE job.
  • domains/security/secrets.md — redaction at the logging boundary is the audit-side complement of secret management.
  • domains/observability/logging.md — audit logs are structured logging with an append-only guarantee; the logging primitives compose here.