Security work that starts with a tool list ends with a long bill and the same breaches. Security work that starts with a question ends better: what are we protecting, from whom, and how would they actually get in? Threat modelling is the habit of asking that question early and writing the answers down, so that every later control (a firewall rule, a code review, an alert) exists for a reason you can name. This module gives you the attacker's way of looking at a system and a lightweight method you can run in an hour on a whiteboard.
- Describe a system as assets, entry points, trust boundaries and data flows
- Enumerate threats with STRIDE and rank them by likelihood and impact
- Explain the attack chain from initial access to impact, and where defenders can break it
- Turn a threat model into concrete, testable controls and accepted risks
- Run a one-hour threat modelling session for a real feature or service
Think like the attacker, plan like the defender
Attackers are not magicians. They are people with a goal (money, data, access, disruption), a budget of time and skill, and a preference for the easiest path. A threat model is a structured description of your system from that point of view: what is valuable in it, where someone from outside can touch it, and which paths lead from touching it to getting the valuable thing. Defenders who skip this step protect the wrong things beautifully.
Four questions drive every threat model. They come from the threat modelling manifesto and they fit on a sticky note:
- What are we working on? Draw the system: components, data stores, external services, the people and machines that call it.
- What can go wrong? Enumerate threats systematically, not by brainstorming whatever comes to mind first.
- What are we going to do about it? For each threat that matters: mitigate, eliminate, transfer, or accept — and write down which.
- Did we do a good job? Check the controls exist, test them, and revisit the model when the system changes.
A threat model is a living document, not a compliance artefact. The useful version is one page per service, updated when a new entry point, data type or dependency appears.
Draw the system: assets, entry points, trust boundaries
Start with a data flow diagram: boxes for processes, cylinders for data stores, arrows for data flows, and stick figures for external entities (users, third-party APIs, other teams' services). Then draw the trust boundaries: dotted lines where the level of trust changes — the internet edge, the boundary between a web tier and a database, the line between a user's browser and your API, the line between one tenant's data and another's. Almost every interesting threat lives on a boundary crossing.
Next, list the assets, meaning the things an attacker would want or the things whose loss would hurt: customer records, credentials and tokens, the ability to run code on your servers, payment flows, availability of the service itself, and your reputation. Rank them. If you cannot say which asset matters most, you cannot prioritise anything downstream.
Finally, mark the entry points: every place input arrives from a less-trusted side of a boundary. Public HTTP endpoints, webhooks, file uploads, message queues, admin consoles, SSH, the CI pipeline, and the dependency manifest are all entry points. The attack surface is the sum of them; the smaller and better-known it is, the easier the rest of this work becomes.
SYSTEM: order-api (public REST API + worker + Postgres)
ASSETS (ranked)
1. customer PII in Postgres (breach = legal + reputation)
2. payment provider API key (theft = fraud)
3. ability to run code on the hosts (foothold into everything else)
4. availability during business hours
ENTRY POINTS
E1 POST /orders, GET /orders/{id} internet -> api (auth: user JWT)
E2 POST /webhooks/payment payment provider -> api (auth: HMAC signature)
E3 admin UI on :8443 office VPN -> api (auth: SSO + MFA)
E4 container image + pip dependencies build pipeline -> hosts
E5 SSH to hosts ops laptops -> hosts (auth: keys)
TRUST BOUNDARIES
internet | load balancer | api pods | postgres
build pipeline | production registryEnumerate threats with STRIDE
Brainstorming produces the threats you already fear. STRIDE produces the ones you forgot, because you walk every data flow and ask six fixed questions. Each letter is a category of threat, and each maps to the security property it violates:
| Threat | Violates | Example on order-api |
|---|---|---|
| Spoofing | Authentication | A forged webhook claims a payment succeeded |
| Tampering | Integrity | Order total modified in the request body after client-side validation |
| Repudiation | Non-repudiation | An admin deletes records and there is no log of who did it |
| Information disclosure | Confidentiality | Stack traces with database details returned on error |
| Denial of service | Availability | Unbounded page size on GET /orders exhausts the database |
| Elevation of privilege | Authorisation | A user reads another user's order by changing the id |
Apply STRIDE per element: for each entry point and each boundary crossing, ask all six questions. A five-endpoint API with three boundaries yields perhaps forty candidate threats in half an hour. That is fine; the next step is ranking, not fixing everything.
Rank each threat by likelihood (how easy is it, how motivated is the attacker, is it already being exploited in the wild) and impact (which asset, how badly). A simple high/medium/low on each axis is enough; the point is to compare, not to compute. Anything high on both goes to the top of the backlog with a named owner.
For each threat write one sentence in the form: "An attacker with X access can do Y, causing Z." If you cannot fill in all three, it is not yet a threat, it is a worry.
The attack chain and where to break it
Real intrusions are chains, not single events. A typical sequence: reconnaissance (finding your domains, exposed services, employee emails), initial access (phishing, an exposed credential, an unpatched service), execution and persistence (running code and surviving a reboot), privilege escalation, lateral movement to more valuable systems, collection of data, and finally exfiltration or impact (ransomware, fraud, destruction). The public catalogue of these techniques is the MITRE ATT&CK framework; defenders use it as a checklist of what attackers actually do rather than what they theoretically could.
Chains are good news. Breaking any link stops the attack, and the cheapest links to break are usually early ones: fewer exposed services, credentials that expire, MFA on everything that faces the internet, patches applied on a schedule. A defence-in-depth design assumes the first control will fail and places another behind it: the web application firewall does not stop every injection, so the database user also has no permission to read other tables, so the network also blocks egress from the database host.
- Reduce the surface: every service you do not expose is a threat category you do not have.
- Assume breach: design so that one compromised component (a pod, a laptop, a key) does not give away everything.
- Make lateral movement noisy: segmentation and logging turn a quiet intrusion into an alert.
- Shorten credential lifetimes: a stolen token that expires in an hour is a much smaller problem than a key from 2021.
The most common initial access today is not a clever exploit. It is a leaked credential, a phished login without MFA, or an internet-facing service that missed a patch. Threat models that only list exotic attacks miss the door that is actually open.
From threats to controls
Each ranked threat gets one of four responses. Mitigate: add a control that reduces likelihood or impact (validate the webhook signature, add rate limits, log admin actions). Eliminate: remove the feature or flow that creates the threat (drop the unauthenticated export endpoint nobody uses). Transfer: make it someone else's problem contractually or technically (use the payment provider's hosted checkout so card numbers never touch your system). Accept: decide the risk is tolerable, write down why and who agreed, and set a date to revisit.
A good control is specific and testable. "Improve input validation" is not a control; "reject page_size above 100 with HTTP 400, covered by test test_orders_page_size_cap" is. Record each control next to the threat it addresses, so that six months later someone can ask why that check exists and get an answer.
T-07 E1 Elevation: user reads another user's order by guessing the id
Likelihood HIGH Impact HIGH -> MITIGATE
Control: every /orders/{id} query is scoped by the caller's user_id
Test: test_get_order_other_user_returns_404
Owner: api team Status: done 2026-03
T-12 E2 Spoofing: forged payment webhook marks an order as paid
Likelihood MED Impact HIGH -> MITIGATE
Control: verify HMAC signature with the provider secret; reject if > 5 min old
Test: test_webhook_bad_signature_rejected
T-19 E5 Information disclosure: SSH keys on laptops without passphrases
Likelihood MED Impact MED -> ACCEPT until SSM/SSO rollout (Q3), owner: platform leadClose the loop with the fourth question. Controls drift: a test gets skipped, a firewall rule gets widened during an incident and never narrowed. Revisit the model on a schedule and whenever the diagram changes: new entry point, new data class, new third-party integration. Later modules in this track are, in effect, the standard controls for the most common threats — identity, cryptography, hardening, supply chain, detection and response.