Authentication Setup Guide
This guide covers how to configure authentication for kube-workspaces, using either an external OIDC provider or built-in local (username/password) accounts. The two methods can be enabled independently or together.
Overview
kube-workspaces supports two authentication methods:
-
OIDC (OpenID Connect) - federate to an external identity provider:
- Dex (bundled or external) - federated OIDC proxy supporting GitHub, GitLab, LDAP, SAML, and more
- Okta - enterprise identity provider
- Any OIDC-compliant provider - Google, Auth0, Keycloak, Azure AD, etc.
- Local auth - username/password accounts stored as Kubernetes Secrets, with no external dependency. A default admin user is created automatically the first time local auth is enabled.
Authentication is opt-in and disabled by default. When disabled, the system operates without auth (everyone has full access).
Architecture
- Session tokens are HMAC-SHA256 signed JWTs stored in a
kw-sessionhttpOnly cookie - The signing key is stored in a Kubernetes Secret, and is used for sessions regardless of which authentication method issued them
- Local user passwords are bcrypt-hashed and stored in a per-user Kubernetes
Secret (
kw-user-<slug>-local-auth), referenced from theUserCR - User state is stored in
UserCRDs (cluster-scoped) - Configuration is stored in an
AuthConfigCRD (singleton nameddefault)
Prerequisites
- kube-workspaces deployed (controller, API, frontend)
- CRDs installed (
kubectl apply --server-side -k deploy/kustomize/crds/) - For OIDC: an OIDC provider configured with a client ID and secret, and the API reachable at a stable URL for the OIDC callback
- For local auth: nothing extra — see Option 5 below
Option 1: Bundled Dex (Helm)
The Helm chart includes Dex as an optional sub-chart. This is the simplest setup.
1. Create values file
# values-auth.yaml
auth:
enabled: true
oidc:
issuerURL: "https://dex.workspaces.example.com"
clientID: "kube-workspaces"
clientSecret:
create: true
value: "a-strong-random-secret"
scopes: ["openid", "email", "profile", "groups"]
session:
signingKey:
create: true
# Leave value empty for auto-generation, or set explicitly:
# value: "my-32-char-signing-key-here!!!!"
personalNamespaces:
enabled: true
template: ""
registration:
autoProvision: true
defaultRole: "editor"
adminEmails:
- "[email protected]"
api:
externalHost: "workspaces.example.com"
dex:
enabled: true
config:
issuer: "https://dex.workspaces.example.com"
storage:
type: kubernetes
config:
inCluster: true
web:
http: 0.0.0.0:5556
staticClients:
- id: kube-workspaces
name: "Kube Workspaces"
secret: "a-strong-random-secret"
redirectURIs:
- "https://workspaces.example.com/auth/callback"
connectors:
- type: github
id: github
name: GitHub
config:
clientID: "$GITHUB_CLIENT_ID"
clientSecret: "$GITHUB_CLIENT_SECRET"
redirectURI: "https://dex.workspaces.example.com/callback"
orgs:
- name: your-org
ingress:
enabled: true
className: nginx
hosts:
- host: dex.workspaces.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: dex-tls
hosts:
- dex.workspaces.example.com
2. Install
helm dependency update deploy/helm/kube-workspaces/
helm install kube-workspaces deploy/helm/kube-workspaces/ \
--namespace kube-workspaces-system \
--create-namespace \
-f values-auth.yaml
3. Verify
# Check AuthConfig status
kubectl get authconfig default -o yaml
# Should show:
# status:
# enabled: true
# issuerReachable: true
# conditions:
# - type: Ready
# status: "True"
# reason: IssuerVerified
Option 2: External Dex
If you already have Dex deployed separately, just configure the OIDC settings to point to it.
1. Create secrets
# Session signing key
kubectl create secret generic kube-workspaces-session \
--namespace kube-workspaces-system \
--from-literal=signing-key="$(openssl rand -base64 32)"
# OIDC client secret (must match what's configured in Dex)
kubectl create secret generic kube-workspaces-oidc \
--namespace kube-workspaces-system \
--from-literal=client-secret="your-dex-client-secret"
2. Create AuthConfig
apiVersion: kubeworkspaces.io/v1alpha1
kind: AuthConfig
metadata:
name: default
spec:
enabled: true
oidc:
issuerURL: "https://dex.your-cluster.example.com"
clientID: "kube-workspaces"
clientSecret:
name: "kube-workspaces-oidc"
key: "client-secret"
scopes: ["openid", "email", "profile", "groups"]
usernameClaim: "email"
groupsClaim: "groups"
session:
signingKey:
name: "kube-workspaces-session"
key: "signing-key"
tokenExpiry: "24h"
refreshExpiry: "7d"
personalNamespaces:
enabled: true
template: ""
registration:
autoProvision: true
defaultRole: "editor"
adminEmails:
- "[email protected]"
kubectl apply --server-side -f authconfig.yaml
3. Configure Dex static client
In your external Dex configuration, add:
staticClients:
- id: kube-workspaces
name: "Kube Workspaces"
secret: "your-dex-client-secret"
redirectURIs:
- "https://workspaces.example.com/auth/callback"
Option 3: Okta
1. Create Okta application
- In the Okta Admin Console, go to Applications > Create App Integration
- Select OIDC - OpenID Connect and Web Application
- Configure:
- App name: Kube Workspaces
-
Sign-in redirect URIs:
https://workspaces.example.com/auth/callback -
Sign-out redirect URIs:
https://workspaces.example.com - Assignments: Assign users/groups who should have access
- Note the Client ID and Client Secret
- Note your Okta domain (e.g.,
dev-12345.okta.com)
2. Create secrets
kubectl create secret generic kube-workspaces-session \
--namespace kube-workspaces-system \
--from-literal=signing-key="$(openssl rand -base64 32)"
kubectl create secret generic kube-workspaces-oidc \
--namespace kube-workspaces-system \
--from-literal=client-secret="<okta-client-secret>"
3. Create AuthConfig
apiVersion: kubeworkspaces.io/v1alpha1
kind: AuthConfig
metadata:
name: default
spec:
enabled: true
oidc:
issuerURL: "https://dev-12345.okta.com"
clientID: "0oa1234567890abcdef"
clientSecret:
name: "kube-workspaces-oidc"
key: "client-secret"
scopes: ["openid", "email", "profile", "groups"]
usernameClaim: "email"
groupsClaim: "groups"
session:
signingKey:
name: "kube-workspaces-session"
key: "signing-key"
tokenExpiry: "24h"
refreshExpiry: "7d"
personalNamespaces:
enabled: true
template: ""
registration:
autoProvision: true
defaultRole: "editor"
allowedDomains:
- "yourcompany.com"
adminEmails:
- "[email protected]"
4. Okta groups claim
To use Okta groups, add a Groups claim to the authorization server:
- Go to Security > API > default
- Click the Claims tab
- Add a claim:
-
Name:
groups - Include in token type: ID Token, Always
- Value type: Groups
-
Filter: Matches regex
.*(or specify specific groups)
-
Name:
Option 4: Generic OIDC Provider
Any OIDC-compliant provider (Google, Auth0, Keycloak, Azure AD, etc.) can be used.
Requirements
Your OIDC provider must:
- Support the Authorization Code flow
- Expose a
/.well-known/openid-configurationendpoint at the issuer URL - Return an
emailclaim in the ID token (or configureusernameClaimaccordingly) - Support the configured
redirectURI(https://<your-host>/auth/callback)
Configuration
apiVersion: kubeworkspaces.io/v1alpha1
kind: AuthConfig
metadata:
name: default
spec:
enabled: true
oidc:
issuerURL: "<provider-issuer-url>"
clientID: "<your-client-id>"
clientSecret:
name: "kube-workspaces-oidc"
key: "client-secret"
scopes: ["openid", "email", "profile"]
usernameClaim: "email" # Adjust if your provider uses a different claim
groupsClaim: "groups" # Adjust or remove if not available
session:
signingKey:
name: "kube-workspaces-session"
key: "signing-key"
personalNamespaces:
enabled: true
template: ""
registration:
autoProvision: true
defaultRole: "editor"
adminEmails:
- "[email protected]"
Provider-specific notes
| Provider | Issuer URL | Notes |
|---|---|---|
https://accounts.google.com |
Requires usernameClaim: "email", no native groups claim |
|
| Auth0 | https://your-tenant.auth0.com/ |
Trailing slash required; configure Rules for groups claim |
| Keycloak | https://keycloak.example.com/realms/your-realm |
Native groups support via realm roles/groups mapper |
| Azure AD | https://login.microsoftonline.com/<tenant-id>/v2.0 |
Use groupsClaim: "groups"; requires API permissions for group claims |
Option 5: Local Authentication
Local auth provides username/password login with no external identity provider. It is the fastest way to get authentication running, and can be enabled independently of OIDC, or alongside it (a user can have local auth, OIDC, or both configured for the same email).
Quick start (Kustomize)
kubectl apply --server-side -k deploy/kustomize/crds/
kubectl apply --server-side -k deploy/kustomize/overlays/auth-local/
This creates:
- An
AuthConfigwithspec.enabled: trueandspec.localAuth.enabled: true(no OIDC) - A session signing key Secret (
kube-workspaces-session) — required for all sessions regardless of auth method, so it must exist even for local-only setups
Within a few seconds, the controller auto-creates a default admin User
(admin@local by default) with a randomly generated password stored in a
Secret named kw-user-admin-at-local-local-auth in kube-workspaces-system.
Quick start (Helm)
helm install kube-workspaces helm/kube-workspaces/ \
--namespace kube-workspaces-system --create-namespace \
--set auth.enabled=true \
--set auth.localAuth.enabled=true
Or via make:
make deploy-auth-local
Retrieving the bootstrap admin password
make get-admin-password
# or directly:
kubectl get secret kw-user-admin-at-local-local-auth \
-n kube-workspaces-system -o jsonpath='{.data.password}' | base64 -d
The plaintext password key only exists until the admin changes their
password for the first time (they are required to on first login) — after
that, only the bcrypt passwordHash key remains, and get-admin-password
will report an error.
Customizing the bootstrap admin
auth:
enabled: true
localAuth:
enabled: true
bootstrapAdmin:
email: "[email protected]" # defaults to admin@local
skip: false # set true to skip auto-creation entirely
Enabling local auth alongside OIDC
Add spec.localAuth to an AuthConfig that already has spec.oidc configured
(see deploy/kustomize/overlays/auth/authconfig.yaml for a commented example),
or with Helm:
helm upgrade kube-workspaces helm/kube-workspaces/ \
--reuse-values \
--set auth.oidc.issuerURL=https://dex.example.com \
--set auth.localAuth.enabled=true
Both login methods appear on the login page; users can be created with either method independently.
Creating additional local users
Via the admin UI (/admin/users → New User → Auth method: Local password), or
via the API:
curl -X POST https://workspaces.example.com/admin/users \
-H "Content-Type: application/json" \
-b "kw-session=<admin-session-cookie>" \
-d '{
"email": "[email protected]",
"displayName": "Jane Doe",
"role": "viewer",
"authMethod": "local"
}'
Response includes a one-time password field (auto-generated if omitted from
the request). The user must change it on first login
(spec.localAuth.mustChangePassword: true).
Resetting a local user’s password
curl -X POST https://workspaces.example.com/admin/users/jane-doe-at-example-com/reset-password \
-b "kw-session=<admin-session-cookie>"
Returns a new one-time password and sets mustChangePassword: true again.
Password policy and lockout
- Minimum password length: 12 characters (enforced server-side on change/reset)
- Passwords are bcrypt-hashed (cost 12) before being stored
- After 5 consecutive failed login attempts for a user, that account is
temporarily locked with exponential backoff (1m, 5m, 15m, then 30m), tracked
in
User.status.failedLoginAttempts/status.lockedUntil - The
/auth/login/localendpoint additionally applies a coarse per-IP rate limit, independent of per-user lockout
User Management
Creating users manually
Users are auto-created on first login when registration.autoProvision is true (OIDC), or via the admin API/UI (local auth — see Option 5). You can also pre-create OIDC users directly as a CR:
apiVersion: kubeworkspaces.io/v1alpha1
kind: User
metadata:
name: jane-doe # typically slugified email
spec:
email: "[email protected]"
displayName: "Jane Doe"
role: editor
namespaceAccess:
- namespace: shared-team
role: admin
kubectl apply -f user.yaml
Note: pre-creating a User this way does not give them local-auth password
login — that requires a password Secret and spec.localAuth, which the admin
API sets up for you (see Option 5).
Roles
| Role | Capabilities |
|---|---|
admin |
Full access to all workspaces in assigned namespaces; can manage users via admin API |
editor |
Create, edit, delete workspaces in assigned namespaces |
viewer |
Read-only access to workspaces in assigned namespaces |
A user’s authentication method (OIDC, local, or both) is independent of their
role — spec.role applies regardless of how they signed in.
Disabling a user
kubectl patch user jane-doe --type=merge -p '{"spec":{"disabled":true}}'
Disabling blocks both OIDC and local login for that user, and pauses reconciliation of their namespace/RBAC.
Granting shared namespace access
kubectl patch user jane-doe --type=json -p '[
{"op": "add", "path": "/spec/namespaceAccess/-", "value": {"namespace": "team-alpha", "role": "editor"}}
]'
By default, new users (local or OIDC) have no namespace access beyond their personal namespace (if enabled) — namespace access must be granted explicitly.
Troubleshooting
AuthConfig shows IssuerUnreachable
kubectl get authconfig default -o jsonpath='{.status.conditions[0].message}'
Check that:
- The issuer URL is correct and accessible from within the cluster
- DNS resolution works from the controller pod
- TLS certificates are valid (the controller does NOT skip TLS verification)
Login redirects fail
Ensure:
-
AUTH_CALLBACK_URLenv var is set on the API deployment (orEXTERNAL_HOSTis configured) - The OIDC provider has
https://<your-host>/auth/callbackin its allowed redirect URIs - Ingress routes
/auth/*paths to the API service
Session cookie not sent
- The
kw-sessioncookie is httpOnly and Secure (requires HTTPS) - All frontend
fetch()calls must includecredentials: "include" - CORS must allow the frontend origin (check
ALLOWED_ORIGINSon the API)
User created but no namespace appears
- Check that
personalNamespaces.enabled: truein AuthConfig - Look at the User controller logs:
kubectl logs -l app.kubernetes.io/component=controller -n kube-workspaces-system - Verify the User CR status:
kubectl get user <name> -o yaml
Local login returns “account_locked”
The account has 5 or more consecutive failed login attempts. Check:
kubectl get user <name> -o jsonpath='{.status.failedLoginAttempts} {.status.lockedUntil}'
Wait for lockedUntil to pass, or clear it manually:
kubectl patch user <name> --subresource=status --type=merge \
-p '{"status":{"failedLoginAttempts":0,"lockedUntil":null}}'
make get-admin-password reports an error
The plaintext password is removed from the Secret as soon as the admin changes
it for the first time — this is expected. If the admin has forgotten their
password, reset it as another admin via the admin API/UI, or, if no other
admin account exists, delete the User and its password Secret to let the
controller re-create the bootstrap admin:
kubectl delete user admin-at-local
kubectl delete secret kw-user-admin-at-local-local-auth -n kube-workspaces-system
The controller re-reconciles the AuthConfig on its periodic 5-minute
recheck (or immediately if you touch the CR, e.g.
kubectl annotate authconfig default kubectl.kubernetes.io/restartedAt="$(date +%s)" --overwrite),
recreating both the User and its password Secret.
Password change returns HTTP 500 “failed to update password”
This error means the API’s ServiceAccount does not have write access to the
kube-workspaces-system namespace where password Secrets are stored.
Helm installs into a different release namespace (e.g. a shared namespace)
are the most common cause. The local-auth RBAC in the Helm chart creates a
Role/RoleBinding scoped to .Release.Namespace, but password Secrets
always live in kube-workspaces-system. Chart version 0.3.2+ adds the
cross-namespace Role automatically when auth.localAuth.enabled: true. If you
are on an older version, apply it manually:
kubectl apply --server-side -f - <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: kube-workspaces-local-auth-secrets
namespace: kube-workspaces-system
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kube-workspaces-local-auth-secrets
namespace: kube-workspaces-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kube-workspaces-local-auth-secrets
subjects:
- kind: ServiceAccount
name: kube-workspaces # adjust to your release name
namespace: <your-release-ns> # the namespace you installed into
EOF
Verify access was granted:
kubectl auth can-i update secrets \
--as=system:serviceaccount:<release-ns>:<sa-name> \
-n kube-workspaces-system
# expected: yes
Kustomize base installs use kube-workspaces-system as both the release
and secrets namespace, so the bundled rbac.yaml already grants the necessary
access — no action required.