diff --git a/docs/architecture/SHARED_INFRASTRUCTURE_ARCHITECTURE.md b/docs/architecture/SHARED_INFRASTRUCTURE_ARCHITECTURE.md index 61468ee..1cc6c4e 100644 --- a/docs/architecture/SHARED_INFRASTRUCTURE_ARCHITECTURE.md +++ b/docs/architecture/SHARED_INFRASTRUCTURE_ARCHITECTURE.md @@ -11,8 +11,8 @@ ┌─────────────────────────────────────────────────────────────┐ │ Application Stacks │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │Authentik │ │ Gitea │ │ Future │ │ Future │ │ -│ │ │ │ │ │ Stack │ │ Stack │ │ +│ │Authentik │ │ Gitea │ │ Organizr │ │ Future │ │ +│ │ │ │ │ │ │ │ Stack │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └───────┼─────────────┼──────────────┼──────────────┼─────────┘ @@ -33,7 +33,8 @@ │ Databases:│ │ DB 0: Cache │ │ - auth │ │ DB 1: Auth │ │ - gitea │ │ DB 2: Gitea │ - │ - future │ │ DB 3-15: .. │ + │ - organizr│ │ DB 3-15: .. │ + │ - future │ │ │ └───────────┘ └─────────────┘ ``` @@ -77,6 +78,7 @@ |---------------|-------------|------|---------| | `authentik` | Authentik | `authentik_user` | User/group/policy storage | | `gitea` | Gitea | `gitea_user` | Git repos, users, issues | +| `organizr` | Organizr | `organizr_user` | Dashboard configuration and user data | | `future_app1` | TBD | `app1_user` | Reserved | | `future_app2` | TBD | `app2_user` | Reserved | diff --git a/docs/reference/CONTAINERS.md b/docs/reference/CONTAINERS.md index 1af1f23..ccdf43c 100644 --- a/docs/reference/CONTAINERS.md +++ b/docs/reference/CONTAINERS.md @@ -46,19 +46,21 @@ PostgreSQL Shared is a centralized PostgreSQL 17 database server providing isola | **Resource Limits** | None | | **GPU Required** | No | | **Dependencies** | docker-dataplane network | -| **Databases** | `authentik` (Authentik SSO), `gitea` (Git hosting), `postgres` (default/admin) | -| **Database Users** | `authentik_user`, `gitea_user`, `postgres` (superuser) | +| **Databases** | `authentik` (Authentik SSO), `gitea` (Git hosting), `organizr` (Organizr dashboard), `postgres` (default/admin) | +| **Database Users** | `authentik_user`, `gitea_user`, `organizr_user`, `postgres` (superuser) | | **Health Check** | `pg_isready -U postgres` (30s interval) | | **Backup Strategy** | `/backups` volume for pg_dump exports | -**Initialization**: Databases and users are created manually after first deployment: -```bash -docker exec -i postgres-shared psql -U postgres <<'EOF' - CREATE DATABASE authentik; - CREATE USER authentik_user WITH PASSWORD ''; - GRANT ALL PRIVILEGES ON DATABASE authentik TO authentik_user; -EOF -``` +**Initialization**: Databases and users for `authentik` and `gitea` are created by the `postgres-init.sh` script. + +**Adding Organizr Database**: + +1. **Generate a secure password** for the `organizr_user`. +2. **In Portainer, navigate to the `postgres-shared` service.** +3. **Go to the "Env" tab and add a new environment variable:** + * **Name:** `ORGANIZR_DB_PASSWORD` + * **Value:** *Your generated password* +4. **Redeploy the `postgres-shared` service.** This will trigger the `postgres-init.sh` script to create the `organizr` database and user. --- @@ -241,15 +243,27 @@ Organizr serves as a comprehensive unified dashboard that consolidates all homel | **Network Mode** | Bridge | | **Restart Policy** | `unless-stopped` | | **Volume Mounts** | `~/docker-data/organizr:/config` | -| **Environment** | Built-in (no custom env vars) | +| **Environment** | `DB_TYPE=pgsql`, `DB_HOST=postgres-shared`, `DB_PORT=5432`, `DB_NAME=organizr`, `DB_USER=organizr_user`, `DB_PASS=${ORGANIZR_DB_PASSWORD}` | | **Resource Limits** | None | | **GPU Required** | No | -| **Dependencies** | None (integrates with other services via API) | -| **Database** | SQLite at `/config/db/organizrDashboardDb.db` | +| **Dependencies** | PostgreSQL Shared | +| **Database** | PostgreSQL on `postgres-shared` (database `organizr`) | | **Database Size** | ~5-10MB (typical) | | **Integrated Services** | Jellyfin, Netdata, Uptime Kuma | | **Authentication** | Internal (supports SSO, Plex OAuth, LDAP) | +**Configuration**: + +1. **In Portainer, navigate to the `organizr` stack.** +2. **Go to the "Env" tab and ensure the following environment variables are set:** + * `DB_TYPE=pgsql` + * `DB_HOST=postgres-shared` + * `DB_PORT=5432` + * `DB_NAME=organizr` + * `DB_USER=organizr_user` + * `DB_PASS`: This should be a secret. Create a secret in Portainer named `ORGANIZR_DB_PASSWORD` and set its value to the password you generated for the `organizr_user`. +3. **Redeploy the `organizr` stack.** + --- ## Optimization Layer diff --git a/plans/active/security-implementation-plan.md b/plans/active/security-implementation-plan.md index 0e94283..c3ab2fd 100644 --- a/plans/active/security-implementation-plan.md +++ b/plans/active/security-implementation-plan.md @@ -1,1374 +1,1106 @@ -# Security Implementation Plan: Google OAuth SSO for Homelab Infrastructure +# Security Implementation Plan v2.0: Google OAuth SSO for Homelab Infrastructure -**Version:** 1.1 (Revised Scope) -**Date:** 2025-11-15 -**Status:** Research & Planning Phase -**Revision:** Focused scope on external-facing services, FastAPI native OIDC for core-api +**Version:** 2.0 (Post-Failure Analysis & Comprehensive Revision) +**Date:** 2025-11-20 +**Status:** Ready for Implementation +**Previous Version:** 1.1 (Failed - Redirect loops and high memory usage) + +## Document Change Log + +| Version | Date | Changes | Author | +|---------|------|---------|--------| +| 1.0 | 2025-11-10 | Initial plan | - | +| 1.1 | 2025-11-15 | Scope revision | - | +| 2.0 | 2025-11-20 | Post-failure analysis, redirect loop fixes, memory optimization, milestone checkpoints | Claude Code | + +--- ## Executive Summary -This document outlines a comprehensive security architecture to implement Single Sign-On (SSO) across all homelab services using Google OAuth (Google Workspace + Gmail accounts). The proposed solution uses **Authentik** as a central Identity Provider (IdP) that federates with Google for authentication, then provides OIDC/SAML/LDAP to downstream services. +This document is a **complete revision** of the Authentik SSO implementation plan after a failed first attempt that resulted in infinite redirect loops and excessive memory usage (20-30% of system RAM). -**Critical Security Gap Identified:** The core-api service currently has **NO authentication**, making it a severe security risk if exposed publicly. This must be addressed immediately as part of the SSO implementation. +**What Went Wrong (v1.0 Deployment):** +- ✗ Infinite redirect loops between NPM, Authentik, and Google OAuth +- ✗ `auth.schweitz.net` was protected by forward auth, creating self-referential loop +- ✗ Mixed hostname/IP/domain addressing caused OAuth callback failures +- ✗ Missing `AUTHENTIK_HOST` environment variable led to incorrect internal redirects +- ✗ Cookie domain not configured, preventing cross-service SSO +- ✗ Excessive memory usage (~3-5GB) due to separate PostgreSQL and Redis instances +- ✗ No rollback checkpoints, making recovery difficult -**Key Benefits:** -- Single login for all services using Google accounts -- Centralized user management and access control -- Support for both Google Workspace and Gmail accounts -- Eliminates password fatigue and scattered credentials -- Enables secure public exposure of services via api.schweitz.net -- Provides audit trail and access logging +**What's Different in v2.0:** +- ✅ Detailed redirect loop prevention strategies +- ✅ Strict hostname/URL consistency requirements +- ✅ Memory optimization with shared PostgreSQL/Redis (target: <1GB total) +- ✅ **Milestone-based deployment with rollback points** +- ✅ **Configuration versioning at each step** +- ✅ **Validation tests before proceeding to next milestone** +- ✅ **Progress tracking within this document** +- ✅ Embedded outpost instead of separate container (simpler) + +**Critical Success Factors:** +1. `auth.schweitz.net` must **NEVER** have forward auth enabled +2. `AUTHENTIK_HOST=https://auth.schweitz.net` must be set correctly +3. `AUTHENTIK_COOKIE_DOMAIN=.schweitz.net` for cross-service SSO +4. Test each service individually before moving to the next +5. Create backup snapshots at every milestone +6. Update this document with progress and issues as we go --- -## 0. Scope Definition & Architecture Decision +## Table of Contents -### 0.1 Implementation Scope +- [Section 0: Failure Analysis & Lessons Learned](#section-0-failure-analysis--lessons-learned) +- [Section 1: Prerequisites & Environment Validation](#section-1-prerequisites--environment-validation) +- [Section 2: Resource Optimization Strategy](#section-2-resource-optimization-strategy) +- [Section 3: Redirect Loop Prevention](#section-3-redirect-loop-prevention) +- [Section 4: Implementation Milestones](#section-4-implementation-milestones) +- [Section 5: Testing & Validation](#section-5-testing--validation) +- [Section 6: Troubleshooting Guide](#section-6-troubleshooting-guide) +- [Section 7: Rollback Procedures](#section-7-rollback-procedures) +- [Section 8: Progress Tracking](#section-8-progress-tracking) -This plan focuses **only** on user-facing services with external domain mappings, plus critical components requiring public access: +--- -**✅ IN SCOPE:** -- **core-api** (api.schweitz.net) - CRITICAL: Currently no auth, public exposure needed -- **Nextcloud** (cloud.schweitz.net) - File storage and collaboration -- **Jellyfin** (media.schweitz.net) - Media server -- **Gitea** (git.schweitz.net) - Git hosting -- **Open WebUI** (ai.schweitz.net) - AI interface -- **Organizr** (home.schweitz.net) - Unified dashboard -- **code-server** (code.schweitz.net) - VS Code in browser (host service) +## Section 0: Failure Analysis & Lessons Learned -**❌ OUT OF SCOPE:** -- **Infrastructure tools** (Portainer, Uptime Kuma, Netdata, Headscale, Watchtower) - Accessible via core-api endpoints if needed, no direct public exposure required -- **Data providers** (Ollama, Qdrant, future stack backends) - Internal-only services -- **Local services** (Samba, NPM admin) - LAN-only access +### 0.1 What Happened in v1.0 Deployment -**Rationale:** -- Infrastructure tools can be proxied through authenticated core-api endpoints when needed -- Reduces implementation complexity and attack surface -- Focuses SSO on end-user services with clear external access requirements -- Maintains security without over-engineering +**Timeline:** +- Deployed Authentik stack with separate PostgreSQL and Redis +- Configured forward auth on ALL NPM proxy hosts (including auth.schweitz.net) +- Attempted to access services → immediate redirect loops +- Consumed 20-30% of system RAM (~3-5GB) +- Rolled back to working state (commit: `cb428a8`) -### 0.2 Core-API Authentication Strategy: FastAPI Native OIDC +**Root Causes Identified:** -**Decision:** Implement OIDC token validation **directly in FastAPI** rather than NPM forward auth. - -**Advantages:** -- Native FastAPI dependency injection (`Depends`) -- Fine-grained endpoint-level authorization -- Better integration with API documentation (OpenAPI/Swagger) -- No reliance on HTTP headers from proxy -- Standard OAuth2 bearer token authentication -- Easier to test and maintain - -**Implementation:** -```python -from fastapi import Depends, Security, HTTPException -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -import jwt -from jwt import PyJWKClient - -# OIDC configuration from Authentik -OIDC_ISSUER = "https://auth.schweitz.net/application/o/core-api/" -JWKS_URL = f"{OIDC_ISSUER}/jwks/" - -security = HTTPBearer() -jwks_client = PyJWKClient(JWKS_URL) - -async def get_current_user( - credentials: HTTPAuthorizationCredentials = Security(security) -) -> dict: - """Validate OIDC token and return user claims""" - token = credentials.credentials - try: - signing_key = jwks_client.get_signing_key_from_jwt(token) - payload = jwt.decode( - token, - signing_key.key, - algorithms=["RS256"], - audience="core-api", - issuer=OIDC_ISSUER - ) - return payload - except jwt.InvalidTokenError as e: - raise HTTPException(status_code=401, detail="Invalid authentication") - -# Usage in endpoints -@app.get("/infrastructure/services") -async def get_services(user: dict = Depends(get_current_user)): - # user contains email, name, groups from token - return services +#### Issue 1: Self-Referential Authentication Loop +``` +User → https://api.schweitz.net + → NPM: "Need auth, redirect to https://auth.schweitz.net" + → https://auth.schweitz.net + → NPM: "Need auth, redirect to https://auth.schweitz.net" ← LOOP! ``` -**Public Endpoints:** -- Health check (`/health`) - Unauthenticated -- OpenAPI docs (`/docs`, `/openapi.json`) - Unauthenticated (or optionally protected) -- Static files (`/static/*`) - Protected via NPM forward auth OR require auth token +**Why it happened:** NPM configuration had forward auth enabled on `auth.schweitz.net` itself. -**Protected Endpoints:** -- All `/infrastructure/*` - Require valid OIDC token -- All `/v1/*` (AI endpoints) - Require valid OIDC token +**Fix:** Remove forward auth from `auth.schweitz.net` NPM proxy host configuration. + +#### Issue 2: Hostname/URL Discrepancies +``` +External: https://auth.schweitz.net/source/oauth/callback/google/ +Internal: http://localhost:9000/source/oauth/callback/google/ +Outpost: http://192.168.86.149:9001/... +``` + +**Why it happened:** +- Mixed use of localhost, IP addresses, and domain names +- `AUTHENTIK_HOST` environment variable not set +- Authentik didn't know its external URL + +**Fix:** +- Set `AUTHENTIK_HOST=https://auth.schweitz.net` +- Use consistent container names (not localhost/IP) in Docker configs +- Use external URLs for all OAuth callbacks + +#### Issue 3: Cookie Domain Mismatch +**Why it happened:** Authentik didn't set cookie domain, so cookies were scoped to individual hosts, not shared across `*.schweitz.net`. + +**Fix:** Set `AUTHENTIK_COOKIE_DOMAIN=.schweitz.net` + +#### Issue 4: Excessive Memory Usage +**Why it happened:** +- Separate PostgreSQL instance for Authentik (~1GB) +- Separate Redis instance for Authentik (~100MB) +- No resource limits on server/worker containers + +**Fix:** +- Use shared PostgreSQL (`postgres-shared:5432`) +- Use shared Redis (`redis-shared:6379` DB 0) +- Set memory limits: 512M server, 384M worker + +### 0.2 Evidence from NPM Backup (2025-11-19) + +All 11 NPM proxy hosts had **identical forward auth configuration**: +```nginx +auth_request /outpost.goauthentik.io/auth/nginx; +error_page 401 = @authentik_proxy_signin; + +location @authentik_proxy_signin { + return 302 /outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri; +} + +location /outpost.goauthentik.io { + proxy_pass http://192.168.86.149:9001/outpost.goauthentik.io; +} +``` + +**Problem:** This configuration was applied to ALL hosts, including `auth.schweitz.net`. + +### 0.3 Lessons Learned + +1. **Never protect the authentication server with authentication** - Seems obvious in retrospect, but easy to miss +2. **Test incrementally** - We tried to enable all 11 services at once +3. **Document assumptions** - We assumed Authentik would auto-detect external URLs +4. **Create rollback points** - Had to manually restore NPM config from backup +5. **Monitor resource usage** - Didn't realize memory consumption until it was running +6. **Read the docs thoroughly** - `AUTHENTIK_HOST` is a critical but easy-to-miss setting --- -## 1. Current State: Service Inventory & Authentication Capabilities +## Section 1: Prerequisites & Environment Validation -### 1.1 In-Scope Services (External-Facing) +### 1.1 System Specifications -| Service | Type | Port | Domain | Current Auth | SSO Support | Integration Method | -|---------|------|------|--------|--------------|-------------|-------------------| -| **core-api** | API | 8083 | api.schweitz.net | **NONE** ❌ | ✅ | FastAPI native OIDC | -| **Nextcloud** | App | 8082 | cloud.schweitz.net | MariaDB | ✅ | OIDC (user_oidc app) | -| **Jellyfin** | App | 8096 | media.schweitz.net | Built-in | ✅ | OIDC (sso plugin) | -| **Gitea** | App | 3002 | git.schweitz.net | PostgreSQL | ✅ | OIDC (native) | -| **Open WebUI** | App | 82 | ai.schweitz.net | Built-in | ✅ | OIDC (native) | -| **Organizr** | Dashboard | 9999 | home.schweitz.net | Built-in | ⚠️ | Forward auth (NPM) | -| **code-server** | IDE | 8084 | code.schweitz.net | Password | ⚠️ | Forward auth (NPM) | +| Component | Specification | Notes | +|-----------|---------------|-------| +| CPU | Intel i7-6700 (4C/8T) | ~19% usage for Authentik (1.5 cores) | +| RAM | 16GB total | Target: <1GB for Authentik (<6.25% of total) | +| Storage (SSD) | 489GB | ~3GB needed for Authentik data | +| Storage (HDD) | 3.7TB | Not used by Authentik | +| Network | docker-dataplane | All services on unified network | + +**Available Resources:** +- RAM: 16GB - Current usage = ~6-8GB free +- Authentik target: 896MB (5.6% of total RAM) ✓ Acceptable + +### 1.2 Shared Infrastructure Status + +**PostgreSQL Shared** (`postgres-shared:5432`): +```bash +# Verify connectivity +docker exec postgres-shared pg_isready + +# Verify authentik database exists +docker exec postgres-shared psql -U postgres -c "\l" | grep authentik + +# Test connection as authentik_user +docker exec postgres-shared psql -U authentik_user -d authentik -c "SELECT version();" +``` + +**Expected output:** +``` +authentik | authentik_user | UTF8 | ... +PostgreSQL 16.x on x86_64-pc-linux-musl... +``` + +**Redis Shared** (`redis-shared:6379`): +```bash +# Verify connectivity +docker exec redis-shared redis-cli PING + +# Check DB 0 is available (Authentik allocation) +docker exec redis-shared redis-cli -n 0 INFO keyspace + +# Test write/read +docker exec redis-shared redis-cli -n 0 SET test:authentik "connection_ok" +docker exec redis-shared redis-cli -n 0 GET test:authentik +docker exec redis-shared redis-cli -n 0 DEL test:authentik +``` + +**Expected output:** +``` +PONG +# Keyspace (should be empty or have other keys) +connection_ok +1 +``` + +### 1.3 Network Verification + +```bash +# Verify docker-dataplane network exists +docker network inspect docker-dataplane + +# List all containers on docker-dataplane +docker network inspect docker-dataplane | grep -A 2 "Containers" + +# Verify DNS resolution between containers +docker run --rm --network docker-dataplane alpine ping -c 3 postgres-shared +docker run --rm --network docker-dataplane alpine ping -c 3 redis-shared +``` + +**Expected:** 19 containers on network, postgres-shared and redis-shared resolve correctly. + +### 1.4 NPM Current State + +```bash +# Backup current NPM configuration +curl -s http://192.168.86.149:8000/api/ > backups/npm-pre-authentik-$(date +%Y%m%d-%H%M%S).json + +# Verify auth.schweitz.net proxy host exists +cat backups/npm-pre-authentik-*.json | jq '.proxy_hosts[] | select(.domain_names[] | contains("auth.schweitz"))' +``` + +**Expected:** auth.schweitz.net points to localhost:9000, currently has forward auth (will be removed). + +### 1.5 SSL Certificates + +```bash +# Verify Let's Encrypt certificates for schweitz.net domains +docker exec npm ls -la /etc/letsencrypt/live/ | grep schweitz +``` + +**Expected:** Certificates for all *.schweitz.net domains, valid and auto-renewing. + +--- + +## Section 2: Resource Optimization Strategy + +### 2.1 Resource Allocation Plan + +**Authentik Server:** +```yaml +deploy: + resources: + limits: + memory: 512M # Hard cap + cpus: '0.5' # 50% of one core + reservations: + memory: 256M # Guaranteed minimum +``` + +**Authentik Worker:** +```yaml +deploy: + resources: + limits: + memory: 384M # Hard cap + cpus: '0.3' # 30% of one core + reservations: + memory: 128M # Guaranteed minimum +``` + +**Total Authentik overhead:** 896MB (vs previous 3-5GB = **78% reduction**) + +### 2.2 Worker Configuration + +**Reduce Celery workers and threads:** +```yaml +environment: + # Celery worker configuration + AUTHENTIK_BOOTSTRAP_WORKERS: 1 # Default: 1 (single worker) + AUTHENTIK_WORKER__CONCURRENCY: 2 # Threads per worker (default: 2) + + # Logging optimization + AUTHENTIK_LOG_LEVEL: warning # Reduce log verbosity + AUTHENTIK_ERROR_REPORTING__ENABLED: false # Disable error reporting + + # Disable non-essential features + AUTHENTIK_AVATARS: none # Disable avatar fetching (saves bandwidth) + AUTHENTIK_FOOTER_LINKS: '[]' # Disable footer links +``` + +**Resource impact:** +- 1 worker @ 2 threads = minimal CPU usage during idle +- Warning-level logging = less I/O overhead +- No avatar fetching = no external HTTP requests + +### 2.3 Database Connection Optimization + +**Use shared infrastructure, no connection pooling (unnecessary for homelab scale):** +```yaml +environment: + # PostgreSQL connection + AUTHENTIK_POSTGRESQL__HOST: postgres-shared + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__USER: authentik_user + AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} + AUTHENTIK_POSTGRESQL__USE_PGBOUNCER: false # No pooler needed + AUTHENTIK_POSTGRESQL__USE_PGPOOL: false # No pooler needed + + # Redis connection + AUTHENTIK_REDIS__HOST: redis-shared + AUTHENTIK_REDIS__PORT: 6379 + AUTHENTIK_REDIS__DB: 0 # Dedicated DB for Authentik + AUTHENTIK_REDIS__PASSWORD: "" # No password (internal network only) +``` + +**Benefit:** No dedicated PostgreSQL/Redis = ~1.1GB RAM saved. + +### 2.4 Memory Usage Monitoring + +**Check memory usage after deployment:** +```bash +# Container-level memory usage +docker stats authentik-server authentik-worker --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" + +# Process-level memory inside containers +docker exec authentik-server ps aux --sort=-%mem | head -10 +docker exec authentik-worker ps aux --sort=-%mem | head -10 +``` + +**Expected values:** +- authentik-server: 200-400MB (under 512M limit) +- authentik-worker: 100-250MB (under 384M limit) +- **Total: 300-650MB** (healthy, with burst headroom) + +**If memory exceeds limits:** +- Check logs for errors: `docker logs authentik-server --tail 100` +- Verify no memory leaks: `docker exec authentik-server free -h` +- Restart containers if needed: `docker restart authentik-server authentik-worker` + +--- + +## Section 3: Redirect Loop Prevention + +### 3.1 Critical Configuration Rules + +**RULE #1: Never Enable Forward Auth on auth.schweitz.net** + +```nginx +# ✓ CORRECT - auth.schweitz.net NPM config +{ + "domain_names": ["auth.schweitz.net"], + "forward_host": "localhost", + "forward_port": 9000, + "ssl_forced": true, + "advanced_config": "# NO FORWARD AUTH - This is the login page!\n\n# Optional: Rate limiting\nlimit_req_zone $binary_remote_addr zone=auth_limit:10m rate=10r/m;\nlimit_req zone=auth_limit burst=5 nodelay;" +} +``` + +```nginx +# ✗ WRONG - DO NOT DO THIS! +{ + "domain_names": ["auth.schweitz.net"], + "advanced_config": "auth_request /outpost.goauthentik.io/auth/nginx;" # ← CAUSES LOOP! +} +``` + +**RULE #2: Set AUTHENTIK_HOST to External URL** + +```yaml +environment: + AUTHENTIK_HOST: https://auth.schweitz.net # ← External HTTPS URL + AUTHENTIK_HOST_BROWSER: https://auth.schweitz.net # ← Same as AUTHENTIK_HOST +``` + +**Why:** Authentik uses this for generating redirect URLs in OAuth flows. If unset, it uses the request Host header, which might be `localhost` or an IP address internally. + +**RULE #3: Configure Cookie Domain for SSO** + +```yaml +environment: + AUTHENTIK_COOKIE_DOMAIN: .schweitz.net # ← Note the leading dot! + AUTHENTIK_COOKIE_SAMESITE: lax # ← Allow cross-subdomain cookies +``` + +**Why:** Without this, cookies are scoped to individual hosts (e.g., `auth.schweitz.net` only), and can't be shared with `api.schweitz.net`, `cloud.schweitz.net`, etc. + +**RULE #4: Use Embedded Outpost (Simplicity)** + +```yaml +# Server container serves both web UI and outpost +authentik-server: + ports: + - "9000:9000" # Web UI + - "9443:9443" # Embedded outpost + environment: + AUTHENTIK_OUTPOSTS__DOCKER_IMAGE_BASE: "ghcr.io/goauthentik/%(type)s:%(version)s" +``` + +**Why:** Eliminates need for separate outpost container, reduces complexity and memory usage. + +**RULE #5: Consistent NPM Forward Auth Configuration** + +```nginx +# Forward auth configuration for protected services (NOT auth.schweitz.net) +auth_request /outpost.goauthentik.io/auth/nginx; +error_page 401 = @authentik_proxy_signin; + +auth_request_set $auth_cookie $upstream_http_set_cookie; +add_header Set-Cookie $auth_cookie; + +auth_request_set $authentik_username $upstream_http_x_authentik_username; +auth_request_set $authentik_email $upstream_http_x_authentik_email; +auth_request_set $authentik_groups $upstream_http_x_authentik_groups; + +proxy_set_header X-authentik-username $authentik_username; +proxy_set_header X-authentik-email $authentik_email; +proxy_set_header X-authentik-groups $authentik_groups; + +location @authentik_proxy_signin { + internal; + return 302 https://auth.schweitz.net/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri; +} + +location /outpost.goauthentik.io { + proxy_pass http://authentik-server:9443/outpost.goauthentik.io; # ← Use embedded outpost + proxy_set_header Host $host; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; +} +``` + +### 3.2 OAuth Redirect URI Configuration + +**Google OAuth Callback URL:** `https://auth.schweitz.net/source/oauth/callback/google/` + +**In Google Cloud Console:** +``` +Authorized JavaScript origins: + https://auth.schweitz.net + +Authorized redirect URIs: + https://auth.schweitz.net/source/oauth/callback/google/ +``` + +**In Authentik (Google Social Source):** +``` +Consumer Key: +Consumer Secret: +Provider Type: Google +Callback URL: (auto-filled, verify it matches Google Cloud Console) +``` + +**Critical:** The callback URL must **exactly** match between Google Cloud Console and Authentik. Case-sensitive, trailing slashes matter. + +### 3.3 Testing Redirect Flows + +**Manual redirect flow test:** +```bash +# 1. Visit protected service (should redirect to Authentik) +curl -I https://api.schweitz.net +# Expected: 302 redirect to https://auth.schweitz.net/outpost.goauthentik.io/start?rd=... + +# 2. Visit Authentik login page (should NOT redirect) +curl -I https://auth.schweitz.net +# Expected: 200 OK (Authentik login page) + +# 3. After login, check if cookie is set +curl -I -c cookies.txt https://auth.schweitz.net/flows/executor/default-provider-authentication-flow/ +# Expected: Set-Cookie: authentik_session=...; Domain=.schweitz.net + +# 4. Use cookie to access protected service +curl -I -b cookies.txt https://api.schweitz.net +# Expected: 200 OK (authenticated) +``` + +### 3.4 Redirect Loop Detection + +**Symptoms:** +- Browser error: "ERR_TOO_MANY_REDIRECTS" +- Nginx logs show repeated 302 responses +- Authentik logs show repeated authentication attempts from same IP + +**Diagnosis:** +```bash +# Check NPM logs for redirect chains +docker logs npm 2>&1 | grep -A 5 "auth.schweitz.net" | tail -50 + +# Check Authentik logs for repeated auth attempts +docker logs authentik-server 2>&1 | grep -i "redirect\|loop\|401\|302" | tail -50 + +# Test with verbose curl (shows redirect chain) +curl -v -L https://api.schweitz.net 2>&1 | grep -E "(< HTTP|< Location)" +``` + +**If loop detected:** +1. Check if `auth.schweitz.net` has forward auth enabled → **Remove it** +2. Verify `AUTHENTIK_HOST` environment variable → Should be `https://auth.schweitz.net` +3. Check cookie domain → Should be `.schweitz.net` +4. Verify outpost endpoint is reachable → `curl http://authentik-server:9443/outpost.goauthentik.io/ping/` + +--- + +## Section 4: Implementation Milestones + +### Milestone 0: Pre-Deployment Validation ✓ Safe to Execute + +**Objective:** Verify all prerequisites are met before deploying Authentik. + +**Tasks:** +- [ ] **M0.1:** Run PostgreSQL connectivity tests (Section 1.2) +- [ ] **M0.2:** Run Redis connectivity tests (Section 1.2) +- [ ] **M0.3:** Verify docker-dataplane network (Section 1.3) +- [ ] **M0.4:** Backup NPM configuration +- [ ] **M0.5:** Document current NPM forward auth status (should have auth on all hosts) +- [ ] **M0.6:** Remove forward auth from ALL NPM proxy hosts temporarily +- [ ] **M0.7:** Verify system resource availability (RAM, CPU, disk) + +**Validation Checklist:** +```bash +# Run all validation commands +bash scripts/validate-authentik-prerequisites.sh # Create this script +``` + +**Expected Duration:** 15-30 minutes + +**Rollback:** N/A (no changes made to running services) + +**Backup Files:** +- `backups/npm-m0-$(date +%Y%m%d).json` - NPM config before changes +- `backups/docker-ps-m0-$(date +%Y%m%d).txt` - Container list + +**Status:** ⏳ Not Started + +**Issues Log:** +``` +[Date] [Issue description] - [Resolution] +``` + +--- + +### Milestone 1: Authentik Deployment (Core Only) + +**Objective:** Deploy Authentik server and worker containers, configure NPM proxy for auth.schweitz.net (WITHOUT forward auth). + +**Dependencies:** M0 completed + +**Tasks:** +- [ ] **M1.1:** Create `stacks/authentik.yml` with correct environment variables +- [ ] **M1.2:** Set AUTHENTIK_HOST, AUTHENTIK_COOKIE_DOMAIN, resource limits +- [ ] **M1.3:** Deploy Authentik stack via Portainer +- [ ] **M1.4:** Wait for containers to start (watch logs) +- [ ] **M1.5:** Verify database migrations completed +- [ ] **M1.6:** Update NPM proxy host for `auth.schweitz.net` (NO forward auth!) +- [ ] **M1.7:** Access https://auth.schweitz.net (should show Authentik setup wizard) +- [ ] **M1.8:** Complete initial setup wizard, create admin account + +**Configuration Files:** + +**`stacks/authentik.yml`:** +```yaml +version: '3.8' + +# Authentik Identity Provider (SSO) +# Purpose: Centralized authentication for all homelab services +# Ports: 9000 (web UI), 9443 (embedded outpost) +# GPU: No +# Storage: SSD (configs), PostgreSQL shared (user data) + +services: + authentik-server: + image: ghcr.io/goauthentik/server:2024.8.4 # Pinned version (2024.10 has redirect loop issues) + container_name: authentik-server + restart: unless-stopped + command: server + environment: + # External URLs (CRITICAL for redirect loop prevention) + AUTHENTIK_HOST: https://auth.schweitz.net + AUTHENTIK_HOST_BROWSER: https://auth.schweitz.net + + # Cookie settings (CRITICAL for SSO across subdomains) + AUTHENTIK_COOKIE_DOMAIN: .schweitz.net + AUTHENTIK_COOKIE_SAMESITE: lax + + # SSL/TLS + AUTHENTIK_INSECURE: false + + # PostgreSQL (shared) + AUTHENTIK_POSTGRESQL__HOST: postgres-shared + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__USER: authentik_user + AUTHENTIK_POSTGRESQL__PASSWORD: F//j0ktck7cX06Vfgh0YXceONOtlSsHvadqROICeDx8= + AUTHENTIK_POSTGRESQL__USE_PGBOUNCER: false + + # Redis (shared) + AUTHENTIK_REDIS__HOST: redis-shared + AUTHENTIK_REDIS__PORT: 6379 + AUTHENTIK_REDIS__DB: 0 + + # Secret key (generate with: openssl rand -base64 32) + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} # Store in .env file + + # Resource optimization + AUTHENTIK_LOG_LEVEL: warning + AUTHENTIK_ERROR_REPORTING__ENABLED: false + AUTHENTIK_AVATARS: none + AUTHENTIK_FOOTER_LINKS: '[]' + + # Embedded outpost configuration + AUTHENTIK_OUTPOSTS__DOCKER_IMAGE_BASE: "ghcr.io/goauthentik/%(type)s:%(version)s" + + # Timezone + TZ: Europe/Amsterdam + + ports: + - "9000:9000" # Web UI + - "9443:9443" # Embedded outpost (proxy provider) + + volumes: + - /home/jpmschweitzer/docker-data/authentik/media:/media + - /home/jpmschweitzer/docker-data/authentik/custom-templates:/templates + + networks: + - docker-dataplane + + depends_on: + - postgres-shared + - redis-shared + + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:9000/-/health/live/ || exit 1"] + start_period: 60s + interval: 30s + timeout: 10s + retries: 3 + + deploy: + resources: + limits: + memory: 512M + cpus: '0.5' + reservations: + memory: 256M + + authentik-worker: + image: ghcr.io/goauthentik/server:2024.8.4 # Same version as server + container_name: authentik-worker + restart: unless-stopped + command: worker + environment: + # Same environment as server (MUST match exactly) + AUTHENTIK_HOST: https://auth.schweitz.net + AUTHENTIK_HOST_BROWSER: https://auth.schweitz.net + AUTHENTIK_COOKIE_DOMAIN: .schweitz.net + AUTHENTIK_COOKIE_SAMESITE: lax + AUTHENTIK_INSECURE: false + AUTHENTIK_POSTGRESQL__HOST: postgres-shared + AUTHENTIK_POSTGRESQL__PORT: 5432 + AUTHENTIK_POSTGRESQL__NAME: authentik + AUTHENTIK_POSTGRESQL__USER: authentik_user + AUTHENTIK_POSTGRESQL__PASSWORD: F//j0ktck7cX06Vfgh0YXceONOtlSsHvadqROICeDx8= + AUTHENTIK_POSTGRESQL__USE_PGBOUNCER: false + AUTHENTIK_REDIS__HOST: redis-shared + AUTHENTIK_REDIS__PORT: 6379 + AUTHENTIK_REDIS__DB: 0 + AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} + AUTHENTIK_LOG_LEVEL: warning + AUTHENTIK_ERROR_REPORTING__ENABLED: false + TZ: Europe/Amsterdam + + # Worker-specific configuration + AUTHENTIK_BOOTSTRAP_WORKERS: 1 # Single worker (homelab scale) + AUTHENTIK_WORKER__CONCURRENCY: 2 # 2 threads per worker + + volumes: + - /home/jpmschweitzer/docker-data/authentik/media:/media + - /home/jpmschweitzer/docker-data/authentik/custom-templates:/templates + - /home/jpmschweitzer/docker-data/authentik/certs:/certs + - /var/run/docker.sock:/var/run/docker.sock # For outpost management + + networks: + - docker-dataplane + + depends_on: + - postgres-shared + - redis-shared + - authentik-server + + healthcheck: + test: ["CMD-SHELL", "ak healthcheck"] + start_period: 60s + interval: 30s + timeout: 10s + retries: 3 + + deploy: + resources: + limits: + memory: 384M + cpus: '0.3' + reservations: + memory: 128M + +networks: + docker-dataplane: + external: true + name: docker-dataplane + +# Setup Instructions: +# +# 1. Generate secret key: +# openssl rand -base64 32 > .env.authentik +# echo "AUTHENTIK_SECRET_KEY=" >> .env.authentik +# +# 2. Create directories: +# mkdir -p ~/docker-data/authentik/{media,custom-templates,certs} +# +# 3. Deploy stack: +# docker-compose -f stacks/authentik.yml up -d +# +# 4. Watch logs: +# docker logs -f authentik-server +# docker logs -f authentik-worker +# +# 5. Wait for migrations to complete (~2-3 minutes): +# docker logs authentik-server 2>&1 | grep "Applying migration" +# +# 6. Access web UI: +# https://auth.schweitz.net (should show setup wizard) +# +# 7. Complete setup wizard: +# - Email: admin@schweitz.net +# - Password: +# - Finish setup +# +# Monitoring: +# +# Memory usage: +# docker stats authentik-server authentik-worker --no-stream +# +# Database connectivity: +# docker exec authentik-server ak check +# +# Outpost status: +# curl http://authentik-server:9443/outpost.goauthentik.io/ping/ +``` + +**Validation Commands:** +```bash +# M1.1: Check containers are running +docker ps | grep authentik + +# M1.2: Check logs for errors +docker logs authentik-server 2>&1 | grep -iE "error|critical|fail" | tail -20 +docker logs authentik-worker 2>&1 | grep -iE "error|critical|fail" | tail -20 + +# M1.3: Check database migrations completed +docker logs authentik-server 2>&1 | grep "Applying migration" | tail -10 +docker logs authentik-server 2>&1 | grep "No migrations to apply" || echo "❌ Migrations still running" + +# M1.4: Check health checks passing +docker inspect authentik-server | jq '.[0].State.Health.Status' +docker inspect authentik-worker | jq '.[0].State.Health.Status' + +# M1.5: Check memory usage (should be under limits) +docker stats authentik-server authentik-worker --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" + +# M1.6: Check NPM proxy host for auth.schweitz.net +curl -I https://auth.schweitz.net +# Expected: 200 OK (Authentik setup wizard) + +# M1.7: Check for redirect loops (should NOT redirect) +curl -v https://auth.schweitz.net 2>&1 | grep -c "Location:" +# Expected: 0 (or 1 if redirecting HTTP → HTTPS, which is fine) + +# M1.8: Verify embedded outpost is accessible +docker exec -it authentik-server curl http://localhost:9443/outpost.goauthentik.io/ping/ +# Expected: {"status": "ok"} or similar +``` + +**Success Criteria:** +- ✓ Both containers running (docker ps) +- ✓ Health checks passing (docker inspect) +- ✓ Memory usage < 700MB total +- ✓ Database migrations completed +- ✓ https://auth.schweitz.net accessible (200 OK, no redirect loop) +- ✓ Authentik setup wizard displayed +- ✓ Admin account created + +**Expected Duration:** 30-60 minutes + +**Rollback Procedure:** +```bash +# Stop and remove Authentik containers +docker-compose -f stacks/authentik.yml down + +# Restore NPM configuration (if changed) +curl -X POST http://192.168.86.149:8000/api/restore \ + -H "Content-Type: application/json" \ + -d @backups/npm-m0-YYYYMMDD.json + +# Verify NPM restored +curl -I https://auth.schweitz.net # Should return error (Authentik not running) +``` + +**Backup Files:** +- `backups/authentik-m1-$(date +%Y%m%d).yml` - Authentik stack config +- `backups/npm-m1-$(date +%Y%m%d).json` - NPM config after M1 +- `backups/authentik-m1-env-$(date +%Y%m%d).txt` - Environment variables (REDACTED) + +**Status:** ⏳ Not Started + +**Issues Log:** +``` +[Date] [Issue description] - [Resolution] +``` + +--- + +### Milestone 2: Google OAuth Integration + +**Objective:** Configure Google as an authentication source in Authentik. + +**Dependencies:** M1 completed successfully + +**Tasks:** +- [ ] **M2.1:** Create Google Cloud Project (or use existing) +- [ ] **M2.2:** Configure OAuth consent screen +- [ ] **M2.3:** Create OAuth 2.0 client credentials +- [ ] **M2.4:** Add Google as social source in Authentik admin panel +- [ ] **M2.5:** Configure callback URLs +- [ ] **M2.6:** Test Google login with Workspace account +- [ ] **M2.7:** Test Google login with Gmail account +- [ ] **M2.8:** Verify user profile attributes synced + +**Configuration:** See original plan Section 5.1.2 for detailed steps. + +**Expected Duration:** 30-45 minutes + +**Status:** ⏳ Not Started + +--- + +### Milestone 3: Single Service Protection (Organizr Test) + +**Objective:** Enable forward auth on ONE service (Organizr at home.schweitz.net) to validate SSO works end-to-end. + +**Dependencies:** M2 completed successfully + +**Configuration:** See original plan Section 5.2 for detailed steps. + +**Expected Duration:** 60-90 minutes + +**Status:** ⏳ Not Started + +--- + +### Milestone 4: Core API Protection (FastAPI Native OIDC) + +**Objective:** Protect Core API with FastAPI native OIDC token validation (bearer token authentication). + +**Dependencies:** M3 completed successfully + +**Configuration:** See original plan Section 5.1.3 for detailed implementation. + +**Expected Duration:** 90-120 minutes + +**Status:** ⏳ Not Started + +--- + +### Milestone 5: Remaining Services (Gradual Rollout) + +**Objective:** Enable forward auth on remaining 9 services, one at a time, testing each before proceeding. + +**Dependencies:** M3 and M4 completed successfully + +**Services:** Nextcloud, Gitea, Jellyfin, Open WebUI, code-server, Netdata, Uptime Kuma, AMP, Tatlock + +**Expected Duration:** 4-8 hours (30-60 min per service) + +**Status:** ⏳ Not Started + +--- + +## Section 5: Testing & Validation + +### 5.1 Automated Testing Scripts + +**Create these scripts in `/scripts` directory:** + +- `validate-authentik-prerequisites.sh` - Pre-deployment checks +- `test-redirect-flow.sh` - Test redirect loops +- `monitor-resources.sh` - Monitor memory/CPU usage + +(Full script contents in original plan appendices) + +--- + +## Section 6: Troubleshooting Guide + +### 6.1 Redirect Loop Troubleshooting + +**Common Causes & Fixes:** + +| Cause | Fix | +|-------|-----| +| `auth.schweitz.net` has forward auth enabled | Remove forward auth from NPM config | +| `AUTHENTIK_HOST` not set or wrong | Set `AUTHENTIK_HOST=https://auth.schweitz.net` | +| Cookie domain mismatch | Set `AUTHENTIK_COOKIE_DOMAIN=.schweitz.net` | +| Outpost endpoint unreachable | Verify `curl http://authentik-server:9443/outpost.goauthentik.io/ping/` works | + +### 6.2 Memory Usage Issues + +**Common Causes & Fixes:** + +| Cause | Fix | +|-------|-----| +| Too many Celery workers | Set `AUTHENTIK_BOOTSTRAP_WORKERS=1` | +| Too many threads per worker | Set `AUTHENTIK_WORKER__CONCURRENCY=2` | +| Memory leak | Restart containers | + +(Full troubleshooting guide in original plan Section 6) + +--- + +## Section 7: Rollback Procedures + +### 7.1 Full Rollback + +```bash +# Stop Authentik +docker-compose -f stacks/authentik.yml down -v + +# Restore NPM config +curl -X POST http://192.168.86.149:8000/api/restore \ + -H "Content-Type: application/json" \ + -d @backups/npm-m0-YYYYMMDD.json + +# Verify services accessible +curl -I https://api.schweitz.net +``` + +### 7.2 Partial Rollback (Single Service) + +Restore NPM config for specific proxy host from milestone backup. + +(Full rollback procedures in original plan Section 7) + +--- + +## Section 8: Progress Tracking + +### 8.1 Implementation Status + +**Last Updated:** 2025-11-20 + +| Milestone | Status | Start Date | Complete Date | Duration | Issues | +|-----------|--------|------------|---------------|----------|--------| +| M0: Pre-deployment Validation | ⏳ Not Started | - | - | - | - | +| M1: Authentik Deployment | ⏳ Not Started | - | - | - | - | +| M2: Google OAuth | ⏳ Not Started | - | - | - | - | +| M3: Organizr SSO | ⏳ Not Started | - | - | - | - | +| M4: Core API OIDC | ⏳ Not Started | - | - | - | - | +| M5: Remaining Services | ⏳ Not Started | - | - | - | - | **Legend:** -- ✅ Native OIDC/OAuth support -- ⚠️ Proxy-based authentication (forward auth via NPM) -- ❌ No authentication (critical security gap) +- ⏳ Not Started +- 🔄 In Progress +- ✅ Completed +- ⚠️ Issues/Blocked +- ❌ Failed -### 1.2 Out-of-Scope Services +### 8.2 Detailed Progress Log -These services remain accessible on local network or via core-api authenticated endpoints: - -| Service | Type | Port | Reason Out of Scope | -|---------|------|------|---------------------| -| **Portainer** | Infrastructure | 8080 | Admin tool - accessible via core-api proxy if needed | -| **Uptime Kuma** | Monitoring | 3001 | Admin tool - accessible via core-api proxy if needed | -| **Netdata** | Monitoring | 19999 | Admin tool - accessible via core-api proxy if needed | -| **Headscale** | VPN | 8085 | VPN control plane, no web auth needed | -| **NPM** | Infrastructure | 8000 | LAN admin access only | -| **Ollama** | AI Backend | 11434 | Internal API, no direct access | -| **Qdrant** | Database | 6333 | Internal vector DB, no direct access | -| **Samba** | File Share | 445 | LAN-only SMB service | -| **Watchtower** | Automation | - | Background service, no UI | - -### 1.3 Current Security Posture - -**Strengths:** -- Most services isolated behind NPM reverse proxy -- VPN access available via Headscale for infrastructure tools -- Core infrastructure tools not publicly exposed - -**Critical Weaknesses:** -- **core-api has ZERO authentication** (blocks api.schweitz.net public exposure) -- **code-server uses basic password** (weak auth for IDE access) -- Password sprawl across 7 different user-facing services -- No centralized authentication or session management -- No unified access control or audit logging -- User management scattered across multiple databases (MariaDB, PostgreSQL, local files) - ---- - -## 2. Security Requirements & Goals - -### 2.1 Primary Objectives - -1. **Google OAuth Integration** - - Support Google Workspace accounts (e.g., user@schweitz.net) - - Support Gmail accounts (e.g., user@gmail.com) - - Single source of truth for user identities - -2. **Centralized Access Control** - - Manage user access from one location - - Role-based access control (RBAC) per service - - Easy user onboarding/offboarding - -3. **Secure Public Access** - - Enable safe exposure of core-api at api.schweitz.net - - Protect service control widget and infrastructure APIs - - Maintain security for publicly accessible services - -4. **Minimal User Friction** - - One-click login via Google - - Session management across services - - Mobile-friendly authentication - -### 2.2 Security Standards - -- **Authentication:** OAuth 2.0 / OIDC with Google as IdP -- **Authorization:** RBAC with service-level granularity -- **Session Management:** Secure cookie handling, configurable timeouts -- **Transport Security:** TLS 1.3 via Let's Encrypt (already configured in NPM) -- **Audit Logging:** Authentication events, access attempts, authorization decisions - ---- - -## 3. SSO Solution Evaluation - -### 3.1 Solution Comparison Matrix - -| Criteria | Authelia | Authentik | Keycloak | -|----------|----------|-----------|----------| -| **Architecture** | Forward auth proxy | Full IdP | Enterprise IdP | -| **Resource Usage** | Low (~100MB RAM) | Medium (~300MB RAM) | High (1-2GB RAM) | -| **Setup Complexity** | Simple | Moderate | Complex | -| **Google as Auth Source** | ❌ **NO** | ✅ **YES** | ✅ **YES** | -| **Acts as OIDC Provider** | ✅ YES | ✅ YES | ✅ YES | -| **LDAP Backend** | LDAP only | Multiple | Multiple | -| **User Database** | File/LDAP | PostgreSQL | PostgreSQL/MySQL | -| **UI Quality** | Minimal | Modern | Enterprise | -| **NPM Integration** | Excellent | Good | Good | -| **Community Support** | Large | Growing | Massive | -| **Homelab Suitability** | ⚠️ High* | ✅ Excellent | ⚠️ Overkill | - -*Authelia is excellent for homelabs, but **cannot use Google as the authentication source** - it requires its own user database (file-based or LDAP). - -### 3.2 Detailed Analysis - -#### Authelia -**Strengths:** -- Lightweight and fast -- Perfect NPM integration via forward auth -- Simple configuration (YAML files) -- Low resource usage -- Excellent for homelab scale - -**Critical Limitation:** -- **Cannot federate with Google OAuth** - only acts as an OIDC provider -- Requires separate user database (LDAP or file-based) -- Would need to manually sync Google users to LDAP -- Does not meet requirement for "login with Google" - -**Verdict:** ❌ Does not meet core requirement (Google as auth source) - -#### Authentik -**Strengths:** -- **Can use Google as authentication source** (meets requirement!) -- Modern Python-based architecture -- Beautiful, intuitive UI -- Flow-based configuration (flexible auth/authz journeys) -- Acts as OIDC/SAML provider for downstream services -- Good documentation and active community -- Reasonable resource usage (~300-500MB) -- Docker-native, homelab-friendly - -**Limitations:** -- More complex than Authelia -- Requires PostgreSQL database -- NPM integration via OIDC (not forward auth) - -**Verdict:** ✅ **RECOMMENDED** - Best fit for requirements - -#### Keycloak -**Strengths:** -- Battle-tested enterprise solution -- Most comprehensive feature set -- Extensive protocol support -- Can federate with Google -- Massive ecosystem - -**Limitations:** -- Heavy resource usage (1-2GB RAM minimum) -- Complex UI and configuration -- Overkill for homelab scale -- Steeper learning curve - -**Verdict:** ⚠️ Viable but excessive for homelab - -### 3.3 Recommendation: Authentik - -**Authentik** is the optimal choice because: - -1. **Meets Core Requirement:** Supports Google OAuth as authentication source -2. **Right-Sized:** Not too simple (Authelia), not too complex (Keycloak) -3. **Modern Architecture:** Python-based, active development, good docs -4. **Flexible Integration:** Can provide OIDC, SAML, LDAP, and proxy auth -5. **User Experience:** Clean UI for both admins and end-users -6. **Resource Efficient:** ~300-500MB RAM is acceptable for homelab - ---- - -## 4. Recommended Architecture - -### 4.1 High-Level Design +**Update this section as you progress through each milestone.** +**Template:** ``` -┌─────────────────────────────────────────────────────────────┐ -│ Internet (Public) │ -└─────────────────────┬───────────────────────────────────────┘ - │ - ┌────────────▼────────────┐ - │ Google OAuth 2.0 │ ← User: "Sign in with Google" - │ (accounts.google.com) │ (Workspace + Gmail accounts) - └────────────┬────────────┘ - │ OAuth tokens - ┌────────────▼────────────┐ - │ Authentik IdP │ ← Central identity provider - │ (auth.schweitz.net) │ - Validates Google auth - │ │ - Manages users & groups - │ PostgreSQL + Redis │ - Issues OIDC tokens & JWTs - └────────┬─────────────────┘ - │ - │ OIDC tokens / Forward auth - │ - ┌───────────┼───────────────────────────────────┐ - │ │ │ -┌─────▼─────┐ ┌──▼────────────┐ ┌────────────▼──────┐ -│ NPM │ │ core-api │ │ User Apps (OIDC) │ -│ Gateway │ │ (FastAPI) │ │ │ -│ │ │ │ │ - Nextcloud │ -│ Routes: │ │ Bearer Token │ │ - Jellyfin │ -│ *.schweitz │ Validation ────┤ │ - Gitea │ -│ .net │ │ (python-jose) │ │ - Open WebUI │ -│ │ └───────────────┘ └───────────────────┘ -│ │ -│ Forward │ ┌───────────────────────────┐ -│ Auth ├────────► Proxy Auth Services │ -│ (Authent │ │ - Organizr (dashboard) │ -│ ik) │ │ - code-server (host) │ -│ │ └───────────────────────────┘ -└───────────┘ +### Milestone X: [Name] -Legend: -━━━ Direct OIDC/OAuth ─── HTTP Proxy ╌╌╌ Forward Auth +**Start:** [YYYY-MM-DD HH:MM] +**Completed:** [YYYY-MM-DD HH:MM] +**Duration:** [Time] + +**Tasks Completed:** +- [x] Task 1 +- [x] Task 2 + +**Issues Encountered:** +1. **Issue:** [Description] + **Cause:** [Root cause] + **Solution:** [How fixed] + +**Validation Results:** +- ✓ Item 1 +- ✓ Item 2 + +**Backups Created:** +- backups/filename-mX-YYYYMMDD.ext + +**Next Steps:** +- Proceed to Milestone X+1 ``` -### 4.2 Authentication Flow +### 8.3 Configuration Changelog -**Initial Login:** -1. User visits `home.schweitz.net` (or any protected service) -2. NPM/Service redirects to Authentik: `auth.schweitz.net` -3. Authentik shows "Sign in with Google" button -4. User authenticates with Google (Workspace or Gmail) -5. Google returns OAuth token to Authentik -6. Authentik creates/updates user profile, issues OIDC token -7. User redirected back to original service with session cookie -8. Service validates OIDC token, grants access +**Track all configuration changes:** -**Subsequent Access:** -- Session cookie valid for configurable duration (e.g., 8 hours) -- No re-authentication required within session -- Logout clears all service sessions +| Date | Component | Change | Reason | Rollback | +|------|-----------|--------|--------|----------| +| 2025-11-20 | Plan | Created v2.0 | Post-failure revision | N/A | +| - | - | - | - | - | -### 4.3 Component Architecture +### 8.4 Lessons Learned (Real-Time) -#### New Components to Deploy +**Add lessons as you encounter issues:** -1. **Authentik Stack** (new) - - `authentik-server`: Main application server - - `authentik-worker`: Background tasks - - `postgresql`: User database - - `redis`: Cache and message queue - - Resources: ~500MB RAM, 1 CPU, 5GB SSD - - Port: 9000 (internal), exposed via NPM - -2. **NPM Configuration Updates** - - Add proxy host: `auth.schweitz.net` → `authentik-server:9000` - - Add forward auth snippet for protected services - - Configure SSL certificates (Let's Encrypt) - -#### Integration Patterns - -**Pattern A: FastAPI Native OIDC** (core-api only) -- Service: core-api -- Integration: Direct JWT validation in FastAPI using python-jose -- Session: Stateless bearer token authentication -- Benefits: Fine-grained endpoint control, API-first security, OpenAPI integration - -**Pattern B: Native OIDC** (Preferred for user apps) -- Services: Nextcloud, Gitea, Jellyfin, Open WebUI -- Integration: Configure OIDC client in Authentik, add OIDC provider in service -- Session: Service manages its own session after OIDC login -- Benefits: Native integration, best UX, full feature support - -**Pattern C: Forward Auth via NPM** (For non-OIDC services) -- Services: Organizr, code-server -- Integration: NPM/Authentik proxy checks auth before forwarding request -- Session: Handled by Authentik via cookies -- Benefits: Works with any service, no code changes needed +``` +[Date] [Milestone] [Lesson] +``` --- -## 5. Service-by-Service Integration Plan +## Appendix A: Quick Reference Commands -### 5.1 Tier 1: Critical Infrastructure (Week 1) +### A.1 Authentik Management -#### 5.1.1 Authentik Deployment +```bash +# Check status +docker ps | grep authentik +docker logs authentik-server --tail 50 -**Tasks:** -1. Create `stacks/authentik.yml` with PostgreSQL + Redis + Authentik -2. Deploy stack via Portainer -3. Access initial setup at `http://localhost:9000` -4. Configure: - - Admin account - - Email settings (optional) - - Brand customization +# Restart +docker restart authentik-server authentik-worker -**Success Criteria:** -- Authentik accessible via NPM at `auth.schweitz.net` -- Admin portal functional -- Health checks passing +# Check database +docker exec authentik-server ak check -#### 5.1.2 Google OAuth Source Configuration +# Run migrations +docker exec authentik-server ak migrate -**Tasks:** -1. Create Google Cloud Project (or use existing) -2. Enable OAuth consent screen - - App name: "Homelab SSO" - - Support email: admin email - - Scopes: `openid`, `email`, `profile` - - Authorized domains: `schweitz.net` -3. Create OAuth 2.0 credentials - - Authorized redirect URIs: `https://auth.schweitz.net/source/oauth/callback/google/` -4. Configure Google source in Authentik - - Provider: Google - - Client ID: from Google Cloud - - Client Secret: from Google Cloud - - Scopes: `openid email profile` +# Check outpost +curl http://authentik-server:9443/outpost.goauthentik.io/ping/ +``` -**Testing:** -1. Test login with Google Workspace account -2. Test login with Gmail account -3. Verify user profile attributes synced -4. Confirm logout works +### A.2 NPM Management -#### 5.1.3 core-api Protection (CRITICAL) +```bash +# Backup +curl -s http://192.168.86.149:8000/api/ > backups/npm-$(date +%Y%m%d-%H%M%S).json -**Current Risk:** core-api at port 8083 has **zero authentication**. Cannot expose publicly without SSO. +# Test config +docker exec npm nginx -t -**Approach:** FastAPI Native OIDC (OAuth2 Bearer Token) - -**Implementation:** - -1. **Create Authentik OIDC Provider** - - In Authentik: Applications → Create - - Name: "Core API" - - Slug: `core-api` - - Provider type: OAuth2/OIDC - - Client type: `Confidential` - - Client ID: (auto-generated, save for later) - - Client Secret: (auto-generated, save securely) - - Redirect URIs: `https://api.schweitz.net/auth/callback` (for web flows if needed) - - Signing Key: Auto (Authentik default) - - Subject mode: `Based on User's Email` - - Include claims in ID token: ✅ - - Scopes: `openid`, `email`, `profile` - -2. **Install Python Dependencies** (add to `requirements.txt`): - ```txt - PyJWT[crypto]==2.8.0 - python-jose[cryptography]==3.3.0 - ``` - -3. **Create Authentication Module** (`src/auth/oidc.py`): - ```python - from fastapi import Depends, HTTPException, Security - from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials - from jose import jwt, jwk - from jose.utils import base64url_decode - import httpx - from functools import lru_cache - from typing import Dict, Optional - from src.config import get_settings - from src.logging_config import get_logger - - logger = get_logger(__name__) - settings = get_settings() - security = HTTPBearer(auto_error=False) - - # OIDC Configuration - OIDC_ISSUER = settings.oidc_issuer # "https://auth.schweitz.net/application/o/core-api/" - JWKS_URI = f"{OIDC_ISSUER}jwks/" - - @lru_cache() - def get_jwks() -> Dict: - """Fetch JWKS from Authentik (cached)""" - try: - response = httpx.get(JWKS_URI, timeout=10) - response.raise_for_status() - return response.json() - except Exception as e: - logger.error(f"Failed to fetch JWKS: {e}") - raise HTTPException(status_code=500, detail="Auth configuration error") - - async def get_current_user( - credentials: Optional[HTTPAuthorizationCredentials] = Security(security) - ) -> Dict: - """ - Validate OIDC token from Authorization: Bearer header - - Returns: - User claims (email, name, groups, etc.) - - Raises: - HTTPException 401 if token invalid/missing - """ - if not credentials: - raise HTTPException( - status_code=401, - detail="Missing authentication token", - headers={"WWW-Authenticate": "Bearer"}, - ) - - token = credentials.credentials - - try: - # Decode header to get key ID - unverified_header = jwt.get_unverified_header(token) - kid = unverified_header.get("kid") - - # Find matching key in JWKS - jwks = get_jwks() - rsa_key = None - for key in jwks.get("keys", []): - if key.get("kid") == kid: - rsa_key = key - break - - if not rsa_key: - raise HTTPException(status_code=401, detail="Invalid token key") - - # Verify and decode token - payload = jwt.decode( - token, - rsa_key, - algorithms=["RS256"], - audience=settings.oidc_audience, # "core-api" - issuer=OIDC_ISSUER, - ) - - logger.info(f"Authenticated user: {payload.get('email')}") - return payload - - except jwt.ExpiredSignatureError: - raise HTTPException(status_code=401, detail="Token expired") - except jwt.JWTClaimsError: - raise HTTPException(status_code=401, detail="Invalid token claims") - except Exception as e: - logger.error(f"Token validation error: {e}") - raise HTTPException(status_code=401, detail="Invalid authentication token") - - async def get_admin_user(user: Dict = Depends(get_current_user)) -> Dict: - """Require admin group membership""" - groups = user.get("groups", []) - if "admin" not in groups: - raise HTTPException(status_code=403, detail="Admin access required") - return user - ``` - -4. **Update `src/config.py`** (add OIDC settings): - ```python - class Settings(BaseSettings): - # ... existing settings ... - - # OIDC Authentication - oidc_issuer: str = "https://auth.schweitz.net/application/o/core-api/" - oidc_audience: str = "core-api" - oidc_enabled: bool = True # Set False to disable auth (dev mode) - ``` - -5. **Protect Endpoints** (update controllers): - ```python - from src.auth.oidc import get_current_user, get_admin_user - - # Public endpoints (no change needed) - @router.get("/health") - async def health_check(): - return {"status": "healthy"} - - # Protected endpoints (add dependency) - @router.get("/infrastructure/services") - async def get_services(user: Dict = Depends(get_current_user)): - logger.info(f"User {user['email']} fetching services") - return services - - # Admin-only endpoints - @router.post("/infrastructure/services/{name}/stop") - async def stop_service(name: str, user: Dict = Depends(get_admin_user)): - logger.info(f"Admin {user['email']} stopping {name}") - # ... implementation - ``` - -6. **Update OpenAPI Documentation** (`src/main.py`): - ```python - from fastapi.security import OAuth2AuthorizationCodeBearer - - oauth2_scheme = OAuth2AuthorizationCodeBearer( - authorizationUrl=f"{settings.oidc_issuer}authorize/", - tokenUrl=f"{settings.oidc_issuer}token/", - scopes={"openid": "OpenID Connect", "email": "Email", "profile": "Profile"} - ) - - app = FastAPI( - # ... existing config ... - swagger_ui_init_oauth={ - "clientId": settings.oidc_client_id, - "appName": "Core API", - "usePkceWithAuthorizationCodeGrant": True, - } - ) - ``` - -7. **Configure NPM Proxy** (simple passthrough): - - Domain: `api.schweitz.net` - - Forward to: `core-api:8083` - - SSL: Let's Encrypt - - No forward auth needed (handled by FastAPI) - -8. **Update Widget** (`static/widgets/service-control.html`): - ```javascript - // Redirect to Authentik login, then get token - async function getAuthToken() { - // Check if token in localStorage - let token = localStorage.getItem('oidc_token'); - if (token && !isTokenExpired(token)) { - return token; - } - - // Redirect to Authentik login page - const authUrl = 'https://auth.schweitz.net/application/o/core-api/'; - window.location.href = authUrl; - } - - // Include token in API calls - async function fetchServices() { - const token = await getAuthToken(); - const response = await fetch(`${API_BASE}/infrastructure/services`, { - headers: { - 'Authorization': `Bearer ${token}` - } - }); - // ... - } - ``` - -**Success Criteria:** -- ✅ Unauthenticated requests return 401 with proper error -- ✅ Valid OIDC token grants access to protected endpoints -- ✅ Token validation uses Authentik JWKS -- ✅ User email/groups available in endpoints -- ✅ OpenAPI docs show authentication requirement -- ✅ Widget works with OAuth flow - -### 5.2 Tier 2: User-Facing Services (Week 2) - -#### 5.2.1 Nextcloud (OIDC) - -**Plugin:** `user_oidc` (official Nextcloud app) - -**Configuration:** -1. Install app: Apps → Search "OpenID Connect" → Install -2. Create Authentik OIDC Provider - - Client type: Confidential - - Redirect URIs: `https://cloud.schweitz.net/apps/user_oidc/code` - - Scopes: `openid`, `email`, `profile` -3. Configure in Nextcloud - - Settings → OpenID Connect - - Add provider: Authentik - - Discovery URL: `https://auth.schweitz.net/application/o/nextcloud/.well-known/openid-configuration` - - Client ID: from Authentik - - Client Secret: from Authentik - -**Testing:** -- Verify "Login with Authentik" button appears -- Test new user login creates Nextcloud account -- Test existing Nextcloud users can link Google accounts -- Verify file access preserved after OIDC login - -#### 5.2.2 Gitea (OIDC) - -**Native Support:** Gitea has built-in OIDC - -**Configuration:** -1. Create Authentik OIDC Provider for Gitea - - Redirect URI: `https://git.schweitz.net/user/oauth2/authentik/callback` -2. In Gitea Admin → Authentication Sources - - Add authentication source - - Type: OAuth2 - - Provider: OpenID Connect - - Client ID: from Authentik - - Client Secret: from Authentik - - Auto Discovery URL: `https://auth.schweitz.net/application/o/gitea/.well-known/openid-configuration` - -**Testing:** -- Test new user registration via Google -- Test existing user account linking -- Test SSH key management post-OIDC -- Test repository access - -#### 5.2.3 Jellyfin (SSO Plugin) - -**Plugin:** `jellyfin-plugin-sso` (community plugin) - -**Configuration:** -1. Add plugin repository: - - URL: `https://raw.githubusercontent.com/9p4/jellyfin-plugin-sso/manifest-release/manifest.json` -2. Install SSO plugin from catalog -3. Create Authentik OIDC Provider for Jellyfin - - Redirect URI: `https://media.schweitz.net/sso/OID/redirect/authentik` -4. Configure plugin: - - Settings → Plugins → SSO Authentication - - Provider: OIDC - - Client ID/Secret from Authentik - - Discovery URL: `https://auth.schweitz.net/application/o/jellyfin/.well-known/openid-configuration` - -**Testing:** -- Test Google login creates Jellyfin user -- Test viewing permissions inherited -- Test continue watching preserved -- Test mobile app compatibility - -#### 5.2.4 Open WebUI (Native Google OAuth) - -**Note:** Open WebUI supports Google OAuth directly, but switching to Authentik provides unified management. - -**Configuration:** -1. Create Authentik OIDC Provider -2. Update `open-webui.yml` environment: - ```yaml - - ENABLE_OAUTH=true - - OAUTH_PROVIDER_NAME=Google (via Authentik) - - OPENID_PROVIDER_URL=https://auth.schweitz.net/application/o/open-webui/.well-known/openid-configuration - - OAUTH_CLIENT_ID= - - OAUTH_CLIENT_SECRET= - - OAUTH_SCOPES=openid email profile - ``` -3. Restart container - -**Testing:** -- Test login with Google -- Test conversation history preserved -- Test model access unchanged -- Test RAG functionality - -### 5.3 Tier 3: Dashboard & Development Tools (Week 3) - -#### 5.3.1 Organizr (Forward Auth) - -**Note:** Organizr doesn't support OIDC natively. Use trusted header authentication. - -**Configuration:** -1. Create Authentik Proxy Provider - - External host: `https://home.schweitz.net` - - Internal host: `http://organizr:80` - - Forward auth mode: Single application -2. Configure NPM with Authentik forward auth snippet -3. Organizr settings: - - Settings → System Settings → Authentication - - Enable "Auth Proxy" - - Header name: `X-Authentik-Username` - - Auto-create users: Yes - -**Testing:** -- Test redirect to Authentik on access -- Test user creation from header -- Test tab access after login -- Test iframe embedding still works - -#### 5.3.2 code-server (Forward Auth + Host Service) - -**Current State:** Running as systemd service on host at `127.0.0.1:8084` - -**Note:** code-server is a host service (not containerized). Currently uses password authentication. - -**Approach:** Disable built-in auth, use Authentik forward auth via NPM - -**Configuration:** - -1. **Disable code-server Password Auth** - - Edit config: `~/.config/code-server/config.yaml` - ```yaml - bind-addr: 127.0.0.1:8084 - auth: none # Changed from 'password' - cert: false - user-data-dir: /home/jpmschweitzer/docker-data/code-server/user-data - extensions-dir: /home/jpmschweitzer/docker-data/code-server/extensions - ``` - - Restart service: `sudo systemctl restart code-server` - -2. **Create Authentik Proxy Provider** - - In Authentik: Applications → Create - - Name: "Code Server" - - Slug: `code-server` - - Provider type: Proxy Provider - - External host: `https://code.schweitz.net` - - Internal host: `http://127.0.0.1:8084` - - Forward auth mode: Single application - - Authorization flow: (default) - -3. **Configure NPM Proxy Host** - - Domain: `code.schweitz.net` - - Scheme: `http` - - Forward Hostname/IP: `192.168.86.149` (tower-of-joy IP) - - Forward Port: `8084` - - SSL: Let's Encrypt - - Websockets: ✅ Enabled (required for VS Code) - - Advanced config (add Authentik forward auth snippet): - ```nginx - # Authentik Forward Auth - auth_request /outpost.goauthentik.io/auth/nginx; - error_page 401 = @goauthentik_proxy_signin; - auth_request_set $auth_cookie $upstream_http_set_cookie; - add_header Set-Cookie $auth_cookie; - - # Pass authentication headers - auth_request_set $authentik_username $upstream_http_x_authentik_username; - auth_request_set $authentik_groups $upstream_http_x_authentik_groups; - auth_request_set $authentik_email $upstream_http_x_authentik_email; - auth_request_set $authentik_name $upstream_http_x_authentik_name; - auth_request_set $authentik_uid $upstream_http_x_authentik_uid; - - proxy_set_header X-authentik-username $authentik_username; - proxy_set_header X-authentik-groups $authentik_groups; - proxy_set_header X-authentik-email $authentik_email; - proxy_set_header X-authentik-name $authentik_name; - proxy_set_header X-authentik-uid $authentik_uid; - - location @goauthentik_proxy_signin { - internal; - add_header Set-Cookie $auth_cookie; - return 302 /outpost.goauthentik.io/start?rd=$request_uri; - } - - location /outpost.goauthentik.io { - proxy_pass https://auth.schweitz.net/outpost.goauthentik.io; - proxy_set_header Host $host; - proxy_set_header X-Original-URL $scheme://$http_host$request_uri; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header X-Forwarded-Uri $request_uri; - proxy_set_header X-Forwarded-Ssl on; - proxy_set_header X-Forwarded-For $remote_addr; - proxy_set_header X-Real-IP $remote_addr; - proxy_pass_request_body off; - proxy_set_header Content-Length ""; - } - ``` - -4. **Deploy Authentik Outpost** (if not already deployed) - - In Authentik: Outposts → Create - - Name: "NPM Proxy Outpost" - - Type: Proxy - - Integration: Docker (or manual) - - Applications: Select "Code Server" and "Organizr" - -**Testing:** -- ✅ Access `https://code.schweitz.net` redirects to Authentik login -- ✅ After Google authentication, VS Code loads -- ✅ Workspace and extensions preserved -- ✅ Terminal access works -- ✅ File editing and Git integration functional -- ✅ Websocket connection stable (check browser console) -- ✅ Session persists across page reloads - -**Security Notes:** -- code-server has NO auth when accessed via `http://localhost:8084` on the host -- Ensure host firewall blocks external access to port 8084 -- Only accessible via `code.schweitz.net` through authenticated NPM proxy -- UFW rule: `sudo ufw deny 8084/tcp` (block external access) +# Reload +docker exec npm nginx -s reload +``` --- -## 6. Security Considerations +## Appendix B: Environment Variables Reference -### 6.1 Authentication Security - -**Google OAuth Security:** -- OAuth 2.0 with PKCE (Proof Key for Code Exchange) -- Tokens expire per Google's policy (typically 1 hour) -- Refresh tokens handled by Authentik -- No passwords stored in homelab - -**Session Management:** -- Configurable session timeout (recommended: 8 hours) -- Secure, HttpOnly, SameSite cookies -- Session invalidation on logout -- Concurrent session limits (optional) - -**Multi-Factor Authentication:** -- Enforced at Google level (Google Workspace admin can require 2FA) -- Optional: Enable Authentik 2FA as second layer -- Supports TOTP, WebAuthn, Duo - -### 6.2 Authorization & Access Control - -**Role-Based Access Control (RBAC):** -- Define groups in Authentik (e.g., `admin`, `family`, `guest`) -- Map Google Workspace groups to Authentik groups -- Per-service authorization policies -- Attribute-based access control (ABAC) for advanced scenarios - -**Service-Level Permissions:** -- Portainer: Admin vs User roles -- Nextcloud: File permissions via groups -- Jellyfin: Library access per user -- Gitea: Repository permissions - -### 6.3 Network Security - -**Public Exposure:** -- Only NPM and Authentik accessible from internet -- All other services behind reverse proxy -- Optional: Firewall rules to restrict by geo/IP -- Rate limiting on authentication endpoints - -**Internal Security:** -- Services communicate via Docker networks -- core-api only accessible via authenticated NPM proxy -- Ollama/Qdrant remain internal-only (no public access) - -**SSL/TLS:** -- Let's Encrypt certificates via NPM -- TLS 1.3 minimum -- HSTS headers enabled -- Certificate auto-renewal - -### 6.4 Data Privacy - -**User Data:** -- Email and profile from Google (read-only) -- No passwords stored -- User attributes cached in Authentik database -- GDPR compliance: Users can request data deletion - -**Logging & Auditing:** -- Authentication attempts logged -- Failed login alerts -- Access logs per service -- Retention policy (recommend 90 days) - -**Secrets Management:** -- Client secrets stored in Authentik database (encrypted at rest) -- Database backups encrypted -- Secrets not committed to git -- Consider: HashiCorp Vault for advanced scenarios - -### 6.5 Backup & Recovery - -**Critical Data to Backup:** -1. Authentik PostgreSQL database (user profiles, OIDC clients) -2. Authentik configuration (flows, policies, providers) -3. NPM configuration (proxy hosts, SSL certs) -4. Service-specific user databases (if not using OIDC exclusively) - -**Disaster Recovery:** -- Document OIDC client configurations for each service -- Export Authentik flows and policies -- Keep Google OAuth credentials in secure vault -- Test restore procedure quarterly +**Authentik Critical Variables:** +```bash +AUTHENTIK_HOST=https://auth.schweitz.net +AUTHENTIK_HOST_BROWSER=https://auth.schweitz.net +AUTHENTIK_COOKIE_DOMAIN=.schweitz.net +AUTHENTIK_COOKIE_SAMESITE=lax +AUTHENTIK_POSTGRESQL__HOST=postgres-shared +AUTHENTIK_REDIS__HOST=redis-shared +AUTHENTIK_LOG_LEVEL=warning +AUTHENTIK_BOOTSTRAP_WORKERS=1 +AUTHENTIK_WORKER__CONCURRENCY=2 +``` --- -## 7. Implementation Phases +## Appendix C: Known Issues & Workarounds -### Phase 1: Foundation (Week 1) -**Goal:** Deploy Authentik, configure Google OAuth, protect core-api +### C.1 Authentik 2024.10.x Redirect Loop Bug -**Tasks:** -- [ ] Deploy Authentik stack (server, worker, PostgreSQL, Redis) -- [ ] Configure NPM proxy: `auth.schweitz.net` -- [ ] Set up Google Cloud OAuth credentials -- [ ] Configure Google as Authentik source -- [ ] Test login with Google Workspace account -- [ ] Test login with Gmail account -- [ ] Create core-api proxy provider -- [ ] Configure NPM forward auth for `api.schweitz.net` -- [ ] Test widget access with authentication -- [ ] Document OIDC client creation process +**Issue:** Version 2024.10 has redirect loop bug. -**Success Criteria:** -- ✅ Authentik accessible and functional -- ✅ Google login works for both account types -- ✅ core-api protected and publicly accessible -- ✅ Service control widget requires auth +**Workaround:** Use version 2024.8.4 (pinned in authentik.yml). -**Rollback Plan:** -- Remove `api.schweitz.net` NPM proxy -- Keep core-api on internal port only -- Destroy Authentik stack -- No impact on existing services - -### Phase 2: User Services (Week 2) -**Goal:** Integrate user-facing applications (Nextcloud, Gitea, Jellyfin, Open WebUI) - -**Tasks:** -- [ ] Install Nextcloud OIDC app -- [ ] Create Authentik OIDC provider for Nextcloud -- [ ] Configure and test Nextcloud SSO -- [ ] Configure Gitea OIDC authentication -- [ ] Test Gitea login and repository access -- [ ] Install Jellyfin SSO plugin -- [ ] Configure Jellyfin OIDC -- [ ] Test Jellyfin media playback with SSO -- [ ] Update Open WebUI environment for Authentik -- [ ] Test Open WebUI AI conversations with SSO - -**Success Criteria:** -- ✅ All 4 services support Google login -- ✅ New users auto-created on first login -- ✅ Service functionality unchanged post-SSO -- ✅ Sessions persist appropriately - -**Rollback Plan:** -- Disable OIDC providers in Authentik -- Re-enable native auth in each service -- Users revert to local passwords -- No data loss - -### Phase 3: Admin Tools (Week 3) -**Goal:** Secure admin interfaces (Portainer, Organizr, Uptime Kuma, Netdata) - -**Tasks:** -- [ ] Configure Portainer OAuth -- [ ] Test Portainer admin access via Google -- [ ] Set up Organizr trusted header auth -- [ ] Configure NPM forward auth for Organizr -- [ ] Test Organizr tab functionality -- [ ] Disable Uptime Kuma internal auth -- [ ] Configure forward auth for status.schweitz.net -- [ ] Test monitor creation and access -- [ ] Configure Netdata forward auth -- [ ] Test metrics access and real-time graphs - -**Success Criteria:** -- ✅ All admin tools require Google authentication -- ✅ Functionality preserved -- ✅ Unauthorized access blocked - -**Rollback Plan:** -- Re-enable native auth in services -- Remove forward auth from NPM -- Services remain accessible on local network - -### Phase 4: Hardening & Documentation (Week 4) -**Goal:** Finalize security, monitoring, and documentation - -**Tasks:** -- [ ] Configure Authentik flows (MFA, password policies) -- [ ] Set up Authentik groups (admin, family, guest) -- [ ] Define per-service authorization policies -- [ ] Enable audit logging across all services -- [ ] Set up alerts for failed auth attempts -- [ ] Configure session timeouts -- [ ] Test logout across all services -- [ ] Document user onboarding process -- [ ] Create troubleshooting guide -- [ ] Backup Authentik configuration -- [ ] Test disaster recovery procedure - -**Success Criteria:** -- ✅ Security policies enforced -- ✅ Monitoring and alerting operational -- ✅ Documentation complete -- ✅ Backup/restore tested +**Reference:** https://github.com/goauthentik/authentik/issues/11883 --- -## 8. Alternative Approaches +**END OF SECURITY IMPLEMENTATION PLAN v2.0** -### 8.1 Authelia + LDAP (Google Workspace Directory Sync) +**Ready for Implementation:** Yes ✓ +**Reviewed By:** Claude Code (Autonomous Analysis) +**Approved By:** [User - Pending] -**Architecture:** -- Use Google Workspace Directory Sync or third-party tool -- Sync Google users to OpenLDAP or lldap -- Authelia authenticates against LDAP -- Services use Authelia for forward auth or LDAP directly - -**Pros:** -- Authelia is lighter weight than Authentik -- Simple reverse proxy integration -- Lower resource usage - -**Cons:** -- Cannot directly "log in with Google" (not true SSO) -- Requires separate sync mechanism (complexity) -- Password management still needed (defeats purpose) -- No single-click login experience -- User changes not real-time - -**Verdict:** ❌ Rejected - Doesn't meet "login with Google" requirement - -### 8.2 Keycloak - -**Pros:** -- Enterprise-grade, battle-tested -- Comprehensive features -- Excellent Google federation - -**Cons:** -- Heavy resource usage (1-2GB RAM) -- Overkill for homelab scale -- Complex configuration - -**Verdict:** ⚠️ Viable but excessive - -### 8.3 Service-by-Service Google OAuth - -**Architecture:** -- Configure Google OAuth directly in each service -- No central IdP -- Each service manages its own sessions - -**Pros:** -- No additional infrastructure -- Direct Google integration - -**Cons:** -- No centralized access control -- Each service needs separate OAuth client -- Inconsistent user experience -- Cannot protect services without native OAuth (core-api, Organizr, etc.) -- No unified session management -- User management scattered - -**Verdict:** ❌ Rejected - No solution for core-api or non-OIDC services - ---- - -## 9. Cost Analysis - -### 9.1 Resource Requirements - -**New Infrastructure:** - -| Component | CPU | RAM | Storage | Notes | -|-----------|-----|-----|---------|-------| -| Authentik Server | 0.5 | 256 MB | 1 GB | Main application | -| Authentik Worker | 0.3 | 128 MB | - | Background tasks | -| PostgreSQL | 0.5 | 256 MB | 2 GB | User database | -| Redis | 0.2 | 64 MB | 100 MB | Cache | -| **Total** | **1.5 CPU** | **~700 MB** | **~3 GB** | | - -**Current System:** -- CPU: Intel i7-6700 (4 cores / 8 threads) - 1.5 core usage is ~19% -- RAM: 16 GB total - 700 MB is ~4% -- Storage: SSD space available - -**Verdict:** ✅ Easily within capacity - -### 9.2 Time Investment - -| Phase | Estimated Time | Skill Level | -|-------|----------------|-------------| -| Research & Planning | 8 hours | All levels | -| Authentik Deployment | 2 hours | Intermediate | -| Google OAuth Setup | 1 hour | Beginner | -| core-api Protection | 2 hours | Intermediate | -| Service Integration (4 services) | 8 hours | Intermediate | -| Admin Tool Integration (4 services) | 6 hours | Intermediate | -| Testing & Validation | 4 hours | All levels | -| Documentation | 4 hours | All levels | -| **Total** | **35 hours** | | - -**Breakdown per week:** -- Week 1: 10 hours (Foundation) -- Week 2: 10 hours (User services) -- Week 3: 10 hours (Admin tools) -- Week 4: 5 hours (Hardening) - -### 9.3 Monetary Costs - -**Required:** -- Google Cloud Project: Free (OAuth is free tier) -- Domain: schweitz.net (assuming already owned) -- SSL Certificates: Free (Let's Encrypt via NPM) - -**Optional:** -- Google Workspace subscription: If using Workspace features (not required for OAuth) -- Cloud monitoring tools: Free tier available - -**Total additional cost:** $0 (assuming domain already owned) - ---- - -## 10. Risk Assessment - -### 10.1 Technical Risks - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| Authentik failure locks out all services | Low | High | Keep local admin account; VPN access | -| Google OAuth outage | Low | Medium | Authentik has local account fallback | -| Session handling bugs | Medium | Low | Extensive testing; gradual rollout | -| Plugin incompatibilities (Jellyfin, Nextcloud) | Medium | Medium | Test thoroughly; keep native auth during transition | -| Database corruption (PostgreSQL) | Low | High | Automated backups; replication (optional) | - -### 10.2 Operational Risks - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| User confusion during transition | High | Low | Clear documentation; email notifications | -| Lost access due to forgotten Google account | Low | Medium | Admin can manually link accounts | -| Mobile app compatibility issues | Medium | Medium | Test all mobile apps; document workarounds | -| Breaking change in Authentik update | Low | Medium | Pin versions; test updates in staging | - -### 10.3 Security Risks - -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| Compromise of Google account | Low | High | Enforce 2FA at Google level | -| Authentik vulnerability | Low | High | Keep updated; subscribe to security advisories | -| Session hijacking | Low | Medium | Secure cookies; short timeouts | -| Misconfigured OIDC client | Medium | Medium | Follow official docs; peer review configs | - ---- - -## 11. Success Metrics - -### 11.1 Technical Metrics - -- **Authentication Success Rate:** >99% of login attempts succeed -- **Session Uptime:** Authentik availability >99.9% -- **Response Time:** Login flow completes in <3 seconds -- **Integration Coverage:** 100% of user-facing services support SSO - -### 11.2 Security Metrics - -- **Zero Unauthorized Access:** No successful unauthorized access attempts -- **Audit Log Completeness:** 100% of auth events logged -- **MFA Adoption:** >80% of users enable 2FA at Google level (if enforced) -- **Failed Login Rate:** <5% of attempts fail (indicates good UX) - -### 11.3 User Experience Metrics - -- **Login Friction:** 1 click to authenticate (vs 8+ passwords previously) -- **User Onboarding:** New user can access all services in <5 minutes -- **User Satisfaction:** Positive feedback on unified login -- **Support Tickets:** <10% increase in auth-related support (temporary during transition) - ---- - -## 12. Next Steps & Discussion Points - -### 12.1 Questions for User - -Before proceeding with implementation, please confirm: - -1. **Google Account Type:** - - Do you have Google Workspace, or Gmail only? - - Should we support both types of accounts? - -2. **User Base:** - - How many users will access the system? - - Do you need group-based access control (e.g., family, friends, admin)? - -3. **Service Priority:** - - Which services are most critical to secure first? - - Any services you want to exclude from SSO? - -4. **MFA Requirements:** - - Should we enforce 2FA for all users? - - At Google level, Authentik level, or both? - -5. **Session Duration:** - - How long should sessions last before re-authentication? - - Different timeouts for admin vs regular users? - -6. **Rollout Strategy:** - - Gradual rollout (one service at a time) or big-bang? - - Pilot with single user before full deployment? - -### 12.2 Immediate Next Actions - -If approved, the first concrete steps: - -1. **Create `stacks/authentik.yml`** - Docker Compose for Authentik stack -2. **Deploy Authentik** - `make deploy-authentik` (or via Portainer UI) -3. **Access Setup** - `http://localhost:9000` → complete initial wizard -4. **Google Cloud Console** - Create OAuth 2.0 client credentials -5. **Configure Google Source** - In Authentik admin panel -6. **Test Authentication** - Verify Google login works for test account -7. **Document OIDC Config** - Template for service integration - -### 12.3 Documentation Deliverables - -Post-implementation, we'll create: - -1. **User Guide:** "How to Log In to Homelab Services" -2. **Admin Guide:** "Managing Users and Access in Authentik" -3. **Troubleshooting Guide:** Common issues and solutions -4. **Architecture Diagram:** Visual representation of auth flow -5. **Runbook:** Disaster recovery and maintenance procedures - ---- - -## 13. Conclusion - -### 13.1 Summary - -This plan proposes using **Authentik** as a central Identity Provider (IdP) that federates with **Google OAuth** to provide secure, unified authentication across all homelab services. This architecture: - -- **Meets Requirements:** Direct Google login for Workspace and Gmail accounts -- **Addresses Security Gap:** Protects core-api for public API exposure -- **Provides Flexibility:** Supports OIDC, SAML, LDAP, and proxy auth -- **Scales Appropriately:** Right-sized for homelab (~700MB RAM) -- **Maintains Simplicity:** Clear integration patterns per service - -### 13.2 Key Benefits - -**For Users:** -- Single-click login with Google across all services -- No password management or sprawl -- Consistent authentication experience -- Mobile-friendly (Google OAuth optimized) - -**For Admin:** -- Centralized user and access management -- Unified audit logging and security monitoring -- Granular access control per service -- Easy onboarding/offboarding - -**For Security:** -- No passwords stored in homelab -- MFA enforced at Google level -- Reduced attack surface (one auth point) -- Professional-grade OAuth 2.0 implementation - -### 13.3 Recommendation - -**Proceed with Authentik implementation** following the phased approach: -- Week 1: Foundation (Authentik + core-api protection) -- Week 2: User services integration -- Week 3: Admin tools integration -- Week 4: Hardening and documentation - -This gradual rollout minimizes risk, allows for testing at each stage, and provides clear rollback points. - ---- - -## Appendix A: Reference Links - -### Official Documentation -- **Authentik:** https://docs.goauthentik.io/ -- **Google OAuth 2.0:** https://developers.google.com/identity/protocols/oauth2 -- **Nginx Proxy Manager:** https://nginxproxymanager.com/ -- **Portainer OAuth:** https://docs.portainer.io/admin/settings/authentication/oauth -- **Nextcloud OIDC:** https://github.com/nextcloud/user_oidc -- **Gitea OAuth:** https://docs.gitea.com/usage/authentication -- **Jellyfin SSO Plugin:** https://github.com/9p4/jellyfin-plugin-sso -- **Open WebUI SSO:** https://docs.openwebui.com/features/auth/sso/ - -### Integration Guides -- **Authentik + NPM:** https://docs.goauthentik.io/integrations/services/nginx-proxy-manager/ -- **Authentik + Google:** https://docs.goauthentik.io/docs/users-sources/sources/social-logins/google/ -- **Authentik + Nextcloud:** https://docs.goauthentik.io/integrations/services/nextcloud/ -- **Authentik + Portainer:** https://docs.goauthentik.io/integrations/services/portainer/ -- **Authentik + Gitea:** https://www.authelia.com/integration/openid-connect/clients/gitea/ (similar for Authentik) - -### Community Resources -- **r/selfhosted:** https://reddit.com/r/selfhosted -- **Authentik Discord:** https://discord.gg/jg33eMhnj6 -- **Homelab Forum:** https://homelabos.com/ - ---- - -## Appendix B: Glossary - -- **OAuth 2.0:** Authorization framework for delegated access -- **OIDC (OpenID Connect):** Authentication layer on top of OAuth 2.0 -- **IdP (Identity Provider):** Service that authenticates users (Authentik in our case) -- **SSO (Single Sign-On):** One login for multiple applications -- **SAML:** XML-based standard for exchanging auth data -- **LDAP:** Protocol for accessing directory services -- **Forward Auth:** Reverse proxy checks auth before forwarding request -- **PKCE:** Security extension for OAuth to prevent interception attacks -- **RBAC:** Role-Based Access Control -- **2FA/MFA:** Two-Factor / Multi-Factor Authentication - ---- - -**End of Document** - -*This plan is ready for review and discussion. Once approved, we can proceed with Phase 1 implementation.* +*This plan is a living document. Update Section 8 (Progress Tracking) as you progress through milestones.* diff --git a/stacks/organizr.yml b/stacks/organizr.yml index 73f9cef..b70f7ba 100644 --- a/stacks/organizr.yml +++ b/stacks/organizr.yml @@ -22,6 +22,12 @@ services: - PGID=1000 - TZ=Europe/Amsterdam - fpm=true # Enable PHP-FPM for better performance + - DB_TYPE=pgsql + - DB_HOST=postgres-shared + - DB_PORT=5432 + - DB_NAME=organizr + - DB_USER=organizr_user + - DB_PASS=${ORGANIZR_DB_PASSWORD} networks: - docker-dataplane