0056: Policy Scoping and Multi-Tenancy Strategy
Date: 2024-12-10
Status: Accepted
Context
The Citadel platform is a multi-tenant system where multiple organizations (tenants) share the same infrastructure. The policy-service must support policies at different scopes while ensuring strict tenant isolation.
Key requirements:
- Platform operators need to define system-wide policies that apply to all tenants
- Tenant administrators need to define policies specific to their organization
- Default behaviors must exist when no explicit policy matches
- Tenant isolation must be enforced at every layer
Decision
We will implement a three-tier policy scoping model with strict multi-tenancy enforcement.
Policy Scopes
1. System Policies
Definition: Platform-wide policies that are immutable by tenants and managed by platform operators.
Characteristics:
- Stored in
/policies/system/*.cedarfiles - Loaded at service startup
- Cannot be created, modified, or deleted via API by tenants
- Apply to ALL authorization requests regardless of tenant
- Examples:
- Platform security boundaries (e.g., "No access to internal admin resources")
- Compliance requirements (e.g., "Deny all access outside business hours for SOC2 compliance")
- Feature gates (e.g., "Only premium tier can access feature X")
Cedar Example:
// System policy: Block access to internal resources from any tenant
forbid(
principal,
action,
resource
) when {
resource.type == "internal" &&
principal.type == "tenant_user"
};
2. Tenant Policies
Definition: Policies scoped to a specific tenant and managed by that tenant's administrators.
Characteristics:
- Stored in
/policies/tenants/{tenant_id}/*.cedarfiles (MVP: file-based) - Future: Stored in database via
DatabasePolicyStoreadapter - Can be created, modified, and deleted by tenant admins via API
- Only evaluated for requests from that specific tenant
- Scoped by
tenant_idextracted fromX-Enriched-Headers - Examples:
- Custom role definitions (e.g., "Marketing team can only view reports")
- Resource access rules (e.g., "Only managers can approve invoices > $10,000")
- Time-based restrictions (e.g., "Contractors cannot access after 6 PM")
Cedar Example:
// Tenant policy: Only managers can approve large invoices
permit(
principal,
action == Action::"invoice:approve",
resource
) when {
principal.role == "manager" &&
resource.amount <= 10000
};
forbid(
principal,
action == Action::"invoice:approve",
resource
) when {
principal.role != "manager" &&
resource.amount > 10000
};
3. Default/Fallback Policies
Definition: Policies that apply when no explicit policy (system or tenant) matches the request.
Behavior:
- Default decision: DENY (secure by default)
- Can be overridden per-action via explicit default policies
- Logged for audit when applied
Cedar Example:
// Default policy: Allow read access to own resources
permit(
principal,
action == Action::"read",
resource
) when {
resource.owner == principal.id
};
// Implicit: All other requests are denied by default
Multi-Tenancy Enforcement
All policy operations are scoped by tenant_id extracted from X-Enriched-Headers, set by the API gateway after authentication.
Enforcement Rules
| Operation | Enforcement |
|---|---|
| Authorization Check | tenant_id from X-Enriched-Headers determines which tenant policies to load |
| Policy Creation | New policies are scoped to the caller's tenant_id |
| Policy Listing | Only returns policies for the caller's tenant_id |
| Policy Update/Delete | Only allowed for policies within the caller's tenant_id |
Header Extraction
X-Enriched-User-ID: user-123
X-Enriched-Tenant-ID: tenant-456
X-Enriched-Roles: admin,user
X-Enriched-Email: user@example.com
The policy-service extracts X-Enriched-Tenant-ID and uses it to:
- Filter which tenant policies to load
- Populate the
context.tenant_idin Cedar evaluation - Enforce that cross-tenant policy access is forbidden
Policy Evaluation Order
┌─────────────────────────────────────┐
│ 1. System Policies (DENY) │ ← Explicit denies take precedence
├─────────────────────────────────────┤
│ 2. Tenant Policies │ ← Tenant-specific rules
├─────────────────────────────────────┤
│ 3. System Policies (PERMIT) │ ← System-wide permits
├─────────────────────────────────────┤
│ 4. Default Policies │ ← Fallback behavior
├─────────────────────────────────────┤
│ 5. Implicit DENY │ ← Secure by default
└─────────────────────────────────────┘
Cedar semantics: In Cedar, forbid policies always override permit policies. This natural behavior supports our security model.
Consequences
Positive
- Tenant isolation: Strict enforcement prevents cross-tenant policy access
- Operator control: System policies ensure platform-wide security
- Tenant flexibility: Tenants can customize policies for their needs
- Secure defaults: Deny-by-default prevents accidental exposure
- Auditability: Clear policy hierarchy makes decisions traceable
Negative
- Policy debugging: Multiple tiers can make it harder to understand why a request was denied (mitigated by detailed decision logging)
- File management: File-based tenant policies require deployment for changes (mitigated by future
DatabasePolicyStore) - Header trust: Relies on API gateway setting X-Enriched-Headers correctly (mitigated by gateway validation)
Future Enhancements
- Policy inheritance: Allow tenants to extend/override system templates
- Policy groups: Bundle related policies for easier management
- Policy simulation: "What-if" API to test policy changes before deployment