Chapter 4 — Threat modeling at scale
In one sentence
Threat modeling stops being a one-off workshop and becomes a continuous, lightweight practice woven into design review, with heavier methods reserved for the highest-risk changes.
Where we are
G1 Ch 52 taught STRIDE and the four questions on a whiteboard. That works for one system, once. An org ships hundreds of changes a week. Scaling threat modeling means: making the cheap version a habit, the expensive version targeted, and the outputs tracked like any other engineering work.
How it works
Tiered practice:
TIER 0 -- every change: a "security notes" field in the design doc / PR
template. 3 prompts: "What trust boundary does this touch? What new
data or capability does it expose? What's the worst thing a malicious
caller could do?" 5 minutes. Reviewer can escalate.
TIER 1 -- new service / significant redesign: a 60-90 min session, the
four questions + STRIDE per element, a DFD kept as code
(`pytm`, Threagile, or Mermaid in the repo), threats tracked as
issues with mitigate/eliminate/transfer/accept + owner + date.
TIER 2 -- crown-jewel systems, new tenancy model, auth changes, crypto:
add attack trees and/or an attacker-goal method (PASTA, or "attack
path" from Ch 1). Bring an adversary-emulation plan (Ch 2). Consider
an external review.
TIER 3 -- pre-launch / high regulatory exposure: full PASTA or a
dedicated red-team objective; DPIA if personal data (Part 7 Ch 47).
Methods, and when each earns its cost:
STRIDE default. Per-element prompt for "what can go wrong".
Attack trees when you have a specific attacker goal to decompose
("read another tenant's data" -> OR of sub-goals).
Attack paths when the system is graph-shaped (cloud/k8s/microservices)
-- Ch 1. Often more useful than STRIDE here.
PASTA risk-centric, 7 stages, ties threats to business impact;
heavyweight; good for Tier 2/3 and for exec buy-in.
LINDDUN privacy threats (linkability, identifiability,
non-repudiation-as-harm, detectability, disclosure,
unawareness, non-compliance). Use alongside STRIDE
whenever personal data is involved (Part 7).
Kill-chain / ATT&CK frame threats as adversary progression -> directly
feeds detection coverage.
Keep it alive:
[ ] DFD + threat list live in the repo, versioned, reviewed in PRs that
change the design.
[ ] Threats are issues in the tracker with the same lifecycle as bugs
(owner, priority, done).
[ ] Accepted risks: written, with an owner and an expiry, visible to
someone accountable. Re-reviewed at expiry.
[ ] Re-run after any incident touching the system -- a real attack is
ground truth.
[ ] A quarterly 30-min "has the model drifted?" check for Tier 1+ systems.
Worked example
Tier 1 threat model for SecureShop's multi-tenant data layer (the piece Project 2 will build). Abbreviated.
WHAT: Aurora PostgreSQL, one schema, `tenant_id` column on every table,
Postgres Row-Level Security (RLS) policies keyed off a session GUC
`app.tenant_id` set by the connection pool from the request's validated
tenant claim. Plus per-tenant DEKs (Part 7) for column encryption of
PII. Read models in OpenSearch, one index alias per tenant.
TRUST BOUNDARIES: request -> BFF (sets tenant context); BFF -> data
service; data service -> DB (sets GUC); data service -> OpenSearch;
admin tooling -> DB (bypasses the app!).
STRIDE (selected):
S a service connects to the DB without setting `app.tenant_id`
-> RLS with `FORCE ROW LEVEL SECURITY` + a default-deny policy;
connection pool refuses to hand out a connection until the GUC
is set; DB user is NOT the table owner (owners bypass RLS).
T a caller passes a `tenant_id` in the request body and a service
trusts it -> tenant context derived ONLY from the verified token
claim, never from the body; body `tenant_id` is rejected if present.
I cross-tenant read via a missing `WHERE tenant_id` -> RLS makes the
WHERE implicit and mandatory; plus an integration test suite that
runs every query "as tenant B" and asserts zero rows.
I admin tooling / a DBA runs `SELECT * FROM orders` -> RLS is
FORCEd even for the admin role; break-glass DBA access is JIT,
logged, and alerts (Ch 49); PII columns are ciphertext without the
per-tenant DEK.
R "tenant B's data leaked and we can't tell who accessed it" ->
DB audit (pgAudit) + data-access logging keyed by tenant and
principal; canary rows per tenant.
D one tenant's query load starves others -> per-tenant statement
timeout, connection quota, and a slow-query circuit breaker (Ch 28).
E SQL injection -> parameterised queries (G1 Ch 42) AND RLS as the
backstop: even a successful injection is tenant-scoped.
ATTACK TREE for goal "read Tenant B's invoices as Tenant A":
OR
- bypass RLS: be the table owner / a superuser / a role without
FORCE -> mitigated (dedicated non-owner role, FORCE RLS)
- poison tenant context: get the pool to set app.tenant_id=B
-> mitigated (context from signed claim only; pool asserts)
- go around the DB: read the OpenSearch index for B
-> mitigated (per-tenant index + document-level security +
the search client scopes by the same claim)
- go around the app: exfil a DB snapshot / backup
-> Part 7: backups envelope-encrypted; snapshot sharing blocked
by SCP; restore requires the security account
- steal Tenant B's own credentials -> out of scope for this model
(covered by the auth threat model), noted as a dependency.
ACCEPTED RISK: a compromise of the connection-pool service itself could
set an arbitrary GUC. Owner: platform lead. Mitigation roadmap: move
tenant assertion into a sidecar that the pool can't override; review
2026-Q1.
That model produces: RLS with FORCE + non-owner role, context-from-claim-only, the "as tenant B" test suite, pgAudit, per-tenant DEKs, per-tenant search indices, and two tracked issues. It took ninety minutes and it's in the repo.
Real-world scenario: Microsoft's Security Development Lifecycle
STRIDE (the model this chapter builds on throughout) was DEVELOPED
AT MICROSOFT as part of their Security Development Lifecycle (SDL),
introduced in 2004 after a period of severe, repeated worm outbreaks
(Code Red, Nimda, Blaster) against Windows and IIS. Microsoft has
published, over two decades, real before/after vulnerability data
from mandating SDL (including required threat modeling) across
Windows and Office development.
Two SDL details worth adopting directly, both aimed at exactly this
chapter's "make the cheap tier a genuine habit" problem:
- Microsoft built the "Elevation of Privilege" CARD GAME
specifically because engineers who found STRIDE's category list
dry or abstract would skip threat modeling entirely -- turning
the same six letters into a structured, playable exercise
measurably increased real adoption across teams who wouldn't
otherwise have done it.
- SDL made threat modeling a REQUIRED GATE before a design review
could be signed off, not an optional best practice -- the same
"wire it into the process, don't rely on willingness" principle
this chapter applies to the Tier 0 design-doc prompt and the PR
checklist item.
The lesson generalises well past Microsoft: the threat-modeling
METHOD matters less than whether your organisation has actually made
it a habit that survives deadline pressure.
Practice (60-90 min)
- Add a Tier-0 "security notes" section to your team's design-doc / PR template with the three prompts. Use it on your next real change.
- Do a Tier-1 model of one real subsystem: DFD as code, STRIDE per element, an attack tree for the single worst attacker goal, threats as tracked issues, accepted risks with owners and expiries.
- If it involves personal data, run LINDDUN over the same DFD and compare what it surfaces that STRIDE didn't.
- Put the artefact in the repo. Add a PR checklist item: "does this change the threat model?"
- Complete SecureShop's full model (extend the example) — you'll use it in every Part and in Project 1.
Common mistakes
- One tier for everything. A config tweak doesn't need PASTA; a new tenancy model needs more than a whiteboard.
- The artefact rots. A threat model not in the repo, not reviewed in PRs, is archaeology within a quarter.
- Threats that aren't tracked. If they're not issues with owners, they don't get fixed.
- Verbal risk acceptance. Unwritten = an unfixed bug with deniability.
- STRIDE everywhere. For graph-shaped systems, attack paths (Ch 1) often beat STRIDE. For privacy, you need LINDDUN. Match method to system.
Check yourself
- Describe the four tiers of threat-modeling practice and what triggers each.
- When does an attack tree earn its cost over plain STRIDE? When attack paths?
- What must be true of a threat model for it to stay useful over a year?
- What does LINDDUN cover that STRIDE does not?
- In the worked example, name three independent layers stopping a cross-tenant read.
Further reading
- Book: Threat Modeling: Designing for Security, Adam Shostack; and the
Threat Modeling Manifesto (
threatmodelingmanifesto.org). - Method: Risk Centric Threat Modeling (PASTA), Tony UcedaVelez &
Marco Morana. LINDDUN (
linddun.org) with its card deck. - Tools: OWASP
pytm, Threagile, OWASP Threat Dragon, IriusRisk (commercial, scales the practice). - Talk: "Threat Modeling at the speed of DevOps" and Shostack's "Fast, Cheap and Good" threat-modeling talks.
End of Part 1 — Milestone check
- I can model a system as an attack-path graph and identify its chokepoints
- I prioritise by path risk (including the detection term), not per-finding severity
- I have run adversary-emulation techniques and filled a prevent/detect/log/miss grid
- I can do an assume-breach analysis of a component and state its blast radius in one sentence
- I have a tiered threat-modeling practice and a living model in the repo
- I have completed SecureShop's cloud-native threat model