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 now | Still entirely yours |
|---|---|
| Patching the operating system and language runtime | Your function code and every dependency bundled with it |
| Isolating your functions from other customers' workloads | The permissions each function's execution role holds |
| Host hardening, SSH, long-lived server access | Which events can trigger each function, and who can send them |
| Scaling capacity up and down | Limits on that scaling, and what it costs when it happens |
| Physical and network infrastructure | Secrets, 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:
- What can this role read, write and delete? Look for wildcards in actions and resources.
- 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.
- 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:
| Category | The serverless version of the question |
|---|---|
| Spoofing | Are 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? |
| Tampering | Who 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? |
| Repudiation | Is 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 disclosure | Are 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 service | What'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 privilege | Does 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.