Tenant isolation
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 20 min · 5 figures

    Multi-tenancy and isolation

    How a SaaS platform stops one tenant from reading, changing, slowing down or learning about another’s data: the threat model, tenant context, BOLA, PostgreSQL row-level security and its traps, the side doors, sandboxes, and how to prove isolation.

    Multi-tenancy means many customers, called tenants, share one running system. Isolation is the set of walls that keeps each tenant to itself, so that no tenant can read, change, slow down or even learn about another tenant’s data. Sharing makes SaaS cheap; isolation makes it trustworthy, and it has to hold in every query, cache, queue, index, log and support tool, not just at the login screen.

    Picture it first: an apartment block

    Two hundred families share an apartment block in Bengaluru: one lift, one water tank, one mailroom, one caretaker. Sharing keeps the rent affordable. Nobody expects the family in 4B to walk into 4C, read 4C’s post, or run the tank dry.

    Trouble rarely starts at the flat doors. It starts in the mailroom, where a letter lands in the wrong pigeonhole; with the caretaker’s master key; with the family that holds the lift for an hour; and with the renovation in 4B that knocks through a shared wall.

    Map it back:

    • The flats are tenants’ data; the door lock is your authorisation check.
    • The mailroom is every store holding many tenants’ data: caches, queues, search indexes, logs.
    • The master key is a database superuser, a support console or a migration job.
    • The lift is shared capacity: CPU, connections, rate limits.
    • The shared wall is the kernel and host under tenants’ code.

    A safe building needs locks, a careful mailroom, a logged master key and solid walls. So does a multi-tenant system.

    The threat model

    A customer’s first security question is usually some form of “can another customer see my data?” AWS’s tenant isolation whitepaper calls crossing a tenant boundary a “potentially un-recoverable event” for a SaaS business. Five kinds of crossing matter:

    Threat What it looks like Main defence
    Cross-tenant read Tenant B opens tenant A’s invoice or file The tenant in every lookup, enforced again by the database
    Cross-tenant write B edits or deletes A’s record, or inserts a row tagged with A’s ID The same checks on writes; the server, not the client, sets tenant_id
    Learning about others Sequential IDs reveal volumes; a 403 confirms an ID exists Random IDs, one 404 for “not yours” and “not found”
    Noisy neighbour One tenant’s bulk export starves everyone else Per-tenant quotas, rate limits and separate pools
    Escape from shared compute Customer code or an AI agent breaks out to the host and other tenants Real sandboxes, network rules outside them, no shared secrets on hosts

    Who causes these? Mostly your own code: an endpoint that forgets the tenant filter, a shortened cache key, a job that lost its context. Then tenants who sign up for a free plan just to probe you, compromised tenant accounts, and insiders with powerful tools. Design for all four, but expect bugs first.

    Tenancy models in one table

    Before building walls, decide what is shared. The AWS whitepaper names three models:

    Model What is shared Isolation comes from Typical fit
    Silo Nothing per tenant: own database, often own compute or account Infrastructure boundaries and separate credentials A few large or regulated tenants
    Pool Everything: one app fleet, one database, rows tagged with tenant_id Run-time policy: code, database rules, scoped credentials Thousands of small tenants
    Bridge Some layers pooled, some siloed, such as a shared app with a database per tenant A mix Mid-market tiers

    Two of the whitepaper’s tenets shape everything below. Authentication and authorisation are not isolation: passing the login screen says who the user is, not that every later data access stays inside their tenant. And pooled does not mean unisolated: policies applied at run time can isolate shared resources. Azure’s tenancy models guide treats isolation as a spectrum you set per tier of the system; the business side lives in tenancy models: silo, pool and bridge.

    The isolation ladder

    Order the mechanisms by what has to go wrong before one tenant sees another’s data, and you get a ladder:

    1. Application checks. Every query carries WHERE tenant_id = ?. One forgotten filter leaks.
    2. Database-enforced rows. PostgreSQL row-level security (RLS) adds the filter even when the code forgets, so a leak needs a code bug and a gap in the policy or role.
    3. Schema per tenant. Each tenant’s tables live in their own namespace; a leak needs the wrong search_path or a bad grant.
    4. Database per tenant. Separate credentials per tenant; a leak needs the wrong connection.
    5. Cloud account, project or subscription per tenant. The cloud provider’s IAM boundary sits between tenants.
    6. A dedicated cell or stamp. The whole stack, deployments included, runs per tenant.
    The isolation ladder, from a filter in every query to a whole stack per tenant.Notice Each rung up turns a leak from one forgotten line into a rarer, different mistake, and that every rung costs more per tenant.

    In the interview

    Rungs combine, so say “two independent controls”. In a pooled design, the repository adds the tenant to every query and the database enforces it with RLS, so a leak needs two unrelated mistakes.

    Tenant context: derive, carry, enforce

    Every wall needs one answer: which tenant is this request for? Get it wrong and every layer below faithfully enforces the wrong tenant.

    Derive it from the verified identity. The tenant comes from a claim in a token your server has validated, from an OpenID Connect login mapped to a tenant, or from a server-side session. It never comes from a query parameter, the request body or an X-Tenant-ID header on its own. If one user belongs to several tenants, such as an accountant filing GST for 40 clients, a header or subdomain may select a tenant, but the server checks on every request that the user is an active member of it. OWASP’s multi-tenant cheat sheet calls client-supplied tenant IDs “selectors only”.

    Carry it; don’t re-read it. Put the tenant into a request-scoped context object at the edge. Between services, carry it in a signed internal token, or a header your mesh only accepts from authenticated workloads. On a queue, the producer stamps it into the message envelope from its own verified context, and the consumer re-establishes it before touching data.

    Enforce it at every data access. Not in each handler, where someone will forget, but in shared plumbing: a repository that scopes every query, RLS beneath it, and tenant-scoped cloud credentials. The AWS whitepaper says isolation enforcement “should not be left to service developers”.

    Tenant context is derived once from the verified token, then carried, never re-read from client input, to every data access, including asynchronous work.Notice The worker re-establishes the tenant from the message envelope and gets its own scoped credentials; nothing downstream trusts a value the client could set.

    The classic bug: broken object level authorisation

    The most common cross-tenant leak is also OWASP’s number one API risk: API1:2023, Broken Object Level Authorization (BOLA), also called IDOR. An endpoint takes an object ID from the client and acts on that object without checking that the caller may.

    Picture a GST invoicing SaaS serving 20,000 Indian businesses. An accountant at Kaveri Textiles opens her invoice at /invoices/88231, then edits the URL to /invoices/88232. The handler runs:

    SELECT * FROM invoices WHERE id = 88232;

    Invoice 88232 belongs to Sharma Traders, and the page shows their buyer, GSTIN, line items and amounts. Her token was valid and she was allowed to call the endpoint; only the object was wrong. That is why login and role checks never catch BOLA, and why OWASP warns that merely comparing the user ID in the JWT with an ID in the URL covers only a small subset of cases.

    The same request against two handlers. Invoice IDs are sequential across one shared table, so the neighbouring ID usually belongs to someone else.Notice The fix is one predicate whose value comes from the token, and that the safe reply is the same 404 a missing invoice gets.

    Size the damage. At about 3 invoices a day per business, the platform creates 20,000 × 3 = 60,000 invoices a day. A script making 10 requests a second walks 10 × 86,400 = 864,000 IDs a day: 864,000 ÷ 60,000 ≈ 14 days of every tenant’s invoices per day of scraping.

    Sequential IDs leak even without BOLA. A tenant who raises one invoice at 10:00 (ID 88,231) and another at 11:00 (ID 90,731) learns that the platform issues about 2,500 invoices an hour, a number your competitors would love.

    The fixes, most important first:

    • Scope every lookup by tenant in the shared repository: WHERE id = :id AND tenant_id = :tid, with :tid from the context. Make keys and unique constraints per tenant: (tenant_id, …).
    • Return 404, not 403, for another tenant’s object, so the reply does not confirm it exists.
    • Use random IDs (UUIDv4 or prefixed random strings) for anything exposed: defence in depth, not authorisation.
    • Watch for probing. Alert when one principal racks up hundreds of 404s on object IDs, and rate-limit per tenant.

    Row-level security in PostgreSQL

    Application checks fail the day someone forgets one. RLS makes the database apply the tenant predicate to every query on a table, so a forgotten WHERE returns nothing instead of everything.

    ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
    ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
    
    CREATE POLICY tenant_isolation ON invoices
      USING (tenant_id = current_setting('app.tenant_id')::uuid)
      WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid);
    
    -- per request, on a pooled connection
    BEGIN;
    SELECT set_config('app.tenant_id', $1, true);  -- true: this transaction only
    SELECT * FROM invoices WHERE id = $2;          -- RLS adds the tenant predicate
    COMMIT;

    USING decides which rows a query can see, update or delete; WITH CHECK rejects inserts and updates that would write a row tagged with another tenant. With RLS enabled and no matching policy, the PostgreSQL docs say the default is deny. And because current_setting is called without its missing_ok flag, a request that never set the tenant gets an error (a missing setting, or an empty one that fails the ::uuid cast), not someone else’s rows.

    The docs and OWASP’s cheat sheet list the traps:

    • Some roles skip RLS entirely. Superusers and BYPASSRLS roles always bypass it, and a table’s owner does too unless you FORCE ROW LEVEL SECURITY. Serve requests as a low-privilege role that owns nothing, keep the owner for migrations, and fail a deploy check if the request role has rolsuper or rolbypassrls.
    • Pooled connections remember session settings. A plain SET app.tenant_id lasts until the session ends, and a pooled session outlives the request. The next request to borrow that connection and skip its own SET inherits the previous tenant. Use SET LOCAL, or set_config with true, inside a transaction, every time.
    • Views run with their owner’s policies. A view created by a migration role that bypasses RLS shows every tenant’s rows unless it is created with security_invoker (PostgreSQL 15 and later).
    • Constraints see every row. Unique and foreign-key checks bypass RLS, so if invoice_no is unique across the whole table, tenant B learns from a “duplicate key” error that someone already used INV-2026-0042. Make uniqueness per tenant: UNIQUE (tenant_id, invoice_no).
    • New tables start unprotected. RLS is per table. A CI check compares every tenant-scoped table with pg_class.relrowsecurity and pg_policies, and fails when one is missing.
    • Performance. Keep the policy a plain comparison and lead indexes with tenant_id, so the extra predicate costs almost nothing.
    How a session-level setting leaks through a connection pool. Request A sets the tenant and returns the connection; request B borrows it and forgets to set its own.Notice RLS worked exactly as designed: it enforced a tenant, just the wrong one. Scoping the setting to the transaction closes the gap.

    Test through the real role and the real pool

    A test that connects as a superuser makes a correct policy look broken; a test that uses a tidy low-privilege role passes even if production connects as the table owner and skips RLS; a test on a fresh connection never sees a leaked setting. Run isolation tests through the same role, pooler and pooling mode the application uses.

    RLS is a safety net under application checks, not a replacement. It does nothing for caches, search indexes, object storage or exports.

    Stronger walls: schemas, databases, accounts and cells

    Higher rungs replace “remember to filter” with “cannot reach the other tenant at all”. A schema per tenant still has a pooled-session trap: a stale search_path. A database per tenant adds separate credentials and per-tenant restore and deletion, but multiplies connections and migrations. An account per tenant puts the cloud’s IAM wall in between, with its own quotas and bill. A dedicated cell isolates deployments too, so a bad config push reaches one customer, not all of them.

    Strategy Protects against Blast radius of one bug Cost per tenant Operational burden
    App checks only Mistakes that review and tests catch Every tenant in the table Lowest Low, but every engineer must remember
    App checks + RLS A forgotten filter Every tenant, but only after two failures Lowest Policy coverage tests; careful pooling
    Schema per tenant Query bugs that touch the wrong tables Tenants sharing that database Low Migrations × tenants; search_path hygiene
    Database per tenant Query bugs and most credential mix-ups One tenant Medium Thousands of databases to patch and back up
    Account per tenant IAM mistakes and shared quotas One tenant High Account vending and cross-account tooling
    Dedicated cell Bad deploys, noisy neighbours, residency rules One tenant Highest A fleet of stacks to deploy and watch
    Isolation strategies. Lower rows stop more kinds of mistake and cost more per tenant; most platforms use several at once.

    Identity, network, compute and keys

    Data is not the only shared layer. Four more walls matter.

    Tenant-scoped credentials

    A pooled service usually holds credentials that reach every tenant’s objects. Narrow them per request, as the AWS ABAC post shows: the service calls sts:AssumeRole with a TenantID session tag taken from the verified JWT, and the role’s policy uses that tag as a variable, so one role serves any number of tenants:

    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::invoice-pdfs/tenants/${aws:PrincipalTag/TenantID}/*"
    }

    Now even a bug that builds the wrong path gets “access denied” from S3. DynamoDB does the same with a dynamodb:LeadingKeys condition on the partition key. End every prefix with a delimiter: tenants/t4* also matches tenants/t42, just as OWASP’s example acme matches acme-west.

    Network isolation

    Tenant workloads should not reach each other. Kubernetes lets every pod talk to every other by default; its multi-tenancy guide recommends starting from a default-deny network policy that allows only DNS, and warns that policies are silently ignored without a network plugin that enforces them. Block the cloud metadata endpoint and internal subnets from tenant workloads, and enforce those rules outside the tenant’s sandbox, where its own root user cannot delete them.

    Compute isolation for untrusted code

    If customers run code, notebooks, plugins or AI agents on your platform, assume that code is hostile. The Kubernetes guide recommends sandboxing when workloads “are assumed to be malicious”, because containers share the host kernel.

    Where tenant code runs Boundary An escape needs You pay
    Inside your own process The language runtime A bug in your own sandboxing Nothing; unsafe for untrusted code
    Container Namespaces and cgroups on a shared kernel One kernel bug Little; starts in milliseconds
    gVisor A user-space kernel that handles system calls A gVisor bug, then a host bug Slower syscall-heavy code; some incompatibility
    MicroVM (Firecracker) A guest kernel behind hardware virtualisation A hypervisor or VMM bug Reserved guest memory; KVM-capable hosts
    Dedicated node or VM A separate machine Moving sideways through shared control planes Idle capacity per tenant

    Dedicated nodes still share the cluster’s API server, so the guide suggests separate clusters when that matters. The agent platform design sizes a microVM pool.

    Encryption isolation

    Per-tenant keys add a cryptographic wall: bind the tenant ID into each ciphertext and a row copied into another tenant’s context fails to decrypt; a bank holding its own key gets a kill switch; destroying a tenant’s key makes its backups unreadable. See encryption and key management. Your running code still sees plaintext, so keys complement access checks.

    The side doors

    Most real leaks come through doors nobody thought of as doors: shared stores holding many tenants’ data, and back-office paths that never pass the request’s tenant check.

    The front door checks the tenant on every request. The side doors are shared stores the service reads, and back-office paths that skip the request entirely.Notice Every red label is a place where many tenants’ data sits together without the request’s tenant check in front of it.
    • Caches. A key like invoice:88232 breaks the day two tenants share an ID space, or someone caches a rendered dashboard by URL. Put the tenant in every non-global key, t:{tid}:invoice:{id}, and authorise before reading; a correct key is not a permission check. See caching 101.
    • Search indexes and vector stores. A shared index with a tenant filter is fine only if the filter runs inside the search. Post-filtering, where you fetch the 40 nearest neighbours and then drop other tenants’ hits, pulls their documents into your process, where one missed step shows them, and wrecks recall: for a tenant holding 1% of the vectors, 40 × 1% = 0.4 results survive on average. Filter during the search, give large or sensitive tenants their own index or partition, and recheck permissions before results reach a prompt. RAG over company documents and vector databases work through the shared-index-versus-silo choice.
    • Logs, analytics and data-lake exports. A nightly export runs as a privileged role, so RLS does nothing. Tag every row with tenant_id, enforce tenant scope wherever customers can query, and keep request bodies out of logs.
    • Background jobs. A job that runs as “system” and takes its tenant from the payload can be pointed at anyone. Bind the tenant at enqueue time and re-establish it in the worker, as in Figure 2.
    • Object storage. Tenant prefixes with a trailing delimiter, scoped credentials, and short-lived pre-signed URLs issued only after an authorisation check.
    • Shared ML models and LLM caches. A model fine-tuned on several tenants’ data can repeat one tenant’s text to another, so fine-tune per tenant or only on data every tenant agreed to share. Prompt caches leak through timing: a cached prefix returns faster, revealing that someone sent it. Gu et al. detected prompt caches shared across all users at seven LLM API providers; after their October 2024 disclosure, at least five made changes. Scope prompt and semantic caches per tenant.
    • Errors and identifiers. “Invoice belongs to another organisation”, stack traces naming other tenants and sequential IDs all leak metadata. Send the same 404.
    • Admin and support tools. The support console is the master key. Give it no standing access: a time-limited grant to one tenant, tied to a ticket and ideally the customer’s consent, read-only by default and logged where the customer can see it.

    Noisy neighbours are an availability attack

    A tenant who saturates shared capacity reads nobody’s data but still breaks isolation: other tenants’ requests time out. In a GST SaaS, the days before a filing deadline are when one large tenant’s bulk upload competes with thousands of small ones. Limit each tenant on every shared bottleneck (request rate, concurrent jobs, queue depth, connections), queue fairly, and move tenants who outgrow the pool into their own capacity. See noisy neighbours and fairness and the noisy neighbour anti-pattern.

    Mixing models by tier

    You rarely choose one model for everyone. A common shape for an Indian B2B SaaS:

    • Free and small business: pooled app and database, application checks plus RLS, tenant-keyed caches.
    • Mid-market: a shared app tier, a database per tenant, per-tenant keys.
    • Regulated enterprise: a bank or insurer gets a silo: a dedicated cell in an Indian region, its own database, keys it controls, and dedicated compute for any code it runs. RBI’s April 2018 directive requires payment system data to be stored only in India, and banks pass such requirements on to vendors by contract, so residency often settles this tier.

    The trick is one codebase in several shapes. A tenant catalogue records each tenant’s tier and cell, routing sends each request there, and the same tenant context, RLS and tests run everywhere, so a silo is a deployment choice, not a fork. You gain cheap service for the long tail and a hard wall where contracts demand one; you pay by testing and operating several shapes of the same system.

    Testing and proving isolation

    A bank’s security review will not accept “we filter by tenant” without evidence. Build it in layers:

    • Two-tenant tests for every endpoint. Seed tenants A and B with look-alike data, and generate a test for every operation that takes an object ID: B’s token, A’s IDs, expect a 404 and no change. With 140 such operations, both directions make 140 × 2 = 280 tests, cheap enough for every pull request.
    • Static checks. Flag queries on tenant tables without a tenant predicate; OWASP notes that an ORM’s automatic tenant filter misses raw SQL and bulk operations.
    • RLS coverage and role checks in CI and against the deployed configuration, as above.
    • Connection-reuse tests. Run tenant A then tenant B over the same pooled connection, and prove B sees nothing of A.
    • Canary tenants. Seed synthetic tenants whose data carries unique marker strings, and alert if a marker appears in another tenant’s responses, exports, search results or logs.
    • Audit logs that record the verified tenant on every access, with alerts on bursts of denied cross-tenant attempts.
    • Penetration tests and bug bounties aimed at tenant boundaries, with two accounts per tester.

    Going deeper: cells and blast radius

    In a cell-based architecture, each cell is a complete stack serving a slice of tenants, and changes roll out cell by cell. Spread 20,000 tenants over 10 cells and a bad deploy or a cross-tenant bug in one cell reaches at most 20,000 ÷ 10 = 2,000 tenants, and you stop the rollout before the second cell. A dedicated cell brings that number to one.

    Going deeper: a real escape from shared compute

    In August 2021, Wiz researchers Nir Ohfeld and Sagi Tzadik showed how a convenience feature broke tenant isolation in Azure Cosmos DB, research they called ChaosDB. Their walkthrough describes the chain:

    1. The Jupyter Notebook feature ran customers’ code in a container on a shared host.
    2. Code in its C# kernel ran as root.
    3. The firewall rules blocking internal addresses lived inside the container, so root deleted them.
    4. That opened the host’s metadata service and WireServer, and WireServer handed out certificates and private keys.
    5. With those, the local Service Fabric gave up the primary keys of other customers’ databases, which the walkthrough likens to a database root password.

    Microsoft’s MSRC post says the issue was reported on 12 August 2021 and mitigated by turning the notebook feature off for all customers, that its investigation found no customer data accessed by third parties or researchers, and that it told potentially affected customers to regenerate their primary keys.

    The lessons travel. Enforce network rules where the tenant cannot touch them. Keep credentials that work for many tenants off any host running tenant code. Prefer short-lived, narrowly scoped credentials, so a leaked one expires on its own.

    Going deeper: offboarding and deletion

    A tenant leaving is an isolation event too. Put the tenant into a state that rejects new operations, revoke its sessions, API keys and webhooks, then delete its data everywhere: rows, caches, search and vector indexes, object storage, lake exports and backups. In a pool there is no database to drop, so deletion means finding every row by tenant_id. Destroying the tenant’s key handles backups you cannot edit (crypto-shredding, in encryption and key management). Restores need the same care in reverse: restore a shared backup somewhere separate and copy back only the one tenant’s rows.

    Cheat sheet

    Topic Remember it as
    Goal No tenant can read, change, slow down or learn about another
    Tenant source Verified token or session; client values only select, then membership is checked
    Propagation Context object, signed internal token, message envelope; workers re-establish
    BOLA WHERE id = ? AND tenant_id = ?; 404 not 403; random IDs as extra depth
    RLS FORCE, a request role that owns nothing, no BYPASSRLS, SET LOCAL per transaction
    Ladder App checks, RLS, schema, database, account, cell
    Side doors Cache keys, post-filtered vectors, exports, jobs, prefixes, prompt caches, support tools
    Untrusted code Container, then gVisor, then microVM, then dedicated node; network rules outside the sandbox
    Tiers Pool for small tenants, bridge for mid-market, silo for banks
    Proof Two-tenant tests per endpoint, RLS coverage, canary tenants, audit logs

    How to say it in the interview

    “I treat isolation as a property of every data access, not of the login screen. The tenant comes from the verified token; a header can only select among tenants the user belongs to. I carry it in a context object, internal tokens and queue envelopes. Every lookup is scoped by tenant in a shared repository, and PostgreSQL row-level security with a transaction-local setting catches anything that slips, under a role that can’t bypass it. Cache keys, vector indexes, storage prefixes and cloud credentials are tenant-scoped too. Small tenants share a pool; a bank gets its own cell, database and keys in an Indian region. Two-tenant tests on every endpoint and canary tenants prove it.”

    Isn’t a tenant_id filter in every query enough? It is correct but fragile: one forgotten filter in one new endpoint leaks. Add a second, independent control, such as RLS or tenant-scoped credentials, so a leak needs two mistakes, and test both.

    Why not give every tenant its own database? You gain a hard wall and per-tenant restore and deletion. You pay in connections, migrations across thousands of databases and idle capacity. Do it for tenants who need or pay for it; pool the rest.

    Customers can run code on your platform. What changes? Assume the code is hostile. Run it in gVisor or a microVM rather than a plain container, keep multi-tenant credentials off the host, block the metadata endpoint, and enforce network rules outside the sandbox. ChaosDB showed what happens when those rules live inside the container.

    How would you know you had a cross-tenant leak? Canary tenants whose markers must never appear elsewhere, audit logs carrying the verified tenant, and alerts when one principal piles up 404s on object IDs.

    Where to go next

    References and further reading

    1. SaaS Tenant Isolation Strategies (AWS whitepaper, 2020)docs.aws.amazon.comSilo, pool and bridge isolation, the “isolation mindset” tenets, and run-time tenant-scoped credentials for pooled services.
    2. How to implement SaaS tenant isolation with ABAC and AWS IAM (AWS Security Blog)aws.amazon.comSession tags and aws:PrincipalTag/TenantID in IAM policies, dynamodb:LeadingKeys and S3 prefix-per-tenant, with one role for any number of tenants.
    3. Tenancy models for a multitenant solution (Azure Architecture Center)learn.microsoft.comIsolation as a spectrum set per tier, tenants versus deployments (stamps), and how isolation affects security, cost, performance and reliability.
    4. PostgreSQL documentation: Row Security Policiespostgresql.orgDefault deny, USING and WITH CHECK, owners and BYPASSRLS bypassing policies, FORCE ROW LEVEL SECURITY, and covert channels through constraints.
    5. OWASP API Security Top 10 2023: API1 Broken Object Level Authorizationowasp.orgWhy BOLA is the top API risk, example attacks, and prevention: per-object checks, random IDs and tests.
    6. OWASP Multi-Tenant Application Security Cheat Sheetcheatsheetseries.owasp.orgTenant context as a selector, transaction-scoped RLS settings, coverage tests, cache keys, async work, storage prefixes, restores and offboarding.
    7. Kubernetes documentation: Multi-tenancykubernetes.ioNamespaces, RBAC, default-deny network policies, sandboxing untrusted code, node isolation and what stays shared.
    8. ChaosDB explained: Azure’s Cosmos DB vulnerability walkthrough (Wiz Research, 2021)wiz.ioThe researchers’ step-by-step chain from a notebook container to other customers’ database keys.
    9. Update on the vulnerability in the Azure Cosmos DB Jupyter Notebook feature (MSRC, 27 August 2021)microsoft.comMicrosoft’s account: report date, mitigation, investigation findings and the advice to regenerate keys.
    10. Auditing Prompt Caching in Language Model APIs (Gu et al., ICML 2025)arxiv.orgTiming audits that detected prompt caches shared across all users at seven LLM API providers, and the disclosure that followed.