---ci--- project: atelier phase: 6 milestone: v0.3 status: complete requirements: covered: [ATELIER-60..91] partial: [] ---/ci---
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 < ...andUPDATE audit_log SET actor = ...both succeed. The log is a draft, not a record. -
Routine
DELETEas "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
UPDATEthat rewritessvc-deploy→admindestroys 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
DELETEjobs.# 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) anddomains/compliance/first-principles.mdP1.
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 withSELECTonaudit_logcan 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) anddomains/compliance/first-principles.mdP9. Crossdomains/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-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 theUPDATErewrite.domains/compliance/evidence.md— an audit log that can beDELETEd 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-hocDELETEjob.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.