commit 8a4beb879ae29d7cba04a768ddaad6c2df28f260 Author: Kroonk Date: Mon Apr 13 20:42:59 2026 +0200 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 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..761a76e --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65206b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Secrets +.env + +# PostgreSQL data +pgdata/ + +# OS files +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# IDE +.vscode/ +.idea/ + +# Claude Code +.claude/ diff --git a/Brain.md b/Brain.md new file mode 100644 index 0000000..e547303 --- /dev/null +++ b/Brain.md @@ -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' => '', + '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= + 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= + 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= + ``` +- 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) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9a59ce5 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/realm/mischlabs-realm.json b/realm/mischlabs-realm.json new file mode 100644 index 0000000..92755a8 --- /dev/null +++ b/realm/mischlabs-realm.json @@ -0,0 +1,235 @@ +{ + "realm": "mischlabs", + "enabled": true, + "displayName": "MischLabs", + "displayNameHtml": "MischLabs", + "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" +}