Fundamentalstrustboundariesvalidationzero trustzonesauthorization

Trust Boundaries

A trust boundary is any point where data or control crosses from something you do not control into something you do — and every one of them is a place where an assumption must be re-validated rather than inherited.

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
The correctness of every assumption the code downstream makes: that the user id is really this user, that the amount is a number, that the URL points where it claims, that the caller is allowed.
Attacker & capability
Anyone positioned on the untrusted side of a boundary — an internet client, a compromised service, a malicious dependency, a document retrieved by an agent.
Trust boundary
This lesson is about the boundary itself: where it is, what it must validate, and what goes wrong when it is drawn on a diagram but not enforced in code.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

A boundary is where you stop assuming

Inside one process, code trusts itself: a function that receives an Order object assumes the fields are the right types and the invariants hold, because a constructor enforced them. That assumption is cheap and correct. The bugs happen where the assumption *crosses* into a place it was never established — a JSON body from a browser, a message from a queue, a row from a table another team writes to, a response from an API you do not own.

The rule is simple to state and hard to hold: inherited trust is the vulnerability. When code on the inside treats a value as validated because "the gateway checks that", and the gateway believes "the service validates it", nobody validates it. Both statements are defensible in isolation, which is why boundary bugs survive review.

A boundary is therefore identified by a question, not by a network diagram: *if the thing on the other side were hostile, what would I have to check?* If the answer is "nothing, it is internal", you have found a boundary that is being crossed on faith.

Internet / Browser
untrusted
Fully attacker-controlled. Headers, cookies, body, order of requests, timing — all of it. Client-side validation is a UX feature and provides zero security value.
┃ trust boundary ┃
Browser → Edge: TLS, authentication, request size and rate limits, and removal of any client-supplied header the system later treats as trusted.
Edge / API Gateway
semi-trusted
Terminates TLS, authenticates, rate-limits. Knows *who*, not *whether they may*. Anything it strips or rewrites (like `X-Forwarded-For`) becomes a trusted claim downstream — so it must strip what clients could forge.
┃ trust boundary ┃
Edge → Service: propagate the authenticated principal (not just "a valid service token"), so the service can still decide authorization for *this user*.
Application Services
trusted
Where authorization decisions belong, because only here is there enough context: this user, this resource, this action, this tenant.
┃ trust boundary ┃
Service → Service: authenticate the calling workload (mTLS or signed tokens) and keep carrying the user identity — a service identity alone reduces every internal call to "someone on the network asked".
Data & Secrets
privileged
The database role, the object store, the signing key. The scope of these credentials is the ceiling on the damage of every bug above them.

The boundaries teams forget

The browser → API boundary is well known. The ones that produce incidents are the ones that do not look like boundaries because both sides are "ours": a queue consumer that trusts message contents because your own producer wrote them (until an attacker finds a way to enqueue), a database column populated by an admin tool and later rendered as HTML, a cache whose key is partly user-controlled, a log line that ends up in a dashboard that renders markup.

Two modern ones matter especially. CI/CD is a boundary: code from a fork, a dependency's install script, and a build container all run with the permissions of the pipeline, on the inside of the network — see CI/CD Security. And AI agents create a boundary that did not exist before: content retrieved from a document or a web page enters a context window where it sits next to your instructions, and unless the system treats it as data, the model may act on it. That is the whole of Prompt Injection and Tool Output Is Untrusted.

A useful habit when reading an architecture diagram: mark every arrow with the answer to "who can cause this arrow to carry a value they chose?" Arrows where the answer is "an outsider" are the boundaries; arrows where the answer is "an outsider, two hops upstream" are the ones that get missed.

Boundaries in a system that also runs an agent
boundary 1: authnboundary 2: carry principalboundary 3: scoped roleenqueueboundary 4: message is inputboundary 5: content ≠ instructionsBrowser (untrusted)Fetched page (untrusted)API GatewayApplicationDatabaseQueueAgentWorker
UserLLMAgentToolDataDecisionHumanGuardrail

Validating at the boundary, deciding inside it

Two different jobs happen at a crossing and conflating them is the most common design error. Validation is structural and semantic: is this a well-formed request, is quantity an integer in range, is url a URL with an allowed scheme? It belongs at the edge of the component, as early as possible, and it should reject rather than sanitise, because sanitising guesses at intent.

Authorization is contextual: may *this* principal perform *this* action on *this* resource right now? It cannot be done at the gateway, because the gateway does not know that invoice 101 belongs to tenant B. It must happen where the resource is loaded — which is why the fix for Broken Access Control (IDOR / BOLA) is never "add a check at the edge".

Parse, then validate, then authorize, then act. Each step narrows what the next one has to worry about, and each one is testable in isolation. When a security review asks "where is this validated?" and the answer is a layer diagram rather than a line of code, the boundary is decorative.

Trust inherited across the boundary
1// gateway already authenticated the request, so the service trusts the header
2app.get('/invoices/:id', async (req, res) => {
3 const tenantId = req.header('x-tenant-id') // set by the gateway… and forgeable
4 const invoice = await db.invoice(req.params.id) // no ownership check at all
5 res.json(invoice)
6})
Re-establish identity, then decide authorization where the resource is known
1app.get('/invoices/:id', requireAuth, async (req, res) => {
2 const id = InvoiceId.parse(req.params.id) // 1. parse: reject malformed input
3 const invoice = await db.invoice(id) // 2. load the resource
4 if (!invoice) return res.sendStatus(404)
5 if (invoice.tenantId !== req.principal.tenantId) // 3. authorize with full context
6 return res.sendStatus(404) // 404, not 403: do not confirm existence
7 res.json(invoice)
8})

The vulnerable version reads identity from a header the gateway *happens* to set — a value the service cannot distinguish from one a client supplied, unless the gateway strips it, which nobody verified. The hardened version derives the principal from the authenticated session and makes the ownership decision after loading the resource, which is the only point where tenant, actor and action are all known.

Key points

  • A trust boundary is defined by a question — "if the other side were hostile, what would I check?" — not by a network topology.
  • Inherited trust is the bug: "the gateway checks it" plus "the service checks it" frequently equals nobody checking it.
  • Validation (structural, at the edge) and authorization (contextual, at the resource) are different jobs and cannot substitute for each other.
  • Queues, database columns, CI pipelines and retrieved documents are boundaries that do not look like boundaries.
  • A header the client could set is not an identity, no matter which proxy is supposed to overwrite it.

Boundary control exercise

This lesson uses the shared boundary-control exercise.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Boundary control exercise

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Attacker → find an inherited assumption: a value that one component validates and another consumes without checking.
  2. 2
    Inherited assumption → forge the value: set the header, craft the queue message, poison the cached entry, plant the instruction in a document.
  3. 3
    Forged value → cross the boundary: the downstream component treats it as established fact because "it came from inside".
  4. 4
    Inside → act with the trust of the inner zone: read another tenant, call an internal-only endpoint, use a privileged credential.
Blast radius
  • A single forged field can promote an outsider to the privileges of the innermost zone that trusts it.
  • Because the trust was inherited, the request looks legitimate in logs — the identity is wrong, not the shape.
  • Blast radius equals the scope of the credentials held by the zone the attacker reaches, not the zone they started in.

Defend, detect, recover

One prevention is a single point of security failure. Layer it and make failure observable.

Prevent
  • • Strip and re-set every trusted header at the edge, so no client-supplied value can impersonate an infrastructure claim.
  • • Propagate the authenticated principal end to end (a signed token carrying the user, audience and expiry), so every hop can authorize rather than assume.
  • • Parse into typed values at every entry point — HTTP handler, queue consumer, webhook receiver, tool result handler — and reject what does not fit.
  • • Authenticate workload-to-workload calls (mTLS or signed service tokens) so network position is not an identity.
Detect
  • • Alert when a request arrives at an internal service carrying a principal header without a valid signature.
  • • Log the *source zone* alongside the identity so a call that skipped the gateway is visible in the data rather than only in theory.
  • • Compare gateway request counts with backend request counts; a persistent gap means something is reaching the backend directly.
Respond & recover
  • • Treat any bypass as identity compromise: revoke sessions and tokens that could have been minted through the bypassed path.
  • • Close the network path first (security group, service mesh policy), then fix the trust logic — the network fix is faster and buys time.
  • • Re-derive what the forged identity could reach and audit those resources rather than only the endpoint that was abused.
Residual risk
  • • Boundaries multiply with every new integration, and the newest one is always the least reviewed.
  • • A correct boundary can be undone by a configuration change — a proxy rule, a mesh policy, a firewall exception made during an incident.
  • • Internal tooling (admin panels, support consoles, debug endpoints) routinely sits inside all boundaries with the widest privileges.

Misconceptions

Claim
“It is behind the firewall, so it is fine.”
Reality
A firewall establishes reachability, not identity. Once any host inside is compromised — through SSRF, a dependency, a phished laptop — everything reachable from it is reachable by the attacker.
Claim
“The frontend already validated it.”
Reality
The frontend runs on the attacker's machine. Every request it can make, an attacker can make directly with different values; every check it performs, an attacker can skip.