# Homelab RBAC Design **Authentik as Universal Identity Provider** — Single source of truth for all identity, groups, and permissions. Services validate JWTs and trust claims; no separate auth systems. ## Design Principles 1. **Authentik = Policy Decision Point** — All access decisions computed in Authentik, embedded in JWT claims 2. **Services = Policy Enforcement Points** — Validate JWT signature via JWKS, trust claims blindly 3. **Zero Service-Side RBAC** — No local user/role tables, no permission logic in services 4. **Claims Are The API** — Each service defines what claims it needs; Authentik provides them --- ## Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ Clients │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ Browser │ │ CLI │ │ Mobile │ │ Agent │ │ CI/CD │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ └────────┼─────────────┼─────────────┼─────────────┼─────────────┼────────────┘ │ │ │ │ │ │ auth_code │ device │ device │ client │ client │ + PKCE │ code │ code │ credentials│ credentials │ │ │ │ │ └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┐ │ ▼ ┌─────────────────────────────────────────────────────────────────────────────────┐ │ AUTHENTIK (Policy Decision Point) │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌───────────────────────────────┐ │ │ │ Users │ │ Groups │ │ User Attributes │ │ │ │ │ │ │ │ │ │ │ │ rock (human) │ │ homelab-admins │ │ memory_projects: ["*"] │ │ │ │ portfolio-sa │ │ llm-admins │ │ memory_visibility: private │ │ │ │ (svc account) │ │ memory-admins │ │ minio_policy: consoleAdmin │ │ │ └─────────────────┘ └─────────────────┘ └───────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────────────┐ │ │ │ Property Mappings (Scope → Claim) │ │ │ │ │ │ │ │ scope: groups → claim: groups (group names list) │ │ │ │ scope: permissions → claim: permissions (computed from groups) │ │ │ │ scope: memory → claims: memory_projects, memory_visibility, │ │ │ │ memory_role (from user attrs + groups) │ │ │ │ scope: minio → claim: policy (MinIO policy name) │ │ │ └──────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────────────┐ │ │ │ JWT Token (signed) │ │ │ │ { │ │ │ │ "sub": "abc123", │ │ │ │ "groups": ["homelab-admins", "memory-admins"], │ │ │ │ "permissions": ["*"], │ │ │ │ "memory_projects": ["*"], │ │ │ │ "memory_visibility": "private", │ │ │ │ "memory_role": "admin", │ │ │ │ "policy": "consoleAdmin" │ │ │ │ } │ │ │ └──────────────────────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────┬────────────────────────────────────────┘ │ JWT (Authorization: Bearer) │ ┌──────────────────────────────┼──────────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ API Gateway │ │ Memory Service │ │ MinIO │ │ │ │ (Poimen) │ │ │ │ 1. Validate JWT │ │ 1. Validate JWT │ │ 1. Validate JWT │ │ 2. Read claims: │ │ 2. Read claims: │ │ 2. Read claims: │ │ - permissions │ │ - memory_* │ │ - policy │ │ 3. Allow/deny │ │ 3. Filter rows │ │ 3. Map to IAM │ │ │ │ │ │ │ │ NO local RBAC │ │ NO local RBAC │ │ NO local RBAC │ └───────────────────┘ └───────────────────┘ └───────────────────┘ ``` --- ## Authentik Configuration ### Groups (in Authentik) | Group | Purpose | `is_superuser` | |-------|---------|----------------| | `homelab-admins` | Universal admin, full access to everything | `true` | | `llm-admins` | LLM service management | `false` | | `memory-admins` | Memory service management | `false` | | `minio-admins` | MinIO storage management | `false` | | `llm-users` | Can call LLM inference (no admin) | `false` | | `memory-users` | Can query memory (no write) | `false` | ### User Attributes (stored on user object) Fine-grained scopes stored as user attributes in Authentik: | Attribute | Type | Example | Purpose | |-----------|------|---------|---------| | `memory_projects` | `string[]` | `["homelab", "portfolio"]` or `["*"]` | Which projects user can access | | `memory_visibility` | `string` | `"public"` or `"private"` | Max visibility level | | `minio_policy` | `string` | `"portfolio-rw"` | Override MinIO policy (optional) | ### Property Mappings (Scopes → Claims) #### 1. `permissions` scope (existing) ```python # Already in authentik-provision.py GROUP_PERMISSIONS = { "homelab-admins": ["*"], "llm-admins": ["llm:read", "llm:write", "llm:inference"], "llm-users": ["llm:inference"], "memory-admins": ["memory:read", "memory:write", "memory:admin"], "memory-users": ["memory:read"], "minio-admins": ["minio:read", "minio:write"], # ... } perms = set() for group in request.user.groups.all(): perms.update(GROUP_PERMISSIONS.get(group.name, [])) return {"permissions": sorted(perms)} ``` #### 2. `memory` scope (NEW - to add) ```python # Memory service claims - reads from user attributes + group membership _MEMORY_EXPR = """ # Default scopes projects = request.user.attributes.get("memory_projects", []) visibility = request.user.attributes.get("memory_visibility", "public") role = "user" # Admin overrides if request.user.ak_groups.filter(name="homelab-admins").exists(): projects = ["*"] visibility = "private" role = "admin" elif request.user.ak_groups.filter(name="memory-admins").exists(): visibility = "private" role = "admin" # Service account specific configs SA_CONFIGS = { "portfolio-agent": { "projects": ["homelab", "portfolio"], "visibility": "public", "role": "portfolio-agent" }, "memory-agent": { "projects": ["*"], "visibility": "private", "role": "authenticated-user" } } if request.user.username in SA_CONFIGS: cfg = SA_CONFIGS[request.user.username] projects = cfg["projects"] visibility = cfg["visibility"] role = cfg["role"] return { "memory_projects": projects, "memory_visibility": visibility, "memory_role": role } """ ``` #### 3. `minio` scope (existing) ```python # Already in authentik-provision.py _POLICY_EXPR = """ # Check for explicit policy override in user attributes explicit = request.user.attributes.get("minio_policy") if explicit: return {"policy": explicit} # Group-based defaults if request.user.ak_groups.filter(name="homelab-admins").exists(): return {"policy": "consoleAdmin"} elif request.user.ak_groups.filter(name="minio-admins").exists(): return {"policy": "readwrite"} else: return {"policy": "readonly"} """ ``` --- ## Service Claim Contracts Each service documents what JWT claims it expects. Authentik must provide them. ### API Gateway (`api.riotpiao.com`) **Required Claims:** ```json { "permissions": ["llm:inference", "..."] // Check against required permission } ``` **Enforcement:** ```go // Gateway just checks if required permission is in claims if !slices.Contains(claims.Permissions, "llm:inference") { return 403 } ``` ### Memory Service (`memory.riotpiao.com`) **Required Claims:** ```json { "sub": "user-id", // Owner for conversation filtering "permissions": ["memory:read", "..."], // Capability check "memory_projects": ["homelab", "..."], // Project filter (or ["*"]) "memory_visibility": "public|private", // Max visibility level "memory_role": "admin|user|..." // Built-in role (optional) } ``` **Enforcement:** ```rust // Memory service filters results based on claims fn filter_results(results: Vec, claims: &Claims) -> Vec { results.into_iter().filter(|doc| { // Project check let project_ok = claims.memory_projects.contains("*") || claims.memory_projects.contains(&doc.project); // Visibility check let visibility_ok = match claims.memory_visibility.as_str() { "private" => true, // Can see everything "public" => doc.visibility == "public", _ => false, }; // Owner check for conversations let owner_ok = doc.resource_type != "conversation" || doc.owner == claims.sub; project_ok && visibility_ok && owner_ok }).collect() } ``` ### MinIO (`minio.riotpiao.com`) **Required Claims:** ```json { "policy": "consoleAdmin|readwrite|readonly|" } ``` **Enforcement:** - MinIO natively reads `policy` claim via `MINIO_IDENTITY_OPENID_CLAIM_NAME=policy` - Looks up policy by name in MinIO's IAM policy store - Built-in: `consoleAdmin`, `readwrite`, `readonly`, `writeonly`, `diagnostics` - Custom policies created via `mc admin policy create` --- ## Service Accounts Service accounts are Authentik users with `type: service_account` and client credentials grant. ### Creating a Service Account ```python # In authentik-provision.py, add to SERVICES dict: "portfolio-agent": { "type": "service_account", # No redirect URIs "grant_types": ["client_credentials"], "client_secret_source": ("portfolio", "portfolio-oidc", "CLIENT_SECRET"), "generate_if_missing": True, "display_name": "Portfolio Agent", "groups": ["llm-users", "memory-users"], # Group membership "attributes": { # User attributes "memory_projects": ["homelab", "portfolio"], "memory_visibility": "public" } } ``` ### Service Account Token Flow ```bash # 1. Service requests token with client credentials curl -X POST https://authentik.riotpiao.com/application/o/token/ \ -d "grant_type=client_credentials" \ -d "client_id=portfolio-agent" \ -d "client_secret=$SECRET" \ -d "scope=openid permissions memory" # 2. Authentik returns JWT with claims { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 300 } # 3. Service uses token to call API curl https://api.riotpiao.com/v1/chat/completions \ -H "Authorization: Bearer eyJ..." ``` --- ## User Onboarding ### Admin Creates User ```python # 1. Create user via Authentik API POST /api/v3/core/users/ { "username": "alice", "email": "alice@example.com", "groups": ["llm-users", "memory-users"], "attributes": { "memory_projects": ["homelab"], "memory_visibility": "public" } } # 2. Set password or send invite POST /api/v3/core/users/{pk}/set_password/ or POST /api/v3/stages/invitation/invitations/ ``` ### Grant Additional Access ```python # Add to group (grants permissions via GROUP_PERMISSIONS mapping) PATCH /api/v3/core/users/{pk}/ { "groups": ["llm-users", "memory-users", "memory-admins"] # Added memory-admins } # Or update attributes for fine-grained scopes PATCH /api/v3/core/users/{pk}/ { "attributes": { "memory_projects": ["homelab", "new-project"], # Added project "memory_visibility": "private" # Upgraded visibility } } ``` ### Offboard User ```python # Deactivate (preserves audit trail) PATCH /api/v3/core/users/{pk}/ {"is_active": false} # Revoke all tokens immediately POST /api/v3/core/users/{pk}/revoke_tokens/ ``` --- ## Implementation Checklist ### Phase 1: Add Memory Claims ⬜ 1. Add `memory` scope property mapping to `authentik-provision.py` 2. Add scope to `poimen-memory` OAuth provider 3. Update Memory service to read claims instead of local RBAC ### Phase 2: Service Accounts ⬜ 1. Add service account creation to `authentik-provision.py` 2. Create `portfolio-agent` service account 3. Test client credentials flow ### Phase 3: MinIO Fine-Grained ⬜ 1. Create custom MinIO policies (`portfolio-rw`, `memory-rw`) 2. Update `minio` scope mapping for service accounts 3. Test bucket-level access --- ## Appendix: Claim Reference | Claim | Source | Services | |-------|--------|----------| | `sub` | User ID (hashed) | All (owner checks) | | `groups` | Group membership | ArgoCD, Grafana | | `permissions` | Computed from groups | API Gateway, Memory | | `memory_projects` | User attr + group | Memory | | `memory_visibility` | User attr + group | Memory | | `memory_role` | User attr + group | Memory | | `policy` | User attr + group | MinIO | | `immich_role` | Group membership | Immich |