run_id audit trails), and the Stage 5 deployment posts on secrets and RBAC (referenced by title — they ship in this stage).Maria (customer, forwarding an email): "Procurement sent over the vendor security questionnaire. Fifty questions. Lisa says nothing moves until we answer them. Can you have this back by Friday?"
Dev (scrolling): "Question 7: 'Describe your data retention and destruction policy for personal data.' Question 23: 'List all personnel with production database access and their justification.' Question 41: 'Provide evidence of immutable audit logging for privileged actions.'" A pause. "We don't have answers to these. We have a product."
Lisa (security, calm): "That's fine. A questionnaire isn't a test you pass by being clever. It's a request for evidence. We have three days to build the evidence."
Enterprise security reviews are not audits of your character. They are audits of your artifacts. This lesson is about producing the artifacts: a PII inventory, a data-flow diagram, an audit trail, and least-privilege access — so that when Lisa's questionnaire lands, you answer with links, not adjectives.
The customer problem
CityOps is technically working. The intake pipeline processes reports, the API serves them, the dashboards render. Maria's department is ready to expand the pilot — and expansion triggers procurement, and procurement triggers the vendor security questionnaire: fifty questions about how citizen data is handled, who can touch it, and what happens when something goes wrong.
Here's why this is a deployment problem, not a paperwork problem. Every question Lisa's questionnaire asks maps to a system property you either built or didn't:
| Questionnaire theme | What it really asks | Where the answer lives |
|---|---|---|
| Data handling | What personal data do you hold, where does it travel, and where does it rest? | Data-flow diagram + PII inventory |
| Access control | Who can touch production data, and why do they need it? | RBAC matrix + access reviews |
| Audit & logging | Can you reconstruct who did what, when? | Audit trail with per-action records |
| Secrets & keys | Where do credentials live, and how are they rotated? | Secrets management (Stage 5) |
| Incident response | What happens on a bad day? | Runbook + postmortem practice |
Dev's instinct is to write fifty paragraphs. Lisa's correction is the lesson: the questionnaire is the spec. You don't answer it with prose. You answer it by pointing at systems that already produce the evidence.
Clarify the ask
Before building anything, the team does what it always does: clarifies. Dev reads the actual questions instead of panicking about them, and three things become clear.
First, "evidence" has a specific shape. A reviewer doesn't want "we take security seriously." They want: a diagram of where data flows, a list of who has access, a sample audit record, a retention schedule. Concrete artifacts with dates on them. Anything you claim without an artifact attached is a promise, and promises don't pass reviews.
Second, the questionnaire is finite and categorizable. Fifty questions sounds infinite; grouped into five themes (above), it's five artifacts. This is the same move as the Pydantic lesson — trust no input — applied to process: don't trust a scary document; classify it.
Third, Lisa is an ally, not an adversary. Her job isn't to block the launch; it's to make sure a launch-day data incident doesn't become a headline. Every artifact the team builds for her doubles as operational tooling for Tom. Security review prep is just reliability engineering with a different audience.
Must know
- The questionnaire is the spec. Answer each question with a link to an artifact — a diagram, a log sample, an access list — not a paragraph of reassurance.
- Group questions by theme (data, access, audit, secrets, incidents). Five artifacts beat fifty paragraphs.
- Lisa owns the questionnaire, but the evidence is built by engineering. She reviews; you produce.
The minimum concept
You need exactly three concepts to answer ninety percent of any security questionnaire. Everything else is elaboration.
1. The PII boundary. Personal data is not a vibe; it's an inventory. A PII boundary is the line you draw around the components that store, process, or transmit personal data — and you can only defend a boundary you can draw. If you can't list which fields are personal, you can't answer "how do you protect personal data?" The inventory comes first; controls come second.
2. Least privilege. Every actor — human or service — gets the minimum access needed to do its job, and nothing more. Not because people are untrustworthy, but because blast radius is a property of permissions, not intentions. A compromised read-only reporting key is a bad day; a compromised admin key is a career event.
3. The audit trail. For every sensitive action, a record: who, what, when, on which resource, and whether it was allowed. This is the Stage 4 principle carried forward — trust is a system property, not a model property — applied to humans and services. You don't trust that nobody looked at citizen phone numbers; you have the log that shows exactly who did.
auth + rate limit] B --> C[CityOps API
business logic] C --> D[(Postgres
reports + PII)] C --> E[Object storage
attachments] F[Operator UI] --> B G[Batch jobs
service account] --> D style D fill:#3b1d1d,stroke:#f87171 style E fill:#3b1d1d,stroke:#f87171 H[Monitoring
metrics only
no PII] -.-> C subgraph PII boundary D E end
The diagram is the first artifact. It answers a dozen questionnaire questions at a glance: where PII lives (Postgres, object storage), where it enters (API gateway), and what's outside the boundary (monitoring sees metrics only — no PII). Draw yours before you write a word of policy.
Don't memorize
- Specific compliance regime names and clause numbers — the questionnaire will name the ones that matter; your artifacts satisfy all of them the same way.
- The exact list of fifty questions — they vary by customer. The five themes don't.
Build it: the evidence pack
Four artifacts. Each one is small; together they answer the questionnaire. Dev builds them in an afternoon, and each doubles as something the team needed anyway.
Artifact 1: the PII inventory
A table of every personal-data field in CityOps: what it is, where it lives, how long it's kept, and who owns the decision. Start from the CityOps intake schema from the FastAPI lesson and the validation contracts in the Pydantic lesson — your schemas are already a field list; the inventory adds classification.
| Field | Classification | Stored in | Retention | Owner |
|---|---|---|---|---|
| reporter_name | PII — direct identifier | Postgres reports | 2 years, then anonymize | Maria (data owner) |
| reporter_phone | PII — contact | Postgres reports | 2 years, then anonymize | Maria (data owner) |
| reporter_email | PII — contact | Postgres reports | 2 years, then anonymize | Maria (data owner) |
| report_text | PII — free text, may contain PII | Postgres reports | 2 years, then anonymize | Maria (data owner) |
| photo attachment | PII — may depict people/plates | Object storage | 2 years, then delete | Maria (data owner) |
| location (lat/lon) | Quasi-identifier | Postgres reports | 2 years | Dev (engineering) |
| status, timestamps | Operational, not PII | Postgres reports | 7 years (audit) | Tom (ops) |
Two decisions here carry weight with Lisa. First, the customer owns the retention policy — Maria, not engineering, decides the two-year window (the same principle as quarantine policy from the Pydantic review: the data owner decides, the lesson implements). Second, free text and photos are classified as PII by default because you can't prove they contain no PII. Minimize data before you redact it — the Stage 4 rule — and where you must collect it, treat it as sensitive until proven otherwise.
Artifact 2: minimize and redact at the boundary
The inventory tells you where PII is. Code enforces what happens to it. Two small, real utilities: detect-and-redact for anything that leaves the boundary (logs, exports, error messages), and a rule that PII never lands in application logs in the first place.
import re
PII_PATTERNS = {
"email": re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"),
"phone": re.compile(r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b"),
"ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
}
def redact(text: str) -> tuple[str, list[str]]:
"""Return (redacted_text, list of detected PII kinds)."""
found = []
out = text
for kind, pat in PII_PATTERNS.items():
if pat.search(out):
found.append(kind)
out = pat.sub(f"[REDACTED:{kind.upper()}]", out)
return out, found
sample = "Caller Maria Santos, phone 415-555-0182, maria.santos@example.com reports a pothole."
clean, kinds = redact(sample)
print("detected:", kinds)
print("cleaned :", clean)
Running it produces exactly what you'd want to show Lisa:
detected: ['email', 'phone']
cleaned : Caller Maria Santos, phone [REDACTED:PHONE], [REDACTED:EMAIL] reports a pothole.
Note what this function does not claim: it does not guarantee PII never reaches the model, the log, or the export. Redaction is one imperfect signal — the Stage 4 security standard. The guarantee comes from the next artifact: the audit trail that shows what actually happened.
Artifact 3: the audit trail
Lisa's question 41 asks for "evidence of immutable audit logging for privileged actions." The honest engineering answer: an append-only record of every sensitive action — reads of PII included, not just writes — with enough fields to reconstruct the story later. Dev adds a small FastAPI middleware plus explicit audit calls on the sensitive endpoints, reusing the Stage 4 pattern of a per-run run_id so every record ties back to one request.
import json, uuid, datetime
from fastapi import Request
def audit(action: str, actor: str, resource: str, decision: str, **extra):
record = {
"ts": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"run_id": str(getattr(audit, "run_id", "run-" + uuid.uuid4().hex[:8])),
"actor": actor, # "operator:lena" — who, with role
"action": action, # "report.read" — what
"resource": resource, # "report/4821" — on what
"decision": decision, # "allow" or "deny"
**extra,
}
with open("/var/log/cityops/audit.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
# On the sensitive endpoint — log the read AND the denial:
@app.get("/reports/{report_id}")
def get_report(report_id: int, user: User = Depends(current_user)):
if not user.can("report:read"):
audit("report.read", f"{user.role}:{user.name}",
f"report/{report_id}", "deny",
reason=f"role '{user.role}' lacks 'report:read'")
raise HTTPException(403, "forbidden")
report = db.get(report_id)
audit("report.read", f"{user.role}:{user.name}",
f"report/{report_id}", "allow",
pii_touched=["phone", "email"])
return report
A real record from this code looks like this — this is the artifact Lisa actually wants to see, one line per sensitive action:
{"ts": "2026-10-04T10:31:02-07:00", "run_id": "run-00000000", "actor": "operator:lena",
"action": "report.read", "resource": "report/4821", "decision": "allow",
"pii_touched": ["phone", "email"], "result": "200"}
And the denial — which most teams forget to log — is the more valuable record:
{"actor": "viewer:chen", "action": "report.export",
"decision": "deny", "reason": "role 'viewer' lacks 'report:export'"}
Three design choices matter here. Denials are logged — a reviewer reads denied attempts as evidence the controls actually fire. PII fields touched are named — so a later investigation can answer "whose data did this request see?" The log is append-only — rotated to immutable storage daily, and the application role that writes it cannot delete from it. An audit log the application can rewrite is a diary, not evidence.
Artifact 4: least-privilege access, written down
The last artifact is a matrix, not code — but the code enforces it. Every role gets named permissions; the API checks them on every request (authorization, the Stage 4 concept, kept separate from human approval):
| Role | Can do | Cannot do | Held by |
|---|---|---|---|
| viewer | List reports, see statuses | Read PII fields, export, change config | Read-only dashboards |
| operator | Read full reports incl. PII, update status | Export bulk data, manage users | Lena, frontline staff |
| admin | Manage users, configure pipeline | Direct database access | Dev |
| service:ingest | Insert reports | Read PII, delete anything | Intake worker only |
| break-glass | Full access, time-boxed 1h | Use without ticket + second approver | Nobody standing — provisioned per incident |
The break-glass row is what separates a real answer from a theater answer. Production will need emergency access someday; the questionnaire asks what happens then. The answer: a time-boxed, ticket-linked, dual-approved elevation whose every action lands in the audit trail — and which pages Lisa automatically. Least privilege isn't "nobody can do anything"; it's "everyone can do exactly their job, and emergencies are loud."
Break it: audit your own evidence
Artifacts that nobody tests are theater. Before Lisa sees anything, Dev tries to break the evidence pack — because a reviewer certainly will. The method is simple: write a checker that looks for the holes reviewers look for, and run it against the real system.
# gap_check.py — the reviewer's eye, automated
actions = [
("dev", "deploy", True),
("dev", "read-citizen-pii", True),
("operator", "read-citizen-pii", False), # the UI read path forgot audit()
("lisa", "export-audit", True),
]
for actor, action, audited in actions:
if not audited:
print(f"GAP: {actor} performed '{action}' with no audit record")
GAP: operator performed 'read-citizen-pii' with no audit record
And there it is: the operator UI's read path — the most common PII touch in the whole system — never calls audit(). The API endpoint logs; the UI's direct query doesn't. This is the classic failure mode: the control exists on the path the engineer tested, not on the path the users actually take.
Dev's break-it pass finds three holes in total:
- The unaudited read path (above). Fix: move the audit call into the data-access layer instead of the endpoint, so every caller — API, UI, batch job — is covered by construction.
- An over-permissioned service account. The intake worker's database role includes
DELETEon the reports table "because the migration needed it once." Fix: split migration privileges (used once, by a human, with break-glass) from runtime privileges (insert-only, forever). - PII in an error message. A validation failure echoed the raw report text — including the phone number — into the application log. Fix: the
redact()utility on every log line that can carry user input, plus a log-scan in CI that fails the build if a phone-number pattern appears in log output.
Must know
- Test the evidence, not just the system. A gap checker that hunts for unaudited sensitive actions is itself a questionnaire artifact — "we continuously verify audit coverage" is a strong answer to question 41.
- The most dangerous path is the one you didn't instrument. Put audit calls in the data layer, not the endpoint, so new callers inherit them.
- "The migration needed it once" is how service accounts accumulate dangerous privileges. Split one-time privileges from runtime privileges.
Productionize: the evidence pack as a living system
Answering the questionnaire once is a project. Staying answerable is a practice. The team turns the four artifacts into a maintained evidence pack with owners and cadence:
| Artifact | Lives at | Reviewed | Owner |
|---|---|---|---|
| Data-flow diagram | Repo docs/security/, versioned | Every architecture change | Dev |
| PII inventory | Repo docs/security/pii-inventory.md | Quarterly + on schema change | Maria (data owner) |
| Audit trail sample | Immutable log storage, 7-year retention | Weekly gap-check in CI | Tom |
| Access matrix + reviews | Repo + identity provider | Quarterly access recertification | Lisa |
| Retention/destruction log | Batch job output, archived | Each run (the 2-year anonymization) | Maria + Tom |
Two production details reviewers consistently probe. Retention needs a destruction log — "we delete after two years" is a promise; a job that anonymizes expired records and writes a signed summary ("4,112 records anonymized on 2026-09-30, job run_8841") is evidence. Access reviews need recertification — Lisa re-confirms every quarter that each person's access still matches their job, and the confirmations are filed. Access that was correct in January and wrong by June is the norm, not the exception; the review is the control.
And the CI log-scan from the break-it section deserves emphasis: it's the cheapest control in the pack. A ten-line check that fails the build when PII-shaped patterns appear in test log output. It catches the "echoed the phone number into the logs" class of bug before it ever reaches production — evidence that prevention, not just detection, is in place.
The pre-submission ritual: Lisa reads it first
One process addition makes the whole pack stronger: before any questionnaire response leaves the building, Lisa does a red-team read — she reads the answers as the most skeptical reviewer she can imagine and marks every claim she can't verify from the attached artifacts. Her rules are simple:
+ artifact links] --> B{Lisa red-team read} B -->|claim has artifact| C[Ship it] B -->|claim is a promise| D[Build the artifact
or soften the claim] D --> B B -->|artifact exists
but is stale| E[Refresh artifact,
note the date] E --> B
This loop is where most questionnaires are actually won or lost. The failure mode isn't missing controls — it's stale evidence: a diagram from three architectures ago, an access review signed by someone who left, a retention log whose latest entry is last year. Lisa's read catches the staleness, and every refresh makes the pack more honest. A questionnaire answer with a last-verified date on every artifact reads as a living system. One without dates reads as a document written the night before — because it usually is.
Dev's team adopts a standing rule from this: no artifact without a date, no date without an owner. It's the operational version of "the questionnaire is the spec" — the spec has a revision history.
Communicate: answering with evidence, not promises
Friday morning. Maria sends the questionnaire back. Dev's cover note is one paragraph: "Answers below. Each answer links to the artifact — diagram, log sample, policy, or review record — and names the owner who maintains it." Then the answers themselves, which read nothing like the anxious drafts from Monday:
| Question (paraphrased) | Answer |
|---|---|
| Describe your data retention and destruction policy for personal data. | Two-year retention, then anonymization, per the PII inventory (owner: Maria). Destruction is a scheduled job; each run writes a signed summary to immutable storage — latest run anonymized 4,112 records on 2026-09-30. Policy: docs/security/pii-inventory.md; sample run log attached. |
| List all personnel with production database access and justification. | Three humans (Dev — admin, Tom — ops, break-glass — none standing) and two service accounts (ingest: insert-only; reporting: read-only, no PII columns). Full matrix with quarterly recertification records: docs/security/access-matrix.md, last review 2026-09-15, signed by Lisa. |
| Provide evidence of audit logging for privileged actions. | Append-only JSONL audit trail, every sensitive action including denials, per-request run_id correlation, 7-year retention in immutable storage. Sample records attached (allow + deny). Coverage is verified weekly by an automated gap-check in CI. |
| How are credentials and secrets managed? | All secrets in the Stage 5 secrets manager; none in code, config, or logs (CI scan enforces). Rotation: 90 days, automated; break-glass credentials are single-use. See the Stage 5 secrets-management post (same stage, by title). |
| Describe your incident response process. | Runbook at docs/security/incident-runbook.md: detect (monitoring alerts), contain (revoke + break-glass), investigate (audit trail), notify (Lisa within 1h, customer per contract), postmortem (blameless, client-facing — see Milestone 5). |
Lisa's reply comes back Monday: approved, with two follow-ups the team can answer from artifacts they already have. Notice what happened — the questionnaire stopped being a threat and became a sales asset. Maria now forwards the evidence pack to the next department's procurement team before they even ask. The team that can answer "how do you handle our citizens' data?" with links instead of adjectives wins expansions.
What Lisa actually does with your answers
It helps to know what happens on the other side of the send button, because it explains why evidence beats prose. Lisa scores the response on three axes:
- Completeness: is every question answered, or are the hard ones skipped? A skipped question is a failed question — she'd rather read "we don't do X, and here's the compensating control" than silence.
- Verifiability: can she click through to the artifact and confirm the claim? "Audit logging is enabled" with a sample record attached scores; the same sentence alone doesn't.
- Freshness: are the artifacts dated and owned? A diagram dated last week with Dev's name on it beats a beautiful diagram with no date at all.
Maria adds the business translation: "She told me our response was the fastest she'd cleared this quarter — not because we're the most secure vendor, but because we were the easiest to verify." Ease of verification is a competitive advantage. The evidence pack doesn't just pass the review; it shortens every future review.
Later, not now
- Formal certifications (SOC 2, ISO 27001) — the evidence pack is the raw material an auditor will ask for, but the certification process itself is a later-stage investment.
- Penetration testing cadence — valuable, and Lisa will ask about it at the next review cycle; not required to answer this questionnaire.
CityOps: the review, applied
Map the lesson onto the system the team actually runs. CityOps holds citizen names, phones, emails, free-text reports, and photos — real PII from real residents. That makes the evidence pack non-optional, and it makes each artifact concrete:
- PII boundary: the diagram above is CityOps — Postgres and object storage inside, monitoring and the public dashboard outside. The dashboard only ever sees aggregated counts, never rows.
- Least privilege: the intake worker inserts reports it can never read back; the dashboard service account can't see PII columns at all. If either credential leaks, the blast radius is bounded by the matrix.
- Audit trail: every operator read of a citizen's phone number is a log line with a name on it. When Maria asks "who looked at the Elm Street reports?" the answer is a query, not a shrug.
- The questionnaire as spec: the next time a department asks to onboard, Maria sends the evidence pack proactively. Compliance becomes a feature of the deployment, not a tax on it.
This is the Stage 4 security arc reaching its conclusion. "Trust is a system property, not a model property" became guardrails and audit trails around the AI; here it becomes boundaries and evidence around the data. Same principle, wider radius.
The questionnaire you'll write yourself
One final turn of the lens: CityOps calls a third-party AI provider (the Responses API from Stage 4). That makes the team a customer doing vendor review, and Lisa hands Dev a blank copy of the questionnaire with new instructions: "Send this to our AI provider. Score their answers the way I scored ours."
Dev discovers the review is harder from the other side — the provider's answers are polished, and completeness is easy to fake. So he applies Lisa's three axes ruthlessly: every claim needs an artifact, every artifact needs a date. The provider's DPA gets the same treatment Maria's legal team gave CityOps. Two findings come back: the provider retains API inputs for 30 days for abuse monitoring (acceptable — documented, and CityOps redacts PII before sending, per the Stage 4 data-minimization rule), and their subprocessor list changed without notice last quarter (flagged — added to the quarterly review checklist).
The lesson generalizes: your security boundary includes your vendors. The questionnaire isn't just something done to you; it's a tool you wield on everyone who touches your data downstream. The team that just survived the review is now qualified to administer it.
Field check
- Lisa's questionnaire asks for "all personnel with production database access." You have the access matrix — but the break-glass role shows "nobody standing." Is that an acceptable answer, or a gap? What would make it a strong answer?
- Your gap-checker flags an unaudited PII read in a new batch job. The job's author says "it's read-only, it can't change anything." Why is the audit gap still a problem?
- Maria wants to keep citizen reports for five years "for trend analysis." The current policy says two. Who decides, and what artifact changes if she wins the argument?
- A reviewer asks: "Prove no PII is in your application logs." You have the
redact()utility and the CI log-scan. Is that proof? What's the honest answer?
Field check answers
1. "Nobody standing" is a fine fact but a weak answer on its own. The strong answer adds the mechanism: break-glass is provisioned per incident, time-boxed to one hour, requires a ticket plus a second approver, pages Lisa automatically, and every action lands in the audit trail. Reviewers don't fear emergency access — they fear unaccountable emergency access.
2. Reads are the sensitive action for PII — the harm isn't modification, it's exposure. An unaudited read means you cannot answer "who saw this citizen's phone number?" after an incident. Auditability is about reconstructing the past, and reads are most of the past.
3. Maria decides — she's the data owner, and retention policy is a business/legal decision, not an engineering one. If she wins, the PII inventory's retention column changes, the anonymization job's schedule changes, and the destruction log proves the new policy is followed. Engineering implements; the owner decides.
4. Not proof — it's two imperfect signals, and the honest answer says so: "We minimize collection, redact at log boundaries, and fail the build on PII-shaped patterns in test logs. We cannot prove a negative; what we can show is the prevention pipeline and the audit trail of what the system actually did." Reviewers respect that honesty more than a claim of impossibility.