Chapter 3: Security

Security in Multi-Tenant Systems

Understanding security challenges and best practices for multi-tenant architectures - data isolation, access control, and compliance considerations.

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

  1. Tenant Isolation Testing

    • Try to access other tenants' data
    • Test parameter manipulation
    • Verify database queries include tenant filters
  2. Penetration Testing

    • External security assessment
    • Focus on cross-tenant vulnerabilities
  3. 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.