Capstone: Secure Multi-Tenant Application
Auth, Tenant Isolation, Encrypted Storage, Secure API, Secrets & Audit
Introduction
This capstone integrates every security principle from the course into a single cohesive application: a secure multi-tenant API where each tenant's data is cryptographically isolated, users authenticate with industry-standard tokens, access is controlled by roles, and all configuration is managed safely. This is the architecture pattern used by SaaS companies handling sensitive customer data.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐ │ Secure Multi-Tenant API │ │ │ │ ┌──────────┐ TLS ┌──────────────────────────────────┐ │ │ │ Client │◄────────►│ API Gateway │ │ │ └──────────┘ │ Rate Limiting + Security Headers │ │ │ └────────────────┬─────────────────┘ │ │ │ │ │ ┌───────────────▼──────────────────┐ │ │ │ Auth Middleware │ │ │ │ JWT validation + RBAC check │ │ │ └───────────────┬──────────────────┘ │ │ │ │ │ ┌──────────────────────────▼──────────────────┐ │ │ │ Application Layer │ │ │ │ ┌──────────────┐ ┌──────────────────┐ │ │ │ │ │ Tenant A Data│ │ Tenant B Data │ │ │ │ │ │ AES-GCM Key A│ │ AES-GCM Key B │ │ │ │ │ └──────────────┘ └──────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ │ ┌───────────▼──────────────┐ │ │ │ Secrets Manager │ │ │ │ (env vars / Vault / AWS) │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘
Secure User Authentication
Install: pip install bcrypt PyJWT pydantic pydantic-settings
# auth.py, bcrypt + JWT authentication
import bcrypt
import jwt
import secrets
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass
from pydantic import SecretStr, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
jwt_secret: SecretStr = Field(..., min_length=32)
jwt_algorithm: str = "HS256"
access_token_ttl_minutes: int = 15
refresh_token_ttl_days: int = 30
settings = Settings()
@dataclass
class User:
user_id: int
tenant_id: str
email: str
password_hash: bytes
roles: list[str]
# In-memory user store (use database in production)
USERS: dict[str, User] = {}
def register_user(email: str, password: str, tenant_id: str) -> User:
"""Register with securely hashed password."""
hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12))
user = User(
user_id=len(USERS) + 1,
tenant_id=tenant_id,
email=email,
password_hash=hashed,
roles=["member"],
)
USERS[email] = user
return user
def authenticate(email: str, password: str) -> User | None:
"""Verify credentials. Returns user or None."""
user = USERS.get(email)
if user and bcrypt.checkpw(password.encode("utf-8"), user.password_hash):
return user
return None
def issue_tokens(user: User) -> dict:
"""Issue JWT access + refresh tokens."""
now = datetime.now(timezone.utc)
secret = settings.jwt_secret.get_secret_value()
access_payload = {
"sub": str(user.user_id),
"tenant_id": user.tenant_id,
"roles": user.roles,
"iat": now,
"exp": now + timedelta(minutes=settings.access_token_ttl_minutes),
"type": "access",
}
refresh_payload = {
"sub": str(user.user_id),
"iat": now,
"exp": now + timedelta(days=settings.refresh_token_ttl_days),
"type": "refresh",
}
return {
"access_token": jwt.encode(access_payload, secret, algorithm=settings.jwt_algorithm),
"refresh_token": jwt.encode(refresh_payload, secret, algorithm=settings.jwt_algorithm),
"token_type": "Bearer",
}
# Demo
user = register_user("alice@tenant-a.com", "SecureP@ss2024!", "tenant-a")
tokens = issue_tokens(user)
print(f"User registered: {user.email} in {user.tenant_id}")
print(f"Access token: {tokens['access_token'][:50]}...")
Expected Output:
User registered: alice@tenant-a.com in tenant-a Access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOi...
Tenant Isolation with RBAC Middleware
# tenant_middleware.py, JWT + RBAC enforcement
import jwt
from functools import wraps
from dataclasses import dataclass
@dataclass
class TokenClaims:
user_id: str
tenant_id: str
roles: list[str]
def decode_token(token: str) -> TokenClaims | None:
"""Verify and decode access token."""
try:
secret = settings.jwt_secret.get_secret_value()
payload = jwt.decode(
token, secret,
algorithms=[settings.jwt_algorithm],
options={"require": ["exp", "sub", "tenant_id"]},
)
if payload.get("type") != "access":
return None
return TokenClaims(
user_id=payload["sub"],
tenant_id=payload["tenant_id"],
roles=payload.get("roles", []),
)
except jwt.InvalidTokenError:
return None
def require_auth(required_role: str | None = None):
"""Decorator: validate JWT + optionally check role."""
def decorator(func):
@wraps(func)
def wrapper(bearer_token: str, *args, **kwargs):
if not bearer_token.startswith("Bearer "):
raise PermissionError("Missing or malformed Authorization header")
token = bearer_token[7:]
claims = decode_token(token)
if not claims:
raise PermissionError("Invalid or expired token")
if required_role and required_role not in claims.roles:
raise PermissionError(f"Role '{required_role}' required")
# Inject claims into function
return func(claims, *args, **kwargs)
return wrapper
return decorator
@require_auth()
def get_my_profile(claims: TokenClaims) -> dict:
return {"user_id": claims.user_id, "tenant": claims.tenant_id, "roles": claims.roles}
@require_auth(required_role="admin")
def delete_user(claims: TokenClaims, target_user_id: str) -> str:
return f"Admin {claims.user_id} deleted user {target_user_id}"
# Test
auth_header = f"Bearer {tokens['access_token']}"
profile = get_my_profile(auth_header)
print(f"Profile: {profile}")
try:
delete_user(auth_header, "user-99")
except PermissionError as e:
print(f"Access denied: {e}") # alice is 'member', not 'admin'
Encrypted Data Storage (Per-Tenant Keys)
# encrypted_storage.py, AES-GCM per-tenant encryption
import os
import secrets
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
# Each tenant gets a unique encryption key (store in secrets manager)
_tenant_keys: dict[str, bytes] = {}
def get_tenant_key(tenant_id: str) -> bytes:
"""Get or generate a tenant-specific AES-256 encryption key."""
if tenant_id not in _tenant_keys:
# In production: retrieve from AWS Secrets Manager / Vault
_tenant_keys[tenant_id] = secrets.token_bytes(32)
return _tenant_keys[tenant_id]
def encrypt_record(tenant_id: str, plaintext: bytes) -> bytes:
"""Encrypt a record with the tenant's key. Returns nonce||ciphertext."""
key = get_tenant_key(tenant_id)
nonce = os.urandom(12)
aad = tenant_id.encode() # Bind ciphertext to this tenant
ciphertext = AESGCM(key).encrypt(nonce, plaintext, aad)
return nonce + ciphertext
def decrypt_record(tenant_id: str, blob: bytes) -> bytes:
"""Decrypt a record. Raises if wrong tenant or tampered."""
key = get_tenant_key(tenant_id)
nonce, ciphertext = blob[:12], blob[12:]
aad = tenant_id.encode()
return AESGCM(key).decrypt(nonce, ciphertext, aad)
# Store sensitive data
record = b'{"ssn": "123-45-6789", "account": "9876"}'
encrypted_a = encrypt_record("tenant-a", record)
print(f"Encrypted ({len(encrypted_a)} bytes): {encrypted_a.hex()[:40]}...")
# Tenant A can decrypt their own data
decrypted = decrypt_record("tenant-a", encrypted_a)
print(f"Decrypted: {decrypted.decode()}")
# Tenant B CANNOT decrypt Tenant A's data (different key + wrong AAD)
from cryptography.exceptions import InvalidTag
try:
decrypt_record("tenant-b", encrypted_a)
except InvalidTag:
print("Cross-tenant access DENIED ✅, tenant-b cannot read tenant-a's data")
Expected Output:
Encrypted (54 bytes): a3f2e1d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0...
Decrypted: {"ssn": "123-45-6789", "account": "9876"}
Cross-tenant access DENIED ✅, tenant-b cannot read tenant-a's dataSecure API Layer & Security Audit
Install: pip install fastapi uvicorn pydantic
# secure_api.py, FastAPI with security headers + input validation
from fastapi import FastAPI, Request, Response, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field, field_validator
import re
app = FastAPI(title="Secure Multi-Tenant API", docs_url=None) # Disable docs in production
security = HTTPBearer()
# ── Security headers middleware ───────────────────────────────
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response: Response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "default-src 'self'"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
# ── Input validation with Pydantic ───────────────────────────
class CreateRecordRequest(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1, max_length=10000)
tags: list[str] = Field(default=[], max_length=10)
@field_validator("title", "content")
@classmethod
def no_null_bytes(cls, v: str) -> str:
if "\x00" in v:
raise ValueError("Null bytes not allowed")
return v.strip()
@field_validator("tags")
@classmethod
def validate_tags(cls, tags: list[str]) -> list[str]:
pattern = re.compile(r"^[a-zA-Z0-9-_]+$")
for tag in tags:
if not pattern.match(tag):
raise ValueError(f"Invalid tag: {tag!r}. Only alphanumeric, hyphen, underscore allowed.")
return tags
# ── Protected endpoint ────────────────────────────────────────
@app.post("/api/records")
async def create_record(
body: CreateRecordRequest,
credentials: HTTPAuthorizationCredentials = Depends(security),
):
claims = decode_token(credentials.credentials)
if not claims:
raise HTTPException(status_code=401, detail="Invalid token")
# Encrypt record with tenant's key
record_data = f"{body.title}:{body.content}".encode()
encrypted = encrypt_record(claims.tenant_id, record_data)
return {
"record_id": secrets.token_hex(16),
"tenant_id": claims.tenant_id,
"encrypted_size": len(encrypted),
"status": "created",
}
print("FastAPI app with:")
print(" ✅ Security headers middleware")
print(" ✅ JWT authentication (Depends)")
print(" ✅ Pydantic input validation (injection prevention)")
print(" ✅ Per-tenant AES-GCM encryption")
print(" ✅ Tenant isolation via claims")
Course Topics Applied in This Capstone
Key Takeaways, Course Complete
- Security is layered, no single control is sufficient; the capstone applies 14 independent security controls
- Cryptography is the foundation, bcrypt for passwords, AES-GCM for data, RSA/ECDHE for key exchange, JWT for tokens
- Tenant isolation requires both logic and cryptography, RBAC prevents cross-tenant access in code; separate encryption keys prevent it at the data layer
- Input validation is non-negotiable, Pydantic on every API boundary; validate type, length, format, and content
- Secrets need lifecycle management, not just storage; generation, rotation, and revocation are equally important
- Security testing belongs in CI/CD, bandit, pip audit, and Hypothesis run automatically on every change
- Monitor and respond, a breach you detect in minutes causes less damage than one you detect in months
Congratulations!
You've completed the Network & Security course. You now have the knowledge and Python skills to build production-grade secure applications, conduct security testing, and operate secure infrastructure.
Continue applying these skills in your projects. Security engineering is a practice, not a destination, the threat landscape evolves, and so should your defenses.