- Add 'memory' scope property mapping with claims: - memory_projects: project-level access filter - memory_visibility: public/private visibility - memory_role: admin/user/portfolio-agent - Add capability groups: llm-users, memory-users, memory-writers - Add service account provisioning for portfolio-agent, memory-agent - Service accounts use client_credentials flow - Add homelab-rbac.md design doc (Authentik-centric model)
17 KiB
17 KiB
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
- Authentik = Policy Decision Point — All access decisions computed in Authentik, embedded in JWT claims
- Services = Policy Enforcement Points — Validate JWT signature via JWKS, trust claims blindly
- Zero Service-Side RBAC — No local user/role tables, no permission logic in services
- 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)
# 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)
# 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)
# 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:
{
"permissions": ["llm:inference", "..."] // Check against required permission
}
Enforcement:
// 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:
{
"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:
// Memory service filters results based on claims
fn filter_results(results: Vec<Doc>, claims: &Claims) -> Vec<Doc> {
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:
{
"policy": "consoleAdmin|readwrite|readonly|<custom>"
}
Enforcement:
- MinIO natively reads
policyclaim viaMINIO_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
# 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
# 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
# 1. Create user via Authentik API
POST /api/v3/core/users/
{
"username": "alice",
"email": "[email protected]",
"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
# 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
# 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 ⬜
- Add
memoryscope property mapping toauthentik-provision.py - Add scope to
poimen-memoryOAuth provider - Update Memory service to read claims instead of local RBAC
Phase 2: Service Accounts ⬜
- Add service account creation to
authentik-provision.py - Create
portfolio-agentservice account - Test client credentials flow
Phase 3: MinIO Fine-Grained ⬜
- Create custom MinIO policies (
portfolio-rw,memory-rw) - Update
minioscope mapping for service accounts - 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 |