Security Multi-Cloud

Practical Threat Modeling: STRIDE, Data-Flow Diagrams, and Attack Trees for Real Systems

Most “threat modeling” I see in the wild is a one-off whiteboard session that produces a photo nobody looks at again. That is not threat modeling; it is theater. Done properly, threat modeling is the cheapest security control you own: it finds design flaws before a single line of code is written, when the cost of changing your mind is a sticky note rather than a re-architecture. The method below is the one I run with platform and application teams. It is deliberately mechanical so a mid-level engineer can drive it without a security PhD, and it produces artifacts you can diff, track, and re-run when the system changes.

The whole exercise hangs on four questions — the frame Adam Shostack popularized: What are we building? What can go wrong? What are we going to do about it? Did we do a good enough job? Every technique in this article slots into exactly one of those. Data-flow diagrams answer the first. STRIDE and attack trees answer the second. Risk rating and the mitigation catalog answer the third. Coverage checks and tests answer the fourth. If a “threat modeling” activity does not advance one of those four questions, it is ceremony, and you should cut it.

By the end you will be able to take an architecture you have never seen, decompose it into a diagram that is actually worth analyzing, enumerate its threats exhaustively rather than by intuition, rank them with a scheme that survives an audit, map each one to a concrete testable control, and wire the whole thing into your delivery pipeline so it stays alive. We finish with a full worked example — a three-tier web app modeled end to end, DFD to ranked threats to mitigations — because the method only clicks when you watch it grind through a real system.

What problem this solves

The economic argument is the entire case. A SQL injection caught in a design review is a one-line note: “parameterize this query.” The same flaw caught in penetration testing is a bug ticket, a code review, a redeploy, and a regression test. Caught in production it is an incident, a forensics bill, a customer-notification letter, and possibly a regulator. Studies that get quoted to death (IBM’s System Science Institute, the NIST cost-of-defect curves) all say the same directional thing: the cost of fixing a defect climbs by roughly an order of magnitude at each stage it survives — requirements, design, code, test, production. Threat modeling is the one activity that operates at the design stage, where the multiplier is smallest.

What breaks without it is subtle. Teams do not skip security entirely; they buy scanners. But a SAST or DAST tool finds implementation bugs — the injection, the missing output encoding, the hardcoded secret. It cannot find design flaws: the microservice that trusts a header any client can set, the object store whose IAM role reads every tenant’s data, the reset-password flow that lets you enumerate accounts, the queue that no one authenticates to. Roughly half of real-world security defects are design flaws, not code bugs, and no amount of scanning surfaces them because the code is doing exactly what the flawed design told it to. Threat modeling is the only widely practiced technique that catches that class.

Who hits this: everyone building anything that stores, moves, or trusts data across a boundary. It bites hardest on multi-tenant SaaS (cross-tenant isolation is a design property, not a library), on systems integrating third parties (every integration is a new trust boundary), on anything handling regulated data (PII, PCI, PHI, where a design flaw is a reportable breach), and on fast-moving teams who ship features weekly and never revisit the assumptions the original design baked in.

To frame the field before the deep dive, here is the whole method on one line per phase, mapped to Shostack’s four questions and to the artifact each produces:

Phase Shostack question Technique Artifact it produces Lives in
Scope & model What are we building? Data-flow diagram with trust boundaries DFD (as text/code) Repo, next to the code
Enumerate What can go wrong? STRIDE per element + attack trees Threat list; goal trees threats/*.yaml, attack-trees/*.txt
Decide What do we do about it? Risk rating + response choice Ranked, owned threats Same YAML, status/owner fields
Trace (still: what do we do) Mitigation → control mapping Control catalog + tests controls/*.yaml, tests/security/
Validate Did we do well enough? Coverage & orphan checks in CI Green build or a failing gate CI pipeline

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should be comfortable reading an architecture diagram, know the basic security properties (authentication, authorization, confidentiality, integrity, availability, non-repudiation), and have deployed at least one real system with a database, an API, and some external dependency. Familiarity with cloud primitives — IAM roles, object storage, load balancers, queues, secrets managers — makes the examples land, since most of them are drawn from AWS/Azure/GCP designs. No prior security-team experience is assumed; the entire point of the method is that a developer can run it.

This sits at the front of the secure-SDLC. Threat modeling is upstream of everything: it defines the security requirements that your code, your tests, and your scanners then enforce. It pairs directly with Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars, which supplies the control patterns your mitigations reach for, and with Shift-Left Testing and Quality Gates in CI/CD, which is the pipeline machinery that keeps the model honest. When a threat you find turns real, Engineering Incident Response: Runbooks, Tabletop Exercises, and Cloud Forensics is the downstream discipline that handles it. Where threat modeling ends — a ranked list of design risks — is exactly where your control catalog and your test suite begin.

Here is who typically owns what during a modeling session, so you invite the right people and do not stall waiting on the wrong one:

Role What they bring to the model Without them you miss
Application engineer How the code actually flows, real dependencies Hidden flows, undocumented integrations
Architect / tech lead Trust boundaries, intended design The “why” behind a boundary; over-modeling
Security engineer Threat intuition, control catalog, ratings Realistic attacker paths; consistent scoring
Product owner Data classification, business impact Correct impact ratings; which threats to accept
SRE / platform Deployment topology, IAM, network reality Infra-level threats (SSRF, over-broad roles)
QA / test What is testable, existing coverage Whether a control is actually verified

Core concepts

Six ideas carry the entire method. Get these and the rest is mechanics.

Threat modeling is structured anticipation, not prediction. You are not guessing which CVE will drop next year. You are reasoning about your own design’s assumptions — every place you decided to trust something — and asking what happens when that trust is misplaced. The output is a list of things that could go wrong given how you built it, ranked so you fix the ones that matter.

A threat is not a vulnerability, and neither is a risk. A threat is a potential negative event (“an attacker reads another tenant’s invoices”). A vulnerability is the specific weakness that makes the threat possible (“the object key is client-supplied and unvalidated”). A risk is the threat weighted by likelihood and impact (“high — trivially exploitable, exposes regulated PII”). A control (or mitigation) is what you add to reduce the risk (“derive the key prefix server-side from the token”). Keeping these four words distinct stops most of the confused arguments in a modeling session.

STRIDE is a mnemonic that makes enumeration exhaustive by construction. Each letter is the negation of one security property, so walking all six against an element guarantees you considered every property that can be violated. It was invented at Microsoft in 1999 by Loren Kohnfelder and Praerit Garg, and it is the closest thing the field has to a checklist that does not miss whole categories:

STRIDE Threat Security property it violates One-line example
S Spoofing Authentication Forge a JWT and act as another user
T Tampering Integrity Modify a price field in transit
R Repudiation Non-repudiation Delete an action with no audit trail
I Information disclosure Confidentiality Read another tenant’s data via IDOR
D Denial of service Availability Zip-bomb the file converter
E Elevation of privilege Authorization Escape a sandbox to run as root

A data-flow diagram is the substrate everything runs on. You cannot enumerate threats against a system you cannot see. The DFD is a deliberately impoverished picture — four element types and a boundary line — because the constraint forces you to represent how data moves rather than how the code is organized. Threats live on the diagram, specifically on the boundaries.

Trust boundaries are where the threats cluster. A trust boundary is any line across which the privilege or trust level changes: internet to your edge, DMZ to app tier, app to database, tenant A to tenant B, your process to a third-party API. An attacker’s input meeting your assumptions happens at these lines. If you only had time to model the boundary crossings and nothing else, you would still catch most of the serious flaws.

The model must be an asset, not an event. The single biggest failure mode is a beautiful model produced once and never touched again. A model that lives in the repo as text, gets diffed in pull requests, and is gated in CI is worth ten whiteboard photos. Everything below is designed to keep the model diffable and re-runnable.

Pin the vocabulary side by side before the deep sections — the glossary repeats these for lookup:

Term One-line definition Why it matters
Asset Something worth protecting (data, function, reputation) Threats target assets; impact is measured in asset terms
Threat A potential negative event against an asset The thing STRIDE enumerates
Vulnerability A weakness that enables a threat What a pen test finds; what you patch
Attack / exploit The act of realizing a threat via a vulnerability What the attack tree’s leaves describe
Trust boundary A line where trust/privilege level changes Where threats concentrate
Control / mitigation A measure that reduces a risk What every open threat must trace to
Risk Threat weighted by likelihood x impact What you rank and prioritize by
Attack surface The sum of points an attacker can reach Shrinking it eliminates threats outright
DFD Data-flow diagram: elements + flows + boundaries The model you analyze
Actor / entity A person or system outside your control A source of untrusted input

Drawing a data-flow diagram worth modeling

The DFD uses exactly four element types, and the discipline is to use only these. Anything you want to draw is one of them:

Element Classic shape Meaning Examples
External entity Rectangle An actor or system outside your control End user, admin, third-party API, another team’s service, a mobile app
Process Circle Code that transforms data A microservice, a Lambda, a cron job, a browser SPA, an nginx that does auth
Data store Two parallel lines Where data rests RDBMS, S3 bucket, Redis, a queue, a log, a config file
Data flow Directed arrow Data in motion between the above An HTTPS request, a SQL query, a Kafka message, a file read

The fifth thing on the page — and the most important — is the trust boundary, drawn as a dashed line that cuts across flows. When a flow crosses a boundary, you are handing data from one trust level to another, and that is where you owe an analysis. Common boundaries you should always look for:

Trust boundary Where it appears The trust that changes Threats it invites
Internet edge Client to your WAF/LB/CDN Anonymous public to your perimeter Spoofing, DoS, injection
Process boundary One service to another One codebase’s assumptions to another’s Tampering, spoofing (unauth internal calls)
Machine/network boundary App subnet to data subnet Compute tier to data tier Lateral movement, info disclosure
Tenant boundary Tenant A’s data to tenant B One customer to another IDOR, cross-tenant disclosure (I, E)
Third-party boundary Your app to Stripe/Twilio/etc. Your control to a vendor’s Repudiation, tampering, supply-chain
Privilege boundary User context to admin/root Low privilege to high Elevation of privilege
Kernel/sandbox boundary Container to host Sandboxed to unsandboxed E (container escape), RCE blast radius

DFDs come in levels, and knowing when to stop drilling is the skill that separates a useful model from a two-week time sink:

Level Name What it shows When it is enough
Level 0 Context diagram One process (the whole system), external entities, major data stores Always start here; often enough for a small service
Level 1 High-level The system decomposed into its major processes and stores The default working level for most systems
Level 2 Detailed One process expanded into its sub-processes Only where new threats emerge on drill-down
Level 3+ Low-level Function-level detail Almost never; you are over-modeling

The rule that saves weeks: decompose only until the threats stop changing. If expanding a process reveals no new trust boundary and no new class of threat, you have gone too deep. Conversely, if a “simple” process actually talks to three data stores across two boundaries, it deserves a Level 2.

Keep the DFD as text so it diffs. A picture in a wiki rots; a text model in the repo gets reviewed in pull requests like code. I use pytm (Python) or a Threat Dragon JSON model; here is a minimal DFD-as-code for a document-upload service that we will reuse:

# tm_upload.py  — run: python tm_upload.py --dfd | dot -Tpng -o dfd.png
from pytm import TM, Server, Datastore, Actor, Boundary, Dataflow

tm = TM("Document Upload Service")
internet = Boundary("Internet")
vpc      = Boundary("VPC / private")

user = Actor("Browser user")
waf  = Server("CDN / WAF");            waf.inBoundary = internet
api  = Server("Upload API");           api.inBoundary = vpc
conv = Server("Converter (untrusted input)"); conv.inBoundary = vpc
s3   = Datastore("Object store (S3)"); s3.inBoundary = vpc
db   = Datastore("Metadata DB");       db.inBoundary = vpc
q    = Datastore("Convert queue");     q.inBoundary = vpc

Dataflow(user, waf, "HTTPS multipart upload")
Dataflow(waf, api,  "Forwarded request + auth token")
Dataflow(api, s3,   "PUT object (SSE-KMS)")
Dataflow(api, db,   "INSERT metadata")
Dataflow(api, q,    "Enqueue {objectKey}")
Dataflow(q,  conv,  "Dequeue job")
Dataflow(conv, s3,  "GET object / PUT rendered")
Dataflow(conv, db,  "UPDATE status")

tm.process()

Two labeling rules that pay off later. First, every flow that crosses a trust boundary carries a label describing protocol, authentication, and data sensitivity — HTTPS + OIDC token, PII not just an arrow. Unlabeled crossings are where teams hand-wave, and hand-waving is where breaches hide. Second, name your data. “Data” on an arrow tells you nothing; “customer PAN (card number)” tells you the impact rating before you even run STRIDE.

STRIDE per element: the complete grid

STRIDE’s superpower is applied through STRIDE-per-element: walk every element in the DFD, and for each, ask which STRIDE categories can apply to that element type. Not every threat applies to every element — an external entity you do not control cannot suffer your denial of service in a way you model, a data flow cannot itself be spoofed. Microsoft’s canonical mapping is the grid you start from:

Element type S T R I D E
External entity (actor) yes yes
Process yes yes yes yes yes yes
Data store yes yes* yes yes
Data flow yes yes yes

(*Repudiation against a data store applies specifically when it is a log or audit store — someone tampering with the evidence.)

Read the grid as a worklist, not a truth. A process is the richest target: all six apply, because code authenticates callers (S), can be fed bad input (T), records actions ®, holds secrets (I), can be exhausted (D), and runs with privilege (E). A data flow is narrower: it can be tampered or read in transit (T, I) or flooded (D), but it does not authenticate and does not run code. The discipline is to walk each element and, for every applicable cell, either write a concrete threat or explicitly mark it “N/A because…”. An empty cell you reasoned about is worlds apart from one you never saw.

The value is in the concreteness. “Spoofing on the API” is useless. Here is the per-cell catalog I use to force specificity — element type, STRIDE letter, a concrete threat phrased as an attacker would, and the control family it points to:

Element STRIDE Concrete threat (say it like an attacker) Control family
External entity S “I phish a user’s credentials and log in as them” Strong auth, MFA, phishing-resistant creds
External entity R “I place an order then deny I ever did” Signed receipts, audit log of principal + action
Process S “I forge/replay a token and the service accepts it” Validate signature, audience, expiry; mTLS
Process T “I send ../ in a path and write outside my prefix” Input validation, canonicalization, allow-lists
Process R “I trigger an admin action that leaves no trace” Structured audit logging with actor identity
Process I “I request another tenant’s ID and read their data” Server-side authorization on every object
Process D “I send a 10 GB body and pin the worker’s CPU” Size limits, timeouts, rate limits, quotas
Process E “A crafted file triggers RCE and I run as the role” Sandboxing, least-privilege role, drop root
Data store T “I edit rows directly via leaked DB creds” Least-privilege DB users, integrity checks
Data store R “I delete log entries to cover the intrusion” Append-only/WORM logs, off-box shipping
Data store I “I read an unencrypted backup from the bucket” Encryption at rest, bucket policy, KMS
Data store D “I fill the disk/table to take the store down” Quotas, autoscaling storage, throttling
Data flow T “I MITM the internal call and change the amount” TLS everywhere, HMAC/signing, mutual TLS
Data flow I “I sniff an unencrypted internal flow for tokens” Encrypt in transit even inside the VPC
Data flow D “I flood the flow/connection pool to starve it” Connection limits, backpressure, circuit breakers

Two refinements worth knowing. STRIDE-per-interaction is the alternative the newer Microsoft tooling favors: instead of walking elements, you walk each (source, flow, destination) tuple and ask STRIDE of the interaction. It generates more threats and fewer false "N/A"s, at the cost of a longer list — good for high-assurance systems, overkill for a small service. And LINDDUN is STRIDE’s privacy-focused sibling (Linkability, Identifiability, Non-repudiation, Detectability, Disclosure, Unawareness, Non-compliance) — reach for it when the system’s main risk is privacy harm rather than classic security, or run both.

Capture threats in a structured, diffable format that lives in the same pull request as the code:

# threats/upload-api.yaml
- id: TM-UPLOAD-007
  element: "Upload API (process)"
  stride: I               # Information disclosure
  interaction: "Upload API -> Metadata DB"
  title: "IDOR allows cross-tenant metadata read"
  description: >
    objectKey is client-supplied and used directly in the metadata
    lookup. A user can request another tenant's key and read status,
    filename, and size.
  likelihood: high         # trivially reproducible, no special access
  impact: high             # exposes regulated PII across tenants
  risk: critical
  response: mitigate       # mitigate | eliminate | transfer | accept
  status: open             # open | mitigated | accepted | transferred
  mitigation_ref: CTRL-AUTHZ-OBJ-SCOPE
  owner: platform-security

Attack trees: reasoning from the adversary’s goal

STRIDE enumerates bottom-up, element by element. Attack trees reason top-down from an adversary’s goal, and the two are complementary: STRIDE finds the threats, attack trees show you the paths to a goal and therefore where one control breaks many paths at once. Reach for an attack tree when a threat is serious enough that you need to understand the full set of ways to reach it — usually your top three or four risks, not all sixty.

The structure is a tree of goals. The root is the attacker’s objective. Each node is refined into children joined by boolean logic:

Node type Semantics How to read it How to defend
OR node Any one child suffices Attacker picks the easiest path You must block every child
AND node All children required together Attacker needs the whole set Break any one child to stop the path
Leaf A concrete, atomic action The thing you actually mitigate Attach a control here

That asymmetry is the whole point. Under an OR, the defender must cover all branches — the attacker only needs one. Under an AND, the defender is lucky: killing a single child collapses the branch. So when you build the tree, you are hunting for AND nodes where one cheap control breaks the chain. Here is the upload service’s top goal as a text tree (text, because it diffs and needs no tool):

GOAL: Exfiltrate another tenant's documents
|
+-- OR 1. Read objects directly from the store
|     +-- AND
|         +-- 1a. Obtain the object key
|         |        (OR: IDOR on API; guess UUID; find it in a leaked log)
|         +-- 1b. Bypass store authorization
|                  (OR: over-permissive bucket policy; stolen IAM creds;
|                       SSRF to the instance metadata service)
|
+-- OR 2. Abuse the converter
|     +-- AND
|         +-- 2a. Upload a malicious file that triggers RCE in the converter
|         +-- 2b. Converter IAM role can read all tenants' objects
|
+-- OR 3. Compromise the metadata DB
      +-- OR
          +-- 3a. SQL injection on a query path
          +-- 3b. Stolen DB credential from env / secrets store

Reading it pays off immediately. Notice that “converter IAM role can read all tenants’ objects” (2b) is an AND-child on path 2 — and it is also, in effect, the enabling condition behind the “bypass store authorization” branch of path 1. Scoping that role to a single per-job tenant prefix breaks the AND on path 2 and removes a major OR-child on path 1. That is a single high-leverage control the per-element STRIDE pass would not have highlighted, because STRIDE looks at one element at a time while the tree shows you the chain.

The second job of the tree is pruning by feasibility. Not every leaf is worth defending equally; annotate each with the attacker cost, skill, and detectability, then prune paths that are absurd for your threat model:

Attribute Cheap / easy (prioritize) Expensive / hard (deprioritize)
Cost Free (a curl loop) Requires buying a 0-day
Skill Script-kiddie / OWASP Top 10 Nation-state cryptanalysis
Access needed Anonymous internet Physical datacenter access
Time Seconds, automatable Months of patient effort
Detectability Silent, no logs tripped Trips every alert you have

You can carry a single value per leaf — the classic is cheapest cost to achieve, propagated up the tree (an OR node takes the minimum of its children, an AND node the sum). The cost at the root is your cheapest total attack path, and the leaf that dominates it is where your next control belongs. For most commercial systems you prune anything that needs a 0-day or physical access and focus on the internet-reachable, low-skill, low-cost leaves — because that is who actually shows up. A model that spends its energy defending against a nation-state while leaving an IDOR open has its priorities inverted.

Rating and prioritizing threats

A list of sixty threats with no priority is noise. You need a consistent score so the riskiest items rise and the team fixes them in the right order. Three schemes dominate, and choosing well matters.

DREAD, and why it is deprecated

DREAD scores each threat 1–3 (or 1–10) across five dimensions and averages them:

DREAD dimension The question The problem with it
Damage How bad if exploited? The one dimension that is genuinely useful
Reproducibility How reliably can it be repeated? Overlaps heavily with Exploitability
Exploitability How hard to pull off? Subjective; varies wildly by rater
Affected users How many are hit? Often just a proxy for Damage
Discoverability How easy to find the flaw? Rewards security by obscurity

Microsoft themselves deprecated DREAD because it fails in practice. The averaging hides high-damage/low-everything-else threats (a catastrophic but “hard to find” flaw scores medium). The dimensions overlap and are not independent. And the Discoverability axis actively encourages the worst instinct in security — assuming attackers will not find something because it is hidden. If you inherit a DREAD process, at minimum drop Discoverability and pin down a written rubric so “Damage = 3” means the same thing to every rater. But prefer to move off it.

CVSS, and where it fits

The Common Vulnerability Scoring System is the industry standard for scoring known vulnerabilities (it is what every CVE carries). CVSS 4.0 (released late 2023) computes a 0–10 base score from metrics that describe the vulnerability’s intrinsic character:

CVSS 4.0 base metric Values What it captures
Attack Vector (AV) Network / Adjacent / Local / Physical How remote the attacker can be
Attack Complexity (AC) Low / High Conditions outside attacker control
Attack Requirements (AT) None / Present Preconditions the attack needs
Privileges Required (PR) None / Low / High Access the attacker must already have
User Interaction (UI) None / Passive / Active Whether a victim must act
Vuln. Confidentiality/Integrity/Availability (VC/VI/VA) High / Low / None Impact on the vulnerable system
Subsequent Conf./Integrity/Avail. (SC/SI/SA) High / Low / None Impact beyond it (replaces old “Scope”)

CVSS is precise and comparable across organizations, which is exactly why it is the wrong tool for design-stage threat modeling. It scores a concrete vulnerability with a known exploit path; a threat is a potential flaw you have not built yet. Use CVSS when a threat model finding maps to a real CVE in a dependency, or to communicate severity to teams that already speak CVSS. Do not contort your abstract design threats into a CVSS vector — you will invent the metric values, and invented precision is worse than honest coarseness.

Likelihood x impact — the default

For design threats, a calibrated likelihood x impact matrix is almost always the right choice. It speaks the language your risk register and your leadership already use, it avoids DREAD’s pseudo-precision, and it forces the two questions that actually matter — how easy is this, and how bad is it:

Likelihood \ Impact Low Medium High
High Medium High Critical
Medium Low Medium High
Low Low Low Medium

The trick that makes it defensible is anchoring each axis with a written rubric so two engineers rate the same threat the same way:

Rating Likelihood anchor Impact anchor
High Anonymous internet, automatable, no special skill Regulated data breach, funds loss, full compromise
Medium Needs some access or moderate skill Single-tenant exposure, degraded service
Low Needs insider access or a chained 0-day Minor info leak, cosmetic, self-DoS only

Whichever scheme you pick, three rules hold: be consistent within a model, record the rationale next to the score, and remember the score is an input to a decision, not the decision. Here is how the three compare so you can choose deliberately:

Scheme Best for Strength Weakness
DREAD (legacy only) Simple to teach Subjective; deprecated; obscurity bias
CVSS 4.0 Known CVEs, dependency findings Standard, comparable, precise Wrong altitude for design threats
Likelihood x impact Design-stage threats (default) Business-aligned, fast, defensible Only as good as your rubric

From threats to mitigations

This is where threat modeling earns its keep — a threat you rate but never fix is just anxiety. Every rated threat gets exactly one of four responses, chosen explicitly:

Response What it means When to use it The catch
Mitigate Add a control that cuts likelihood or impact The default for High/Critical Costs engineering effort; must be tested
Eliminate Remove the feature/flow that creates the threat When the risk outweighs the feature Sometimes the feature is the point
Transfer Push the risk elsewhere (managed service, insurance, contract) When someone else does it better/cheaper You still own the residual and the vendor risk
Accept Sign off and live with it Low risk, or cost of fix > cost of loss Requires a named owner and an expiry date

“Accept” without a name and a date is just “ignore,” and it is how findings quietly die. Make acceptance auditable: a person, a reason, and a review date, recorded in the same YAML as the threat.

When you choose mitigate, STRIDE hands you the control family for free — each letter maps to a well-known class of defense. This is the mitigation catalog, the single most useful reference in the whole practice:

STRIDE Property to restore Mitigation family Concrete controls (cloud reality)
S Spoofing Authentication Prove identity MFA, phishing-resistant passkeys, OIDC/SAML, mTLS, signed tokens with audience+expiry checks, managed identities
T Tampering Integrity Detect/prevent change TLS in transit, HMAC/digital signatures, hashes, input validation, WORM storage, DB constraints, least-privilege writes
R Repudiation Non-repudiation Prove who did what Structured audit logs with actor identity, append-only/immutable logs shipped off-box, signed receipts, timestamps
I Info disclosure Confidentiality Keep secrets secret Encryption at rest (KMS) and in transit, least-privilege authz on every object, tokenization/masking, private networking
D Denial of service Availability Keep it up Rate limiting, quotas, size/timeout limits, autoscaling, CDN/edge caching, WAF, circuit breakers, backpressure
E Elevation of privilege Authorization Contain privilege RBAC/ABAC, least-privilege IAM roles, input validation, sandboxing, drop root, network segmentation, JIT access

The output is traceability: keep a control catalog and reference each control by ID so one control can cover many threats, and every threat points back to a testable assertion:

# controls/catalog.yaml
- id: CTRL-AUTHZ-OBJ-SCOPE
  title: "Per-tenant object authorization on every store access"
  detail: >
    Resolve the caller's tenant from the validated token, derive the
    allowed key prefix server-side, and reject any objectKey outside it.
    Never trust a client-supplied key for authorization.
  verifies: [TM-UPLOAD-007]
  test: "tests/security/test_cross_tenant.py::test_idor_returns_403"
  owner: platform-security
  status: implemented

For the upload service, the STRIDE pass and the attack tree converge on a short, high-value control set — and the scoped-role control is worth showing in real IaC, because “least privilege” is a slogan until it is an IAM condition. The policy lets the converter role touch only keys under its job’s tenant prefix, resolved from a principal tag:

data "aws_iam_policy_document" "converter_scoped" {
  statement {
    sid       = "ReadWriteTenantPrefixOnly"
    effect    = "Allow"
    actions   = ["s3:GetObject", "s3:PutObject"]
    # $${aws:PrincipalTag/tenant} is resolved at request time from the
    # session tag set per job -- the role cannot read another prefix.
    resources = ["${aws_s3_bucket.docs.arn}/tenants/$${aws:PrincipalTag/tenant}/*"]
  }
}

The point is the chain: open the threat, follow mitigation_ref to the control, follow the control’s test to the assertion that proves it still works. No orphan threats, no orphan controls.

Threat modeling in Agile and DevSecOps

The classic objection is “we ship every week, we cannot do a two-day modeling workshop per feature.” Correct — so you do not. You right-size the model to the change, and you split system-level from story-level work:

Cadence Scope Effort Who runs it Trigger
System-level model The whole service / bounded context Half a day to two days Architect + security + team New system; major re-architecture; annual refresh
Feature/epic model One new capability that adds flows 1–2 hours The feature team New trust boundary or data class in an epic
Story-level check One user story 5–15 min, in refinement The developer Every story, as a checklist
Delta model An incremental change to an existing model 15–30 min Whoever makes the change Any PR that changes a flow or boundary

The story-level check is the one that scales, because it is lightweight enough to survive contact with a sprint. Bake it into your “definition of ready” or “definition of done” as a handful of questions the developer answers themselves:

Story-level question If “yes”, do this
Does this story add a new data flow across a trust boundary? Add it to the DFD; run STRIDE on the crossing
Does it handle a new class of data (PII, secrets, money)? Re-check confidentiality + integrity threats
Does it add authentication or authorization logic? Model spoofing + elevation explicitly
Does it call a new external service? New third-party boundary; model R + T
Does it accept file uploads or untrusted input? Model tampering + DoS + E (RCE)
None of the above? No model needed; note “no security-relevant change”

The decisive move is keeping the model alive by making it code, not a document. Wire three gates:

  1. Design-review gate. Every RFC for a system that crosses a trust boundary must link a threat model before approval — a checklist item in the RFC template, not an honor system.
  2. PR gate for the model itself. The threat YAML and DFD live in the repo. A CI check fails the build if a threat is open with no mitigation_ref, or a control references a threat ID that does not exist:
# .github/workflows/threat-model.yml
name: threat-model-lint
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate threat models
        run: |
          pip install pyyaml
          python scripts/check_threats.py   # fails on open threats w/o mitigation_ref
          python scripts/check_coverage.py --dfd tm_upload.py --threats threats/
  1. Right-sized cadence enforced by the trigger list. A full pass for greenfield; a delta model for incremental change. Calibrate effort to the size of the architectural change, or teams route around the process — and a process people route around is worse than none, because it produces false assurance.

This is the same shift-left philosophy that governs testing; if your pipeline already runs Shift-Left Testing and Quality Gates in CI/CD, the threat-model gate slots in beside your existing quality gates rather than as a separate ceremony.

Tooling: from whiteboard to threat-model-as-code

You can threat-model with a whiteboard and a text file, and for a first pass you should — the tool is never the point. But tooling helps at scale, and the field now has genuinely good options across the spectrum from GUI to code:

Tool Type Model format STRIDE support Cost Best for
OWASP Threat Dragon Web + desktop (Electron) JSON (diffable) STRIDE + LINDDUN Free, OSS Teams wanting a GUI whose model still lives in git
Microsoft Threat Modeling Tool Windows desktop .tm7 (XML) Auto-generates STRIDE from DFD Free Azure shops; auto-suggested threats
pytm Python library Python code STRIDE via threat library Free, OSS Threat-model-as-code; CI integration
Threagile Go, YAML-driven YAML model Rule-based risk engine Free, OSS Agile-as-code; rich auto-generated risk report
IriusRisk Commercial SaaS Proprietary + API Extensive, questionnaire-driven Paid Enterprises needing governance, scale, compliance mapping

The two worth knowing hands-on are the free auto-generators. Microsoft’s Threat Modeling Tool ingests a DFD you draw and generates candidate STRIDE threats per interaction, which is a fast way to avoid missing a category — though it over-generates and you must prune. OWASP Threat Dragon does the same in-browser, stores the model as JSON you commit, and supports both STRIDE and privacy-focused LINDDUN.

For engineering-led teams, threat-model-as-code is the endgame. pytm (by Izar Tarandach) lets you define the model in Python and generate a data-flow diagram, a sequence diagram, and a findings report from a built-in threat library:

pip install pytm
python tm_upload.py --dfd   | dot -Tpng -o dfd.png       # data-flow diagram
python tm_upload.py --seq   | java -jar plantuml.jar -pipe > seq.png
python tm_upload.py --report docs/report.md              # threats + findings

Threagile takes a YAML model and produces a full risk report (PDF, Excel, JSON) plus a generated DFD, running its risk rules over your described architecture — ideal when you want the whole thing to run in a container in CI:

# Threagile runs from a single YAML model and emits a risk report
docker run --rm -v "$PWD:/app/work" threagile/threagile \
  -verbose -model /app/work/threagile-model.yaml -output /app/work/out
# out/ now holds risks.xlsx, report.pdf, data-flow-diagram.png, risks.json

The right progression for most teams: start on a whiteboard to learn the muscle, move the model into Threat Dragon or a text file so it diffs, and adopt pytm/Threagile once threat modeling is a routine part of your pipeline rather than an event. Do not start with the heaviest tool — a beautiful IriusRisk instance that nobody updates is the same failure as a whiteboard photo, just more expensive.

Worked example: a three-tier web app, end to end

Enough theory. Model OrderFlow, a fictional but realistic multi-tenant e-commerce order service, from blank page to ranked, mitigated threats. This is the pattern you will reuse on your own systems.

Question 1 — what are we building?

OrderFlow is a classic three-tier web app on AWS. Customers browse and place orders; an admin console manages catalog and refunds; payments go to Stripe. The Level 1 DFD elements:

# Element Type Trust zone
E1 Customer (browser SPA) External entity Internet
E2 Admin user External entity Internet (privileged)
E3 Stripe payment API External entity Third-party
P1 ALB + WAF Process Edge (public subnet)
P2 Order API (ECS service) Process App subnet
P3 Auth service (OIDC) Process App subnet
P4 Fulfilment worker Process App subnet
D1 Orders DB (RDS PostgreSQL) Data store Data subnet
D2 Session cache (Redis) Data store Data subnet
D3 Invoice store (S3) Data store Data subnet
D4 Audit log (CloudWatch/S3) Data store Data subnet

The trust boundaries: Internet → Edge (at the ALB/WAF), Edge → App (public to private subnet), App → Data (app to data subnet), and App → Stripe (your control to a vendor). The flows: customer → WAF → Order API (HTTPS + OIDC token, contains order + PII); Order API → Auth service (validate token); Order API → Orders DB (SQL, PII + order); Order API → Redis (session); Order API → Stripe (HTTPS, card token — never raw PAN); Order API → SQS → Fulfilment worker; worker → S3 (invoice PDF); all processes → Audit log.

Question 2 — what can go wrong? (the STRIDE pass)

Walk each element against its applicable STRIDE cells. Here is the abbreviated pass — one representative threat per high-value cell, which in a real model would be the full grid:

ID Element STRIDE Threat L I
OF-01 E1 Customer S Stolen session/JWT lets attacker act as a customer M H
OF-02 E2 Admin S Phished admin creds grant refund + catalog control M H
OF-03 P2 Order API S Missing token audience check accepts tokens from another app L H
OF-04 P2 Order API T Mass-assignment lets client set price or tenantId in the body H H
OF-05 P2 Order API I IDOR: GET /orders/{id} returns another tenant’s order H H
OF-06 P2 Order API D Unbounded search query exhausts DB connections M M
OF-07 P2 Order API E SSRF in an image-URL field reaches the instance metadata service M H
OF-08 P3 Auth service S JWT signed with a weak/none alg is accepted L H
OF-09 P4 Worker E Malicious PDF triggers RCE in the render library L H
OF-10 Flow API→DB I Unencrypted DB connection sniffed inside the VPC L H
OF-11 D1 Orders DB I Unencrypted RDS snapshot shared/leaked L H
OF-12 D3 Invoice S3 I Public/over-broad bucket policy exposes invoices M H
OF-13 D4 Audit log R App role can delete audit entries to hide activity L H
OF-14 Flow API→Stripe T Webhook from “Stripe” is spoofed; signature unchecked M H
OF-15 E3 Stripe R Customer disputes a charge; no signed proof retained M M

Rank

Apply the likelihood x impact matrix. The ones that rise to Critical/High and get worked first:

ID L x I Risk Why it is top
OF-05 H x H Critical IDOR is trivially automatable; cross-tenant PII = reportable breach
OF-04 H x H Critical Mass-assignment on price/tenantId is silent fraud + tenant escape
OF-07 M x H High SSRF → metadata → IAM creds is a full-compromise chain
OF-02 M x H High Admin takeover controls money and catalog
OF-12 M x H High One bucket misconfig exposes every invoice
OF-14 M x H High Spoofed webhook can mark unpaid orders paid
OF-09 L x H Medium Serious impact but needs a render-lib exploit

Question 3 — what do we do about it? (threats → controls)

Each top threat gets a response and a control ID. Notice several threats share one control — that is the leverage attack trees promised:

ID Response Control What it is
OF-05 Mitigate CTRL-AUTHZ-ROW Row-level tenant check on every query; derive tenant from token, never from the request
OF-04 Mitigate CTRL-INPUT-ALLOWLIST Explicit DTO allow-list; server sets price/tenantId, ignores client values
OF-07 Mitigate CTRL-EGRESS-METADATA Block egress to 169.254.169.254 + IMDSv2 required; allow-list outbound hosts
OF-02 Mitigate CTRL-ADMIN-MFA Phishing-resistant MFA + PIM/JIT for admin actions
OF-12 Mitigate CTRL-S3-PRIVATE Block Public Access on; bucket policy least-privilege; access via presigned URLs
OF-14 Mitigate CTRL-WEBHOOK-SIG Verify Stripe webhook signature + timestamp before trusting it
OF-09 Transfer + Mitigate CTRL-SANDBOX-RENDER Render in a no-egress sandbox with a scoped role (blast radius = one job)
OF-13 Mitigate CTRL-WORM-AUDIT Append-only audit log shipped off-box; app role cannot delete
OF-11 Mitigate CTRL-ENCRYPT-REST KMS encryption on RDS + snapshots; snapshots not shared
OF-10 Mitigate CTRL-TLS-INTERNAL TLS on the DB connection even inside the VPC

Question 4 — did we do well enough?

Coverage check: every trust-boundary crossing produced at least one threat (the Internet→Edge, Edge→App, App→Data, App→Stripe crossings all have entries). Every Critical/High threat has a control with a test. Every AND node in the exfiltration attack tree is broken by at least one control. That is a model you can sign off — and re-run when OrderFlow adds its next feature.

Architecture at a glance

Picture the OrderFlow model as three horizontal bands separated by dashed trust-boundary lines, with data flowing top to bottom and the audit log running down the right margin catching everything.

The top band is the internet — two external entities, the customer’s browser SPA and the privileged admin console, both anonymous and untrusted until proven otherwise. They meet your system at a single dashed line: the Internet → Edge boundary, drawn across the flow into the ALB + WAF. That first line is where spoofing, injection, and volumetric DoS threats concentrate, which is why the WAF, TLS termination, and rate limiting all sit exactly on it.

The middle band is the app tier inside a private subnet, behind a second dashed line — the Edge → App boundary. Here live the three processes: the Order API that does the real work, the Auth service that validates every OIDC token crossing in, and the Fulfilment worker that consumes the queue. This band is where the six-way STRIDE richness applies, because these are processes: they authenticate callers (spoofing), accept input (tampering, SSRF-driven elevation), hold logic that can leak (disclosure), can be exhausted (DoS), and run with IAM roles worth stealing (elevation). To the right, a flow leaves the band across the App → Stripe third-party boundary — the only place your data crosses into a vendor’s control, and therefore where webhook-signature verification and non-repudiation live.

The bottom band is the data tier in its own subnet, behind the third dashed line — the App → Data boundary. Four data stores rest here: the Orders DB (PII, the crown jewels), the Redis session cache, the S3 invoice store, and the append-only audit log. Data stores get the narrow STRIDE set — tampering, disclosure, DoS, and for the audit store, repudiation — which is why the controls clustered here are encryption at rest, least-privilege database and bucket policies, and immutability on the log. Trace any customer request straight down through the three bands and you have walked every trust boundary in order; that top-to-bottom path is the threat model, and each dashed line you cross is a place you owed an analysis and paid it.

Real-world scenario

A fintech platform team I worked with ran a multi-tenant document service almost identical to OrderFlow’s invoice path — it processed KYC documents (passports, proof-of-address) for dozens of business customers. The original design used a Lambda converter running headless LibreOffice to render uploaded .docx/.xlsx files to PDF, with a single execution role that held s3:GetObject on the whole bucket. It passed code review cleanly, because “it is our own bucket, and Lambda is ephemeral.” No one had drawn a data-flow diagram.

The threat model changed the conversation in an afternoon. The STRIDE-per-element pass flagged Elevation of Privilege on the converter process — the E cell that every process owns. That alone was a shrug (“it is sandboxed”) until the attack tree made the chain undeniable. Root goal: exfiltrate another tenant’s KYC documents. Path: untrusted file in → LibreOffice and its font/image parsers have a documented history of memory-corruption RCE → converter achieves code execution → the converter’s role can read every tenant’s objects → one malicious upload equals full cross-tenant exposure of regulated PII. In their jurisdiction that is a reportable breach and a licence-threatening event. The tree turned an abstract “E on a process” into a single sentence the CTO understood in ten seconds.

The constraints were real: they could not drop LibreOffice (nothing else rendered their customers’ document zoo), and a per-tenant Lambda was operationally impossible at their tenant count. But the model had already told them where to cut the chain. It was an AND node — RCE and an over-broad role — so breaking either child stopped the path. They broke both. First, they scoped the converter’s role with an IAM PrincipalTag condition so each invocation could read only the one tenant prefix for its job, resolved from a session tag set per invocation:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "JobScopedRead",
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject"],
    "Resource": "arn:aws:s3:::kyc-docs/tenants/${aws:PrincipalTag/tenantId}/*"
  }]
}

Second, they moved the converter to a per-job execution context with no outbound network egress, so even a successful RCE could not exfiltrate or call home. Third — closing the DoS leaf the same pass had surfaced — they added a decompressed-size and CPU-time guard to kill zip-bomb inputs. The blast radius of the worst-case threat went from “all tenants” to “the single document already being processed for that tenant” — a residual the business could actually accept and sign off with a named owner. None of it required new tooling or a bigger security team. It required drawing the boundary, walking STRIDE against one process, and following one attack tree to its root. That is the entire return on the practice, in one story.

Advantages and disadvantages

Threat modeling is not free, and pretending otherwise is how it gets abandoned. The honest trade-off:

Advantages Disadvantages
Finds design flaws no scanner can (half of real defects) Takes senior time and cross-team coordination
Cheapest place to fix — design stage, before code Quality depends on the modelers’ skill and honesty
Produces security requirements, not just findings Can rot instantly into a stale document if not gated
Prioritizes work by real risk, not scanner noise Easy to over-model and burn weeks on Level-3 detail
Creates shared understanding across dev/sec/ops Analysis paralysis if you chase every theoretical threat
Diffable, auditable, re-runnable when done as code Hard to prove ROI — the breaches you prevented are invisible

The disadvantages are all process failures, not method failures, and every one has a countermeasure covered above: right-size the model (kills over-modeling), gate it in CI (kills rot), anchor the ratings (kills skill-dependence), and prune by feasibility (kills paralysis). Where it genuinely does not pay: a throwaway prototype, a system with no trust boundaries and no sensitive data, or a change that touches nothing security-relevant — which is exactly what the story-level checklist is for. The method’s overhead should scale with the risk, and when it does, it is the highest-leverage security activity a team can do.

Hands-on lab

Model a small service with pytm end to end, generate the diagram and report, and add a CI check — all free, on your laptop. Budget 30 minutes.

1. Set up.

mkdir tm-lab && cd tm-lab
python -m venv .venv && source .venv/bin/activate
pip install pytm
# pytm needs Graphviz (dot) for the DFD and Java+PlantUML for sequence diagrams
brew install graphviz          # or: apt-get install graphviz

2. Write the model as tm.py — a tiny login service with a user, an API, and a user DB across two boundaries:

from pytm import TM, Server, Datastore, Actor, Boundary, Dataflow, Classification

tm = TM("Login Service")
tm.description = "Lab model for the KloudVin threat-modeling article"

net = Boundary("Internet")
app = Boundary("App VPC")

user = Actor("User");              user.inBoundary = net
api  = Server("Login API");        api.inBoundary = app
db   = Datastore("User DB");       db.inBoundary = app
db.storesSensitiveData = True

f1 = Dataflow(user, api, "POST /login (email, password)")
f1.protocol = "HTTPS"; f1.dstPort = 443
f1.data = "credentials"; f1.classification = Classification.SECRET

f2 = Dataflow(api, db, "SELECT user WHERE email=?")
f2.protocol = "PostgreSQL"; f2.dstPort = 5432
f2.data = "password hash"; f2.classification = Classification.SECRET

if __name__ == "__main__":
    tm.process()

3. Generate the DFD and the threat report.

python tm.py --dfd | dot -Tpng -o dfd.png       # data-flow diagram
python tm.py --report /dev/stdin <<'EOF' > report.md
# Threat Report
{tm:hyperlinks}
## Threats
{threats:repeat:* {{item.description}}\n}
EOF
python tm.py --list                             # list findings pytm generated

Expected output. --list prints generated findings such as “Reading credentials from a flow without encryption” and “Datastore does not authenticate the caller” — pytm’s threat library matched them from your model. dfd.png shows the user in the Internet boundary, the API and DB in the App VPC, with the two labeled flows crossing the boundary.

4. Turn a finding into a tracked threat. Create threats/login.yaml:

- id: TM-LOGIN-01
  element: "Login API (process)"
  stride: S
  title: "No account-lockout enables credential stuffing"
  likelihood: high
  impact: high
  risk: critical
  response: mitigate
  status: open
  mitigation_ref: CTRL-RATELIMIT-LOGIN

5. Add the CI gate — a check that fails on any open threat with no mitigation_ref:

# scripts/check_threats.py
import sys, glob, yaml
bad = []
for path in glob.glob("threats/*.yaml"):
    for t in yaml.safe_load(open(path)) or []:
        if t.get("status") == "open" and not t.get("mitigation_ref"):
            bad.append(t["id"])
if bad:
    print("Open threats missing mitigation_ref:", ", ".join(bad)); sys.exit(1)
print("Threat model OK")
python scripts/check_threats.py        # exit 0 -> "Threat model OK"

6. Validate. Delete the mitigation_ref line from the YAML and re-run check_threats.py — it should exit non-zero and name TM-LOGIN-01. That failing exit is your pipeline gate working. Restore the line to go green.

7. Teardown. Nothing to tear down — it is all local files. deactivate && cd .. && rm -rf tm-lab. No cloud spend, no resources left running.

Common mistakes & troubleshooting (anti-patterns)

The method fails in predictable ways. Here is the anti-pattern playbook — the symptom you will observe, the root cause, how to confirm it, and the fix:

# Symptom Root cause Confirm Fix
1 Model produced once, never updated Treated as an event, not an asset Model file has one commit, months old Put it in the repo; gate in CI; delta-model every relevant PR
2 200 threats, none prioritized No rating scheme applied No risk field on any threat Apply likelihood x impact with a written rubric; sort
3 Threats are generic (“SQLi possible”) Skipped the DFD; modeled from imagination No DFD in the repo; threats not tied to elements Draw the DFD first; STRIDE per element
4 Modeling took two weeks Over-decomposed to Level 3 DFD has function-level detail Stop at the level where threats stop changing
5 Team defends against nation-states, ignores IDOR No feasibility pruning Attack tree leaves all rated “critical” Annotate cost/skill/access; prune the absurd
6 “Accept” everywhere, nothing fixed Acceptance is not auditable response: accept with no owner/date Require named owner + expiry on every accept
7 Same threat rated 3 and 9 by two people No calibrated rubric Compare two raters on one threat Anchor each axis with written definitions
8 Controls exist but regress silently Mitigations not tested No test maps to the control ID Every implemented control gets a failing-on-regression test
9 Trust boundary crossings have no threats Boundaries not drawn or ignored Count threats per crossing = 0 A clean crossing means you missed something, not that it is safe
10 DREAD scores feel arbitrary Discoverability + averaging distort Recompute without Discoverability Move to likelihood x impact or fix the rubric
11 Security team models in isolation, devs ignore it No developer ownership Devs cannot find or read the model Model with the team; keep it in their repo, in their PRs
12 Findings never become work No link to tickets/backlog Threats have no issue reference Auto-file High/Critical threats as tracked issues

The two that quietly kill the most programs are #1 (the model rots) and #8 (controls regress untested). Both are solved by the same principle: the model and its controls are code, gated in CI, or they are theater. If you take one thing from this article, take that.

Best practices

Security notes

Threat modeling is a security activity, but it has its own security posture worth naming. The threat model itself is sensitive: it is a map of your weaknesses, ranked by exploitability, with the accepted risks helpfully labeled. Store it in the same access-controlled repo as the code it describes, not a public wiki, and treat a leaked threat model as an incident — it hands an attacker your prioritized to-do list.

Apply least privilege to the modeling process the same way you apply it in the model: the CI job that lints threats needs read access to the repo and nothing else; it should not hold cloud credentials. When your mitigations reach for identity controls — and they will, since Spoofing and Elevation dominate most models — lean on the patterns in Zero Trust Architecture Blueprint: Identity, Network, and Data Pillars and Locking Down Workload Identities: Conditional Access, Risk Detection, and Going Secretless. When a model surfaces a secret-handling flaw (the R and I cells around config and credentials), route the fix through Eliminating Secret Sprawl: Pipeline Scanning, Push Protection, and Leaked-Credential Remediation. And remember that threat modeling’s output is a set of requirements: the encryption, the least-privilege role, the audit log are only real once the code and the tests exist. A model full of status: mitigated entries with no corresponding test is a model lying to you.

Cost & sizing

The costs here are people-time and, optionally, tooling — not cloud spend. Sizing the effort correctly is the whole game, because an over-heavy process gets abandoned and a too-light one misses the flaws:

Activity Time cost Money cost When it is worth it
Story-level checklist 5–15 min / story ₹0 Every security-relevant story
Delta model on a PR 15–30 min ₹0 Any change to a flow or boundary
Feature/epic model 1–2 hours, small group ₹0 New capability crossing a boundary
System-level model (greenfield) 0.5–2 days, cross-team ₹0 (self-run) New service; major re-architecture
Open-source tooling (pytm, Threat Dragon, Threagile) Setup hours ₹0 (OSS) Teams standardizing on model-as-code
Commercial platform (IriusRisk et al.) Onboarding weeks Licence: lakhs/yr (USD tens of thousands) Large enterprises needing governance + compliance mapping
External threat-model consultancy Per engagement ₹1.5–8 lakh (USD ~2–10k) per system High-stakes system, no in-house skill yet

Rough right-sizing rule: your threat-modeling effort should be a small fraction of the build effort for the same component, scaled by its risk. A payment or KYC service earns a full-day system model plus per-feature passes; an internal read-only dashboard with no sensitive data earns a five-minute checklist and a note. The trap at both ends: heavyweight modeling on trivial systems (waste, and the process gets resented) and no modeling on the crown jewels (the expensive kind of savings). Start free with OSS and a checklist; buy a platform only once threat modeling is routine and the bottleneck is governance at scale, not the modeling itself.

Interview & exam questions

1. What are the four questions that frame a threat model? What are we building? What can go wrong? What are we going to do about it? Did we do a good enough job? They map to modeling (DFD), enumeration (STRIDE/attack trees), response (rating + mitigation), and validation (coverage + tests). Popularized by Adam Shostack.

2. What does STRIDE stand for, and what property does each letter violate? Spoofing (authentication), Tampering (integrity), Repudiation (non-repudiation), Information disclosure (confidentiality), Denial of service (availability), Elevation of privilege (authorization). Each is the negation of a security property, which is why walking all six is exhaustive by construction.

3. What are the four element types in a data-flow diagram, and what is the fifth thing you draw? External entity, process, data store, data flow. The fifth — and most important — is the trust boundary, a dashed line where trust/privilege level changes and where threats concentrate.

4. In STRIDE-per-element, why does a data flow not get Spoofing or Elevation? A data flow does not authenticate anyone (so it cannot be spoofed in the auth sense) and does not run code with privilege (so it cannot elevate). It can be tampered or read in transit (T, I) or flooded (D). Only processes get the full six because only they authenticate, run logic, and hold privilege.

5. Explain AND vs OR nodes in an attack tree and why the distinction matters for defense. OR: any child suffices, so the defender must block every branch. AND: all children are required, so breaking any one child collapses the path. You hunt for AND nodes because a single control there is high-leverage. Cost propagates as minimum across OR, sum across AND.

6. Why did Microsoft deprecate DREAD? It is subjective and inconsistent between raters, its dimensions overlap (Reproducibility vs Exploitability), averaging hides high-damage/low-other threats, and Discoverability rewards security by obscurity. Prefer a calibrated likelihood x impact matrix for design threats.

7. When would you use CVSS instead of likelihood x impact? CVSS scores known, concrete vulnerabilities (every CVE carries one) and is comparable across organizations. Use it for dependency findings and communicating with teams that speak CVSS. It is the wrong altitude for abstract design-stage threats, where you would have to invent the metric values.

8. What is the difference between a threat, a vulnerability, and a risk? A threat is a potential negative event; a vulnerability is the specific weakness enabling it; a risk is the threat weighted by likelihood and impact. A control reduces the risk. Keeping them distinct prevents muddled prioritization.

9. How do you keep a threat model alive in an Agile team? Right-size the effort (story checklist, delta model, feature model, system model), keep the model as diffable code in the repo, and gate it in CI so an open threat with no mitigation fails the build. Re-model on trigger — new boundary, new data class, auth/crypto change — not on a calendar.

10. Name three threat-modeling tools and what distinguishes them. OWASP Threat Dragon (free, GUI, JSON model that diffs, STRIDE + LINDDUN); Microsoft Threat Modeling Tool (free, auto-generates STRIDE per interaction from a DFD, Azure-oriented); pytm/Threagile (threat-model-as-code, generate DFD + findings in CI). IriusRisk is the commercial governance-scale option.

11. What are the four responses to a rated threat, and what does “accept” require? Mitigate, eliminate, transfer, accept. Acceptance must carry a named owner and an expiry date, recorded next to the threat — otherwise it is just “ignore” and the finding dies silently.

12. Why draw the DFD before enumerating threats? Because threats enumerated without a diagram come from the modelers’ biases — you find the risks you already fear and miss the ones you never pictured. The DFD gives you a complete, unbiased worklist: every element, every boundary crossing, analyzed the same way.

These map to secure-design questions in CISSP (Domain 3), CSSLP, the SANS SEC540/DevSecOps track, and most cloud security certifications’ secure-architecture domains.

Quick check

  1. Which STRIDE category maps to a violation of integrity, and give one control family that restores it.
  2. You expand a process into sub-processes and find no new trust boundary and no new threat class. What does the “decompose until threats stop changing” rule tell you to do?
  3. In an attack tree, you find an AND node with two children. Which do you have to break to stop that path?
  4. A threat is rated response: accept. What two fields make that acceptance auditable rather than “ignore”?
  5. Why is a calibrated likelihood x impact matrix usually preferred over DREAD for design-stage threats?

Answers

  1. Tampering (T) violates integrity. Control families that restore it: TLS in transit, HMAC/digital signatures, input validation, WORM storage, least-privilege writes.
  2. Stop — you have gone deep enough (or one level too deep). Decomposing further only adds detail without adding threats, which is wasted effort; roll back to the level where the threats last changed.
  3. Either one. Under an AND node all children are required together, so breaking any single child collapses the whole branch — that is exactly the high-leverage control you hunt for.
  4. A named owner and an expiry/review date. Without both, the accepted risk has no one accountable and no trigger to revisit it, so it silently becomes permanent.
  5. Because likelihood x impact speaks the business’s risk language, avoids DREAD’s subjectivity and overlapping dimensions, and drops Discoverability — which rewards security by obscurity. With anchored axes it is defensible in an audit; DREAD’s averaging hides high-damage threats.

Glossary

Next steps

threat-modelingSTRIDEdata-flow-diagramsattack-treessecure-designrisk-assessmentDevSecOpsOWASP
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading