Where the Trust Boundary Actually Sits

Design for the attacker you can name, not the product you were sold. That’s the thesis of this series, and the first place it bites is the most basic question in security architecture: where does trust actually change?

This post opens a ten-part series on design decisions for when the threat is a person, not a slow dependency. The series hub page will list every post as it ships.

TL;DR

  • A dotted box around a VPC, subnet, or cluster describes network layout. It doesn’t make a trust boundary.
  • A real boundary is a place where every call is checked: who is calling, what they may touch, and on whose authority.
  • A design review can reject four things on sight: trust by network location, the browser talking straight to storage, a deputy acting on unchecked input, and identity passed as a plain header.

The diagram and the ticket

What you get: the gap this whole post is about.

The diagram says “trust boundary.” The ticket still lets the browser talk to the database.

Here is a scene that will feel familiar. It’s an illustration, not a case study. A team presents an architecture: browser, gateway, services, database. A dashed line surrounds everything behind the gateway and is labeled “trusted zone.” The reviewers nod. The boundary is drawn, the database is in a private subnet, and the gateway checks tokens.

Two sprints later, a product ticket arrives: “show live updates without polling.” The quickest way to do that is a client SDK that syncs straight with a hosted data service, so the engineer adds one. The browser now speaks the database’s protocol. The gateway never sees those calls. The diagram hasn’t changed, and nobody opens it.

This isn’t hypothetical in aggregate. In 2024 independent researchers publicly reported scanning for misconfigured Firebase projects and finding hundreds of sites whose databases were readable by anyone, exposing well over a hundred million records between them. Treat the exact numbers as the researchers’ own claims, but the pattern is the point. When the client talks to the data tier directly, the only boundary left is the database’s own rules. If those rules are left in test mode or written loosely, there’s no boundary at all.

Takeaway: the slide shows where you intended trust to change. The running system shows where it does.


What a boundary actually is

What you get: a definition you can test a design against.

Adam Shostack’s Threat Modeling: Designing for Security describes a trust boundary as a line where privilege levels or security assumptions change, and notes that threats cluster at those lines. Notice what isn’t in that definition: networks. A subnet is a fact about addressing. A boundary is a fact about who decides.

The oldest useful rule here comes from Jerome Saltzer and Michael Schroeder’s 1975 paper, The Protection of Information in Computer Systems. One of their design principles is complete mediation: every access to every object must be checked for authority. Not once at login. Not once at the front door. Every access.

Read that against the diagram. A gateway that checks a token at the edge and then hands off to services that check nothing hasn’t mediated anything past the first hop. It has mediated the entrance. Whether the boundary holds depends on what the second, third, and fourth hops do with the caller.

Their paper has a companion principle, fail-safe defaults: base decisions on permission, not exclusion. In practice, a call that crosses the line without an explicit yes should fail closed.

That gives us a working test. A boundary exists wherever a component will refuse a call that arrives without proof of who is calling and what they’re allowed to touch. If a component accepts the call because of where it came from, no boundary is there, whatever the diagram says.

Takeaway: a boundary is a refusal you can demonstrate, not a line you can draw.


The deputy that did what it was told

What you get: the failure that explains most real boundary breaks.

In 1988 Norm Hardy described the confused deputy. His example was a compiler on a shared system. It held special rights so it could write usage statistics to a protected directory. A user passed it the name of a billing file as an output location, and the compiler, using its own authority, overwrote the file. The user couldn’t have done that directly. The compiler never meant to help. It just couldn’t tell its own authority from the caller’s request.

The trouble is what Hardy called ambient authority: power that comes with the program’s identity or environment rather than being handed over by the caller for one specific job. Any component that has more access than its callers, and acts on caller-supplied names, addresses, or IDs, is a candidate deputy. Gateways, proxies, webhook workers, link previewers, image fetchers, and “backend-for-frontend” layers all fit.

Takeaway: every component that holds more power than its callers is a boundary. Whether it enforces one is a separate question.


A deputy at scale: Capital One and IMDSv2

What you get: a real incident that shows the pattern, and the fix its cloud provider shipped.

In 2019 the US Department of Justice charged a former cloud engineer over the theft of data from Capital One. The company said the breach affected over a hundred million people. Public accounts, including the court record and reporting at the time, describe the technical path in outline. A web application firewall running on a cloud instance had a server-side request forgery (SSRF) flaw. SSRF means an attacker can make a server send requests on their behalf. Here the attacker made the firewall call the instance metadata service, a link-local address that hands out temporary credentials to whatever role the instance holds. Those credentials had broad enough access to reach storage the attacker then read.

Look at what each piece was. The firewall sat on the edge, the very component meant to be the boundary. It held a role, which was ambient authority. It accepted a destination from outside, and it could reach a service that answers anyone who asks from inside. It was a deputy with a key.

AWS responded the same year with a second version of its metadata service. Colm MacCárthaigh’s post, Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with IMDSv2, lays out what changed. A caller first has to obtain a short-lived session token with a PUT request, then present it on every read. Requests carrying a forwarding header are refused, which breaks the open-proxy trick. A hop limit stops responses from crossing out of the host’s network namespace into containers by default.

None of that is a bigger perimeter. Each change adds a check that a confused deputy can’t satisfy by accident. That’s the lesson: when network position stops being enough, the fix is to make the callee ask a harder question.

Takeaway: the incident wasn’t a failure to draw a boundary. It was a boundary that didn’t check anything at the point where it mattered.


Four illusions that pass review

What you get: the patterns to look for, because they recur.

1. The private-subnet pass. “It’s internal, so it’s trusted.” Anything that gets code running inside the subnet shares that trust: a compromised container, an SSRF flaw, a build agent. NIST’s Zero Trust Architecture calls the thing you’re building an implicit trust zone, and the aim of its guidance is to shrink such zones, not to enlarge them. Google’s BeyondProd paper makes the same point from the service side: being on the same network or cluster shouldn’t grant trust between services.

2. The plain-header identity. The gateway validates a token and forwards X-User-Id: 42. Downstream services read it as fact. It’s a string. Anything that can reach that service, including a mis-routed request, an internal tool, or a worker, can set it to anything. The gateway’s check is real, but it stops at the gateway.

3. The unmediated data tier. Query builders, generic GraphQL resolvers, and direct-to-database SDKs put the browser’s input into the storage engine with no domain code in between. The OWASP API Security Top 10 lists broken object-level authorization as its first risk: the caller is authenticated, and nobody checks whether this caller may touch this object. Row-level rules can enforce that, but they’re now your whole boundary, and the cost of a slip is the entire table.

4. The unchecked outbound call. Features that fetch a URL for the user, such as webhooks, link previews, and imports, turn your server into the deputy. Without limits on schemes, destinations, and the credentials that travel along, the server’s reach becomes the caller’s reach.

All four rest on the same mistake. They treat position as proof: inside the network, past the gateway, behind the proxy. Position is where the call is, not who is asking.

Takeaway: if a design is safe only because of where a call comes from, it’s one footing error away from unsafe.


What may cross the line

What you get: the rules for a call that’s allowed through.

The boundary isn’t a wall; things have to cross. The design question is what a crossing looks like. Three properties do most of the work.

A narrow, typed request. Across a boundary, send a business intent with a validated shape, such as “cancel order 8841”, not a query, a path, or a predicate. The less expressive the request, the less a caller can make it say.

Proof carried with the call. The receiving side needs to verify who is calling and on whose behalf. That means cryptographic proof, such as a signed token or a mutually authenticated service identity, rather than a field the sender fills in. Workload identity is a post of its own, so I’ll leave the mechanics for Blog 2; the requirement is what matters here.

A check at every hop that has something to lose. No service should assume the one before it did the job. Where the check sits is a design decision with real trade-offs (that’s Blog 4’s subject), but the rule that it exists at the point of use isn’t negotiable.

This costs something. Every extra check adds latency, a token to mint and verify, and a failure mode when the identity service is down. Developers will feel it too, because “just call the other service” stops being one line. That’s the price of buying back a refusal you can demonstrate. A cheaper design exists, but it’s the one in the dashed box.

Takeaway: make every crossing narrow, provable, and checked where the damage would happen.


Four designs to reject in review

What you get: a short list a reviewer can apply to a ticket on Tuesday morning.

Diagram: the same system drawn two ways. On top, a single dashed box labeled "trusted zone" around gateway, services, and database, with an arrow from the browser straight to the database. Below, a boundary at each hop where a call is checked.

Figure 1. Top: the slide. One zone, one check at the door. Bottom: the same system with a check at each hop that holds something to lose.

Reject these, or send them back with a question:

  1. Trust by network location. Any design where a service skips authentication or authorization because the caller is on the VPC, the cluster network, or a peered account. Ask: what proves who is calling?
  2. The browser or client reaching storage directly. Any ticket where client code builds queries, paths, or filters that a data store executes with no domain service in between. Ask: which component refuses a request it shouldn’t serve?
  3. A deputy acting on unchecked input. Any component that makes downstream or outbound calls using its own credentials, based on an address, ID, or URL from a caller. Ask: whose authority is this call using, and who checked it?
  4. Identity as a plain header. Any service that treats X-User-Id, X-Tenant-Id, or a role header as truth without cryptographic proof. Ask: what stops anything else on the network from setting this?

These aren’t a threat-modeling session or a framework. They’re four questions that change a decision, which is the only reason to ask them. The process for finding more is Blog 8. Zero trust as a constraint on all this is Blog 7. The point here is simpler: if a review can’t reject a design, the boundary on the diagram is decoration.

Takeaway: a boundary that a review can’t reject is a slide.


Terms

TermMeaning
Trust boundaryA point where privilege or security assumptions change, and where a call is checked
Complete mediationEvery access to every object is checked for authority
Ambient authorityPower a component holds by identity or environment, not granted by the caller for one task
Confused deputyA component with more authority than its caller that misuses it on the caller’s input
Implicit trust zoneA region where everything inside is trusted because of where it sits
SSRFServer-side request forgery: making a server send requests for the attacker

3 Questions for Your Architecture Team

  1. If an attacker ran code inside one container in our private network, which internal services could they call without presenting a token tied to a real user or workload?
  2. Does our frontend send validated business requests to a service, or can it reach a data store or storage bucket directly? Which component would refuse a bad request?
  3. Which of our components fetch an address, URL, or ID supplied by a caller, and whose credentials do they use when they do?

Sources: Saltzer and Schroeder, “The Protection of Information in Computer Systems” (1975); Norm Hardy, “The Confused Deputy” (1988); Adam Shostack, Threat Modeling: Designing for Security (2014); AWS Security Blog, IMDSv2 (2019); Google, BeyondProd (2019); NIST SP 800-207 (2020); OWASP API Security Top 10 (2023); US Department of Justice press release on the Capital One case (2019).

Architectural field notes, straight to your inbox.

Essays when they publish. The Daily Radar stays on the site.

Leave a Reply

Discover more from TheTechDialogue

Subscribe now to keep reading and get access to the full archive.

Continue reading