# 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 ```python # 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-%'") ``` ```python # 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: ```json { "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 `DELETE`s this record (it is older than 30 days in the team's "retention" — which is actually a storage-economy decision, not a policy), and `UPDATE`s 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-deploy` → `admin` 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. ```python # 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). ```python # 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 `DELETE`s 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-deploy` → `admin` (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. ## Cross-Domain Links - `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 `DELETE`d 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.