Files
portainer-core/plans/active/security-implementation-plan.md
T

1143 lines
35 KiB
Markdown

# Security Implementation Plan v2.0: Google OAuth SSO for Homelab Infrastructure
**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 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).
**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
**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
**SSO Inclusion Policy:**
-**Include:** Web-based admin interfaces, dashboards, APIs requiring browser access
-**Exclude:** Services with native mobile/desktop apps that work better with username/password
-**Exclude:** Media streaming services (Jellyfin) - app integration priority
-**Exclude:** Development tools (code-server) - IDE integration priority
- ⏸️ **Defer:** Disabled/inactive services (Nextcloud) - implement when re-enabled
---
## Table of Contents
- [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)
---
## Section 0: Failure Analysis & Lessons Learned
### 0.1 What Happened in v1.0 Deployment
**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`)
**Root Causes Identified:**
#### 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!
```
**Why it happened:** NPM configuration had forward auth enabled on `auth.schweitz.net` itself.
**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
---
## Section 1: Prerequisites & Environment Validation
### 1.1 System Specifications
| 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: <Google Client ID>
Consumer Secret: <Google Client 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=<paste-key-here>" >> .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: <secure-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
**Status:****COMPLETE** - Using forward auth (shares Organizr Proxy provider)
**Implementation Decision (2025-11-23):**
- Core API already protected with forward auth via NPM
- Shares "Organizr Proxy" provider with home.schweitz.net
- Authentication working correctly with Google OAuth
- Headers forwarded: X-authentik-username, X-authentik-email, X-authentik-groups, X-authentik-name, X-authentik-uid
- **Decision:** Keep current setup, defer separate admin provider to avoid complexity
- **Rationale:** Current implementation is secure and functional for homelab use case
**Configuration:**
- Provider: Organizr Proxy (shared)
- External host: https://api.schweitz.net
- Outpost: Standalone proxy (port 9445)
- Mode: forward_single
**Expected Duration:** ~~90-120 minutes~~ SKIPPED (already functional)
---
### Milestone 5: Remaining Services (Gradual Rollout)
**Objective:** Enable forward auth on remaining services, one at a time, testing each before proceeding.
**Dependencies:** M3 and M4 completed successfully
**Services to Protect:**
- Gitea (git.schweitz.net)
- Open WebUI (no external domain yet)
- Netdata (no external domain yet)
- Uptime Kuma (no external domain yet)
- AMP (amp.schweitz.net)
- Tatlock (tatlock.schweitz.net)
**Services EXCLUDED from SSO (Keep Native Auth):**
-**Jellyfin (media.schweitz.net)** - Better mobile app integration with native auth
-**code-server (code.schweitz.net)** - Better VS Code integration with native auth
-**Nextcloud (cloud.schweitz.net)** - Service disabled, SSO deferred until re-enabled
**Rationale for Exclusions:**
- Jellyfin and code-server have excellent native authentication
- Mobile apps and desktop clients work better with username/password
- SSO adds complexity without significant security benefit for these services
- Nextcloud is not currently in active use
**Expected Duration:** 3-6 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:**
- ⏳ Not Started
- 🔄 In Progress
- ✅ Completed
- ⚠️ Issues/Blocked
- ❌ Failed
### 8.2 Detailed Progress Log
**Update this section as you progress through each milestone.**
**Template:**
```
### Milestone X: [Name]
**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
```
### 8.3 Configuration Changelog
**Track all configuration changes:**
| Date | Component | Change | Reason | Rollback |
|------|-----------|--------|--------|----------|
| 2025-11-20 | Plan | Created v2.0 | Post-failure revision | N/A |
| - | - | - | - | - |
### 8.4 Lessons Learned (Real-Time)
**Add lessons as you encounter issues:**
```
[Date] [Milestone] [Lesson]
```
---
## Appendix A: Quick Reference Commands
### A.1 Authentik Management
```bash
# Check status
docker ps | grep authentik
docker logs authentik-server --tail 50
# Restart
docker restart authentik-server authentik-worker
# Check database
docker exec authentik-server ak check
# Run migrations
docker exec authentik-server ak migrate
# Check outpost
curl http://authentik-server:9443/outpost.goauthentik.io/ping/
```
### A.2 NPM Management
```bash
# Backup
curl -s http://192.168.86.149:8000/api/ > backups/npm-$(date +%Y%m%d-%H%M%S).json
# Test config
docker exec npm nginx -t
# Reload
docker exec npm nginx -s reload
```
---
## Appendix B: Environment Variables Reference
**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
```
---
## Appendix C: Known Issues & Workarounds
### C.1 Authentik 2024.10.x Redirect Loop Bug
**Issue:** Version 2024.10 has redirect loop bug.
**Workaround:** Use version 2024.8.4 (pinned in authentik.yml).
**Reference:** https://github.com/goauthentik/authentik/issues/11883
---
**END OF SECURITY IMPLEMENTATION PLAN v2.0**
**Ready for Implementation:** Yes ✓
**Reviewed By:** Claude Code (Autonomous Analysis)
**Approved By:** [User - Pending]
*This plan is a living document. Update Section 8 (Progress Tracking) as you progress through milestones.*