A multi-tenant API also ingests through a queue worker — how do you model where the tenant filter is enforced?
answer
- more than one arrow into the store
- who told the worker the org id
- identity laundered across the async hop
- verified credential versus data field
- bind the tenant at enqueue time
basics
~20 sModel every process that reaches tenant-owned data, not just the HTTP handlers, and label each inbound flow with where its tenant identity comes from. A worker that reads the organization id from the message body is trusting caller-influenced data.
solid answer
~50 sI draw every arrow into the shared store, not only the request path: HTTP handlers, the queue consumer, batch jobs, exports and support tooling. Then I label each with the source of its tenant identity — a verified credential, or a field someone supplied. In a connected-HVAC telemetry service the HTTP tier derives the organization from the token while the ingest worker takes it from the message body, so an authenticated low-privilege installer technician can publish work naming another organization and rewrite equipment schedules for sites they do not own. That is tampering plus elevation of privilege, with other sites' equipment availability as the asset. Identity was verified once, flattened into a data field, and carried across an async hop where nothing re-derived it. The fix in modeling terms: bind the tenant at enqueue time from the verified caller and enforce below all callers, treating any organization id in the body as untrusted.
go deeper
Be ready to notice that a background worker reaching the same data as the API is a second way in, and that it needs its own answer to the question of which customer the work belongs to.
Explain how a verified identity becomes an ordinary data field when a request turns into a queue message, and why a consumer reading that field is trusting input rather than a credential.
Demonstrate the enumeration habit: list every process holding credentials to the store, label each flow with its source of tenant identity, and name the concrete cross-tenant write an authenticated low-privilege user could cause.
Own the structural call — enforcement at one layer below all callers, with the tenant context established only from verified identity — and the review discipline that keeps newly added paths from silently escaping it.
## The shape of the problem Tenant isolation is a property of *every* path into tenant-owned data, but a threat model usually only draws one of them. The HTTP request path is visible, has a credential attached, and is what everyone reasons about. The other paths — a queue consumer, a scheduled batch job, an export pipeline, a support tool, a data-migration script — reach the same store and often carry no credential at all. If the tenant filter is enforced per handler, those paths are simply outside the enforcement. ## A concrete design Consider a connected-HVAC telemetry service. Field devices and installer apps push readings and schedule changes; a web tier accepts HTTP requests from authenticated installer technicians; both end up as messages on an internal queue, and a worker consumes the queue and writes to a store shared by every customer site. [installer technician] --> || edge || --> [HTTP tier] --(enqueue)--> ( message queue ) | v [ingest worker] --> ( telemetry + schedule store ) The HTTP tier derives the organization from the verified token. The worker takes the organization id from the message body, because that is the field that happens to be there. Nothing between them re-checks that the id in the body is the one the token proved. ## The threat, stated precisely An authenticated low-privilege installer technician — a legitimate paying customer's employee — publishes (directly, or indirectly through an endpoint that copies body fields into the message) a message carrying another organization's id. The worker writes equipment schedules for sites that customer does not own. In STRIDE terms that is **tampering** (integrity of another org's schedules) and **elevation of privilege** (acting with another tenant's authority), and the asset at stake is not customer records but *availability and physical control of other sites' equipment*. Notice what happened to identity: it was verified once at the HTTP crossing, then flattened into an ordinary data field, carried across an asynchronous hop, and read back by a consumer that treats it as trustworthy because it arrived from an internal queue. That is identity laundering, and the diagram is what makes it visible: the store has more than one inbound arrow, and the arrows do not agree on where identity comes from. ## The modeling move Label every inbound flow to a tenant-owned store with **the source of the tenant identity it carries**, and mark each source as verified or attacker-influenced: | Flow into the store | Source of tenant identity | Trustworthy? | |---|---|---| | HTTP read handlers | Claim in the verified credential | Yes | | HTTP write handlers | Claim in the verified credential | Yes | | Queue consumer | Field in the message body | No — caller-influenced | | Nightly rollup job | Iterates all tenants by design | N/A, but must not emit cross-tenant output | | Support tooling | Operator's own identity, tenant chosen by hand | Needs its own authorization and audit | Anything in the "No" row is a finding you can defend without reading code. Anything you cannot fill in is a path you have not modeled yet — and the most common cause of an empty cell is that the process exists in the deployment but was never drawn. ## How to find the paths you forgot Ask, in order: - Which processes hold credentials to this store? (Not which ones *should* — which ones *do*.) - Which of them run on a schedule, a trigger, or a message rather than a request? - Which of them were written by a different team, or predate the current auth model? - Which read paths produce output that leaves the system — exports, reports, notification emails, webhooks, analytics warehouses — and does the tenant filter travel with that output? Comparing the deployment inventory against the diagram is a cheap and consistently productive step. ## Where enforcement belongs If the filter is applied in each handler, correctness depends on every author remembering. If it is applied once at a layer *below* all callers — a scoped data-access path that refuses to build a query without a tenant context, with that context established from a verified credential and never from request or message content — then the number of places to get right collapses, and the model's job becomes enumerating what bypasses that layer. For the queue, the fix in the same spirit is to bind the tenant at *enqueue* time from the verified caller, sign or otherwise fix that binding as metadata the producer cannot choose freely, and have the worker use only the bound value while treating any organization id in the body as untrusted input. ## Answering the "it is internal" objection Teams say the queue is internal, therefore its contents are trusted. Network position describes where bytes travel, not who chose them. The message content originated with an authenticated tenant caller, so a bug in a producer, a compromised producer, or an endpoint that faithfully copies a body field is enough to forge the organization. Trust the identity binding, not the transport. ## Testing rather than arguing The finding becomes undeniable with a two-tenant test: acting as tenant A, publish work that names tenant B, and check whether B's data changed. Pair it with a structural check — the set of call sites that reach the store versus the set that pass a tenant scope — so the same class of defect is caught on the next path someone adds.
- Looking at the diagram alone, what tells you a path is a finding rather than fine?Any inbound flow to a tenant-owned store whose tenant identity cannot be traced back to a verified credential, and any process that exists in the deployment but does not appear on the diagram at all. Scheduled jobs, migration scripts and support tooling are the usual missing boxes, and they usually hold the broadest credentials.
- The team says the queue is internal, so its messages can be trusted. How do you answer?Network position describes where bytes travel, not who chose their contents. The message originated with an authenticated tenant caller, so a buggy producer, a compromised producer, or an endpoint that faithfully copies a body field is enough to forge the organization id. Trust the identity binding established at enqueue time, not the transport.
- How would you demonstrate this finding rather than argue about it?Run a two-tenant test: acting as tenant A, submit work that names tenant B and check whether B's data changed. Pair it with a structural check — the set of call sites that reach the store versus the set that pass a tenant scope — so the same defect class is caught on the next path someone adds.
saying these in an interview costs you the question
- Diagrams only the synchronous HTTP path
- Treats internal network position as tenant authorization
- Assumes the queue carries the caller's identity automatically
- Fixes the one handler instead of the shared path
- Says message signing proves the organization id is legitimate