How to Threat Model a Microservices Architecture

In a monolith, a call from the checkout code into the payments code is a function call. It cannot be intercepted, it cannot be replayed, it carries the caller's context implicitly, and nobody has ever had to decide whether it should be authenticated. Split that monolith into services and the same call becomes a network request between two independently deployed processes, running under separate identities, over a network shared with everything else in the cluster — and every one of those implicit properties becomes an explicit decision that somebody now has to make, usually without noticing that a decision was required. That is the whole security story of microservices in one sentence: the architecture did not create new categories of threat so much as it took a large number of assumptions that used to be enforced by the language runtime and turned them into configuration.

Which is why the obvious approach — threat model each service, one at a time — produces so little. Individually, most services are boring: an HTTP handler, some business logic, a database call. The threats that actually matter in a distributed system live in the spaces between the services, in the shared infrastructure they all depend on, and in the aggregate question of what an attacker reaches next after compromising any single one of them. None of those are visible from inside a single-service model. This guide is about how to scope, draw, and sweep a microservices threat model so that the gaps are what you are looking at.

The short version: Scope by request path, not by service. Model the shared platform — gateway, mesh, broker, registry, secrets store — once, as a model every service inherits. Put the infrastructure on the diagram as real elements, not as a cloud. Treat every hop as a trust boundary crossing until a specific named control makes it not one. Then finish with the question a per-service model can never ask: if this one service is fully compromised, what does the attacker reach next?

Scope by request path, not by service

The unit that makes a microservices threat model tractable is a single end-to-end transaction that crosses several services — checkout, password reset, generating a customer data export, onboarding a new tenant. Pick one, follow it from the client through every hop it touches until it reaches storage and comes back, and model that. A request path has the two properties you need: it is small enough to fit in one session, and it is guaranteed to cross the service seams where the interesting findings are. A per-service scope has neither, because it slices the system along exactly the axis that hides the problem.

This also fixes the scaling question that stops most teams from starting at all. If you have forty services, forty threat models is not a plan — it is a plan to have four of them and then quietly stop. But forty services usually have a much smaller number of genuinely distinct request paths, and the top five or six of those will touch most of the services that matter and nearly all of the sensitive data. Model those, and you have covered the system in a way forty per-service documents would not have, because you will have looked at every seam those paths cross rather than at forty interiors.

Everything else in this guide assumes the workshop mechanics are already handled — the pre-drawn diagram, the room, the timebox, the capture format. If that part is new, running your first threat modeling workshop covers it, and the only change for a distributed system is that you want at least one person who knows the platform — the mesh, the broker, the cluster — in the room alongside the service owners, because a surprising share of the findings will be theirs rather than any single team's.

Model the platform once, then the deltas

Every service in the system runs on the same shared substrate: the same API gateway, the same service mesh or sidecar configuration, the same message broker, the same service registry, the same secrets store, the same deployment pipeline. The threats against that substrate are identical for every service that sits on it, and rediscovering them in each service's session is how a threat modeling programme dies of tedium.

So model the platform once, deliberately, as its own threat model — and treat its output as a set of inherited threats every service's model starts with rather than rederives. Then a per-path session only has to work out its deltas: what this specific flow does that the platform model did not already cover, which usually means the data it carries, the specific privileges its services hold, and any place it steps outside the standard pattern. "This path is the only one that talks to the broker without going through the shared publisher library" is exactly the kind of finding this structure surfaces quickly and a per-service sweep buries.

Draw the infrastructure, not a cloud

The most common failure in a microservices data flow diagram is drawing seven service boxes with arrows between them and labelling everything underneath as "infrastructure." Every element in that omitted layer is a real process or data store that holds credentials, makes routing decisions, or stores messages — which is to say, every one of them is attackable, and several of them are the highest-value targets in the system precisely because every service depends on them.

ElementDraw it asWhy it earns a place on the diagram
API gateway / ingress Process at the outermost boundary Where external identity is established, and where "authenticated" starts meaning something different downstream.
Message broker / queue Data store, not an arrow Messages persist, can be read, replayed, or injected; a topic is a shared trust domain with its own access model.
Service registry / discovery Process plus data store Whoever controls name resolution controls where traffic goes, without touching either endpoint.
Sidecar / mesh proxy Process on the boundary of each service It is where mTLS and policy actually live; if traffic can reach the workload directly, the control is optional.
Secrets / config store Data store with its own boundary Compromise here is compromise everywhere, and access to it is frequently far broader than anyone intended.
Deployment pipeline External entity writing into the cluster It can replace any service's code. It is in scope whether or not the room wants it to be.
Shared datastore One store with multiple writers Two services on one schema means neither owns its invariants, and one service's bug becomes the other's corruption.

Then mark the boundaries honestly. The default assumption should be that every hop crosses a trust boundary — and the way to remove one is to name the specific control that closes it, not to assert that the traffic is internal. "Service A to service B is inside the cluster" is not a control. "Service A to service B is mutual TLS with SPIFFE identities, enforced by the sidecar, with a deny-by-default policy" is a control, and it is also a claim someone in the room can go and verify afterwards. This is the practical content of Zero Trust as applied to east-west traffic, and drawing it this way is what stops "we have a mesh" from silently standing in for "the mesh is configured to deny by default."

The STRIDE sweep, weighted east-west

STRIDE works unchanged here; what changes is which questions turn out to be productive. Running the six categories against a service boundary rather than against a monolith's edge shifts the emphasis substantially:

CategoryWhat to actually ask at each hop
Spoofing How does B know it is really A calling, and not any other workload that can reach the port? Is service identity cryptographic, or is it a header, a shared secret, or the source IP?
Tampering Can a message sitting on the broker be modified or injected? Can config or a discovery record be changed? Can the pipeline deploy an altered build?
Repudiation Can you reconstruct a single user action across all seven hops afterwards? Without a correlation ID propagated end to end, the answer is usually no.
Information disclosure What does this internal API return beyond what the caller needs? What lands in logs and traces as it crosses services — and who can read the tracing backend?
Denial of service What happens when B is slow? Do A's retries amplify it? Is there a circuit breaker, or does one degraded service take the path down through queued work and exhausted connection pools?
Elevation of privilege What can B's service account do that this call needs? Will B perform a privileged action because A asked, without checking whether the original user was allowed to ask?

Threats that only exist because it is distributed

Some findings recur across almost every microservices model, and they are worth having in your head before the session so you recognise them when the room describes one without knowing what it is looking at.

  • The confused deputy. Service A receives a request from an untrusted user, then calls service B using A's own broadly privileged service account. B does the work, because B trusts A. The user's actual permissions were checked once at the edge, if at all, and never again — so anything A can do, a user who can reach A can eventually do too. This is the single most common serious finding in a microservices threat model, and it is invisible in a per-service scope because neither A's model nor B's model is wrong on its own.
  • East-west traffic assumed trusted. Internal APIs that skip authentication entirely because "only other services call them." True until any single service is compromised, or until anything else lands in the same network namespace.
  • Retry storms and cascading failure. A denial of service that originates inside the system rather than outside it: B slows down, A retries, the retries multiply the load, and the path fails in a way no external attacker had to work for. Aggressive client-side retries without backoff are a security finding, not just a reliability one.
  • The broker as an open trust domain. Topic-level rather than message-level authorisation means any service that can publish anywhere can publish anything, and any consumer that can subscribe broadly reads data it was never meant to see. Replay is usually possible too, unless messages carry something that makes them single-use.
  • Mesh bypass. If a workload's port is reachable without going through its sidecar, then every policy expressed in the mesh is advisory. Worth testing rather than assuming — it is a configuration property, and configurations drift.
  • Discovery poisoning. Write access to the service registry redirects traffic without touching either service. Read the registry's own access control as carefully as you read the services'.
  • Observability as an exfiltration path. Distributed traces and structured logs cross service boundaries carrying request payloads, and the tracing backend is frequently the least access-controlled data store in the estate while holding a copy of everything sensitive that flowed through it.
  • Shared database, split ownership. Two services writing one schema means the boundary between them exists in the org chart but not in the system, and neither can enforce an invariant the other can violate.

Finish with the lateral movement question

The question a per-service model structurally cannot ask is the one that matters most: assume one service is fully compromised — pick the most exposed one on the path — and work out what the attacker reaches from there. Which service accounts does it hold? Which internal APIs will accept its identity? What is in its environment variables? What can it publish to the broker, and who consumes that? Which datastores does its network policy permit?

This is naturally an attack tree rather than a list, because the useful structure is the gating: reaching customer records from a compromised frontend might require both a valid service identity and a network path and an authorisation check that does not re-validate the user — an AND-gate whose children are the three independent things that would have to fail together. Where you find an OR-gate, one failure is sufficient, and that is where defense in depth is missing regardless of how many controls the platform lists on paper. Running this exercise for two or three of the most exposed services usually reveals that the blast radius of a single compromise is considerably larger than anyone in the room expected, which is the finding that gets network policy and service account scoping prioritised.

Where microservices threat models go wrong

  • One model per service. Slices the system along the axis that hides the threats, and does not finish.
  • Infrastructure drawn as a cloud. The broker, registry, and secrets store are the highest-value targets; leaving them off the diagram removes them from the sweep.
  • "It's internal" as a control. A network location is not an authentication mechanism, and every hop deserves a named control or an acknowledged gap.
  • Modelling the intended topology. In a system where teams deploy independently, the call graph in the architecture diagram and the one in the traces are rarely identical. Check the traces.
  • Ignoring the pipeline. Anything that can deploy code into the cluster can become any service on the diagram.
  • Stopping before lateral movement. Without it you have a list of per-hop weaknesses and no sense of what any of them actually costs you.

What to record

Record findings against the path, with the owning team of the service where the fix lands as the owner — a confused-deputy finding between A and B is normally B's fix, since B is the one performing a privileged action on someone else's say-so, and recording it against A leaves it with a team who cannot close it. Platform-level findings go to the platform team once, in the platform model, rather than being copied into every path's register entries, or you will be tracking the same mesh misconfiguration in nine places and closing it in none.

The other thing worth writing down is the assumption list: every place the room said "the mesh handles that" or "the gateway validates that." Those are the claims the model rests on, they are cheap to record while everyone is present, and they are the first things to re-check when the attack surface shifts — a new service added to the path, a policy relaxed to debug an incident, a library swapped. In a system where every team deploys independently, an assumption that was true when it was written is a reasonable thing to expect to become false without anybody telling you. That is exactly what a risk register entry with a review date is for.

Multi-tenant SaaS platforms hit the confused-deputy and shared-datastore patterns hardest, because a tenant boundary that exists only in application logic is one bad internal call away from not existing at all. See threat modeling for SaaS platforms, or start from the pre-labelled DFD in the free threat modeling templates.

Model the path, then ask what one compromise reaches

Draw the request path with every hop as a real boundary crossing, sweep each one with STRIDE, then build the lateral movement tree from your most exposed service — AND-gates where several controls have to fail together, OR-gates where one is enough.

Get started free

Not ready to sign up? Get new threat-modeling guides by email instead.