Drawing the DFD: Where Trust Boundaries Sit in the Order Service
Your design review is Thursday. The change looks small: a payments vendor will POST a signed callback to a new /payments/callback endpoint, and the order service flips an order to paid. Before anyone argues about frameworks, one question decides how the rest of the review goes: where does this system stop trusting itself? Threat modeling at architecture level is answering that question with a diagram, not with opinions. Expect this exact exercise on staff-level loops — an interviewer draws boxes, hands you a marker, and watches whether you mark boundaries before you name threats.
Start with a data flow diagram. A DFD has four elements, and using them precisely is what separates a threat model from a napkin sketch.
| Element | Drawn as | In this system |
|---|---|---|
| External entity | rectangle | mobile client, payments vendor |
| Process | rounded box | order-service, fulfillment worker |
| Data store | open-ended rectangle | orders Postgres table, receipts S3 bucket |
| Data flow | labeled arrow | POST /orders, callback payload, SQS message |
The sketch ends up looking like this:
[Mobile client] --HTTPS--> ((API gateway)) --HTTP--> ((order-service))
| |
SQL v v signed callback
[orders db] [payments vendor]
|
v SQS
((fulfillment worker))
Now the load-bearing step: a trust boundary is crossed whenever data moves between entities with different privilege or control, and you draw a dashed line across every such flow. This diagram has three that matter. The internet-to-gateway crossing, because the client can send anything. The service-to-vendor crossing, because data leaves your control and returns under someone else's — a signed callback is still attacker-influenced input, since anyone who obtains the shared secret or replays an old payload can drive it. The service-to-worker crossing through the queue, because a message that was true when published may be stale when consumed, and the worker must re-check rather than trust its producer.
One scaling heuristic keeps this bounded: model deeply only where data crosses a trust boundary you do not fully control, and give internal-only flows a lighter checklist pass. In this diagram that means the client crossing, the vendor crossing, and the queue crossing get the full treatment; the internal metrics exporter gets five minutes and a checklist. Teams that model every box at equal depth burn out after two services; teams that skip boundaries miss the callback that was added 'temporarily' by a vendor engineer with production access.
Each boundary crossing forces exactly one design decision: authenticate the sender, validate the payload, or both. Validation belongs at the first hop after the crossing — before data reaches a parser, an ORM, or a shell. Two mistakes show up constantly in reviews. People authenticate at the boundary and then treat authenticated data as safe: a signature proves who sent bytes, not that the bytes are sane, so a signed callback still needs amount-and-currency validation. And people push all validation into the database layer as stored procedures or constraints: that is the last line of defense, and by the time data reaches it, injection-capable strings have already been through string concatenation you cannot audit.
The cost argument for doing this before the design review is blunt. A validation check discovered now moves ten lines of code and appears in the pull request. Discovered after launch, the same check becomes a migration across every caller, an incident write-up, and — for the callback path — potentially a dispute process with the vendor. Forty-five minutes with a diagram is the cheapest security work you will ever do in this job, which is the economic reason threat modeling happens at design time rather than after the first incident report lands in the on-call channel.
STRIDE per DFD Element: Turning the Diagram into a Threat List
With boundaries marked, the diagram becomes a checklist. STRIDE is a mnemonic that names six classes of attack, each violating one property of the system. Walking it per DFD element is what makes coverage systematic instead of a brainstorm where the loudest person names SQL injection and everyone stops.
| STRIDE | Property broken | Question per element | Threat on this system | Fix |
|---|---|---|---|---|
| Spoofing | authenticity | Who could pretend to be this? | Attacker POSTs a fake payment captured callback |
Verify vendor HMAC signature + timestamp window |
| Tampering | integrity | What could be altered in transit or at rest? | Callback amount edited from 40 to 4000 | TLS everywhere, signed payloads, amount validated against the order |
| Repudiation | non-repudiation | Could someone credibly deny acting? | Support agent runs a refund and denies it tomorrow | Append-only audit log with actor identity and before/after values |
| Information disclosure | confidentiality | Who can read this who should not? | Card tokens written into app logs | Redact at the logging library, allowlist fields |
| Denial of service | availability | What gets exhausted first? | Flood of orders exhausts DB connection pool | Rate limit at gateway, queue backpressure, pool sizing |
| Elevation of privilege | authorization | Who could gain rights they lack? | User edits another user's order by changing an ID | Object-level authorization on every read and write |
Not every element deserves all six questions equally. Data flows are mostly spoofing, tampering, and DoS — bytes on a wire. Data stores lean toward disclosure and repudiation. Processes get all six, because code is where every property can break. Knowing that asymmetry keeps the session at forty-five minutes instead of four hours.
Record the output as a threat list, not prose. A machine-readable list lets you diff it across design revisions and hand items to teams:
{
"threats": [
{ "id": "T1", "element": "payments-callback", "stride": "Spoofing",
"threat": "Forged captured-payment callback flips unpaid orders",
"mitigation": "HMAC signature, 5-minute timestamp window, nonce store",
"status": "mitigated", "owner": "payments-team" },
{ "id": "T2", "element": "order-logs", "stride": "Information disclosure",
"threat": "Card tokens reach the log pipeline and 90-day retention",
"mitigation": "Structural redaction allowlist in logging library",
"status": "accepted", "rationale": "tokens are vault references, not PANs" }
]
}
Notice status and rationale. A threat you consciously accept with a written reason is fine; the same threat unmentioned is a landmine for whoever joins in two years.
STRIDE is the standard interview answer, and the follow-up is predictable: what about privacy? LINDDUN is the privacy counterpart — linkability, identifiability, non-repudiation, detectability, disclosure of information, unawareness, and non-compliance. You ask its questions on the same DFD whenever flows carry personal data: can these two order records be linked to one identity across releases, does the analytics pipeline let someone re-identify pseudonymized rows, does the retention schedule comply with the regulation the customer is under. Mentioning that STRIDE covers security properties and LINDDUN covers privacy properties signals you have modeled more than one review.
The answer interviewers are listening for when they ask how you avoid missing threats: the matrix makes missing them structural work rather than luck. Every element gets asked; every question produces a list entry or an explicit not-applicable; the list carries owners and statuses.
Attack Trees and Cost: Which Threats You Fix Now and Which You Document
A threat list tells you what can go wrong. An attack tree tells you how an attacker would actually get there, and that distinction is what lets you spend a limited budget. Take the threat you fear most, make it the root goal, then decompose it into the conditions an attacker must satisfy. OR nodes mean the attacker picks the cheapest branch; AND nodes mean they must pay every cost — and every AND node you create is a place you can force a price.
GOAL: steal card tokens from the order system
├── OR: read tokens straight from the database
│ ├── AND: obtain prod DB credentials
│ │ └── OR: escalate via CI role OR: exfil from vault
│ └── AND: defeat disk encryption on the primary (impractical)
├── OR: harvest tokens from logs
│ ├── AND: read-log permission in prod OR compromise the log forwarder
│ └── effort: low — one misconfigured IAM grant away
└── OR: compromise order-service at runtime
└── OR: RCE via dependency CVE OR: deserialization bug in the worker
Read the tree from the attacker's side. The DB branch requires two successes, one of which is an escalation path you can hunt with policy review. The runtime branch is expensive for them and expensive for you to close fully — patch cadence and runtime hardening reduce probability but never to zero. The log branch is one grant away and kills two of its sub-leaves with a single change: structural redaction removes the tokens from the stream, and restricting read-log permission closes the other path. That is a choke-point mitigation — it collapses multiple leaves at once, and it wins the budget argument. When a threat list runs long, build trees only for the top three threats; trees are expensive attention, not ceremony.
Trees are also the negotiation artifact. When a director asks why the log-redaction work outranked a feature, the tree answers without hand-waving: here is the branch, here is its cost to the attacker, here is what the two-day fix removes. Trees expose shared prerequisites too — when three feared goals all begin with the same foothold, that foothold is your highest-leverage control no matter which goal leadership cares about most.
Then compare costs in the currency leadership understands. A choke-point fix here is roughly two engineering days: redaction in the logging library plus tightening one IAM policy. The unmitigated incident costs look different: rotate every exposed token, re-encrypt or re-tokenize the affected tables, notify regulators within the 72-hour GDPR window if personal data is involved, field legal, and absorb the churn. The ratio between those two numbers is the entire economic case for modeling at design time, and it is why the interview question is usually framed as a trade-off: describe a security improvement where you chose the cheap control that mattered over the thorough one that did not.
Three operating rules keep this from being a one-time exercise. First, keep the accepted-risks register next to the ADR — decisions like accepting vault-reference tokens in logs need the rationale written where the next reviewer will see it. Second, re-run the model at major shape changes — a new vendor, a new data class, a new public endpoint — not on a calendar. Third, tag each mitigation with the layer it protects, because the mitigations you just chose are individual controls, and a pile of individual controls is not yet a security posture. That layering question is exactly what Defense in Depth takes next: how to stack these controls so that no single failure exposes the whole system.
Quiz
1. In a data flow diagram for a service that accepts signed webhooks, where must authentication and validation controls be placed?
2. You are applying STRIDE to the orders Postgres data store in your DFD. Which pairing of threat class and example is correct?
3. In an attack tree where the root goal 'exfiltrate customer data' has an AND branch requiring both 'steal a CI role credential' and 'find an unegress-filtered path out', what does the AND node represent for your defense plan?
4. Your threat list flags 'card tokens written to 90-day-retained logs' and the team decides to accept the risk because tokens are vault references, not raw card numbers. What makes that acceptance defensible?
Flashcards
Question
What four elements make up a data flow diagram, and what does a trust boundary mark?
Click to reveal answer
Answer
External entities, processes, data stores, and data flows (labeled arrows). A trust boundary marks every point where data crosses between entities with different privilege or control — each crossing is one place where you must decide to authenticate the sender, validate the payload, or both.
Question
What does STRIDE stand for, and which property does each letter protect?
Click to reveal answer
Answer
Spoofing (authenticity), Tampering (integrity), Repudiation (non-repudiation), Information disclosure (confidentiality), Denial of service (availability), Elevation of privilege (authorization).
Question
What is STRIDE-per-element, and why is it preferred over free-form brainstorming?
Click to reveal answer
Answer
Iterating every DFD element (process, data store, data flow) and asking the six STRIDE questions against each. It makes coverage systematic — omissions become checklist misses rather than blind spots — and it produces a bounded threat list instead of an unstructured brainstorm.
Question
In an attack tree, what do OR nodes and AND nodes mean for the attacker and for your defenses?
Click to reveal answer
Answer
OR nodes: the attacker picks the cheapest viable branch, so you must harden the cheapest path. AND nodes: the attacker must satisfy every child condition, so each AND is a forced-price point where making one leg expensive breaks the whole branch.
Question
What does LINDDUN add that STRIDE does not cover?
Click to reveal answer
Answer
LINDDUN is the privacy counterpart: linkability, identifiability, non-repudiation, detectability, disclosure of information, unawareness, and non-compliance. Use it on the same DFD when flows carry personal data — re-identification risk in analytics pipelines, retention compliance, cross-record linkability.
Revision Notes
Key Takeaways
- 1. Threat modeling starts with a DFD because trust boundaries — not components — determine where controls belong
- 2. Authenticate at the boundary, validate at the first hop after it; a signature never makes payload contents sane
- 3. STRIDE per DFD element turns security review into a coverage checklist with a diffable threat list as output
- 4. Attack trees separate expensive branches from cheap ones; AND nodes are where you force a price on the attacker
- 5. The economic case is the ratio: two engineering days of redaction versus rotation, migration, and a 72-hour notification window
Interview Tips
- • "Draw the system and show me where you'd place controls" — draw boxes, then mark trust boundaries before naming a single threat; jumping to mitigations first is the tell that you skipped the model
- • "How do you make sure you don't miss threats?" — STRIDE-per-element as a matrix, explicit not-applicable calls, and a threat list with status and rationale instead of prose
- • "Tell me about a threat model that changed the design" — walk DFD, one STRIDE finding, the choke-point fix, and the cost comparison against the incident it prevented
- • "What's the difference between threat modeling and a penetration test?" — modeling is design-time and enumerative (what could an attacker do with this shape); a pentest is point-in-time and empirical (what can be done to this build)
Cheat Sheet
Architecture-Level Threat Modeling
The sequence: DFD → mark trust boundaries → STRIDE per element → attack trees for top threats → mitigations with cost, owners, and status.
DFD elements: external entity (rectangle), process (rounded), data store (open rect), data flow (labeled arrow). Boundary crossing = data moves between privilege domains → authenticate, validate, or both. Validate at the first hop after the crossing.
STRIDE table:
| Class | Property | Typical fix |
|---|---|---|
| Spoofing | authenticity | signed/authenticated sender |
| Tampering | integrity | TLS + signed payloads + server-side checks |
| Repudiation | non-repudiation | append-only audit log with actor id |
| Information disclosure | confidentiality | redaction, encryption, access limits |
| Denial of service | availability | rate limits, backpressure, sizing |
| Elevation of privilege | authorization | object-level authz checks |
Attack trees: OR = attacker picks cheapest branch; AND = attacker pays every cost (your forced-price lever). Prefer the choke-point mitigation that collapses several leaves.
Cost rule: design-time fix = tens of lines in one PR; post-incident = migration + rotation + 72-hour regulatory notification + legal. Accepted risks stay in the list with rationale and owner.