Files
portainer-core/docs/security-implementation-plan.md
T

1375 lines
49 KiB
Markdown

# Security Implementation Plan: 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
## 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.
**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.
**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
---
## 0. Scope Definition & Architecture Decision
### 0.1 Implementation Scope
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)
**❌ 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
**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
### 0.2 Core-API Authentication Strategy: FastAPI Native OIDC
**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
```
**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
**Protected Endpoints:**
- All `/infrastructure/*` - Require valid OIDC token
- All `/v1/*` (AI endpoints) - Require valid OIDC token
---
## 1. Current State: Service Inventory & Authentication Capabilities
### 1.1 In-Scope Services (External-Facing)
| 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) |
**Legend:**
- ✅ Native OIDC/OAuth support
- ⚠️ Proxy-based authentication (forward auth via NPM)
- ❌ No authentication (critical security gap)
### 1.2 Out-of-Scope Services
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
```
┌─────────────────────────────────────────────────────────────┐
│ 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) │
│ │ └───────────────────────────┘
└───────────┘
Legend:
━━━ Direct OIDC/OAuth ─── HTTP Proxy ╌╌╌ Forward Auth
```
### 4.2 Authentication Flow
**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
**Subsequent Access:**
- Session cookie valid for configurable duration (e.g., 8 hours)
- No re-authentication required within session
- Logout clears all service sessions
### 4.3 Component Architecture
#### New Components to Deploy
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
---
## 5. Service-by-Service Integration Plan
### 5.1 Tier 1: Critical Infrastructure (Week 1)
#### 5.1.1 Authentik Deployment
**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
**Success Criteria:**
- Authentik accessible via NPM at `auth.schweitz.net`
- Admin portal functional
- Health checks passing
#### 5.1.2 Google OAuth Source Configuration
**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`
**Testing:**
1. Test login with Google Workspace account
2. Test login with Gmail account
3. Verify user profile attributes synced
4. Confirm logout works
#### 5.1.3 core-api Protection (CRITICAL)
**Current Risk:** core-api at port 8083 has **zero authentication**. Cannot expose publicly without SSO.
**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=<from_authentik>
- OAUTH_CLIENT_SECRET=<from_authentik>
- 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)
---
## 6. Security Considerations
### 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
---
## 7. Implementation Phases
### Phase 1: Foundation (Week 1)
**Goal:** Deploy Authentik, configure Google OAuth, protect core-api
**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
**Success Criteria:**
- ✅ Authentik accessible and functional
- ✅ Google login works for both account types
- ✅ core-api protected and publicly accessible
- ✅ Service control widget requires auth
**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
---
## 8. Alternative Approaches
### 8.1 Authelia + LDAP (Google Workspace Directory Sync)
**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.*