- .woodpecker.yaml: image paths -> library/autojanet-{agent,dispatcher}
- .woodpecker.yaml: secret names RS_HARBOR_USER / RS_HARBOR_PASS (global)
- container/Dockerfile: restore COPY skills/, skills/ populated from opencode config
- skills/: 84 opencode skills bundled into image
- k8s/manifests: update image refs to library/
5.6 KiB
| name | description |
|---|---|
| adding-keycloak-sso | 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
# 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=<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": "<service-name>",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": false,
"redirectUris": ["https://<hostname>/oauth2/callback", "https://<hostname>/*"],
"webOrigins": ["https://<hostname>"],
"baseUrl": "https://<hostname>"
}'
# 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=="<service-name>") | .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:
COOKIE_SECRET=$(python3 -c "import os,base64; print(base64.urlsafe_b64encode(os.urandom(32)).decode())")
bao kv put secret/production/<namespace>/<name>-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
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: <name>-oauth2proxy-secret
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
refreshInterval: 1h
secretStoreRef:
name: openbao
kind: ClusterSecretStore
target:
name: <name>-oauth2proxy-secret
creationPolicy: Owner
data:
- secretKey: client-secret
remoteRef:
key: secret/production/<namespace>/<name>-oauth2proxy-secret
property: client-secret
- secretKey: cookie-secret
remoteRef:
key: secret/production/<namespace>/<name>-oauth2proxy-secret
property: cookie-secret
Deployment sidecar container
- 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=<service-name>
- --redirect-url=https://<hostname>/oauth2/callback
- --email-domain=*
- --upstream=http://localhost:<app-port>
- --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: <name>-oauth2proxy-secret
key: client-secret
- name: OAUTH2_PROXY_COOKIE_SECRET
valueFrom:
secretKeyRef:
name: <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:
<service-name> - 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
git add -A && git commit -m "feat(<service>): 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://<hostname>/*" 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 |