Threat Modeling That Ships — What to Actually Cover
A threat model is not a document — it's a decision-making exercise that ends in a ranked list of changes. If it doesn't change what gets built or tested, it was theatre. This is the lean process we run before touching a new system, and the one we teach client teams to run themselves.
Start with four questions, in order
Adam Shostack's four questions are still the whole discipline. Everything else is tooling.
- What are we building? One diagram — components, data stores, and the requests between them.
- What can go wrong? The adversarial pass.
- What are we going to do about it? The mitigation list.
- Did we do a good job? The review.
The failure mode is spending 90% of the time on question 1 (a beautiful diagram) and 10% on question 2 (the part that matters). Invert it.
Draw the data flow, then draw the trust boundaries
Sketch the system as a data-flow diagram: external entities, processes, data stores, and the flows between them. Then draw the trust boundaries — the lines where data crosses from something you control to something you don't, or from one privilege level to another:
- Internet → your edge
- Edge → application
- Application → database and third-party APIs
- One tenant's data → another tenant's request path
- Unauthenticated → authenticated → privileged
Every boundary crossing is where a control must exist. Most real vulnerabilities are a missing or wrong control at a boundary — an input trusted after it crossed, an identity assumed instead of verified. The boundaries are your test plan.
Enumerate threats per element with STRIDE — but ruthlessly
STRIDE gives you six lenses. Apply them to each element and flow, and write down only the ones that are plausible for this system:
- Spoofing — can identity be faked? (weak auth, missing mutual TLS, forgeable tokens)
- Tampering — can data or code be modified in transit or at rest? (integrity, signing)
- Repudiation — can an actor deny an action? (audit logging on money and privilege changes)
- Information disclosure — can data leak? (authz on reads, encryption, error verbosity)
- Denial of service — can it be made unavailable? (rate limits, resource caps)
- Elevation of privilege — can a user gain rights they shouldn't? (the highest-severity lens; spend the most time here)
The discipline is subtraction. A STRIDE table with 200 rows is ignored; one with the 15 threats that actually apply to this system gets fixed.
Rank by impact × likelihood, and be honest
For each surviving threat, score impact (what happens if it's real) against likelihood (how reachable it is). You are not aiming for precision — you're aiming to sort. The top of that list is where design changes and testing effort go. The bottom is documented and accepted. An unranked threat model is a to-do list with no order, which is no plan at all.
The elements teams forget
Across the systems we review, the same blind spots recur. Add them to every model:
- The second identity. Service-to-service auth, background jobs, and webhooks are identities too — and they're usually over-privileged. Model them.
- The trust in "internal." "It's behind the VPN" is not a control. Assume the perimeter is porous (see SSRF); model internal services as reachable.
- Multi-tenancy. The boundary between tenants is the highest-value line in any SaaS. Every query, cache key, and object reference either enforces it or leaks across it.
- The supply chain. Dependencies, build pipeline, and the deploy path are part of the system. A compromised CI is game over regardless of app security.
- Failure states. What does the system do when a downstream is unavailable? Fail-open on an authorization check is a vulnerability the happy-path never reveals.
Keep it living
The best threat model is a short living artifact revisited each time the architecture changes, not a 40-page PDF produced once for compliance and never opened. One diagram, a ranked threat list, and a decision on each. That's the whole thing — and it's the cheapest security work a team will ever do.