Part VI · Chapter 12 · The Decisions · lesson 4
Who would attack this, and what would they get?
15 min
Builds on Your diagram has four dependencies. You have nineteen and The document you retrieved told the model what to do
Part VI · Chapter 12 · The Decisions · lesson 4
15 min
Builds on Your diagram has four dependencies. You have nineteen and The document you retrieved told the model what to do
Procurement needs a threat model by Friday. Three already exist: a forty-page spreadsheet with six threat categories crossed against every component, which nobody has opened since it was delivered; a diagram of boxes and arrows with no assets marked on it; and a penetration test whose findings were all in a service that no longer runs.
You have one afternoon. Write the one that gets used.
Predict
A trust boundary is a place where a request changes what it is trusted to do. Not a network edge, not a subnet, not a service seam. If a call crosses from one of your services to another and is trusted with exactly the same things on both sides, that is a component diagram, not a boundary.
The asset is what is behind the boundary, named concretely. Not "data" — "every tenant's customer list and calendar", or "the credential that can deploy".
The outsider is who can get to the boundary. Sometimes anyone on the internet. Often your own paying customers. Sometimes every maintainer of every dependency you install.
The test that keeps it short: if you cannot name a distinct asset behind a boundary, it belongs on a diagram, not in this table. Most drafts have fifteen rows; honest ones have five or six.
Written out, with the worst thing a single request could do at each. This is the whole document.
BOUNDARY OUTSIDE IT ASSET BEHIND IT WORST SINGLE REQUEST
-------------------- ---------------------- ----------------------- --------------------------
Internet -> API Anyone, unauthenticated Everything, filtered An endpoint that forgot to
only by your authz require a session at all
Tenant -> tenant Your own paying Every other customer's Swapping an id in a URL and
customers data, all of it reading somebody else's rows
User -> admin Any logged-in user Impersonation, bulk Reaching an admin route by
capability export, permissions guessing its path
App -> datastore Your own code, and Data at rest, the An injection or an SSRF that
anything that runs in it credentials, backups turns your app into a proxy
CI/CD -> production Every maintainer of Production itself A build step that reads a
every dependency, plus long-lived deploy credential
everyone with push
Staff tools -> Employees, contractors, Everything, with no A support agent viewing or
customer data and whoever phishes exploit needed exporting an account, with
or social-engineers them nothing recorded
Two things about that table are worth dwelling on, because they are where most real incidents come from and neither is on a typical architecture diagram.
The second row is the one your own code is most likely to get wrong. An object identifier in a URL with no ownership check — the booking belongs to tenant A, the request carries tenant B's session, and nothing compares them. This class has been the first entry in both published editions of the OWASP API Security Top Ten, and the reason is structural rather than careless: the check has to be present on every endpoint, forever, including the one somebody adds on a Friday to unblock a customer.
The fifth and sixth rows have no attacker in your system at all. The dependency chain and the help desk are both paths into production that do not involve exploiting your application, and they are over-represented in real intrusions relative to how much attention they get in threat models, which tend to be written by the people who own the application.
Not a complete programme. Three things which, if you have one engineer and six weeks, will reduce the worst case by more than anything else you could buy with the same time. Named concretely, because "improve access control" is not a control.
One. Phishing-resistant multi-factor authentication on every human path into production, behind single sign-on. Concretely: passkeys or hardware security keys using WebAuthn for the cloud console, the identity provider, the source control organisation and the internal admin tools; one identity provider in front of all of them so there is exactly one place to revoke somebody; and no exceptions for the founders. It is first because credential abuse sits at or near the top of the initial-access tables in the annual breach reports year after year, and because it is the only control here that defends six boundaries at once.
The caveat, which matters: this does nothing about your application's own authorisation bugs, and the standard bypass is the recovery path. A reset flow operated by a help desk that will accept a convincing phone call is the real authentication mechanism, whatever is configured on the login page. So the reset path needs the same bar — a second named person, a callback to a known number, or a hardware key — or you have bought a very good front door and left the key under the mat.
Two. Authorisation enforced on the object, in one place, with the tenant scope impossible to omit. Concretely: row-level security in the database so the tenant predicate is applied by the engine rather than remembered by the developer, or a repository layer where every query requires a scope argument and there is no way to construct an unscoped one. Plus a test that enumerates your routes and fails the build when a new one appears without an ownership check.
The structural point is the one that justifies the effort: a code-review habit does not survive four years of new endpoints, staff turnover and Friday deadlines, and this is a vulnerability class where exactly one missed endpoint loses every customer's data at once. The whole value is in making the safe thing the only available thing.
Three. Short-lived scoped credentials everywhere, and an audit log the privileged cannot edit. Concretely: federated identity from your build system to your cloud provider using OpenID Connect, so there is no long-lived deploy key in a secret store to steal; a separate identity per service rather than one shared role; application secrets issued from a secrets manager with rotation rather than baked into images or environment files; and every administrative, support and export action written to an append-only log in an account whose credentials the production roles do not hold.
This one bounds a breach rather than preventing it, which is why it is third and why it is not optional. It converts "they have our keys indefinitely" into "they had a token for an hour", and the audit log is the difference between a postmortem and a guess — you cannot write the timeline from the reliability track if nothing recorded who did what.
What would you do
Something people say
“We are too small to be a target. Nobody is going to attack a ten-person company.”
Given that the attacks arriving are automated and indiscriminate, which of the three controls matters most for a ten-person company?
Check
Five columns, five or six rows, one page. Nothing else.
BOUNDARY | WHO IS OUTSIDE | ASSET BEHIND IT | WORST SINGLE REQUEST | CONTROL, AND WHERE ENFORCED
Two rules that keep it honest and short:
A boundary with no nameable asset behind it goes on the diagram, not in the table. This is the rule that stops the document growing to forty pages, and it is also the rule that makes it useful, because every row now ends in a sentence about something somebody would lose.
The control column names the enforcement point, not the intention. "Row-level security in the database" is a control. "Validate ownership" is a plan. If you cannot name the single place a reviewer could go and look, you are describing something you hope happens in many places, which is the thing the second control above exists to replace.
Then one number, and it is the only measurement this lesson has: count the rows. Six is normal. Nine means you are probably listing components. Fifteen means you have rebuilt the spreadsheet nobody read, and the fix is the first rule — go back and delete every row whose asset you could not name without saying the word "data".
The category-per-component method is not wrong. Spoofing, tampering, repudiation, disclosure, denial of service and privilege escalation are a good prompt list, and run against one boundary by somebody who knows the system they generate real findings in twenty minutes.
What fails is using them as the document's structure, and the failure is mechanical. The cross product grows with your component count, so the document gets longer exactly as the system gets more interesting. Most cells are empty or repeat each other, so signal per page falls. And nothing in the structure forces you to name an asset, so the output is a list of threats ranked by nothing in particular — which means the reader cannot tell which two rows matter, which means they stop reading, which means it is never updated.
The boundary-and-asset shape fails in the opposite, survivable direction: it leaves things out. You will miss denial of service almost entirely, because availability is not a confidentiality asset and does not fit the table — which is exactly why the closing section below is about that.
The repair is to use both at the right size: one page of boundaries and assets as the document, and the six categories as a checklist you run against the two rows that matter whenever one of them changes. If you want one piece of further reading, read your own authorisation layer and count how many distinct places a tenant predicate is applied. That number is the honest version of your threat model's second row.
The whole structure is asset-centred, and it quietly assumes the asset is confidentiality. For a lot of systems it is not.
A public price feed, a status page, a read-only mirror, a documentation site, an open blockchain node: there is nothing behind the boundary to steal, because everything behind it is already published. Run the table and every row collapses into one, and the one asset left is availability, with integrity beside it.
The controls for that are a different list, and two of them are modern enough that they belong in a lesson dated this year. Availability is rate limiting per caller with an identity attached, caching that absorbs the cheap requests, and the admission control from the reliability track. Integrity is signing what you serve so a consumer can detect a substituted response, and keeping the signing key somewhere the serving path cannot reach. And the third, which is the one that surprises people: your bill is a denial-of-service target. A public endpoint backed by usage-priced infrastructure can be attacked by being used, legitimately and enormously, and the outage arrives as a spend alert rather than as a saturated server.
So the crossing is: when the worst case of total disclosure is "they read what was already on the public page", stop modelling confidentiality boundaries and model your spend limits and your signing keys instead. Same afternoon, same one-page discipline, one row — and the control column names a quota, a budget alert with a hard stop behind it, and where the key lives.