Threat model a new feature in one hour with STRIDE
For developers shipping a new feature and needing a fast, defensible threat model without slowing delivery. You will leave with a one-page STRIDE model, a concrete mitigation list, and CI/CD-ready follow-up actions tied to owners and tickets.
TL;DR — In 60 minutes, produce a usable STRIDE threat model by fixing scope first, drawing one data-flow diagram, then walking each trust boundary with a short checklist. The most common failure is modelling the whole system; constrain it to one feature, one entry point, one data path, and turn every real finding into a ticket with an owner. Reading time: ~5 min
Goal
At the end, you will have a committed docs/threat-models/<feature>.md file containing a data-flow diagram, STRIDE findings per component and trust boundary, severity and owner fields, and a short mitigation backlog that your team can execute through CI/CD and normal sprint work.
Prerequisites
- Access to the feature branch or design doc for the feature you are modelling
- Read access to the repo and deployment config
- A shell with
git >= 2.40— check with:
git --version
- Optional but useful:
rg >= 13for code search — check with:
rg --version
- Optional but useful:
jq >= 1.6for reading JSON config — check with:
jq --version
- The feature name, primary user action, entry point URL/queue/topic, and the data stores or third parties it touches
- 60 minutes of uninterrupted time
Steps
Step 1: Create a threat model file and timebox the session
Run these commands from the repo root, replacing feature-name with the actual feature slug:
mkdir -p docs/threat-models
cat > docs/threat-models/feature-name.md <<'EOF'
## Scope
- Feature:
- Change type:
- Entry point:
- Primary actor:
- Sensitive data:
- Out of scope:
## Diagram
```diagram
[User] ---- 1. request ----> [Feature]
STRIDE findings
| Target | S | T | R | I | D | E | Notes | Mitigation | Owner |
|---|
Decisions
Tickets
EOF
git add docs/threat-models/feature-name.md
What you should see when this succeeds: `git status --short` shows `A docs/threat-models/feature-name.md`.
### Step 2: Lock the scope to one feature path
Fill the scope section with literal values. If you have a route, event, or RPC name, use that exact string. For example:
```bash
perl -0pi -e 's/Feature:\n/Feature: export invoices as CSV\n/; s/Change type:\n/Change type: new authenticated API endpoint and background job\n/; s/Entry point:\n/Entry point: POST \/api\/v1\/invoices\/export\n/; s/Primary actor:\n/Primary actor: authenticated finance user\n/; s/Sensitive data:\n/Sensitive data: invoice line items, customer names, billing addresses, totals\n/; s/Out of scope:\n/Out of scope: legacy XML export endpoint, mobile app UI\n/' docs/threat-models/feature-name.md
What you should see when this succeeds: opening the file shows exactly one entry point and a short out-of-scope list.
Step 3: Enumerate the real components from code and config
Use repo search to find the path through the system. Replace the route or queue names with your own:
rg -n "/api/v1/invoices/export|invoices.export|EXPORT_INVOICES|s3|bucket|redis|postgres|kafka|sns|sqs" .
If your app exposes OpenAPI, inspect it directly:
jq '.paths["/api/v1/invoices/export"]' openapi*.json 2>/dev/null
Build a component list from what you find: browser/mobile client, API, auth/session layer, worker, object storage, database, audit log, third-party API. What you should see when this succeeds: a short list of 4-8 concrete components, not generic terms like “backend”.
Step 4: Draw the data-flow diagram with trust boundaries
Replace the placeholder diagram with the actual path. Keep it simple and numbered:
View as text
[User] ---- 1. POST /api/v1/invoices/export ----> [API] [API] ---- 2. enqueue export job ----> [Queue] [Queue] ---- 3. deliver job ----> [Worker] [Worker] ---- 4. SELECT invoices ----> [Postgres] [Worker] ---- 5. write export.csv ----> [Object Storage] [User] <---- 6. GET signed download URL ---- [API] [API] ---- 7. read audit context ----> [Auth Session]
Then add trust boundaries in notes directly under ## Diagram:
cat >> docs/threat-models/feature-name.md <<'EOF'
Trust boundaries:
- Internet between User and API
- Privilege boundary between API and Worker
- Data boundary between Worker and Postgres
- Externalized storage boundary between Worker and Object Storage
EOF
What you should see when this succeeds: every hop crosses a named boundary or stays inside one.
Step 5: Walk STRIDE per component and boundary
Do not brainstorm in the abstract. For each node and edge, ask the shortest useful questions and write one line per real finding.
Use this checklist while editing the table:
- S: can the caller fake identity, tenant, webhook source, or job producer?
- T: can inputs, messages, object keys, or SQL parameters be altered?
- R: is there an audit trail with actor, tenant, request ID, and outcome?
- I: can responses, logs, exports, or signed URLs leak data?
- D: can one request create expensive fan-out, retries, lock contention, or huge exports?
- E: can a user or worker gain broader tenant/admin/data access?
Add rows like this:
| API POST /api/v1/invoices/export | | X | X | X | X | X | Missing per-tenant authorization on export filter; no export size cap; signed URL TTL not defined | Enforce tenant_id in query builder; cap rows at 50k; signed URL TTL 10 min; log actor_id, tenant_id, export_id | api-team |
| Queue -> Worker job | X | X | | | X | X | Worker trusts job payload tenant_id and user_id from producer | Sign job payload or re-hydrate auth context server-side by export_id only | platform |
| Object Storage export.csv | | X | | X | | | Predictable object key could expose another tenant's file if ACLs drift | Randomize key prefix; private bucket/container; one-time signed URL | platform |
What you should see when this succeeds: the table has at least one row for each risky edge or component, and empty cells where no issue was found.
Step 6: Convert findings into CI/CD and backlog actions
Every finding needs a concrete next action. Add tickets in the file and create stubs in your tracker if you use one. Keep mitigations deployable.
cat >> docs/threat-models/feature-name.md <<'EOF'
## Tickets
- TM-001: Add authorization test for cross-tenant export attempt; expect 403
- TM-002: Add max export row count and return 422 when exceeded
- TM-003: Worker reads export request by export_id and derives tenant_id server-side
- TM-004: Add structured audit log fields: actor_id, tenant_id, export_id, request_id
- TM-005: Add object storage lifecycle rule: delete exports after 24h
EOF
If you have CI, add at least one automated check now. Example test names to add to your suite:
printf '%s
' 'cross-tenant export returns 403' 'export over 50000 rows returns 422' 'signed URL expires within 600 seconds'
What you should see when this succeeds: each finding has an owner and at least one testable mitigation.
⚠️ If you add blocking controls immediately, such as stricter authorization or lower payload limits, you can break existing clients or jobs. Merge tests first, then deploy the control behind a flag or with a staged rollout if the feature is already live.
Step 7: Commit the model with the feature branch
Commit the document so it travels with the code review:
git add docs/threat-models/feature-name.md
git commit -m "docs(security): add STRIDE threat model for feature-name"
What you should see when this succeeds: git log -1 --stat includes the threat model file in the same branch as the feature.
Verify it works
Run these checks:
grep -n '^## Scope\|^## Diagram\|^## STRIDE findings\|^## Tickets' docs/threat-models/feature-name.md
rg -n '\| .* \| .* \| .* \| .* \| .* \| .* \| .* \| .* \| .* \|' docs/threat-models/feature-name.md
rg -n '403|422|TTL 10 min|actor_id|tenant_id|request_id|24h' docs/threat-models/feature-name.md
Expected result shape:
1:## Scope
9:## Diagram
18:## STRIDE findings
31:## Tickets
20:| API POST /api/v1/invoices/export | | X | X | X | X | X | ...
21:| Queue -> Worker job | X | X | | | X | X | ...
22:| Object Storage export.csv | | X | | X | | | ...
24:... 403 ...
25:... 422 ...
26:... TTL 10 min ... actor_id ... tenant_id ... request_id ... 24h ...
End-to-end, the result is correct if the file exists in the feature branch, the diagram matches the implemented path, each meaningful trust boundary has STRIDE coverage, and every real finding maps to a ticket or test.
Common pitfalls
Modelling the whole product instead of the changed feature
Mistake: you include login, billing, admin, notifications, and unrelated legacy paths.
Symptom: after 30 minutes you still have no findings table, just a giant diagram.
Fix: rewrite ## Scope to one entry point, one actor, one data path, and add the rest under Out of scope:.
Treating STRIDE as six mandatory findings per component
Mistake: you force one issue into every STRIDE cell.
Symptom: the table fills with filler like “use HTTPS” and nobody trusts the output.
Fix: leave cells empty when there is no credible issue and write rows only for real risks on this feature path.
Forgetting asynchronous boundaries
Mistake: you model the API request but skip queues, workers, cron, webhooks, or object storage.
Symptom: the biggest risks — replayed jobs, forged payload context, stale signed URLs — are absent.
Fix: search the repo with rg -n 'queue|worker|cron|webhook|s3|bucket|blob|topic|publish|consume' . and add those nodes to the diagram.
Writing mitigations that are not testable
Mistake: the mitigation says “improve authorization” or “add logging”.
Symptom: no one knows when the work is done, and the ticket stays open for weeks.
Fix: replace it with a literal outcome such as cross-tenant export returns 403, signed URL TTL 600 seconds, or audit log contains actor_id, tenant_id, request_id.
Missing tenant context on internal hops
Mistake: the worker trusts tenant_id or user_id from the queued payload.
Symptom: privilege escalation appears only in background processing, not in direct API tests.
Fix: pass only export_id in the job and re-load tenant and actor context server-side before data access.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI