L05 · Threat Modeling with STRIDE
Module 1: Security Foundations · ~20 min · Lesson 5 of 71
🎯 What you'll learn
- The four questions behind every threat model, and why the first one is a drawing
- How to draw a data flow diagram (DFD) with trust boundaries for a service you own
- The six STRIDE categories, and which security property from L01 each one breaks
- How to turn "what can go wrong?" into a table of threats, mitigations, and tickets
🔥 Why this matters
Picture a team at an insurer about to ship PATCH /v1/policies/{id}. Code review will check that the code works,
but nobody in code review asks "can a customer change someone else's policy?" or "what if a pod
inside the cluster calls the service directly, skipping Kong?" Those questions are cheap on a
whiteboard and expensive in an incident report. The 2022 Optus breach (L44) came down to an
internet-facing API that didn't properly check who was calling it: exactly the gap a threat model
is built to catch.
1. Four questions
Threat modeling is looking at a design, before or while you build it, to find what could go wrong and decide what to do about it. Adam Shostack, who led threat modeling work at Microsoft, boils it down to four questions:
1. What are we working on? ─────▶ draw it (a data flow diagram)
│
▼
2. What can go wrong? ──────────▶ walk the drawing with STRIDE
│
▼
3. What are we going to do ─────▶ mitigate, eliminate, transfer,
about it? or accept each threat
│
▼
4. Did we do a good job? ───────▶ every element checked? every
│ threat has an owner or ticket?
│
└──▶ design changes? go back to 1
An analogy: it's a home inspection done on the blueprints instead of after you've moved in. Technically, it's a structured design review that produces a list of threats, each tied to a part of the system and a decision. A whiteboard, the engineers who know the system, and an hour are enough.
2. Question 1: draw a data flow diagram
You can't find threats in a system you can't see, so step one is a data flow diagram (DFD): a picture of where data comes from, where it goes, and where it's kept. It has five building blocks:
[ Customer ] EXTERNAL ENTITY people or systems you don't
control: users, partners
( policy-svc ) PROCESS code that runs: services,
gateways, batch jobs
═══════════════
MongoDB DATA STORE data at rest: databases,
═══════════════ buckets, queues, log files
──── F1 ────▶ DATA FLOW data moving between two of
the above (label each one)
─ ─ ─ ─ ─ ─ ─ TRUST BOUNDARY a line where the level of
trust changes
The most important block is the trust boundary: a line where data passes between parts of the system that have different privileges or are trusted to different degrees. The internet and an API gateway sit on opposite sides of one. So do a service and its database, and a batch job running in another account. A boundary often matches a firewall, but it doesn't have to: it's about who controls what, not about cables.
Almost every serious threat lives where a data flow crosses a trust boundary. On one side, someone you don't fully trust controls the data; on the other, your code acts on it. That crossing is where authentication, validation, and authorization checks belong. Keep the DFD coarse: one box per service, not per class.
3. Question 2: what can go wrong? STRIDE
Asking "what could go wrong?" at a diagram tends to produce silence, or one person's favorite worry. STRIDE, created at Microsoft by Loren Kohnfelder and Praerit Garg in 1999, gives you six prompts instead. Each one is the opposite of a security property you met in L01:
| Letter | Threat | Property it violates | Plain-language question |
|---|---|---|---|
| S | Spoofing | Authenticity | Can someone pretend to be a user or a service? |
| T | Tampering | Integrity | Can someone change data in transit or at rest? |
| R | Repudiation | Non-repudiation | Can someone do something and later deny it? |
| I | Information disclosure | Confidentiality | Can someone read data they shouldn't? |
| D | Denial of service | Availability | Can someone make it slow or unusable? |
| E | Elevation of privilege | Authorization | Can someone do more than they're allowed to? |
In L01 you used these properties to say what failed after an incident; STRIDE asks the same questions before one. Authorization wasn't one of L01's named properties, but you saw it in the log examples: it's the rule about who may do what, and elevation of privilege breaks it.
You don't need all six questions for every element. STRIDE per element narrows them by type:
| Element | Threats that usually apply |
|---|---|
| External entity | S, R |
| Process | S, T, R, I, D, E (all six) |
| Data store | T, I, D (plus R if it holds logs) |
| Data flow | T, I, D |
Walk the diagram element by element, asking only the questions on its row, and spend most of your time on elements that touch a trust boundary.
Question 3: what are we going to do about it?
Each threat gets one of four decisions. Mitigate it with a control. Eliminate it by removing the feature or the data. Transfer it to someone else, such as an identity provider or an insurer. Or accept it, on purpose, with a named owner, because the risk (likelihood × impact, from L02) is low enough. "We didn't get to it" is not a decision.
🧪 Worked example: threat-modeling /v1/policies/{id}
Q1: What are we working on? Customers read their policy (GET) and update contact details
(PATCH) from a mobile app. Requests go through Kong to policy-service on EKS, which uses a MongoDB
policies collection. A nightly Airflow job, running in its own environment outside the cluster,
bulk-upserts data from an upstream admin system into the same collection.
[ Customer: mobile app ]
│ F1 GET/PATCH /v1/policies/{id}
│ HTTPS + OAuth access token
─ ─ ─ ─ ─ ─│─ ─ TB1: internet │ company edge ─ ─ ─ ─ ─ ─ ─ ─ ─
▼
( Kong gateway )
│ F2 request + X-Consumer-ID header, mTLS
─ ─ ─ ─ ─ ─│─ ─ TB2: edge │ EKS ─ ─ ─ ─
▼
( policy-service ) ──F4 audit event──▶ ═══ audit log ═══
│
│ F3 find / updateOne ┌ ─ TB4: Airflow env ─ ┐
│ (service DB user) ( Airflow DAG )
│ └ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─┘
│ │ F5 nightly
│ │ upsert
─ ─ ─ ─ ─ ─│─ ─ TB3: app │ data tier ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─
▼ ▼
═══════════════════════════════════════════════════════
MongoDB · policies collection (data store)
═══════════════════════════════════════════════════════
Q2 and Q3: What can go wrong, and what will we do? STRIDE per element, focusing on the boundary crossings (made-up IDs):
| # | Where | STRIDE | Threat | Decision |
|---|---|---|---|---|
| 1 | F1 | S | A stolen or forged token is replayed | Mitigate: Kong checks JWT signature, issuer, audience, expiry; short-lived tokens (L25) |
| 2 | F2 | S | A pod inside the cluster calls policy-service directly with X-Consumer-ID: C-20417 |
Mitigate: network policy so only Kong can reach it; service verifies the token itself (L43, L51) |
| 3 | policy-service | I | GET /v1/policies/PL-100246 returns another customer's policy |
Mitigate: check the policy's owner against the token (L34, L40) |
| 4 | policy-service | E | A PATCH body sneaks in "ownerId" or "status":"PAID_UP" |
Mitigate: allow-list of patchable fields (L41) |
| 5 | F3 | T | {id} arrives as {"$ne": null} and matches any document |
Mitigate: accept only strings matching ^PL-\d{6}$ (L31) |
| 6 | Customer, F4 | R | "I never changed my address," after a fraud claim | Mitigate: audit caller, old/new values, correlation ID to an append-only store (L60) |
| 7 | Kong | D | One client floods GETs or sends a 50 MB PATCH body | Mitigate: per-consumer rate and size limits (L42) |
| 8 | F5 | T, D | A bad upstream file overwrites 40,000 live policies | Mitigate: row-count and schema checks first; tested point-in-time restore |
| 9 | Airflow DAG | E | The DAG's DB user has root, so anyone who can merge a DAG can drop the database |
Mitigate: custom role writing only to policies; password in a vault (L48, L53) |
| 10 | Audit log | I | Logs include full request bodies with tax IDs | Accept for this release: owner platform team, fix by Q1 (logs are encrypted, access is narrow) |
Notice rows 3 and 4. Kong knows who is calling, but only policy-service knows which policies belong to them. A gateway can't fix those threats, and that's an insight the diagram gives you before you read any code.
Q4: Did we do a good job? Every element and every flow across TB1 to TB4 was asked its STRIDE
questions, and every row has a ticket or a named owner. Where you can, turn mitigations into tests:
GET on someone else's policy returns 404, and a PATCH with ownerId is rejected. Keep the
diagram and table in the repo and update them when the design changes.
🛠 Try it (optional, ~5 min)
Run STRIDE on a login endpoint, POST /v1/auth/login, which takes a username and password and
returns a session token. Sketch it first (customer → Kong → auth-service → user store), then write
one threat and one mitigation per letter.
Possible answers, don't peek:
- S credential stuffing with leaked passwords → MFA, rate limits, breached-password checks (L18, L21)
- T a
redirectparameter changed to an attacker's site → allow-list redirect URLs - R failed logins aren't logged → log each attempt's time, IP, and outcome (never the password)
- I "user not found" vs "wrong password" reveals valid usernames → one generic error
- D attackers lock out real users on purpose (the L01 trade-off) → throttle instead of locking
- E the service trusts a
"role"field in the request → roles come only from the user store
🧠 Key takeaways
- Threat modeling answers four questions: what are we working on, what can go wrong, what will we do about it, and did we do a good job.
- Start with a DFD: external entities, processes, data stores, data flows, and trust boundaries.
- Most serious threats sit where a data flow crosses a trust boundary, so put checks there.
- STRIDE is the inverse of L01's properties: S↔authenticity, T↔integrity, R↔non-repudiation, I↔confidentiality, D↔availability, E↔authorization.
- Every threat ends with a decision (mitigate, eliminate, transfer, accept) and an owner.
📖 Words to know
| Term | Meaning |
|---|---|
| Threat modeling | Reviewing a design to find what could go wrong and decide what to do about each threat |
| Data flow diagram (DFD) | A drawing of a system's external entities, processes, data stores, and data flows |
| External entity | A person or system outside your control that sends or receives data |
| Process | Running code that handles data: a service, gateway, or job |
| Data store | Somewhere data rests: a database, bucket, queue, or log |
| Data flow | Data moving between two elements, drawn as a labeled arrow |
| Trust boundary | A line where data passes between parts of a system with different levels of trust |
| STRIDE | Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege |
| STRIDE per element | Applying only the STRIDE categories that fit each DFD element type |
| Risk acceptance | A deliberate, owned decision to live with a threat instead of mitigating it |
⏭ Next time
L06: the Equifax breach of 2017, where an unpatched server, a flat network, and an expired certificate turned one known vulnerability into one of the largest data breaches in US history.
Threat modeling in a sprint (a 30-minute format)
The problem with "big" threat models
Many teams picture threat modeling as a two-day workshop with a security architect, a 40-page document, and a diagram nobody updates. That kind of model is out of date by the next release. The alternative that works on agile teams is small and frequent: a short session on the change, not the whole system, run by the engineers building it.
When to do one
You don't threat-model every story. Use a short list of triggers in backlog refinement. If a story does any of these, it gets a session:
- Adds a new trust boundary crossing: a new public endpoint, a new partner integration, a new webhook, a new caller of an internal service
- Handles a new kind of sensitive data, such as SSNs, bank details, or health information
- Changes authentication or authorization: new roles, new token scopes, a new identity provider
- Adds a new data store or a new writer to an existing one (like the Airflow job in the main lesson)
- Changes who can deploy or run something: CI permissions, IAM roles, DB users
A copy change on a screen, or a refactor that keeps the same flows, doesn't need one.
The 30-minute agenda
0 min 5 20 25 30
│ DRAW │ STRIDE │ DECIDE │ RECORD │
│ the │ only the │ rank by │ tickets, │
│ change │ elements │ likelihood │ accepted │
│ on the │ the change │ × impact; │ risks, │
│ DFD │ touches │ pick an │ tests │
│ │ │ owner │ │
└─── Q1 ────┴──── Q2 ─────┴──── Q3 ─────┴──── Q4 ────┘
Who: the developer building it, the tech lead, and whoever knows the upstream and downstream systems. A security engineer is welcome but shouldn't be required, or sessions won't happen.
Rules that keep it to 30 minutes: start from the existing DFD and draw only what's new (in a different color). Ask STRIDE only of elements the change touches, using the per-element table. Don't design fixes in the room; write the threat, the decision, and an owner, and design the fix in the ticket. If you run out of time, schedule a second session rather than skipping step 4.
🧪 Worked example: "add a partner webhook"
Story: "When a policy's address changes, POST a notification to a partner's URL." The trigger list fires twice: a new flow crosses a trust boundary outward, and the partner is a new external entity.
( policy-service ) ──F7 POST event (HTTPS)──▶ [ Partner system ]
─ ─ ─ ─ TB5 ─ ─ ─ ─
Ten minutes of STRIDE on F7 and the partner produce:
| STRIDE | Threat | Decision |
|---|---|---|
| S | Someone posts fake events to the partner, pretending to be us | Mitigate: HMAC-sign the body with a shared secret per partner (L17) |
| I | The payload includes the full new address, more than the partner needs | Eliminate: send only the policy ID and "address changed"; partner fetches details through its own authorized API |
| T | The partner URL is editable in config, so anyone with config access can point it at their own server | Mitigate: URL changes need a second approver; allow-list partner domains |
| D | The partner is slow, and synchronous calls back up policy-service | Mitigate: send through a queue with retries and a timeout |
| E | The partner URL is set to http://169.254.169.254/... |
Mitigate: block internal and metadata addresses for outbound calls (SSRF, L35) |
The output is five tickets and one changed requirement (the slimmer payload), found before a line of webhook code was written.
Keep the model next to the code
Save the DFD and the threat table in the repo, for example docs/threat-model.md, so pull requests
can update it. Some teams go further with threat modeling as code: tools like OWASP pytm let
you describe the system in Python and generate the diagram and a first list of threats. Free GUI
tools include OWASP Threat Dragon and the Microsoft Threat Modeling Tool. The tool matters far
less than the habit.
🛠 Try it (~5 min)
Look at your team's current sprint board. Which stories hit at least one trigger from the list above? For one of them, write down the new element or flow it adds to your service's DFD, and the one STRIDE letter you think matters most for it.
🧠 Extra takeaways
- Threat-model the change, not the whole system, and do it often.
- Use triggers (new boundary crossing, new sensitive data, auth changes, new data store or writer) to decide which stories need a session.
- Timebox it: draw, STRIDE, decide, record. Design fixes in tickets, not in the room.
- Keep the model in the repo so it changes along with the code.
| Term | Meaning |
|---|---|
| Threat modeling trigger | A kind of change that always gets a threat-modeling session |
| Threat modeling as code | Describing a system in code so diagrams and candidate threats are generated and versioned |
| OWASP pytm | A Python framework for threat modeling as code |
| OWASP Threat Dragon | A free, open-source tool for drawing DFDs and recording threats |
Beyond STRIDE (attack trees, PASTA, LINDDUN)
STRIDE isn't the only lens
STRIDE is a great checklist for "what kinds of things go wrong with this element?" It's weaker at two other questions: "what are the different ways an attacker reaches one specific goal?" and "what privacy harm could this do even if nobody breaks in?" Other methods answer those. You won't use them daily, but you'll see the names in security reviews.
Attack trees: many paths to one goal
An attack tree puts one attacker goal at the root and breaks it into the ways to achieve it. Children of an OR node are alternatives (any one works); children of an AND node must all happen. Bruce Schneier popularized the idea in 1999. Here's a tree for the threat that matters most in the worked example:
GOAL: change another customer's beneficiary
│
├─ OR ─ 1. act as that customer
│ ├─ 1a. phish their password + MFA code (L66)
│ ├─ 1b. credential stuffing, no MFA on login (L18)
│ └─ 1c. steal a token from a logged device
│
├─ OR ─ 2. use your own account against their policy
│ ├─ 2a. PATCH /v1/policies/{their id}: BOLA (L40)
│ └─ 2b. send "ownerId" in a PATCH: mass
│ assignment (L41)
│
└─ OR ─ 3. go around the API
└─ AND ─ 3a. get the Airflow DB credentials
3b. reach MongoDB from the network
3c. update the document directly
The tree makes priorities visible. Path 2 needs only a normal account and a curl command, so it's the cheapest for an attacker and should be fixed first. Path 3 is an AND: blocking any one step (say, the network route in 3b) cuts the whole branch. Teams often annotate each leaf with cost, skill, or likelihood to compare paths.
PASTA: risk-centric and heavier
PASTA (Process for Attack Simulation and Threat Analysis), published by Tony UcedaVélez and Marco Morana in 2015, is a seven-stage method that starts from business objectives rather than the diagram:
1 business objectives → 2 technical scope → 3 decompose the app
→ 4 threat analysis → 5 vulnerability analysis → 6 attack modeling
→ 7 risk and impact analysis
It ties each threat to business impact, which makes it popular with risk and compliance teams in regulated industries. The cost is effort: it's a multi-week exercise for a major system, not a 30-minute sprint session.
LINDDUN: privacy threats
LINDDUN, developed at KU Leuven in Belgium, is STRIDE's counterpart for privacy. Its categories (as named in recent versions) are:
| Letter | Threat | Example in a policy API |
|---|---|---|
| L | Linking | Joining claims data and app analytics by device ID to build a profile |
| I | Identifying | "Anonymized" exports that still contain birth date + ZIP code |
| N | Non-repudiation | Keeping proof of an action the person wanted to be deniable |
| D | Detecting | 403 for "exists, not yours" vs 404 for "no such policy" reveals that a record exists |
| D | Data disclosure | Sending more personal data to a partner than the purpose needs |
| U | Unawareness and unintervenability | Customers can't see or correct what's stored about them |
| N | Non-compliance | Keeping data longer than the retention policy allows |
Notice the twist: in STRIDE, non-repudiation is a goal you protect. In LINDDUN, it can be a threat, because some actions (like anonymous whistleblowing) should be deniable. Same property, different point of view. Privacy gets its own lesson in L69.
Which one when?
| Method | Best at | Effort |
|---|---|---|
| STRIDE | Systematic coverage of a design, element by element | Low to medium |
| Attack tree | Comparing paths to one high-value goal | Low per tree |
| PASTA | Linking threats to business risk for a major system | High |
| LINDDUN | Privacy harms, including ones with no attacker | Medium |
They combine well: STRIDE to find threats, an attack tree for the scariest one, LINDDUN for any flow carrying personal data.
🛠 Try it (~5 min)
Build an attack tree for "read another customer's policy details" in the worked example's system. Aim for at least three OR branches and one AND branch. Mark the cheapest path for an attacker, then check whether the main lesson's threat table already mitigates it.
🧠 Extra takeaways
- Attack trees show the different ways to reach one goal; cut an AND branch anywhere to block it.
- PASTA starts from business objectives and ends in risk analysis; thorough but heavy.
- LINDDUN covers privacy, where even legitimate processing can cause harm.
- Use STRIDE by default and add the others where they answer a question STRIDE can't.
| Term | Meaning |
|---|---|
| Attack tree | A tree with an attacker goal at the root and alternative (OR) or combined (AND) steps below it |
| PASTA | A seven-stage, risk-centric threat modeling method that starts from business objectives |
| LINDDUN | A privacy threat modeling framework: Linking, Identifying, Non-repudiation, Detecting, Data disclosure, Unawareness, Non-compliance |
The quiz for this lesson and the Tests section need JavaScript.