Security in Multi-Tenant Systems
Security is the most critical aspect of multi-tenancy. A single vulnerability can expose one tenant's data to another. Let's understand the security challenges and solutions.
The Fundamental Security Challenge
┌─────────────────────────────────────────────────┐
│ Multi-Tenant System │
│ │
│ Tenant A ←─── Must be INVISIBLE ───→ Tenant B │
│ │
│ But they share: │
│ • Same servers │
│ • Same database │
│ • Same network │
│ • Same application │
└─────────────────────────────────────────────────┘
Types of Tenant Data
Understanding what needs protection:
| Data Type | Examples | Risk Level |
|---|---|---|
| Credentials | API keys, passwords | Critical |
| PII | Names, emails, addresses | High |
| Business Data | Orders, invoices | High |
| Usage Data | Logs, metrics | Medium |
| Configuration | Settings, preferences | Low-Medium |
Common Security Vulnerabilities
1. Broken Access Control
The most common multi-tenant vulnerability:
# VULNERABLE: No tenant check
@app.route('/api/orders/<order_id>')
def get_order(order_id):
return Order.query.get(order_id) # Any tenant can access!
# SECURE: Always filter by tenant
@app.route('/api/orders/<order_id>')
def get_order(order_id):
return Order.query.filter_by(
id=order_id,
tenant_id=current_user.tenant_id # Tenant check
).first_or_404()
2. Tenant ID Manipulation
# Attack: User changes tenant_id in request
POST /api/data
{
"tenant_id": 999, # Attacker's target
"data": "malicious"
}
# Defense: Never trust client-provided tenant_id
# Always derive from authenticated session
3. Insecure Direct Object References (IDOR)
# Predictable URLs
/api/tenants/1/invoices # Tenant 1's invoices
/api/tenants/2/invoices # Tenant 2's invoices (accessible!)
# Defense: Use UUIDs and validate access
/api/tenants/a1b2c3d4/invoices
4. SQL Injection Across Tenants
-- Vulnerable query
SELECT * FROM orders WHERE tenant_id = 1 AND status = '{user_input}'
-- Attack input: '1' OR '1'='1
-- Results in: WHERE tenant_id = 1 AND status = '1' OR '1'='1'
-- This returns ALL tenants' orders!
-- Defense: Parameterized queries
cursor.execute(
"SELECT * FROM orders WHERE tenant_id = ? AND status = ?",
(tenant_id, status)
)
Defense in Depth
Apply security at multiple layers:
┌─────────────────────────────────────────────────┐
│ Application │
│ ┌─────────────────────────────────────────┐ │
│ │ Input Validation Layer │ │
│ │ (Validate all tenant references) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Authorization Layer │ │
│ │ (Check tenant access on every request) │ │
│ └─────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────┐ │
│ │ Data Access Layer │ │
│ │ (Row-level security in database) │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
Row-Level Security (RLS)
Database-enforced tenant isolation:
-- PostgreSQL RLS example
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.current_tenant')::int);
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Now queries automatically filter by tenant
SET app.current_tenant = '123';
SELECT * FROM orders; -- Only sees tenant 123's orders
Pros:
- Defense even if application code has bugs
- Works for all queries, including admin tools
- Database enforces the rule
Cons:
- Database-specific implementation
- Slight performance overhead
- Complex to debug
Encryption Strategies
At-Rest Encryption
Encrypt data stored in the database:
┌─────────────────────────────────────────────┐
│ Encrypted Storage │
├─────────────────────────────────────────────┤
│ Option 1: Transparent Data Encryption │
│ (Database encrypts everything) │
│ - Same key for all tenants │
│ - Protects against disk theft │
├─────────────────────────────────────────────┤
│ Option 2: Application-Level Encryption │
│ (App encrypts before storing) │
│ - Different key per tenant possible │
│ - Protects against database compromise │
├─────────────────────────────────────────────┤
│ Option 3: Customer-Managed Keys │
│ (Tenant provides encryption key) │
│ - Maximum tenant control │
│ - Complex key management │
└─────────────────────────────────────────────┘
Per-Tenant Encryption Keys
┌─────────────────────────────────────────────┐
│ Key Management Service │
├─────────────────────────────────────────────┤
│ Tenant 1 → Key_1 → [Encrypted Data] │
│ Tenant 2 → Key_2 → [Encrypted Data] │
│ Tenant 3 → Key_3 → [Encrypted Data] │
└─────────────────────────────────────────────┘
Benefits:
- Tenant deletion = Key deletion (crypto-shredding)
- Compromised key affects only one tenant
- Meets compliance requirements (GDPR, etc.)
In-Transit Encryption
Client ──[TLS 1.3]──> Load Balancer ──[TLS]──> Application ──[TLS]──> Database
Always encrypt:
- Client to server (HTTPS)
- Between microservices (mTLS)
- Database connections
- Cross-region replication
Authentication & Authorization
Multi-Tenant Authentication Flow
1. User logs in
├── Verify credentials
└── Identify tenant membership
2. Create session/token
├── Include tenant_id claim
└── Sign with server key
3. Every request
├── Validate token
├── Extract tenant_id
└── Apply to all queries
JWT Token Example
{
"sub": "user123",
"tenant_id": "tenant_456",
"roles": ["admin"],
"exp": 1699999999,
"iss": "float16.cloud"
}
RBAC in Multi-Tenant Systems
┌─────────────────────────────────────────────┐
│ Tenant A │
│ ├── Admin Role │
│ │ └── Full access to tenant resources │
│ ├── Developer Role │
│ │ └── Access to development resources │
│ └── Viewer Role │
│ └── Read-only access │
└─────────────────────────────────────────────┘
Roles are scoped TO the tenant, not across tenants.
Tenant A Admin cannot access Tenant B resources.
Audit Logging
Track all tenant-sensitive operations:
{
"timestamp": "2025-01-13T10:30:00Z",
"tenant_id": "tenant_456",
"user_id": "user_123",
"action": "data.export",
"resource": "orders",
"ip_address": "192.168.1.100",
"status": "success",
"details": {
"records_exported": 1500
}
}
Must log:
- All authentication events
- Data access and modifications
- Configuration changes
- Cross-tenant access attempts
- Admin actions
Compliance Considerations
GDPR (Europe)
- Right to data portability (export tenant data)
- Right to erasure (delete tenant completely)
- Data processing agreements
- Data residency requirements
SOC 2
- Access controls documented and tested
- Encryption at rest and in transit
- Audit logging and monitoring
- Incident response procedures
HIPAA (Healthcare)
- Business Associate Agreements
- PHI encryption requirements
- Access audit trails
- Minimum necessary access
Security Testing
Regular Testing Required
-
Tenant Isolation Testing
- Try to access other tenants' data
- Test parameter manipulation
- Verify database queries include tenant filters
-
Penetration Testing
- External security assessment
- Focus on cross-tenant vulnerabilities
-
Code Review
- Review all database queries
- Check authorization on every endpoint
- Audit tenant_id handling
Example Test Cases
def test_cannot_access_other_tenant_data():
# Login as Tenant A
client.login(tenant='A')
# Try to access Tenant B's resource
response = client.get('/api/orders/tenant_b_order_id')
assert response.status_code == 404 # Not found, not 403
# 404 prevents information disclosure about existence
What's Next?
In the next chapter, we'll explore resource management in multi-tenant systems - how to ensure fair resource allocation and prevent noisy neighbor problems.