AI Daddy › Security & Access
Access Control for LLM Systems · Security & Access
Secure access control is essential for multi-user and multi-tenant LLM applications. This chapter covers authentication, authorization, and data isolation…
Access Control for LLM Systems
Secure access control is essential for multi-user and multi-tenant LLM applications. This chapter covers authentication, authorization, and data isolation patterns.
Table of Contents
Access Control Requirements
Security Dimensions
| Dimension | Description | Controls |
|---|
| Authentication | Who is making the request? | API keys, OAuth, JWT |
| Authorization | What can they do? | RBAC, ABAC, policies |
| Isolation | What data can they see? | Tenant filtering, encryption |
| Audit | What did they do? | Logging, compliance reports |
LLM-Specific Concerns
| Concern | Risk | Mitigation |
|---|
| Prompt injection | Bypass access controls | Input validation |
| Data leakage | Cross-tenant exposure | Strict filtering |
| Model output | Expose protected info | Output filtering |
| Context pollution | Inject unauthorized data | Context validation |
Authentication Patterns
API Key Authentication
class APIKeyAuthenticator:
def __init__(self, key_store):
self.key_store = key_store
async def authenticate(self, api_key: str) -> AuthResult:
if not api_key:
return AuthResult(authenticated=False, error="Missing API key")
# Hash the key for lookup
key_hash = self.hash_key(api_key)
# Look up in store
key_record = await self.key_store.get(key_hash)
if not key_record:
return AuthResult(authenticated=False, error="Invalid API key")
if key_record.expired:
return AuthResult(authenticated=False, error="Expired API key")
if key_record.revoked:
return AuthResult(authenticated=False, error="Revoked API key")
return AuthResult(
authenticated=True,
user_id=key_record.user_id,
tenant_id=key_record.tenant_id,
scopes=key_record.scopes
)
def hash_key(self, key: str) -> str:
return hashlib.sha256(key.encode()).hexdigest()
JWT with Scopes
class JWTAuthenticator:
def __init__(self, public_key: str):
self.public_key = public_key
async def authenticate(self, token: str) -> AuthResult:
try:
payload = jwt.decode(
token,
self.public_key,
algorithms=["RS256"],
audience="llm-api"
)
return AuthResult(
authenticated=True,
user_id=payload["sub"],
tenant_id=payload.get("tenant_id"),
scopes=payload.get("scopes", []),
expires_at=datetime.fromtimestamp(payload["exp"])
)
except jwt.ExpiredSignatureError:
return AuthResult(authenticated=False, error="Token expired")
except jwt.InvalidTokenError as e:
return AuthResult(authenticated=False, error=str(e))
Authorization Models
Role-Based Access Control (RBAC)
class RBACAuthorizer:
ROLE_PERMISSIONS = {
"admin": ["*"],
"developer": ["generate", "embed", "fine_tune", "read_metrics"],
"user": ["generate", "embed"],
"viewer": ["read_metrics"]
}
def authorize(self, user: User, action: str) -> bool:
permissions = self.ROLE_PERMISSIONS.get(user.role, [])
if "*" in permissions:
return True
return action in permissions
Attribute-Based Access Control (ABAC)
class ABACAuthorizer:
def __init__(self, policy_engine):
self.policy_engine = policy_engine
async def authorize(
self,
subject: dict, # Who (user attributes)
action: str, # What (operation)
resource: dict, # On what (resource attributes)
context: dict # When/where (environmental)
) -> AuthzResult:
# Evaluate all applicable policies
policies = await self.policy_engine.get_policies(action)
for policy in policies:
result = policy.evaluate(subject, action, resource, context)
if result == PolicyResult.DENY:
return AuthzResult(allowed=False, reason=policy.name)
if result == PolicyResult.ALLOW:
return AuthzResult(allowed=True)
return AuthzResult(allowed=False, reason="No matching policy")
Model-Level Permissions
class ModelAccessControl:
MODEL_TIERS = {
"gpt-4o": ["enterprise", "professional"],
"gpt-4o-mini": ["enterprise", "professional", "starter"],
"claude-3.5-sonnet": ["enterprise"],
"claude-3.5-haiku": ["enterprise", "professional", "starter"]
}
def can_access_model(self, user: User, model: str) -> bool:
allowed_tiers = self.MODEL_TIERS.get(model, [])
return user.tier in allowed_tiers
def get_available_models(self, user: User) -> list[str]:
return [
model for model, tiers in self.MODEL_TIERS.items()
if user.tier in tiers
]
Tenant Isolation
Data Isolation Patterns
class TenantIsolatedVectorStore:
def __init__(self, vector_db):
self.db = vector_db
async def search(
self,
tenant_id: str,
query_embedding: list[float],
top_k: int = 10
) -> list[dict]:
# CRITICAL: Always filter by tenant_id at database level
results = await self.db.search(
query_vector=query_embedding,
top_k=top_k,
filter={"tenant_id": {"$eq": tenant_id}} # Mandatory filter
)
return results
async def insert(
self,
tenant_id: str,
documents: list[dict]
):
# CRITICAL: Always include tenant_id in metadata
for doc in documents:
doc["metadata"]["tenant_id"] = tenant_id
await self.db.insert(documents)
Prompt Isolation
class TenantAwarePromptBuilder:
def build_prompt(
self,
tenant_id: str,
user_query: str,
context: list[dict]
) -> str:
# Verify all context belongs to tenant
for doc in context:
if doc.get("tenant_id") != tenant_id:
raise SecurityError("Cross-tenant context detected")
# Build isolated prompt
return f"""
[Tenant: {tenant_id}]
Context from tenant documents:
{self.format_context(context)}
User query: {user_query}
"""
Cache Isolation
class TenantIsolatedCache:
def __init__(self, cache_backend):
self.cache = cache_backend
def _scoped_key(self, tenant_id: str, key: str) -> str:
return f"tenant:{tenant_id}:{key}"
async def get(self, tenant_id: str, key: str) -> any:
return await self.cache.get(self._scoped_key(tenant_id, key))
async def set(self, tenant_id: str, key: str, value: any, ttl: int = 3600):
await self.cache.set(
self._scoped_key(tenant_id, key),
value,
ttl=ttl
)
API Key Management
Key Lifecycle
class APIKeyManager:
KEY_PREFIX = "llm_"
async def create_key(
self,
user_id: str,
tenant_id: str,
name: str,
scopes: list[str],
expires_in_days: int = 365
) -> APIKey:
# Generate secure key
raw_key = self.KEY_PREFIX + secrets.token_urlsafe(32)
key_hash = self.hash_key(raw_key)
# Store metadata (not the raw key)
key_record = APIKeyRecord(
id=generate_id(),
hash=key_hash,
user_id=user_id,
tenant_id=tenant_id,
name=name,
scopes=scopes,
created_at=datetime.now(),
expires_at=datetime.now() + timedelta(days=expires_in_days)
)
await self.store.save(key_record)
# Return raw key only once (not stored)
return APIKey(
id=key_record.id,
key=raw_key, # Only returned on creation
name=name,
scopes=scopes,
expires_at=key_record.expires_at
)
async def revoke_key(self, key_id: str, reason: str):
await self.store.update(key_id, {
"revoked": True,
"revoked_at": datetime.now(),
"revoke_reason": reason
})
await self.audit_log.log("api_key_revoked", {
"key_id": key_id,
"reason": reason
})
Key Rotation
class KeyRotator:
async def rotate_key(self, old_key_id: str) -> APIKey:
old_key = await self.key_store.get(old_key_id)
# Create new key with same permissions
new_key = await self.key_manager.create_key(
user_id=old_key.user_id,
tenant_id=old_key.tenant_id,
name=f"{old_key.name} (rotated)",
scopes=old_key.scopes
)
# Grace period: old key still works temporarily
await self.key_store.update(old_key_id, {
"deprecated": True,
"deprecated_at": datetime.now(),
"grace_period_ends": datetime.now() + timedelta(days=7)
})
await self.notify_user(old_key.user_id, new_key)
return new_key
Audit and Compliance
Audit Logging
class AuditLogger:
async def log_request(
self,
request: LLMRequest,
response: LLMResponse,
auth: AuthResult
):
audit_entry = {
"timestamp": datetime.now().isoformat(),
"request_id": request.id,
"user_id": auth.user_id,
"tenant_id": auth.tenant_id,
"action": "llm_generate",
"model": request.model,
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"cost": response.cost,
"latency_ms": response.latency_ms,
# Hash content for privacy
"input_hash": self.hash_content(request.prompt),
"output_hash": self.hash_content(response.content)
}
await self.audit_store.append(audit_entry)
Compliance Reports
class ComplianceReporter:
async def generate_report(
self,
tenant_id: str,
start_date: datetime,
end_date: datetime
) -> ComplianceReport:
logs = await self.audit_store.query(
tenant_id=tenant_id,
start=start_date,
end=end_date
)
return ComplianceReport(
tenant_id=tenant_id,
period=(start_date, end_date),
total_requests=len(logs),
unique_users=len(set(l["user_id"] for l in logs)),
models_used=list(set(l["model"] for l in logs)),
total_cost=sum(l["cost"] for l in logs),
data_access_events=self.extract_data_access(logs),
security_events=await self.get_security_events(tenant_id, start_date, end_date)
)
Interview Questions
Q: How do you implement multi-tenant isolation in a RAG system?
Strong answer:
"Multi-tenant isolation requires defense in depth:
Vector database level:
- Every vector includes tenant_id in metadata
- All queries filter by tenant_id at the database level
- Never filter after retrieval (data already leaked to memory)
Cache level:
- All cache keys prefixed with tenant_id
- Semantic cache scoped to tenant
- No cross-tenant cache hits even for identical queries
Prompt level:
- Validate context documents belong to requesting tenant before including
- Never mix context from multiple tenants
Output level:
- Verify response does not contain cross-tenant information
- Output filtering as additional safeguard
Audit:
- Log all access with tenant context
- Monitor for cross-tenant access attempts
The key principle: tenant_id is a mandatory filter at every data access point, not an optional parameter."
Q: How do you manage API keys for an LLM service?
Strong answer:
"Secure API key management:
Creation:
- Generate cryptographically random keys
- Store only the hash, return raw key once
- Associate with user, tenant, scopes, expiration
Validation:
- Hash incoming key, compare to stored hash
- Check expiration and revocation status
- Verify scopes match requested action
Rotation:
- Support key rotation with grace period
- Old key works during transition (7 days)
- Notify users of impending expiration
Security:
- Rate limit failed authentication attempts
- Revoke immediately on suspected compromise
- Audit all key operations
Scopes:
- Fine-grained: model access, operation type, daily limits
- Least privilege by default
The key principle: never store raw keys, support rotation, implement least privilege."
References
Previous: Security Fundamentals