SHADOW AI

Multi-tenancy allows one AI platform to serve many independent users, organizations, teams, or customers while sharing application infrastructure.
This architecture creates a fundamental security requirement:
A tenant must never be able to access, modify, infer, or influence another tenant's data or resources unless an explicitly authorized cross-tenant operation exists.
For an AI platform, tenant isolation is broader than database isolation. Tenant boundaries may exist across:
Authentication
API requests
Databases
Object storage
Vector databases
RAG indexes
AI conversations
Memory
Caches
Queues
Background jobs
Model execution
Logs
Analytics
Notifications
Billing
Temporary files
Browser state
Network resources
Administrative interfaces
A platform can therefore have a perfectly secure database while still suffering from a cross-tenant vulnerability through a cache, object-storage path, vector search, background worker, or AI memory system.
A tenant should be treated as a first-class security boundary.
Example:
Platform
│
├── Tenant A
│ ├── Users
│ ├── Projects
│ ├── Files
│ ├── Conversations
│ ├── AI generations
│ └── Vector data
│
├── Tenant B
│ ├── Users
│ ├── Projects
│ ├── Files
│ ├── Conversations
│ └── Vector data
│
└── Tenant C
├── Users
├── Projects
├── Files
├── Conversations
└── Vector data
The security model must prevent:
Tenant A → Tenant B data
Tenant B → Tenant C data
Tenant C → Tenant A data
even if an attacker knows:
another tenant's identifier
another object's identifier
a file URL
a project identifier
a conversation identifier
a vector-document identifier
an API endpoint
an internal job identifier
Tenant IDs must therefore be treated as authorization attributes, not as security secrets.
Every authenticated request should have a trusted tenant context.
A simplified request context might look like:
Request
↓
Authentication
↓
User identity
↓
Membership lookup
↓
Tenant context
↓
Authorization
↓
Resource access
For example:
type RequestContext = {
userId: string;
tenantId: string;
roles: string[];
permissions: string[];
};
The important rule is:
The application must derive tenant membership from trusted authentication and authorization data rather than trusting arbitrary client-supplied tenant identifiers.
Unsafe conceptual pattern:
POST /api/files
{
tenantId: "tenant-b"
}
if the server simply trusts that value.
Safer conceptual pattern:
Authenticated user
↓
Membership database
↓
Authorized tenant
↓
Server-generated tenant context
↓
Database query
A user may belong to one or multiple tenants.
Example:
User
├── Tenant A → owner
├── Tenant B → member
└── Tenant C → viewer
Therefore authorization should distinguish:
identity ≠ tenant membership ≠ resource permission
A user being authenticated does not automatically mean they can access every tenant.
A robust model can contain:
users
organizations
memberships
roles
permissions
projects
resources
Example relationship:
User
↓
Membership
↓
Tenant
↓
Project
↓
Resource
This supports hierarchical authorization.
A common shared-database architecture is:
Single Database
│
├── users
├── tenants
├── memberships
├── projects
├── files
├── conversations
└── generations
Each tenant-owned record should contain a tenant identifier.
Example:
projects
--------------------------------
id
tenant_id
name
created_at
generations
--------------------------------
id
tenant_id
user_id
project_id
model
status
created_at
The tenant identifier should be part of the authorization boundary.
Instead of conceptually doing:
SELECT *
FROM projects
WHERE id = $1;
the secure model is:
SELECT *
FROM projects
WHERE id = $1
AND tenant_id = $2;
The second condition is critical.
One of the most important multi-tenant controls is object-level authorization.
Suppose:
Tenant A
project_123
Tenant B
project_456
An attacker from Tenant A changes:
project_123
to:
project_456
The API must not return Tenant B's project.
The security decision should be:
Does this authenticated principal
have permission to access
THIS resource
inside THIS tenant?
not simply:
Is the user authenticated?
This protects against insecure direct object reference and broken object-level authorization patterns.
Database-level row-level security can provide an additional defense layer.
Conceptually:
Application authorization
+
Database authorization
=
Defense in depth
For databases that support row-level security, policies can restrict records according to the active tenant context.
Conceptually:
Current tenant = Tenant A
Allowed:
tenant_id = Tenant A
Denied:
tenant_id = Tenant B
tenant_id = Tenant C
This reduces dependence on every individual application query being perfect.
However:
Row-level security should complement application authorization, not replace proper application design.
Tenant identity must survive the entire request lifecycle.
Example:
HTTP Request
↓
API Gateway
↓
Application
↓
Service
↓
Queue
↓
Worker
↓
Database
↓
Object Storage
If tenant context disappears at one stage, isolation can fail.
A job should therefore contain trusted tenant context.
Example:
type GenerationJob = {
jobId: string;
tenantId: string;
userId: string;
projectId: string;
generationId: string;
};
The worker should validate that:
job.tenantId
matches
resource.tenantId
before processing the job.
AI platforms frequently use asynchronous processing.
Example:
User
↓
API
↓
Queue
↓
Worker
↓
AI Model
↓
Storage
A dangerous worker design might process a job using only:
generationId
without verifying its tenant.
A safer design validates the complete ownership chain:
Job
↓
Generation
↓
Project
↓
Tenant
Conceptually:
job.tenant_id
↓
generation.tenant_id
↓
project.tenant_id
All relevant relationships should agree.
Object storage introduces another major tenant boundary.
Example:
bucket/
tenant-a/
project-1/
image.png
tenant-b/
project-7/
image.png
Storage paths can help organization, but paths alone are not authorization.
An attacker should not gain access simply by guessing:
tenant-b/project-7/image.png
The application should authorize access before issuing a download or signed URL.
Recommended conceptual flow:
User request
↓
Authenticate
↓
Authorize resource
↓
Verify tenant ownership
↓
Generate short-lived signed URL
↓
Download
Signed URLs should be:
short-lived
scoped
generated only after authorization
restricted to the required object
protected against unintended reuse
Avoid unnecessarily long expiration periods.
A signed URL should not become a permanent alternative authentication mechanism.
RAG systems create an especially important multi-tenancy problem.
Suppose:
Tenant A documents
Tenant B documents
Tenant C documents
are stored in one vector database.
A naive semantic search could retrieve:
Tenant A query
↓
Global vector search
↓
Tenant B document
This is a severe information-isolation failure.
Every vector search should include a trusted tenant filter.
Conceptually:
query embedding
+
tenant_id = current tenant
↓
vector search
↓
authorized documents only
The tenant boundary must be enforced before or as part of retrieval, not merely after results are returned.
A secure RAG pipeline should look like:
User
↓
Authentication
↓
Tenant authorization
↓
Tenant-specific retrieval
↓
Document authorization
↓
Context construction
↓
AI model
↓
Output policy checks
Not:
User
↓
Global search
↓
Filter results afterward
Filtering after retrieval can still create risks involving:
accidental context leakage
ranking influence
metadata exposure
logging
prompt construction
model inference
The safest approach is to prevent unauthorized data from entering the retrieval result set.
Long-term AI memory must also be tenant-aware.
Example:
Tenant A
└── User 1
└── Memory A
Tenant B
└── User 2
└── Memory B
The system must prevent:
Tenant A request
↓
Memory retrieval
↓
Tenant B memory
Memory should have explicit ownership metadata such as:
tenant_id
user_id
conversation_id
project_id
memory_scope
A memory retrieval operation should enforce those boundaries.
Caches are frequently overlooked.
Suppose an API stores:
cache["project:123"] = project_data
If project identifiers are globally addressable but authorization is not part of the cache design, a second tenant could potentially receive cached data.
A safer conceptual cache key can include the tenant:
tenant:A:project:123
or, preferably, authorization should still be independently checked before returning cached data.
Tenant-aware cache design should apply to:
API responses
AI responses
embeddings
user profiles
permissions
search results
generated media metadata
session state
Session data should never be ambiguously associated with multiple tenants.
A session should identify:
session_id
user_id
tenant_id
created_at
expires_at
When a user switches organizations, the active tenant context should be explicitly updated and re-authorized.
The application should not assume:
same user = same tenant
because one user may belong to several tenants.
Queues can also leak information.
Consider:
queue:
job1 → Tenant A
job2 → Tenant B
job3 → Tenant C
Workers must not assume that a job identifier is enough.
Every job should carry validated ownership context.
Additionally:
avoid exposing internal queue identifiers to clients
authorize job-status requests
validate ownership before cancellation
validate ownership before retry
prevent cross-tenant job substitution
Large platforms may require stronger infrastructure separation.
Possible architecture:
Internet
↓
API Gateway
↓
Tenant-aware services
↓
Internal service network
↓
Data services
Higher-isolation deployments may use:
Tenant
↓
Dedicated namespace
↓
Dedicated service resources
↓
Dedicated database/schema
↓
Dedicated storage
The correct level depends on:
threat model
regulatory requirements
customer expectations
scale
cost
operational complexity
Common multi-tenant database models include:
Database
└── Shared tables
└── tenant_id
Advantages:
lower cost
simpler infrastructure
easier scaling
Risks:
requires strong tenant enforcement
query mistakes can become serious
Database
├── tenant_a_schema
├── tenant_b_schema
└── tenant_c_schema
Provides stronger logical separation but introduces schema-management complexity.
Tenant A → Database A
Tenant B → Database B
Tenant C → Database C
Provides stronger isolation but increases:
operational complexity
migration complexity
monitoring requirements
infrastructure cost
A platform may classify tenants.
Standard tenants
↓
Shared infrastructure
High-isolation tenants
↓
Dedicated infrastructure
This can balance cost and security.
Security testing should consider realistic failure modes.
Tenant A
↓
changes resource ID
↓
Tenant B resource
Expected result:
403 Forbidden
or an equivalent non-disclosing response.
tenant_id=A
↓
change to
tenant_id=B
Expected result:
authorization failure
Tenant A
↓
attempts Tenant B object path
Expected result:
access denied
Tenant A
↓
attempts global vector search
Expected result:
query rejected
or the system automatically applies the trusted tenant boundary.
Tenant A request
↓
cache entry
↓
Tenant B receives same entry
Expected result:
tenant-specific cache isolation
Tenant A
↓
submits Tenant B job ID
Expected result:
authorization failure
AI systems create additional leakage channels.
For example:
Tenant A prompt
↓
retrieval
↓
Tenant B document
↓
LLM
↓
Tenant A response
The model itself may not be the original security failure.
The underlying failure is:
unauthorized context → model
Therefore AI security must protect the entire context pipeline.
Security boundary:
Tenant
↓
Authorization
↓
Retrieval
↓
Context
↓
Model
↓
Output
If multiple tenants share an AI model service, requests should still carry tenant-aware metadata.
Example:
request_id
tenant_id
user_id
model_id
policy_context
The model provider or internal inference layer should not be allowed to accidentally mix conversational state between tenants.
Stateless inference is generally easier to isolate than shared mutable model-session state.
If customer data is used for model improvement, additional controls are required.
A tenant's private data should not automatically become:
global training data
without appropriate authorization, governance, and disclosure.
A safer conceptual separation is:
Production user data
│
├── operational use
│
├── analytics
│
└── optional training pipeline
↓
explicit policy
↓
approved dataset
Training pipelines require provenance and tenant-boundary controls.
Logs can accidentally become a cross-tenant data source.
Example:
logs:
tenant A prompt
tenant B prompt
tenant C prompt
An ordinary tenant user should never gain access to global logs.
Administrative log access should also be:
restricted
audited
justified
monitored
Sensitive AI prompts, uploaded documents, tokens, and generated content should not be unnecessarily written to logs.
Analytics systems can accidentally combine tenant information.
For example:
Tenant A dashboard
↓
global aggregation
↓
Tenant B metrics
Every analytics query should define its intended scope.
Possible levels:
user
project
tenant
platform
Platform-wide analytics should require elevated privileges.
Billing records are tenant-sensitive.
A tenant should only access:
its own invoices
its own subscriptions
its own usage
its own payment metadata
Usage metering should also be tenant-aware:
tenant_id
resource
quantity
timestamp
This prevents one tenant's AI usage from being charged to another tenant.
Notifications should also contain tenant context.
Example:
Tenant A
└── generation completed
Tenant B
└── generation failed
The notification system must prevent:
Tenant A user
↓
Tenant B notification
This includes:
push notifications
SMS
in-app notifications
webhooks
Webhooks should be associated with the correct tenant.
Example:
webhook_id
tenant_id
endpoint
secret_reference
event_type
Event generation should verify:
event.tenant_id
==
webhook.tenant_id
before delivery.
The API gateway can provide an early security layer.
Possible responsibilities:
authentication
request correlation
tenant context
rate limiting
abuse detection
request-size limits
routing
audit metadata
However, downstream services should still perform authorization.
The gateway should not become the only tenant-security mechanism.
Internal requests should carry trusted context.
Conceptually:
API
↓
Generation Service
↓
Storage Service
The storage service should know:
Which tenant?
Which user?
Which resource?
Which permission?
Blindly trusting arbitrary headers from clients is unsafe.
Tenant context should originate from trusted internal authentication mechanisms.
Administrators create special risks.
An administrator may legitimately have access to multiple tenants, but that does not mean:
admin = unrestricted access
Administrative access should still be:
role-based
purpose-limited
audited
monitored
revocable
preferably time-limited for sensitive actions
Support tools should avoid silently impersonating users.
Tenant deletion must be comprehensive.
Deletion may involve:
Database
Storage
Vectors
Caches
Queues
Backups
Search indexes
AI memory
Analytics
Notifications
Webhooks
Logs
A tenant deletion workflow should define:
what is deleted
what is retained
why it is retained
retention duration
who authorized deletion
verification status
Deletion should also prevent orphaned resources.
When a customer leaves the platform:
Tenant disabled
↓
sessions revoked
↓
API credentials revoked
↓
webhooks disabled
↓
jobs stopped/cancelled
↓
access removed
↓
data lifecycle executed
This prevents former users from continuing to access tenant resources.
A serious platform should test tenant boundaries systematically.
Minimum test categories:
Authentication
Authorization
Database
Storage
Vectors
RAG
Memory
Cache
Queues
Workers
APIs
Webhooks
Notifications
Billing
Analytics
Admin tools
Deletion
Backups
A useful test matrix:
| Component | Tenant A → Tenant B test |
|---|---|
| API | Required |
| Database | Required |
| Storage | Required |
| Vector DB | Required |
| RAG | Required |
| Memory | Required |
| Cache | Required |
| Queue | Required |
| Worker | Required |
| Billing | Required |
| Notifications | Required |
| Admin UI | Required |
Automated tests should create at least two tenants:
Tenant A
Tenant B
Then create equivalent resources:
A/project
B/project
Test:
A user → A resource = allowed
A user → B resource = denied
B user → B resource = allowed
B user → A resource = denied
Repeat this across every resource type.
A useful security invariant is:
For every tenant-owned resource R:
request.tenant_id == R.tenant_id
must be true before access is granted.
For hierarchical resources:
request.tenant
=
resource.project.tenant
=
resource.document.tenant
=
resource.vector.tenant
Any mismatch should terminate the operation.
If tenant information is:
missing
malformed
ambiguous
expired
inconsistent
unauthorized
the system should fail closed.
Conceptually:
Unknown tenant
↓
DENY
rather than:
Unknown tenant
↓
fallback to global scope
Global fallback behavior is particularly dangerous in multi-tenant systems.
Security monitoring should detect unusual cross-tenant patterns.
Examples:
User repeatedly requesting foreign resource IDs
Large number of authorization failures
Unexpected tenant changes
Vector searches without tenant filters
Storage access outside tenant prefix
These events can indicate:
application bugs
compromised accounts
malicious users
automation errors
privilege escalation attempts
If cross-tenant leakage is suspected:
Disable affected endpoint, workflow, or integration.
Identify:
which tenants
which resources
which time period
which users
Retain appropriate:
audit logs
request IDs
access logs
authorization decisions
job records
Invalidate:
signed URLs
sessions
API tokens
temporary credentials
as appropriate.
Fix the authorization or isolation failure.
Perform targeted cross-tenant testing.
Determine why existing controls failed.
A secure multi-tenant AI platform can be modeled as:
Internet
│
▼
┌──────────────┐
│ API Gateway │
└──────┬───────┘
│
▼
Authentication
│
▼
Tenant Context
│
▼
Authorization
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Database Storage AI Services
│ │ │
│ │ ┌────┴────┐
│ │ │ RAG │
│ │ │ Memory │
│ │ │ Models │
│ │ └─────────┘
│ │
└──────────────┼──────────────┐
│ │
Queue Cache
│ │
▼ ▼
Workers Tenant Scope
│
▼
Audit / Monitoring
Every branch must preserve the tenant boundary.
The following rules should become platform-wide engineering requirements:
Every tenant-owned resource has an explicit ownership model.
Tenant identity is derived from trusted authentication context.
Client-supplied tenant IDs are never blindly trusted.
Every object access requires authorization.
Database queries enforce tenant scope.
Storage access is tenant-aware.
Vector retrieval is tenant-aware.
RAG context is tenant-aware.
AI memory is tenant-aware.
Cache keys and cache authorization are tenant-aware.
Queue jobs carry validated tenant context.
Workers verify resource ownership.
Notifications are tenant-scoped.
Billing records are tenant-scoped.
Webhooks are tenant-scoped.
Analytics enforce intended scope.
Administrative access is separately controlled.
Unknown tenant context fails closed.
Cross-tenant tests are automated.
Tenant isolation failures trigger incident response.
Multi-tenancy should never be implemented as merely adding:
tenant_id
to database tables.
A true multi-tenant security architecture establishes a consistent boundary across the entire platform:
Identity
↓
Tenant
↓
Authorization
↓
Data
↓
Storage
↓
Retrieval
↓
Memory
↓
Cache
↓
Queue
↓
Worker
↓
AI Model
↓
Output
↓
Audit
The most important invariant is:
A tenant can access only the resources, computations, context, and outputs that belong to that tenant or that have been explicitly authorized for cross-tenant use.
For an AI platform, this boundary must be enforced before sensitive data reaches retrieval systems, AI models, caches, workers, or external services.
A secure multi-tenant platform therefore treats tenant isolation as a system-wide security invariant, not as a feature implemented in one database query.
Comments
Post a Comment