Threat Modelling
Finding design flaws before they ship: drawing data flow diagrams, applying STRIDE per element, prioritising with DREAD, and running a session that produces actionable work.
On this page
Threat modelling asks a question testing cannot: what could go wrong with this design? It finds architectural flaws — a missing trust boundary, an unauthenticated internal call — that no scanner detects because the code correctly implements a flawed design.
Key Takeaways
- Draw the data flow and mark trust boundaries — most threats cross one.
- Apply STRIDE per element rather than brainstorming freely; coverage comes from the structure.
- Prioritise by impact and likelihood, not by how alarming a threat sounds.
- Every threat gets a decision: mitigate, accept or transfer.
- The output is a backlog with owners, not a document.
Draw the system
Trust boundaries are the crossings that matter: internet to gateway, gateway to internal network, application to database, your system to a third party. A threat that crosses one is where an attacker gains something.
Be honest about what is inside a boundary. "Internal network" is not a trust boundary if any compromised pod can reach any service — that is one large boundary containing everything, which is precisely the observation zero-trust design responds to.
STRIDE
| Threat | Question | Typical mitigation |
|---|---|---|
| Spoofing | Can someone pretend to be another identity? | Authentication, mTLS |
| Tampering | Can data be modified in transit or at rest? | TLS, signatures, integrity checks |
| Repudiation | Can someone deny an action? | Audit logging, non-repudiation |
| Information disclosure | Can data leak? | Encryption, access control, output filtering |
| Denial of service | Can availability be destroyed? | Rate limiting, quotas, timeouts |
| Elevation of privilege | Can someone gain unauthorised rights? | Authorisation, least privilege |
Apply it element by element. Free-form brainstorming misses categories; walking each element through six prompts gives coverage.
S: Can something other than the gateway call the order service directly?
→ Yes. Pod networking permits it and the service trusts X-User-Id.
→ MITIGATE: validate the JWT in the service; NetworkPolicy restricting ingress.
T: Can the JWT be modified in transit?
→ Signature prevents modification, but the hop is plaintext HTTP.
→ MITIGATE: mTLS between services.
R: Can a user deny placing an order?
→ Currently yes; we log the request but not the authenticated subject.
→ MITIGATE: audit log with subject, timestamp and correlation id.
I: Can order data leak to another user?
→ Object-level authorisation is checked in the controller but not in the
batch export endpoint.
→ MITIGATE: scope the export query by customer; add a negative test.
D: Can one user exhaust the service?
→ No per-user limit on the search endpoint, which fans out to the database.
→ MITIGATE: per-user rate limit weighted by operation cost.
E: Can a user become an admin?
→ The role claim is read from the JWT; the auth server controls issuance.
→ ACCEPT: dependent on auth server integrity, which is modelled separately.Notice how many outputs are ordinary engineering tasks. Threat modelling mostly surfaces work you would agree with once someone asked the question.
Prioritising
DREAD scores each threat one to three on damage, reproducibility, exploitability, affected users and discoverability. It is rough and it is enough — the purpose is ordering, not precision.
| Threat | D | R | E | A | Di | Total | Decision |
|---|---|---|---|---|---|---|---|
| Direct service call bypassing the gateway | 3 | 3 | 2 | 3 | 2 | 13 | Mitigate now |
| Export endpoint leaks other customers | 3 | 3 | 3 | 3 | 2 | 14 | Mitigate now |
| No per-user rate limit on search | 2 | 3 | 3 | 2 | 3 | 13 | Mitigate this quarter |
| Plaintext hop between services | 2 | 1 | 1 | 3 | 1 | 8 | Mitigate with the mesh rollout |
| Auth server compromise | 3 | 1 | 1 | 3 | 1 | 9 | Transfer — modelled separately |
Resist over-engineering the scoring. Teams spend hours arguing whether exploitability is 2 or 3, which changes nothing about the order of work. Rough scores, quick decisions, move on.
Running a session
Before: someone draws the data flow diagram and circulates it. Doing this live wastes the room's time and the diagram is better when one person has thought about it.
In the room, ninety minutes with three to six people — the engineers who built it, someone with security knowledge, and ideally someone from operations who knows how it actually runs. Walk each element through STRIDE. Record every threat without debating fixes; the fix discussion derails coverage.
After: score, decide, and file tickets. A threat with no ticket did not happen.
Three questions unlock most sessions when the room goes quiet. What is the worst thing an attacker could do here? What would we most hate to explain to a customer? What are we assuming, and what if that assumption is false?
Accepting risk
Not every threat gets fixed, and pretending otherwise makes the model dishonest. Record acceptances explicitly:
THREAT: A compromised auth server could issue tokens for any user.
IMPACT: Complete authentication bypass.
LIKELIHOOD: Low — hardened, isolated, monitored, separately modelled.
DECISION: ACCEPT
RATIONALE: The alternative is a second independent authentication factor on
every request, which the latency budget does not permit.
COMPENSATING: Anomaly alerting on token issuance rate; 15-minute token expiry;
quarterly review of auth server access.
OWNER: platform-security REVIEW: 2027-01-15An acceptance with an owner and a review date is a decision. One without either is an omission with better paperwork.
Keeping it current
A model reflects the design at a moment. Re-examine when a new external integration appears, when a trust boundary moves, when a new data classification enters the system, or after any security incident — the incident is evidence the model missed something.
Store models in the repository next to the code so they are versioned, reviewable in pull requests, and visible to the people changing the system.
What to take away
Draw the data flow, mark the trust boundaries honestly, and walk each element through STRIDE rather than brainstorming. Score roughly and decide quickly. Turn every threat into a ticket with an owner, record accepted risks with a review date, and keep the model in the repository so it changes when the system does.
Frequently Asked Questions
When should we threat model?
Do we need a tool?
How do we stop it becoming a document nobody reads?
Related tutorials
- DevSecOps — Securing the PipelineSecurity gates that catch real problems without blocking delivery: pre-commit secret scanning, SAST with FindSecBugs, dependency and container scanning, DAST, and tuning out the noise.
- Secrets Management & Key SecurityGetting secrets out of configuration: taking an inventory, Vault KV and dynamic database credentials, Kubernetes auth, the External Secrets Operator, and rotation that works.
- Penetration Testing for Java AppsA structured approach to testing your own application: reconnaissance, authentication and authorisation testing, injection, business logic flaws, and the tools that help.
- Testing Spring SecurityWriting security tests that catch real gaps: @WithMockUser and @WithUserDetails, MockMvc request post-processors, testing method security, mock JWTs, and the negative tests that matter.