--- name: adding-keycloak-sso description: Use when adding Keycloak SSO authentication to a service on the homelab cluster at ctz.fyi, whether via oauth2-proxy sidecar or native OIDC configuration. --- # Adding Keycloak SSO ## Overview Two patterns depending on whether the app supports OIDC natively. Both use Keycloak at `sso.ctz.fyi`, realm `ctz`, with secrets stored in OpenBao. ## Pattern Selection | App type | Pattern | |----------|---------| | No auth or basic auth only | **A: oauth2-proxy sidecar** | | Native OIDC/OAuth2 support (Grafana, Jellyfin, Open WebUI) | **B: Native OIDC** | | SPA (React/Vue/etc) | **B: Public PKCE client** (`publicClient: true`, no secret) | **Gotcha:** If an app already uses keycloak-js internally, do NOT also add oauth2-proxy — you'll get double-auth. Pick one. --- ## Step 1: Create Keycloak Client ```bash # Port-forward Keycloak kubectl port-forward -n keycloak svc/keycloak 8080:80 & # Get admin password from OpenBao bao kv get secret/production/keycloak/keycloak-admin # Get admin token TOKEN=$(curl -s http://localhost:8080/realms/master/protocol/openid-connect/token \ -d "client_id=admin-cli&grant_type=password&username=admin&password=" \ | jq -r .access_token) # Create client curl -s -X POST http://localhost:8080/admin/realms/ctz/clients \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "clientId": "", "enabled": true, "protocol": "openid-connect", "publicClient": false, "standardFlowEnabled": true, "directAccessGrantsEnabled": false, "redirectUris": ["https:///oauth2/callback", "https:///*"], "webOrigins": ["https://"], "baseUrl": "https://" }' # Get client UUID, then fetch secret CLIENT_ID=$(curl -s http://localhost:8080/admin/realms/ctz/clients \ -H "Authorization: Bearer $TOKEN" | jq -r '.[] | select(.clientId=="") | .id') CLIENT_SECRET=$(curl -s http://localhost:8080/admin/realms/ctz/clients/$CLIENT_ID/client-secret \ -H "Authorization: Bearer $TOKEN" | jq -r .value) kill %1 # Kill port-forward ``` **Redirect URI must include BOTH** `/oauth2/callback` AND `/*` wildcard — missing wildcard causes `redirect_uri_mismatch` for SPAs using keycloak-js. --- ## Step 2: Write Secrets to OpenBao **Pattern A only — generate cookie secret first:** ```bash COOKIE_SECRET=$(python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())") bao kv put secret/production//-oauth2proxy-secret \ client-secret="$CLIENT_SECRET" \ cookie-secret="$COOKIE_SECRET" ``` **Pattern B:** Store whatever the app needs (client secret, etc.) under an appropriate path. --- ## Step 3: Pattern A — oauth2-proxy Sidecar ### ExternalSecret ```yaml apiVersion: external-secrets.io/v1 kind: ExternalSecret metadata: name: -oauth2proxy-secret annotations: argocd.argoproj.io/sync-wave: "-1" spec: refreshInterval: 1h secretStoreRef: name: openbao kind: ClusterSecretStore target: name: -oauth2proxy-secret creationPolicy: Owner data: - secretKey: client-secret remoteRef: key: secret/production//-oauth2proxy-secret property: client-secret - secretKey: cookie-secret remoteRef: key: secret/production//-oauth2proxy-secret property: cookie-secret ``` ### Deployment sidecar container ```yaml - name: oauth2-proxy image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1 args: - --provider=oidc - --oidc-issuer-url=https://sso.ctz.fyi/realms/ctz - --client-id= - --redirect-url=https:///oauth2/callback - --email-domain=* - --upstream=http://localhost: - --cookie-secure=true - --cookie-samesite=lax - --skip-provider-button=true - --pass-authorization-header=true - --pass-access-token=true - --set-xauthrequest=true - --http-address=0.0.0.0:4180 env: - name: OAUTH2_PROXY_CLIENT_SECRET valueFrom: secretKeyRef: name: -oauth2proxy-secret key: client-secret - name: OAUTH2_PROXY_COOKIE_SECRET valueFrom: secretKeyRef: name: -oauth2proxy-secret key: cookie-secret ports: - containerPort: 4180 ``` ### IngressRoute Update the service port to `4180`. The app's own port no longer needs to be exposed externally. --- ## Step 4: Pattern B — Native OIDC Configure the app using: - **Issuer URL:** `https://sso.ctz.fyi/realms/ctz` - **Client ID:** `` - **Client secret:** from OpenBao (via ExternalSecret or however the app ingests it) - **Callback/redirect URL:** whatever the app expects (configure in Keycloak `redirectUris`) For SPAs: set `"publicClient": true` in client creation, omit secret entirely. --- ## Step 5: Deploy and Verify ```bash git add -A && git commit -m "feat(): add Keycloak SSO" git push # Watch ArgoCD sync ``` Test the login flow manually. Check that: - Unauthenticated requests redirect to Keycloak - Successful login lands back on the app - No double-auth prompts ## Common Mistakes | Mistake | Fix | |---------|-----| | Missing `/*` wildcard in redirectUris | Add `"https:///*"` alongside the callback URI | | Cookie secret wrong length | Must be exactly 32 bytes → use the `python3` command above | | Double-auth on apps with built-in keycloak-js | Remove app's internal auth OR remove oauth2-proxy, not both | | IngressRoute still pointing at app port | Update to port `4180` for Pattern A | | `directAccessGrantsEnabled: true` | Set to `false` — resource owner password grant is not needed |