Skip to content

Lesson 1 of 9 · 30m

Threat Modeling Before You Ship Anything

A lightweight, repeatable way to enumerate who attacks you, what they want, and where your design breaks first.

You're about to ship a webhook receiver that triggers a deploy. The instinct is to harden the code first — but hardening without knowing what you're defending against is how teams end up with WAF rules protecting endpoints nobody attacks and a signing secret committed to the repo. A threat model is a twenty-minute exercise that tells you where the walls actually need to go.

Start with what the attacker wants

Forget frameworks for a minute and answer one question: what is the worst thing an attacker could do here? For the webhook receiver, it's triggering a deploy with attacker-controlled code. That one sentence is your crown jewel — everything else (spoofing the payload, replaying old requests, exhausting your queue) only matters insofar as it gets them closer to it.

Write the crown jewel down. Then enumerate the trust boundaries it crosses: the public internet hits your listener, the listener talks to your queue, the queue feeds a deploy worker, the worker holds cloud credentials. Every boundary is where a threat lives.

STRIDE, used as a checklist not a religion

STRIDE (Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege) is Microsoft's classic taxonomy and it's still the fastest way to enumerate threats per boundary. Apply it to the webhook flow:

  • Spoofing: can anyone forge a POST /webhook? → enforce HMAC signature on every request, using a secret stored in a vault, not the repo.
  • Tampering: can they modify the body after signing? → the signature covers the raw body bytes, verified before JSON parsing.
  • Repudiation: could the sender deny sending it, or could you fail to notice a flood of forged attempts? → log every request with its delivery ID and signature result.
  • Information disclosure: does the endpoint leak versions, stack traces, or timing differences between "bad signature" and "no signature"? → return the same 401 for both, and use a constant-time compare (hmac.compare_digest in Python, crypto.timingSafeEqual in Node 18+).
  • Denial of service: can a flood starve real deliveries? → rate-limit by source and reject oversized bodies before parsing.
  • Elevation of privilege: what does a valid signature let an attacker reach? → the deploy worker should run with the minimum IAM role needed, scoped to one repo's deploy, nothing else.

Six questions, six concrete controls. That's a threat model.

Try this

Take a real service you own — an API endpoint, an admin panel, a CI job. Write one sentence for the crown jewel, draw its trust boundaries, and run STRIDE on each. For every threat you can't immediately counter, write a ticket. If you produce more than five tickets, your exercise worked.

Field notes

  • The bug that ships: teams model the public boundary and forget internal ones. A compromised CI runner that can call your deploy worker directly bypasses every signature check you just wrote. Model internal callers as adversaries too.
  • Threat models rot. Pin the model to the design doc or README — if it isn't where engineers look, it doesn't get updated and lies to you a year later.
  • Resist the urge to rank every threat High/Medium/Low and move on. The output isn't a spreadsheet; it's a set of controls and tickets.