Files
atelier/examples/bad/compliance-audit-log.md
T
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

222 lines
9.7 KiB
Markdown

# 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.