Skip to main content

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​

DimensionDescriptionControls
AuthenticationWho is making the request?API keys, OAuth, JWT
AuthorizationWhat can they do?RBAC, ABAC, policies
IsolationWhat data can they see?Tenant filtering, encryption
AuditWhat did they do?Logging, compliance reports

LLM-Specific Concerns​

ConcernRiskMitigation
Prompt injectionBypass access controlsInput validation
Data leakageCross-tenant exposureStrict filtering
Model outputExpose protected infoOutput filtering
Context pollutionInject unauthorized dataContext 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