Set up single sign-on¶
Kionga signs people in through an OIDC broker, Dex. Dex connects to the identity systems you already run and gives Kionga one trusted issuer, so the gateway never handles LDAP binds, SAML assertions or provider OAuth secrets itself.
| Identity system | Dex connector |
|---|---|
| LDAP, Active Directory | ldap |
| Okta, ADFS, Entra ID, any SAML 2.0 IdP | saml |
| GitHub, GitLab (OAuth) | github, gitlab |
| Google Workspace | google |
| Microsoft Entra ID | microsoft |
Keycloak, Okta OIDC, Auth0, any OIDC provider |
oidc |
Local accounts can stay enabled next to single sign-on as break-glass access, for example when the directory is down.
Before you begin¶
- You operate the deployment: you can edit
deploy/.env(Compose) or the Helm values and secrets (Kubernetes), and restart the gateway and Dex. - For a directory or identity provider, you have an application registration or a bind account, and its client ID and secret.
- After sign-in, an
adminoroperatorgrants access in Users & access (see Manage users, groups and policies).
Try single sign-on locally¶
make -C deploy local-up starts every sign-in method. Open
http://localhost:8080/login.html:
| Option | Try it |
|---|---|
| Company directory (LDAP through Dex, GLAuth test directory) | Password kionga-sso-dev for every user below |
| Demo account (local only) (Dex's no-credentials OAuth round trip) | One click; the first sign-in signs up as a demo user |
| GitHub, Google, GitLab, Microsoft | Appear once their client id and secret are set (below) |
| Local form | admin / mlaiops-local (break-glass) |
| Directory user | Group | Result |
|---|---|---|
ada |
ml-admins |
admin |
grace |
data-team |
engineer |
alan |
everyone |
signs up: an access request waits for approval |
Add an OAuth provider locally¶
- Register an OAuth app at the provider with callback URL
http://localhost:5556/dex/callback. -
Put the client id and secret in
deploy/.env: -
Reload Dex:
Verify: the provider's button appears on http://localhost:8080/login.html.
Dex renders deploy/sso/dex.yaml (a template) with the connectors
whose client ids are set, and writes the login-page list from the same rendered
config to a volume the gateway reads, so a button exists exactly when its
connector does and the gateway needs no restart. Secrets stay $VARS that Dex
expands when it loads the config; the rendered file never holds them.
Connect your directory or identity provider¶
With Compose, point the LDAP connector at your directory with KIONGA_LDAP_HOST,
KIONGA_LDAP_BIND_DN, DEX_LDAP_BIND_PASSWORD, KIONGA_LDAP_BASE_DN,
KIONGA_LDAP_GROUP_BASE_DN and KIONGA_LDAP_INSECURE_NO_SSL=false, or add a
connector to deploy/sso/dex.yaml (SAML, generic OIDC). For your own Dex
deployment use connectors like these, then set the gateway variables.
- type: ldap
id: ldap
name: Company directory
config:
host: dc.example.com:636
rootCA: /etc/dex/ldap-ca.pem
bindDN: CN=dex,OU=Service Accounts,DC=example,DC=com
bindPW: $DEX_LDAP_BIND_PASSWORD
userSearch:
baseDN: OU=People,DC=example,DC=com
filter: "(objectClass=person)"
username: sAMAccountName
idAttr: objectGUID
emailAttr: mail
nameAttr: displayName
groupSearch:
baseDN: OU=Groups,DC=example,DC=com
filter: "(objectClass=group)"
userMatchers: [{userAttr: DN, groupAttr: member}]
nameAttr: cn
Gateway settings (see Configuration):
OIDC_ISSUER=https://kionga.example.com/dex
OIDC_JWKS_URL=https://kionga.example.com/dex/keys
OIDC_AUTH_URL=https://kionga.example.com/dex/auth
OIDC_TOKEN_URL=https://kionga.example.com/dex/token
OIDC_REDIRECT_URL=https://kionga.example.com/auth/callback
OIDC_CLIENT_ID=kionga
OIDC_CLIENT_SECRET=... # same as the Dex static client
OIDC_AUDIENCE=kionga
OIDC_SCOPES="openid profile email groups federated:id" # federated:id is Dex-only
KIONGA_SSO_PROVIDERS='[{"id":"ldap","name":"Company directory","type":"ldap"},{"id":"github","name":"GitHub","type":"github"}]'
# or KIONGA_SSO_PROVIDERS_FILE=/path/providers.json, re-read when it changes
KIONGA_SSO_GROUP_ROLES='{"ml-admins":"admin","data-team":"engineer","analysts":"viewer"}'
KIONGA_SSO_SINGLE_TENANT=true # Dex tokens carry no tenant claim
KIONGA_LOCAL_LOGIN=true # optional break-glass; needs MLAIOPS_LOCAL_PASSWORD
KIONGA_SIGNUP=approval # approval | open | off
KIONGA_SIGNUP_EMAIL_DOMAINS=example.com
compose.public.yaml runs the same Dex at https://$MLAIOPS_DOMAIN/dex with the
test directory and demo account off, Dex's own email and password admin
(DEX_ADMIN_EMAIL, DEX_ADMIN_PASSWORD_HASH) on, and requires
MLAIOPS_LOCAL_PASSWORD. Register OAuth apps with callback
https://$MLAIOPS_DOMAIN/dex/callback.
With GitHub, group names are org:team (for example your-org:ml-admins); map
those names in KIONGA_SSO_GROUP_ROLES.
Verify: curl $MLAIOPS_URL/auth/providers lists your providers, and signing in through one lands you in the console.
Choose how first-time users get access¶
A person signing in through single sign-on for the first time, with no Kionga
account and no directory group mapped to a role above user, gets an account
according to KIONGA_SIGNUP:
| Mode | First sign-in creates | The person sees |
|---|---|---|
approval (default) |
An access request (Signed up with github) for the requested default services |
A "waiting for approval" banner; no service until an administrator provisions them in Users & access |
open |
An active user account with the KIONGA_SIGNUP_PROFILE resource profile (default starter) |
The console, at once |
off |
Nothing | Access only through mapped directory groups or an administrator |
Rules that always hold:
- Signing in again never creates a second request, and a declined request is not refiled. The person can ask again from My access.
- An existing account is never changed by signing in: a suspended account stays suspended.
- People whose directory groups grant
admin,operator,engineerorviewerare skipped, because a stored profile would replace that role. KIONGA_SIGNUP_EMAIL_DOMAINS=example.com,corp.iolimits sign-up to emails at those domains that the identity provider marked verified.KIONGA_SIGNUP_SERVICESoverrides the default services (overview,projects,pipelines,models,features,storage,catalog,workbench,ide).
The gateway logs each sign-up (sign-up for <subject> via "github": requested),
and /api/v1/me reports connector and the latest access_request status.
How sign-in works¶
- The login page reads
/auth/providersand shows a button per provider, plus the local form when local accounts are enabled. - Sign in with … goes to
/auth/sso/login?connector=<id>. The gateway sends the browser to Dex with that connector, a PKCE challenge and a nonce. Unknown connector ids are rejected. - Dex authenticates the user with the upstream system and returns a code. The
gateway exchanges it (proving PKCE), verifies the ID token signature, issuer,
audience, expiry and nonce, and sets an HttpOnly session cookie, marked
Secure when
OIDC_REDIRECT_URLuses HTTPS (production requires HTTPS). The login starts on the callback's host: opening the console at127.0.0.1moves the browser tolocalhostfirst, so the login cookies reach the callback. - Roles come from the token: a
rolesclaim if present, otherwise the user's groups mapped throughKIONGA_SSO_GROUP_ROLES. A group named like a role (viewer) still counts. Identity-provider groups never grant the internalservicerole. Someone with no mapped group is an unprovisioneduser: they reach only their profile, access requests and personal settings. Administrator-managed access profiles keyed by the user's subject override token roles, as for any user. - On a person's first sign-in, sign-up decides whether they get an account.
- Sign out ends the Kionga session (and workspace sessions) for both local and single sign-on users.
Production checklist¶
- Serve Dex over HTTPS and use that URL as
OIDC_ISSUER; production mode rejects a plain HTTP issuer. - Give Dex persistent storage (SQL or Kubernetes CRDs). The Compose stack uses memory storage, which forgets signing keys on restart and signs everyone out.
- Choose a sign-up mode.
approvalis the safe default; useopenonly withKIONGA_SIGNUP_EMAIL_DOMAINSor a provider that admits only your people (a GitHub org, a Google hosted domain, an Entra tenant). - Turn the demo account off (
KIONGA_SSO_DEMO=false; the public overlay does). - Use TLS to the directory (
ldaps, port 636) and a least-privilege bind account. - Keep secrets (
OIDC_CLIENT_SECRET, bind passwords, connector client secrets) in your secret manager and pass them to Dex as environment variables. - Decide whether local break-glass login stays on. If it does, use a long random
MLAIOPS_LOCAL_PASSWORD, keep it in a vault, and deliver it as a file withMLAIOPS_LOCAL_PASSWORD_FILElike the other secrets. WithKIONGA_ENVIRONMENT=productionthe gateway refuses to start when that password is shorter than 16 characters or containslocal,change-me, orpassword. Failed local sign-ins are throttled per account (a doubling delay after 5 failures, up to 60 s). While local sign-in is on, administrators can also issue local passwords to provisioned users; turn it off (KIONGA_LOCAL_LOGINunset) when the identity provider must own every credential. - Map groups to the least privileged roles, and grant project and service access with access control policies.
Troubleshooting¶
| Symptom | Check |
|---|---|
| Login page shows no SSO button | curl $MLAIOPS_URL/auth/providers; KIONGA_SSO_PROVIDERS (or the file) must be valid JSON and OIDC_ISSUER set |
An OAuth button is missing after editing .env |
make -C deploy sso-reload; docker compose logs dex lists each config connector |
invalid login state on /auth/callback |
The login cookies did not come back: the browser blocks cookies for this site, the login took over 10 minutes, or OIDC_REDIRECT_URL names a host the browser cannot reach as the same site |
| A first sign-in created no access request | KIONGA_SIGNUP is off, the email is outside KIONGA_SIGNUP_EMAIL_DOMAINS or unverified, or a directory group already grants a role; the gateway logs sign-up for ... |
unknown sign-in provider |
The button id is not in KIONGA_SSO_PROVIDERS, or does not match a Dex connector id |
| Dex says "Invalid username or password" | Dex logs: bind DN and password, userSearch filter and username attribute |
| Signed in but no access | Waiting for approval (the console banner says so), or the user's groups are not in KIONGA_SSO_GROUP_ROLES; Dex logs list the groups it found at login |
identity token validation failed |
OIDC_ISSUER must equal Dex's issuer exactly; OIDC_AUDIENCE must be the client id; with MLAIOPS_TENANT set, enable KIONGA_SSO_SINGLE_TENANT |
Gateway exits with requires MLAIOPS_LOCAL_PASSWORD |
KIONGA_LOCAL_LOGIN=true next to SSO needs an explicit break-glass password |
Gateway exits with must be at least 16 characters and not a development default |
Production mode rejects a short or default break-glass password; set a long random MLAIOPS_LOCAL_PASSWORD (or MLAIOPS_LOCAL_PASSWORD_FILE) |
| Local form says "Too many failed attempts for this account" | After 5 failures each attempt waits up to 60 s; wait, then use the correct password. Failures reset after 15 minutes or on success |
| SSO works on this machine but not from another | Ports bind to 127.0.0.1 by default, and the browser is sent to localhost. Set KIONGA_BIND_ADDRESS, KIONGA_GATEWAY_URL, and DEX_ISSUER as in Network exposure |