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

49 KiB

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:

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

  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.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.netauthentik-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):

    PyJWT[crypto]==2.8.0
    python-jose[cryptography]==3.3.0
    
  3. Create Authentication Module (src/auth/oidc.py):

    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):

    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):

    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):

    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):

    // 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:
    - 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
    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):
    # 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.


Official Documentation

Integration Guides

Community Resources


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.