Initial commit: Keycloak SSO for MischLabs

Keycloak 26.1 + PostgreSQL 16 Docker stack with pre-configured
realm containing 10 OIDC clients for all MischLabs services.
Includes integration guides for Tier 1-3 services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kroonk
2026-04-13 20:42:59 +02:00
commit 8a4beb879a
5 changed files with 504 additions and 0 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
# PostgreSQL
POSTGRES_DB=keycloak
POSTGRES_USER=keycloak
POSTGRES_PASSWORD=CHANGE_ME_use_a_strong_random_password
# Keycloak Admin (only used on first boot)
KC_ADMIN_USER=admin
KC_ADMIN_PASSWORD=CHANGE_ME_use_a_strong_random_password

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Secrets
.env
# PostgreSQL data
pgdata/
# OS files
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# IDE
.vscode/
.idea/
# Claude Code
.claude/

172
Brain.md Normal file
View File

@@ -0,0 +1,172 @@
# Brain.md - Projekt auth.mischlabs.de (Keycloak SSO)
## Projektziel
Zentrales Single Sign-On (SSO) fuer alle MischLabs-Dienste ueber Keycloak. Laeuft als Docker-Container-Stack (Keycloak + PostgreSQL) auf MischNAS und ist ueber Cloudflare Tunnel erreichbar.
## Status
- [x] Git-Repo initialisiert und Remote verbunden (https://git.mischlabs.de/MrDiderot/auth.git)
- [x] docker-compose.yml mit Keycloak + PostgreSQL erstellt
- [x] Realm-Konfiguration als JSON (mischlabs-realm.json) mit 10 Clients
- [x] .env.example und .gitignore erstellt
- [ ] .env mit echten Passwoertern auf der NAS anlegen
- [ ] Erster Push zum Remote (Git-Auth-Fehler, muss manuell gepusht werden)
- [ ] Deployment auf MischNAS (docker compose up -d)
- [ ] Cloudflare Tunnel Route fuer auth.mischlabs.de -> localhost:8180
- [ ] Client Secrets aus Keycloak Admin-Console holen
- [ ] Tier-1-Dienste anbinden (Gitea, Nextcloud, Mealie, Docs)
- [ ] Tier-2-Dienste anbinden (Audiobookshelf, Vaultwarden, PocketBase)
- [ ] Tier-3-Dienste via Cloudflare Access (Jellyfin, MiChess)
## Dienste-Integration (9 + Cloudflare Access)
| # | Client ID | Subdomain | Tier | Status |
|---|-----------|-----------|------|--------|
| 1 | gitea | git.mischlabs.de | 1 - Nativ OIDC | Offen |
| 2 | nextcloud | drive.mischlabs.de | 1 - Nativ OIDC | Offen |
| 3 | mealie | foodlabs.mischlabs.de | 1 - Nativ OIDC | Offen |
| 4 | docs | docs.mischlabs.de | 1 - Nativ OIDC | Offen |
| 5 | audiobookshelf | audiobook.mischlabs.de | 2 - Teilweise | Offen |
| 6 | vaultwarden | password.mischlabs.de | 2 - Teilweise | Offen |
| 7 | pocketbase | tom.mischlabs.de | 2 - Teilweise | Offen |
| 8 | jellyfin | movies.mischlabs.de | 3 - CF Access | Offen |
| 9 | michess | michess.mischlabs.de | 3 - CF Access | Offen |
| 10 | cloudflare-access | Cloudflare Zero Trust | Proxy-Auth | Offen |
## Integrations-Anleitungen pro Dienst
### Tier 1: Native OIDC
**Gitea** (git.mischlabs.de)
- Admin > Authentication Sources > Add > OAuth2 > OpenID Connect
- Client ID: `gitea`, Secret: aus Keycloak
- Discovery URL: `https://auth.mischlabs.de/realms/mischlabs/.well-known/openid-configuration`
**Nextcloud** (drive.mischlabs.de)
- App `oidc_login` installieren, config.php:
```
'oidc_login_provider_url' => 'https://auth.mischlabs.de/realms/mischlabs',
'oidc_login_client_id' => 'nextcloud',
'oidc_login_client_secret' => '<aus-keycloak>',
'oidc_login_auto_redirect' => false,
'oidc_login_button_text' => 'MischLabs SSO',
```
**Mealie** (foodlabs.mischlabs.de)
- Environment Variables:
```
OIDC_AUTH_ENABLED=true
OIDC_CONFIGURATION_URL=https://auth.mischlabs.de/realms/mischlabs/.well-known/openid-configuration
OIDC_CLIENT_ID=mealie
OIDC_CLIENT_SECRET=<aus-keycloak>
OIDC_AUTO_REDIRECT=false
OIDC_PROVIDER_NAME=MischLabs SSO
OIDC_USER_GROUP=user
OIDC_ADMIN_GROUP=admin
```
**Docs** (docs.mischlabs.de) - BookStack Beispiel:
```
AUTH_METHOD=oidc
OIDC_NAME=MischLabs SSO
OIDC_CLIENT_ID=docs
OIDC_CLIENT_SECRET=<aus-keycloak>
OIDC_ISSUER=https://auth.mischlabs.de/realms/mischlabs
OIDC_ISSUER_DISCOVER=true
```
### Tier 2: Teilweise OIDC
**Audiobookshelf** (audiobook.mischlabs.de)
- Settings > Authentication > OpenID Connect
- Issuer URL: `https://auth.mischlabs.de/realms/mischlabs`
- WICHTIG: In Keycloak Admin > audiobookshelf Client > Advanced > User Info Signed Response Algorithm auf leer/none setzen
- Notfall-Login: `https://audiobook.mischlabs.de/login/?autoLaunch=0`
**Vaultwarden** (password.mischlabs.de)
- Environment Variables:
```
SSO_ENABLED=true
SSO_PKCE=true
SSO_AUTHORITY=https://auth.mischlabs.de/realms/mischlabs
SSO_CLIENT_ID=vaultwarden
SSO_CLIENT_SECRET=<aus-keycloak>
```
- HINWEIS: Master-Passwort bleibt weiterhin noetig!
**PocketBase** (tom.mischlabs.de)
- Admin > Settings > Auth Providers > OIDC konfigurieren
- Erstellt eigene User-Records, verlinkt mit OAuth2-Identitaet
### Tier 3: Cloudflare Access
**Jellyfin + MiChess** - Kein natives OIDC
1. In Cloudflare Zero Trust > Settings > Authentication > Add OpenID Connect:
- App ID: `cloudflare-access`, Secret: aus Keycloak
- Auth URL: `https://auth.mischlabs.de/realms/mischlabs/protocol/openid-connect/auth`
- Token URL: `https://auth.mischlabs.de/realms/mischlabs/protocol/openid-connect/token`
- Certificate URL: `https://auth.mischlabs.de/realms/mischlabs/protocol/openid-connect/certs`
2. Access > Applications > Self-hosted > movies.mischlabs.de / michess.mischlabs.de
3. Policy: Allow > Emails oder Gruppen definieren
## Technische Entscheidungen
- **Keycloak 26.1** (quay.io/keycloak/keycloak:26.1) - aktuelles stabiles Release
- **PostgreSQL 16-alpine** als Datenbank - leichtgewichtig, bewaehrt
- **Realm-Import via JSON** (`--import-realm`) - versionskontrolliert, reproduzierbar
- **`start` (nicht `start-dev`)** - Produktionsmodus mit sicheren Defaults
- **KC_PROXY_HEADERS=xforwarded** - fuer Cloudflare Tunnel
- **KC_HOSTNAME=https://auth.mischlabs.de** - volle URL inkl. Schema
- **KC_HTTP_ENABLED=true** - SSL wird von Cloudflare terminiert
- **Port 8180** - vermeidet Konflikte mit bestehenden Diensten
- **PostgreSQL nicht exponiert** - nur ueber Docker-Netzwerk erreichbar
- **Client Secrets nicht im JSON** - werden von Keycloak auto-generiert
## Docker-Setup
- **Container-Namen**: `auth` (Keycloak), `auth-db` (PostgreSQL)
- **Images**: quay.io/keycloak/keycloak:26.1, postgres:16-alpine
- **Port-Mapping**: 8180:8080 (Keycloak HTTP), 9000:9000 (Health/Metrics, nur lokal!)
- **Volumes**: `auth_pgdata` (PostgreSQL-Daten), `./realm` (Realm-Import, read-only)
- **Netzwerk**: `auth-network` (Bridge, nur intern)
- **Health-Checks**: PostgreSQL via pg_isready, Keycloak via /health/ready (Port 9000)
## Dateistruktur
```
auth/
docker-compose.yml - Keycloak + PostgreSQL Stack
.env.example - Vorlage fuer Secrets (in Git)
.env - Echte Secrets (NICHT in Git!)
realm/
mischlabs-realm.json - Realm mit allen 10 Clients
Brain.md - Dieses Dokument (KI-Uebergabe)
.gitignore - Schliesst .env und .claude/ aus
```
## Deployment-Workflow
1. Repo auf Gitea erstellen: `auth`
2. Code pushen: `git push -u origin master`
3. Auf MischNAS:
```
cd /volume2/docker
git clone https://git.mischlabs.de/MrDiderot/auth.git
cd auth
cp .env.example .env
nano .env # Echte Passwoerter eintragen (openssl rand -base64 32)
docker compose up -d
```
4. Cloudflare Tunnel: Route `auth.mischlabs.de` -> `http://localhost:8180`
5. Admin-Console: https://auth.mischlabs.de/admin/mischlabs/console/
6. Client Secrets holen und in Dienste konfigurieren
## Wichtige URLs
- Admin-Console: https://auth.mischlabs.de/admin/mischlabs/console/
- OIDC Discovery: https://auth.mischlabs.de/realms/mischlabs/.well-known/openid-configuration
- Health-Check: http://localhost:9000/health/ready (nur lokal)
## Erkenntnisse & Notizen
- KC_BOOTSTRAP_ADMIN_* wird nur beim allerersten Start verarbeitet
- Client Secrets muessen nach dem Start aus der Admin-Console geholt werden
- Audiobookshelf hat bekannte Bugs mit Keycloak (UserInfo Signing Algorithm)
- Vaultwarden SSO ersetzt NICHT das Master-Passwort (nur Authentifizierung)
- Jellyfin hat kein natives OIDC - Community-Plugin oder Cloudflare Access
- Port 9000 NICHT ueber Cloudflare Tunnel exponieren (nur Health/Metrics)
- Bei Realm-Import: `--import-realm` ueberspringt bereits existierende Realms
- Git-Push schlug fehl (Auth-Fehler) - muss manuell mit frischem Token gepusht werden
- Verwandtes Projekt: mischlabs Landing Page (d:\Vibecoding\Website\mischlabs)

70
docker-compose.yml Normal file
View File

@@ -0,0 +1,70 @@
services:
auth-db:
image: postgres:16-alpine
container_name: auth-db
restart: unless-stopped
volumes:
- auth_pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- auth-network
auth:
image: quay.io/keycloak/keycloak:26.1
container_name: auth
restart: unless-stopped
command: start --import-realm
environment:
# Database
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://auth-db:5432/${POSTGRES_DB}
KC_DB_USERNAME: ${POSTGRES_USER}
KC_DB_PASSWORD: ${POSTGRES_PASSWORD}
# Hostname & Proxy (Cloudflare Tunnel)
KC_HOSTNAME: https://auth.mischlabs.de
KC_PROXY_HEADERS: xforwarded
KC_HTTP_ENABLED: "true"
# Health & Metrics
KC_HEALTH_ENABLED: "true"
KC_METRICS_ENABLED: "true"
# Bootstrap Admin (only used on first start)
KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_ADMIN_USER}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD}
# Logging
KC_LOG_LEVEL: info
ports:
- "8180:8080"
- "9000:9000"
volumes:
- ./realm:/opt/keycloak/data/import:ro
depends_on:
auth-db:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; timeout 1 cat <&3 | grep -q '200 OK'"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- auth-network
volumes:
auth_pgdata:
driver: local
networks:
auth-network:
driver: bridge

235
realm/mischlabs-realm.json Normal file
View File

@@ -0,0 +1,235 @@
{
"realm": "mischlabs",
"enabled": true,
"displayName": "MischLabs",
"displayNameHtml": "<strong>Misch</strong>Labs",
"sslRequired": "external",
"registrationAllowed": false,
"registrationEmailAsUsername": false,
"loginWithEmailAllowed": true,
"duplicateEmailsAllowed": false,
"resetPasswordAllowed": true,
"editUsernameAllowed": false,
"bruteForceProtected": true,
"permanentLockout": false,
"maxFailureWaitSeconds": 900,
"maxDeltaTimeSeconds": 43200,
"failureFactor": 5,
"accessTokenLifespan": 300,
"ssoSessionIdleTimeout": 1800,
"ssoSessionMaxLifespan": 36000,
"offlineSessionIdleTimeout": 2592000,
"roles": {
"realm": [
{
"name": "user",
"description": "Standard MischLabs user"
},
{
"name": "admin",
"description": "MischLabs administrator"
}
]
},
"defaultRoles": ["user"],
"clients": [
{
"clientId": "gitea",
"name": "Gitea",
"description": "Code-Repositories (git.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"https://git.mischlabs.de/*"
],
"webOrigins": ["https://git.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "nextcloud",
"name": "Nextcloud",
"description": "Cloud-Speicher (drive.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://drive.mischlabs.de/apps/oidc_login/oidc",
"https://drive.mischlabs.de/apps/user_oidc/code"
],
"webOrigins": ["https://drive.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"],
"attributes": {
"pkce.code.challenge.method": "S256",
"id.token.signed.response.alg": "RS256"
}
},
{
"clientId": "mealie",
"name": "Mealie",
"description": "Rezepte (foodlabs.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://foodlabs.mischlabs.de/*"
],
"webOrigins": ["https://foodlabs.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email", "groups"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "audiobookshelf",
"name": "Audiobookshelf",
"description": "Hoerbuecher (audiobook.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://audiobook.mischlabs.de/auth/openid/callback",
"https://audiobook.mischlabs.de/auth/openid/mobile-redirect"
],
"webOrigins": ["https://audiobook.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"],
"attributes": {
"user.info.response.signature.alg": ""
}
},
{
"clientId": "jellyfin",
"name": "Jellyfin",
"description": "Filme & Serien (movies.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://movies.mischlabs.de/*"
],
"webOrigins": ["https://movies.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "vaultwarden",
"name": "Vaultwarden",
"description": "Passwort-Manager (password.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://password.mischlabs.de/identity/connect/oidc-signin"
],
"webOrigins": ["https://password.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "pocketbase",
"name": "PocketBase",
"description": "Persoenliche Seite (tom.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://tom.mischlabs.de/api/oauth2-redirect",
"https://tom.mischlabs.de/_/*"
],
"webOrigins": ["https://tom.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "docs",
"name": "Docs",
"description": "Dokumentation (docs.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://docs.mischlabs.de/*"
],
"webOrigins": ["https://docs.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "michess",
"name": "MiChess",
"description": "Schach (michess.mischlabs.de)",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://michess.mischlabs.de/*"
],
"webOrigins": ["https://michess.mischlabs.de"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
},
{
"clientId": "cloudflare-access",
"name": "Cloudflare Access",
"description": "Cloudflare Zero Trust als Schutzschicht",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": [
"https://*.cloudflareaccess.com/cdn-cgi/access/callback"
],
"webOrigins": ["+"],
"defaultClientScopes": ["openid", "profile", "email"],
"optionalClientScopes": ["offline_access"]
}
],
"clientScopes": [
{
"name": "groups",
"description": "User group memberships",
"protocol": "openid-connect",
"protocolMappers": [
{
"name": "groups",
"protocol": "openid-connect",
"protocolMapper": "oidc-group-membership-mapper",
"config": {
"full.path": "false",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "groups",
"userinfo.token.claim": "true"
}
}
]
}
],
"smtpServer": {},
"loginTheme": "keycloak",
"accountTheme": "keycloak.v3",
"internationalizationEnabled": true,
"supportedLocales": ["de", "en"],
"defaultLocale": "de"
}