Data-flow diagrams and trust boundaries
A data-flow diagram turns a system into elements and the lines data crosses between them. Trust boundaries are those lines where ownership or privilege changes — and where threats concentrate. The DFD is what makes STRIDE-per-element systematic instead of guesswork.
A team threat-modeled their checkout the right way: they listed STRIDE threats for every box on the whiteboard. Six months later they got popped through the recommendations service — an internal gRPC endpoint that “only other services call.” Nobody had drawn the line where the request actually originated, so nobody asked whether that service was reachable from the open internet. It was: a misconfigured ingress route exposed it directly, and it trusted every caller because, on the diagram, it lived safely “inside.” The threats weren’t missed because the team didn’t know STRIDE. They were missed because the diagram had a trust boundary drawn in the wrong place — or not drawn at all.
By the end of this lesson you’ll be able to draw a data-flow diagram of a real web app, place the trust boundaries where ownership or privilege actually changes, and use those crossings to drive STRIDE element by element instead of staring at a blank page.
The four elements of a data-flow diagram
STRIDE gives you the kinds of threat — spoofing, tampering, repudiation, information disclosure, denial of service, elevation of privilege. But STRIDE applied to “the system” is a fog. To make it systematic you first need a model of how data actually moves, and the standard model is the data-flow diagram (DFD). A DFD is deliberately tiny — it has exactly four element types, and every box on your whiteboard is one of them:
- External entity — an actor outside your control that sources or sinks data: a browser, a mobile app, a partner’s API, a human. You don’t run their code; you can only validate what they hand you. Drawn as a rectangle.
- Process — code you run that transforms data: an API server, a Lambda, a background worker, a parser. This is where logic — and therefore most bugs — lives. Drawn as a circle.
- Data store — somewhere data rests: a Postgres table, an S3 bucket, a Redis cache, a log file, a message queue at rest. Drawn as two parallel lines.
- Data flow — the arrow connecting any two of the above: an HTTP request, a SQL query, a Kafka publish. The flow carries the data, and the flow is what an attacker observes or manipulates in transit. Drawn as a directed edge.
That’s the whole vocabulary. The power isn’t in the boxes — it’s in being forced to draw every arrow, because each arrow is a place where data is exposed, and each box is a place where data is trusted or transformed. A DFD you can’t draw is a system you don’t actually understand.
Trust boundaries: where the threats live
Now overlay the one annotation that turns a DFD from documentation into a threat-modeling tool: the trust boundary. A trust boundary is a line you draw across data flows wherever data passes from one principal, privilege level, or ownership domain into another. Concretely, a boundary sits at every point where the answer to “who controls the data on this side?” changes:
- Between a browser and your API (untrusted client → your code).
- Between your API process and your database (application privilege → data privilege; a SQL injection here is a boundary crossing).
- Between your code and a third-party service (your data leaving your control).
- Between two of your own microservices when they run at different privilege or trust levels.
- Between a low-trust DMZ process and a high-trust internal one.
The reason boundaries matter so much is statistical as much as conceptual: threats concentrate at crossings. Data that never leaves a single trust zone is comparatively safe — it’s already inside one principal’s control. The dangerous moments are the handoffs, because that’s where one side must decide whether to trust the other, and a wrong decision is the vulnerability. Spoofing happens crossing into your auth boundary; tampering and disclosure happen on flows crossing a boundary; elevation of privilege happens when a process on the trusted side accepts input that should have been treated as untrusted. The boundary is the address where each STRIDE letter comes to collect.
Attack surface: the sum of every crossing
Once boundaries are drawn, the attack surface falls out of the diagram almost for free. Attack surface analysis, in OWASP’s framing, is the work of enumerating every point where an attacker can try to enter data into or extract data from your system — and on a DFD those points are precisely the data flows that cross a trust boundary. Every such crossing is an entry or exit point: a login form, an API endpoint, a file upload, a webhook receiver, an outbound call to a third party, a queue your code consumes from.
The senior move is to treat attack surface as something you measure and shrink, not just inventory. The cheat sheet’s guidance is blunt about the tradeoff: more entry points, more privileged code, and more trusted external dependencies all enlarge the surface, and the surface — not your lines of code — is what an attacker actually probes. So you ask, per crossing: does this endpoint need to exist? Can it require authentication? Can it run with less privilege? An endpoint you delete is an endpoint that can’t be exploited; an internal service you make unreachable from outside removes a whole boundary from the attacker’s map. Surface reduction is the cheapest control you own because it removes the threat instead of mitigating it.
▸Why this works
Why circles, rectangles, and parallel lines instead of just labeled boxes? The shapes aren’t decoration — they encode who runs the code. A rectangle (external entity) means “not your code, validate everything it sends.” A circle (process) means “your code, where logic bugs live.” Parallel lines (data store) means “data at rest, ask about encryption and access control.” When you mislabel a box — say you draw a partner’s service as a process because it feels like “part of the system” — you quietly move a trust boundary, and the boundary you erased is the one the attacker walks straight through. The notation is a forcing function for honesty about what you actually control.
From DFD to STRIDE-per-element
Here’s the payoff that makes the diagram worth drawing: STRIDE-per-element. Instead of brainstorming threats freeform, you walk the DFD methodically and apply only the STRIDE letters that each element type can actually suffer. The element type tells you which letters are even in play, which collapses an open-ended brainstorm into a finite, reviewable checklist:
- External entities are susceptible to S and R (spoofing, repudiation) — can someone impersonate this actor, and can the actor deny an action?
- Processes can suffer the full STRIDE set — they’re the richest target, especially T, I, and E (tampering, disclosure, elevation).
- Data stores are mainly exposed to T, R, I, D (tampering, repudiation if logging is weak, disclosure, denial of service).
- Data flows are exposed to T, I, D (tampering, disclosure, denial) — which is exactly why a flow crossing a boundary deserves TLS, integrity checks, and rate limits.
You apply this most rigorously at the boundary crossings. A data flow that stays inside one trust zone still gets reviewed, but a flow crossing a boundary is where tampering and disclosure are most likely and most damaging, so it earns the deepest scrutiny. The DFD plus boundaries plus the element-type-to-STRIDE mapping is the entire engine: it turns “what could go wrong?” — a question that produces panic or hand-waving — into “for each element, which of these specific letters apply, and is the crossing controlled?”
Your DFD shows an internal `recommendations` gRPC process that only other services 'should' call, so the team drew it inside the trusted zone with no boundary on its inbound flow. How should the threat model treat it?
| DFD element | Shape | Applicable STRIDE | The question to ask at its boundary |
|---|---|---|---|
| External entity | Rectangle | S, R | Can someone impersonate it? Can it deny an action? |
| Process | Circle | S, T, R, I, D, E (all) | Does it trust input it shouldn’t? Can it be elevated? |
| Data store | Parallel lines | T, R, I, D | Encrypted at rest? Access controlled? Audit-logged? |
| Data flow | Arrow | T, I, D | TLS? Integrity-checked? Rate-limited at the crossing? |
Failure mode: the boundary you didn’t draw
The recommendations incident from the hook is the canonical DFD failure, and it has a name worth internalizing: the missed trust boundary. It almost never looks like incompetence. The team did STRIDE; they just modeled the system as deployed intentions rather than actual reachability. The internal service was “internal,” so they drew it inside the trusted zone, so it sat on no boundary, so STRIDE-per-element never asked “can this caller be spoofed?” — and the control that question would have demanded (authenticate every caller, least privilege) was never built.
The discipline that prevents it: draw boundaries where data can actually cross, not where you intend it to cross. Every time you place a box inside the trusted zone, ask the adversarial question — “what if this is reachable from outside?” — and verify the reachability against real config (ingress rules, security groups, service mesh policy), not the architecture slide. A network you assume is segmented but never verified is a flat network with extra steps; the classic SSRF-to-metadata attack (a server tricked into fetching http://169.254.169.254/) is precisely an attacker using your own process to cross a boundary you assumed they couldn’t reach. The boundary on the diagram is a claim about the world. If you haven’t verified the claim, you haven’t drawn a boundary — you’ve drawn a wish.
In a data-flow diagram, what precisely defines where a trust boundary should be drawn?
Why does STRIDE-per-element start from the DFD element TYPE (external entity, process, data store, flow) rather than just brainstorming threats?
Order the steps of building a threat model from a system, from first to last:
- 1 Draw the DFD: every external entity, process, data store, and data flow
- 2 Place trust boundaries where ownership or privilege actually changes
- 3 Enumerate the attack surface: the flows that cross a boundary
- 4 Apply STRIDE per element, most rigorously at the crossings
- 5 Decide controls or surface reduction for each unmitigated threat
- 01Name the four elements of a data-flow diagram and explain what a trust boundary is and why threats concentrate there.
- 02How does a DFD drive STRIDE-per-element, and what is the 'missed trust boundary' failure mode?
A data-flow diagram models a system with just four elements — external entity (rectangle), process (circle), data store (parallel lines), and data flow (arrow) — and forces you to draw every arrow, because each arrow is where data is exposed and each box is where it is trusted or transformed. Trust boundaries are the lines you draw across flows wherever ownership or privilege changes: browser→API, API→DB, your code→a third party, or between internal services at different trust levels. Threats concentrate at those crossings because a crossing is where one side decides whether to trust the other, and the wrong decision is the bug. The boundaries make the attack surface visible (every crossing is an entry/exit point to authenticate, de-privilege, or delete) and they make STRIDE systematic: per element type you know exactly which letters apply, scrutinized hardest at the crossings. The failure that breaches teams who did everything else right is the missed boundary — modeling a service by where you intend traffic to flow instead of where it can actually reach. So when you draw a box inside the trusted zone, ask the adversarial question and verify it against real config: is this actually unreachable from outside, or have I just drawn a wish?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.