How to Threat Model a Serverless Architecture

A serverless architecture diagram usually looks reassuringly sparse. An API gateway, a dozen small functions, a queue, a storage bucket, a managed database. No servers to patch, no SSH, no load balancer config. It's tempting to conclude there's not much left to threat model.

The opposite is closer to true. Serverless doesn't shrink the attack surface so much as spread it out: across dozens of independently deployed functions, each with its own permissions, each triggered by events from places that were never designed as front doors. The servers left your diagram. The threats didn't.

The short version: in serverless, every trigger is an entry point and every function is its own identity. Model the events, not just the HTTP API, and draw trust boundaries around execution roles rather than networks. Then ask what one compromised function could reach, and what one flood of events would cost.

What actually changes when the server goes away

Functions-as-a-Service moves a clear slice of work to the provider. It's worth writing that line down explicitly before you start, because both sides of it get misread: teams either assume the provider covers more than it does, or keep modeling threats that genuinely aren't theirs any more.

Mostly the provider's problem nowStill entirely yours
Patching the operating system and language runtimeYour function code and every dependency bundled with it
Isolating your functions from other customers' workloadsThe permissions each function's execution role holds
Host hardening, SSH, long-lived server accessWhich events can trigger each function, and who can send them
Scaling capacity up and downLimits on that scaling, and what it costs when it happens
Physical and network infrastructureSecrets, configuration, logging and the wiring between services

The right-hand column is where serverless threat models should spend their time. Our cloud infrastructure guide covers the account-level picture (identity as the perimeter, escalation paths, key management); this post zooms in on what's specific to functions.

Draw every trigger, not just the API

Most teams draw the HTTP path carefully: client, API gateway, function, database. Then they stop. But in a typical serverless system, HTTP is only one of many ways a function gets invoked:

  • Storage events. A file lands in a bucket and a function processes it. Whoever can upload to that bucket can feed input to that function, including the file name, metadata and contents.
  • Queues and topics. A function consumes messages. Whoever can publish to the queue, including other functions and other teams' services, controls its input.
  • Database and change streams. A record changes and a function reacts. Anyone who can write a row has indirect influence over downstream logic.
  • Schedules. Cron-style triggers carry little input, but they often run with broad permissions for housekeeping jobs.
  • Webhooks and third-party events. A payment provider, a source-control system or an identity provider calls a function URL. Is the signature verified, or does the function trust anything that arrives at that address?
  • Direct invocation. Other functions, or anyone with the right cloud permission, can call a function directly and skip the gateway entirely.

In a data flow diagram, each function is a process, each of these triggers is an inbound flow, and each managed service is a data store or external entity. That last point matters: the gateway, the queue and the bucket aren't plumbing to be left off the diagram. They're where the authentication and validation decisions either happen or silently don't.

Event injection: the input nobody validates

Here's the pattern behind a lot of serverless vulnerabilities. The HTTP API sits behind a gateway with authentication, request validation and maybe a web application firewall. The team's mental model of "untrusted input" is anchored there. Then an image-processing function receives a storage event and passes the uploaded file's name into a shell command, a database query or a log line. Nobody thought of the file name as user input, so nobody validated it.

OWASP's Serverless Top 10 project listed this, event-data injection, as its first entry for exactly that reason. The fix isn't exotic: treat every event source as a trust boundary crossing, and apply the same parsing, validation and output encoding you'd apply to an HTTP request body. When you sweep the DFD, each trigger flow gets the same question: who can put data on this flow, and what does the function do with it?

Each function is an identity, and that's the real boundary

In a traditional application, a compromise of the web server gives an attacker whatever that server can reach, which is usually everything. Serverless gives you a genuine opportunity to do better: each function can run with its own execution role, scoped to exactly the resources it needs.

In practice, many teams squander it. A single broad role is shared across every function in a service because it was quicker to set up, and it grants wildcard access to the database, every bucket and the secrets store. Now the least important function in the system, the one that resizes thumbnails, has the same reach as the one that issues refunds. Any injection flaw or compromised dependency in the thumbnail function is a compromise of everything.

So draw the trust boundaries in a serverless DFD around execution roles, not network segments. Functions that share a role sit inside the same boundary. For each boundary, ask three questions:

  1. What can this role read, write and delete? Look for wildcards in actions and resources.
  2. Can this role change permissions or deploy code? A function that can update other functions' code or attach policies is an escalation path, however innocent its day job.
  3. Who else can invoke this function? Resource-based policies and publicly reachable function URLs can make a function callable from outside the path you drew.

State that outlives the invocation

Functions are often described as stateless, and the model encourages you to write them that way. But providers commonly reuse an execution environment across invocations to avoid cold starts, and anything held in global variables or written to local temporary storage can still be there when the next request arrives. That's useful for caching connections; it's a problem when it's one user's data and the next invocation belongs to someone else.

The same goes for secrets. Credentials passed in as environment variables are convenient, but they're readable by anyone with permission to view the function's configuration, and they tend to appear in debugging output and crash reports. A secrets manager fetched at runtime, with the function's role scoped to the specific secrets it needs, keeps the secret out of the configuration and makes access auditable.

Denial of wallet

Classic denial of service tries to exhaust capacity. Serverless platforms are good at absorbing that, because they scale out automatically. The problem is that you pay for every one of those invocations. An attacker who can trigger an expensive function at volume may never take you offline, but they can run up a bill that does real damage. This is usually called denial of wallet.

Two quieter variants deserve a place on the diagram:

  • Recursive triggers. A function writes its output to the same bucket or queue that triggers it. A single event becomes an infinite loop. Some providers now detect certain loop patterns, but treat that as a safety net rather than a design control.
  • Shared concurrency. Functions in one account often draw from a shared pool of concurrent executions. A flood against one public function can throttle unrelated, critical functions that happen to share the pool, so a cheap attack on a minor endpoint becomes an outage of the checkout flow.

The controls are dull and effective: per-function concurrency limits, rate limiting and authentication at the entry point, budget alerts, and a rule that no function writes to its own trigger source.

The STRIDE sweep, at function altitude

With triggers and roles on the diagram, run STRIDE across each function and each inbound flow. These are the questions that tend to matter most:

CategoryThe serverless version of the question
SpoofingAre webhook signatures verified? Can the function be invoked directly, bypassing the gateway's authentication? Does a queue consumer assume every message came from a trusted producer?
TamperingWho can modify the function's code, layers or configuration? Who can write to the bucket, queue or table that triggers it? Are deployment artefacts signed or pinned?
RepudiationIs there a correlation ID that follows a request across every function and queue it passes through? Are invocations and configuration changes logged somewhere the functions themselves can't write to?
Information disclosureAre secrets in environment variables? Does data from one invocation persist in memory or temporary storage for the next? Do error responses or logs include event payloads with personal data?
Denial of serviceWhat's the concurrency limit, and what shares it? Can an unauthenticated caller trigger an expensive function? Can a function trigger itself? What happens to a queue when the consumer keeps failing?
Elevation of privilegeDoes the execution role use wildcards? Can it modify roles, policies or other functions? Do several unrelated functions share one broad role?

Modeling it as an attack tree

An attack tree makes the role-sharing problem vivid. Put "read every customer record" at the root. Branches might include: exploit the customer API function directly; inject through the file-processing function's storage trigger; compromise a dependency bundled into any function that shares the customer-data role; publish a crafted message to a queue consumed by a privileged function; or obtain credentials to update a function's code.

In a well-scoped system, most of those branches dead-end because the compromised function simply can't reach the customer table. In a system with one shared role, every branch reaches the root, and the shortest one usually runs through the least-reviewed function. That's the case for least privilege in one picture, and it's often more persuasive in a design review than any policy document. The same reasoning applies to the code you didn't write; see our post on threat modeling your dependencies.

Common mistakes

  • Modeling only the HTTP path. The gateway is the best-defended entry point. The queue consumers and storage processors behind it are usually the weakest.
  • Treating the provider's isolation as your access control. Tenant isolation between cloud customers says nothing about which of your functions can reach which of your data.
  • One role per service instead of per function. It's the serverless equivalent of running everything as root.
  • No cost ceiling. If nobody has decided what the maximum monthly bill for a function should be, nobody will notice when it's being exceeded on purpose.
  • Diagramming once. Serverless systems change constantly: a new trigger, a new function, a widened role in a hurried fix. Each is a reason to revisit the model, and the deployment pipeline that ships those changes needs its own model too.

Serverless done well is one of the easier architectures to secure: small units, explicit permissions, no long-lived hosts. But only if the threat model follows the architecture's real shape, which is events and identities, not servers. Put every trigger on the diagram, draw a boundary around every role, and the gaps tend to show themselves.

Serverless backends are common in SaaS and fintech products, where event-driven processing touches customer and payment data. To start from a pre-labelled diagram, see the free data flow diagram template.

Put every trigger on the diagram

Model your functions, queues, buckets and webhooks in ThreatTree, draw trust boundaries around each execution role, sweep every event flow with STRIDE, and build the attack tree under "read every customer record" — so the function whose role reaches too far is the first thing you see.

Get started free

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