Skip to content

OIDC login (SSO)

With an OpenID Connect provider, people sign in to kubelatch with their company account: Google Workspace, Microsoft Entra ID, Okta, Keycloak, Authentik, Auth0, or any other provider that publishes a discovery document. This guide registers kubelatch with your provider, connects the two, and explains how each account finds its person.

If your cluster already has SSO

Many clusters already let people in through the company's identity provider: EKS with IAM Identity Center or an OIDC provider of its own, AKS with Entra ID, GKE with Google, DOKS with its single sign-on. kubelatch doesn't replace that, and it doesn't stack a second sign-on on top: it joins the same provider as one more application, like Argo CD or Grafana, and people sign in with the session they already have open in the browser. IAM Identity Center's own directory is the exception: it offers applications SAML, which kubelatch doesn't support, so on EKS kubelatch registers with the provider behind Identity Center (Okta, Entra ID, Google Workspace), and without one people sign in with GitHub or local accounts.

The cluster still doesn't see the person. It sees kubelatch, which connects with its ServiceAccount and impersonates the person as user:<login>, with the kubelatch:* groups of their permissions, just as it does without OIDC (Architecture). What kubelatch adds to the cluster's SSO is what it adds anywhere: permissions by tier that expire, an inventory of credentials and one audit row per request.

What stays apart, for now, is the identity inside the cluster: the bindings your clusters give the provider's users and groups are one thing, and kubelatch's permissions are another. Bringing them together is a later step, and kubelatch already keeps what it needs for it: each account's identity at the provider and its verified e-mail.

What changes when you turn it on

  • The login screen offers Sign in with {name}, where {name} is OIDC_DISPLAY_NAME. With GitHub login on too, it shows both buttons, the provider's first, and each person can have either identity or both.
  • Each existing account links itself on its person's first sign-in, when the provider vouches for the e-mail address: see How accounts are linked. The kubelatch login, the one clusters see as user:<login>, doesn't change.
  • Passwords stop working, except for break-glass accounts, which are marked from the CLI.
  • No automatic sign-up. Someone whose address matches no account is refused, and the login screen says who to ask, taken from KUBELATCH_ACCESS_CONTACT. Accounts are still created in Users (Users and bots).
  • No automatic offboarding. An OpenID Connect provider gives kubelatch no member list. When someone leaves, Disable their account in Users: it closes their sessions and revokes their credentials at once. Until you do, what they already hold keeps working until it expires, even after the provider has closed their account: the web session (12 h), the CLI session (12 h by default) and each credential (up to 30 days by default).
  • Two-factor authentication is up to your provider. kubelatch records how the person authenticated (the amr and acr claims, when the provider sends them) in the event of each sign-in, but requires nothing of them.
  • It works the same in Free and Pro. Linking creates no account, so it never counts against Free's five people.

Before you start

  • Rights to register an application with your provider.
  • kubelatch installed on https, and an administrator who signs in (Install).
  • An e-mail on each account in Users: the address the provider has for that person, which the first sign-in links by. An account without one doesn't link by itself (Other ways to link).
  • The commands use $MGMT_KUBECONFIG, the path to the management cluster's kubeconfig (see Install).

Then prepare a break-glass account.

Do this before restarting with the OIDC_* variables

From that moment on, no password works except break-glass accounts'. Without one, nobody can sign in until you mark one from the CLI (kubelatch-server user set-break-glass).

Mark your current administrator, or create an account just for emergencies:

kubectl --kubeconfig "$MGMT_KUBECONFIG" -n kubelatch exec -it deploy/kubelatch -- /kubelatch-server user set-break-glass admin
# or a new account:
kubectl --kubeconfig "$MGMT_KUBECONFIG" -n kubelatch exec -it deploy/kubelatch -- /kubelatch-server user create rescue --admin --break-glass

It's also your way in when the provider is down or its configuration breaks; the CLI never talks to the provider. Break-glass accounts have no two-factor: keep them few, with long passwords in the secret manager. More in Accounts and recovery.

Register kubelatch in your provider

Whatever the provider, kubelatch needs an OpenID Connect application (a "client" or an "app integration", depending on the provider) like this:

Setting Value
Type A web application that uses the authorization code flow.
Redirect URI <KUBELATCH_BASE_URL>/api/auth/oidc/callback, for example https://kubelatch.example.com/api/auth/oidc/callback. Some providers call it callback URL or sign-in redirect URI. It has to match exactly.
Scopes openid email profile.
Client authentication A client secret for a confidential client, or none for a public one (Public or confidential client).
ID token signature RS256, the usual default. kubelatch refuses any other algorithm. A token without a key ID is accepted only while the provider publishes a single signing key.

kubelatch always sends PKCE (S256), a state and a nonce, and asks for nothing else: no refresh tokens, no logout URL, no API permissions beyond the sign-in scopes. Note three values for the next step: the issuer, the client ID and, for a confidential client, the client secret.

The issuer is the URL that names the provider. kubelatch compares it character by character with the issuer field of the provider's discovery document, trailing slash included, so copy it from there rather than typing it. Each provider's section below says where that document lives; for Google:

curl -s https://accounts.google.com/.well-known/openid-configuration | jq -r .issuer

Configure kubelatch

Variable Value
OIDC_ISSUER The issuer, exactly as the discovery document has it. On Entra ID, it carries your tenant ID (Microsoft Entra ID).
OIDC_CLIENT_ID The client ID.
OIDC_CLIENT_SECRET The client secret. Leave it unset for a public client.
OIDC_ALLOWED_DOMAINS The e-mail domains whose people may sign in, comma-separated: acme.com,acme.org. Exact domains, no wildcards: a subdomain is another domain.
OIDC_DISPLAY_NAME The provider's name on the button, up to 40 characters: Google gives Sign in with Google. SSO by default.
OIDC_SCOPES openid email profile by default. kubelatch won't start without openid and email.
OIDC_SUBJECT_CLAIM The claim that identifies a person: sub by default, oid on Entra ID. It has to belong to one person, and the provider must never change it or hand it to someone else. So kubelatch won't start with a claim that holds an address or a name (email, upn, preferred_username, unique_name, name, phone_number…), one that many people share (tid, hd, iss, aud, idp, locale, groups…), one of the token or the session (nonce, sid, jti…) or the claim in OIDC_EMAIL_VERIFIED_CLAIM: an address that passes to a new hire would open the account of whoever had it before, and a value everyone shares would open the first linked account to all of them.
OIDC_EMAIL_VERIFIED_CLAIM The claim that says the provider verified the e-mail: email_verified by default, xms_edov on Entra ID.
OIDC_AUTOLINK Which accounts a first sign-in links by e-mail: all (the default), invited or off (The link policy).

The first four turn OIDC login on. With any of them set, kubelatch requires the issuer, the client ID and the domains, and won't start while one is missing; the error lists every missing variable. A secret on its own counts too, so a stray one doesn't go unnoticed. The other five alone do nothing. All of them, with their formats, are in Configuration.

On Kubernetes, the client goes in a Secret and the rest in the chart's config.oidc:

  1. Put the client in a Secret:

    kubectl --kubeconfig "$MGMT_KUBECONFIG" -n kubelatch create secret generic kubelatch-oidc \
      --from-literal=OIDC_CLIENT_ID='<client-id>' \
      --from-literal=OIDC_CLIENT_SECRET='<client-secret>'
    

    Or add those keys to kubelatch-secrets, which the pod already loads whole. Never write the secret as a plain value in extraEnv: it would sit in clear text in the Deployment. If it has to go through extraEnv, use valueFrom.secretKeyRef.

  2. In your values, load the Secret and describe the provider:

    extraEnvFrom:
      - secretRef:
          name: kubelatch-oidc
    config:
      oidc:
        issuer: https://accounts.google.com
        allowedDomains: [acme.com]
        displayName: Google
    

    config.oidc also takes scopes, subjectClaim, emailVerifiedClaim and autolink (in lower case: all, invited or off), and the chart refuses an issuer without allowedDomains and the other way round. Every value is in Helm values.

  3. Run helm upgrade with those values, the Secret and config.oidc in the same upgrade: the client ID alone turns OIDC login on, and kubelatch won't start without the issuer and the domains. If you added the keys to kubelatch-secrets, upgrade straight away, because a restart in between would fail. A later change to the Secret doesn't redeploy by itself: restart with kubectl --kubeconfig "$MGMT_KUBECONFIG" -n kubelatch rollout restart deploy/kubelatch.

  4. Check the log. At start, the line OIDC login enabled shows the issuer, public_client (true without a secret), the domains and autolink. Moments later comes OIDC discovery fetched, or the warning OIDC discovery failed; sign-ins will retry it with the error: kubelatch doesn't wait for the provider to start, and a sign-in fetches the document when it's missing. If no administrator can sign in, kubelatch still starts but logs an error with the command that fixes it.
  5. In Users, a line says whether kubelatch has read the provider's discovery document: "{name}: discovery fetched {date}.", "{name}: discovery not fetched yet." or "{name}: the last discovery fetch failed ({error})." A failure also shows in Notices, the bell in the top bar: "kubelatch couldn't read the discovery document of {name}: see it in Users." It also appears as a failing check in Installation health, in Edition (Editions and license). GET /api/oidc/status (admin) answers the same as JSON.

How accounts are linked

An account is linked to an identity at the provider: the issuer, the name of the subject claim and its value (sub, or oid on Entra ID). The e-mail only finds the account on the first sign-in. From then on the identity decides, and kubelatch never swaps an identity of the provider for another one by e-mail.

On a person's first sign-in, kubelatch takes the e-mail only when the provider vouches for it (the claim in OIDC_EMAIL_VERIFIED_CLAIM is true) and its domain is in OIDC_ALLOWED_DOMAINS. Then it looks for people's accounts with that address, ignoring spaces around it and the case of the letters A to Z, but not of any other letter: an account with Álvaro@acme.com never matches the provider's álvaro@acme.com. Write account addresses in lower case, or send such an account a linking link (Other ways to link). Bots are never linked.

Accounts with that e-mail Result
One enabled Linked, if the policy allows it, and signed in. Disabled accounts with the same address don't count.
None unknown_account: the login screen says who to ask for an account. Nothing is created.
Two or more enabled ambiguous_account. The login.failure event lists the candidate logins in candidates.
Only disabled ones disabled.

Each link by e-mail writes an oidc.link event with via: email, the identity and how many pending invitation or reset links of the account it revoked (invites_revoked), then the login.success. An account without a name takes the provider's name. An account linked to GitHub links too and keeps both identities; a break-glass account links and keeps its password.

When the address changes at the provider for the same identity, kubelatch updates the identity's e-mail and writes oidc.email_changed with from and to. The account's own e-mail, the one an administrator set, doesn't change.

OIDC_AUTOLINK decides which accounts a first sign-in may link by e-mail:

Policy Links by e-mail
all (default) Any enabled account the address finds. It's what lets you turn passwords off without sending each person a link.
invited Only accounts that never signed in, or that have a live invitation or reset link: both mean an administrator is expecting that person.
off None: every account links through a link or from My account (Other ways to link). Everyone else gets unknown_account.

The safety net of all is in Notices: each week it tells administrators "{n} people signed in with {name} for the first time this week and were linked by e-mail." and leads to Users. There, the Sign-in column shows "{name} · {email}" for each linked account and "{name} · not signed in yet" for one that isn't, and the Linked to {name} figure counts them. A link you didn't expect shows up the same week.

  • The invitation link of a new account, or the Linking link of an existing one (in the menu of its page in Users), leads to Link with {name} and sign in. It links the identity the person signs in with, even when its address differs from the account's (the link.complete event then carries email_mismatch: true), and replaces any identity the account had: that's how you change someone's identity. The address still has to be verified and in an allowed domain.
  • My account → Identities → Link with {name}: for someone who can already sign in (with GitHub, or a break-glass password) and whose account has no identity of the provider yet.
  • An account without an e-mail, as many created for GitHub are, is never found by address: use one of the two above, or set its e-mail through the API (PATCH /api/subjects/{id}, see HTTP API).

When someone leaves, or an address comes back

  • Disable whoever leaves, always: the provider doesn't tell kubelatch.
  • If the provider recreates someone (a new account for the same person, so a new sub or oid), their next sign-in finds the old identity on the account and gets identity_mismatch. Once you're sure it's the same person, use Unlink {name} in the menu of the account's page in Users: it closes their sessions, and their next sign-in links them again by e-mail, as the policy allows.
  • If the address now belongs to someone else, don't unlink: disable the old account and create a new one. Unlinking would hand the newcomer the old account and its permissions.
  • An account that was never linked and whose address goes to someone new is what all can't tell apart: the newcomer's first sign-in would link it. Disabling accounts when people leave closes that gap; invited narrows it to accounts that never signed in or have a live link.

Changing the provider or the subject claim

An identity belongs to the provider by its issuer and its claim name. Changing OIDC_ISSUER (moving to another provider) or OIDC_SUBJECT_CLAIM (say, from sub to oid) turns the stored identities into another provider's: each account's next sign-in replaces its identity by e-mail, following the policy, and the oidc.link event says what it replaced (replaced_issuer or replaced_claim). With invited or off, accounts that already signed in need a linking link; switching to all for the move spares you one per person.

An identity of the same provider with another value is never replaced by e-mail: that's identity_mismatch, above.

If you turn OIDC login off, the identities stay, unused, and passwords work again unless GitHub login is on.

Public or confidential client

  • Confidential, with OIDC_CLIENT_SECRET: the usual choice when the client is kubelatch's alone. The secret lives in the Secret and in kubelatch's memory, nowhere else.
  • Public, without a secret: kubelatch sends only its client ID and the PKCE verifier, as kubelogin or doctl do. It's for sharing the client with a cluster that trusts the same client ID: DOKS, which asks for a public client, or any apiserver with the --oidc-* flags or an AuthenticationConfiguration. Add kubelatch's redirect URI to that same application.

Either way, PKCE protects the code exchange, and kubelatch never keeps the provider's tokens: the ID token and the access token live only during the callback request and never reach a log, an event or the database. With a client shared with the cluster, that ID token would open the apiserver.

Not every provider offers both:

Provider Confidential Public
Google Workspace Always. Not possible: a Web application client needs its secret, even with PKCE.
Microsoft Entra ID A client secret, with the callback under Web. The callback under Mobile and desktop applications, with Allow public client flows left at No: see its section.
Okta Client authentication: Client secret. A Single-Page Application, the type DigitalOcean documents for DOKS.
Keycloak Client authentication on. Client authentication off.
Authentik Client type: Confidential. Client type: Public.
Auth0 A Regular Web Application. A Single Page Application, the type DigitalOcean documents for DOKS.

No sign-in started by the provider

kubelatch only accepts sign-ins it started itself: the callback must carry the state and nonce of a sign-in begun in kubelatch, so a sign-in launched from the provider's side, with no request from kubelatch, can't complete. To put kubelatch in the provider's app launcher, point the tile at the start of kubelatch's sign-in, which then goes to the provider as usual:

<KUBELATCH_BASE_URL>/api/auth/oidc/login
  • Entra ID (My Apps): put it as the Home page URL in the app registration's Branding & properties. For the tile to show, set Visible to users? to Yes in Enterprise apps → the app → Properties.
  • Okta (the dashboard): in the app's General settings, Login initiated by: Either Okta or App; Application visibility: Display application icon to users; Login flow: Redirect to app to initiate login (OIDC Compliant); Initiate login URI: the URL above.
  • Any other portal or bookmark works the same with that URL.

Providers

Google Workspace

The issuer is https://accounts.google.com, and its discovery document is https://accounts.google.com/.well-known/openid-configuration.

  1. In the Google Cloud console, use a project that belongs to your organization and open Google Auth Platform. If it asks you to get started, give the app a name (people see it when they sign in) and a support e-mail, and choose Internal as its audience: only accounts of your organization can then sign in, and Google stops anyone else with org_internal. You can check it later in Audience.
  2. Clients → Create client → Application type: Web application. In Authorized redirect URIs, add <KUBELATCH_BASE_URL>/api/auth/oidc/callback, then Create. Google warns that a new URI can take from five minutes to a few hours to work.
  3. Copy the Client ID and the Client secret. Google shows the secret only now (you can download it as JSON); later it shows only its last four characters, and a lost one is replaced by rotating it.

In kubelatch:

config:
  oidc:
    issuer: https://accounts.google.com
    allowedDomains: [acme.com]
    displayName: Google

On top of everything else, with Google kubelatch checks the token's hd claim, the domain of the Workspace: it has to be there, be in OIDC_ALLOWED_DOMAINS and match the domain of the address. A personal Gmail account has no hd and gets domain_not_allowed, with Try another account to pick the work one. With a single allowed domain, kubelatch also sends it to Google as the hd hint, which steers the account picker; what decides is the claim in the token. If your Workspace has several domains, list them all and try an account of each. A Google client is always confidential.

Microsoft Entra ID

The issuer is https://login.microsoftonline.com/<tenant-id>/v2.0, and its discovery document is that URL followed by /.well-known/openid-configuration.

  1. In the Microsoft Entra admin center: Entra ID → App registrations → New registration. Give it a name and, under Supported account types, choose the single-tenant option (Single tenant only - <your tenant>). Register.
  2. On its Overview, copy the Application (client) ID, which is OIDC_CLIENT_ID, and the Directory (tenant) ID, a GUID that goes in the issuer. kubelatch won't start with anything else in its place: not common, organizations or consumers, and not a domain name such as contoso.onmicrosoft.com. The same goes for Microsoft's other issuers, on login.microsoftonline.us, login.partner.microsoftonline.cn or sts.windows.net.
  3. Authentication → Add Redirect URI → Web: <KUBELATCH_BASE_URL>/api/auth/oidc/callback → Configure.
  4. Certificates & secrets → Client secrets → New client secret: choose its expiry (24 months at most) and copy its Value, which Entra never shows again. Note the date: once it expires, every sign-in fails with oidc until kubelatch gets a new secret.
  5. Token configuration → Add optional claim → ID → email and xms_edov → Add. If the portal asks to turn on the Microsoft Graph email permission, accept. xms_edov says that the domain of the address is verified, and Entra sends it only together with email. You can add amr too: kubelatch records it in each sign-in's event.

In kubelatch:

config:
  oidc:
    issuer: https://login.microsoftonline.com/<tenant-id>/v2.0
    allowedDomains: [acme.com]
    displayName: Microsoft
    subjectClaim: oid
    emailVerifiedClaim: xms_edov
  • oid identifies the person in your tenant and is the same for every application, while sub changes with each registration: registering the app again would orphan every identity. Entra sends oid only with the profile scope, so keep it in OIDC_SCOPES.
  • Entra sends no email_verified. Without xms_edov, every sign-in gets email_unverified, and kubelatch has no switch to skip that check.
  • Guests (B2B) stay out only through OIDC_ALLOWED_DOMAINS: their address is on another organization's domain, so they get domain_not_allowed. xms_edov doesn't keep them out, because Entra sends it as true for guests too, such as those who sign in with a Microsoft account, a Google account or a one-time passcode. Add another organization's domain only if its guests should sign in.
  • To let only some people in, set Assignment required? to Yes in Enterprise apps → the app → Properties, and assign them in Users and groups. Microsoft stops everyone else before they reach kubelatch.
  • If your tenant doesn't let users consent to applications, an administrator does it once for everyone: API permissions → Grant admin consent for <tenant>.
  • For a public client, the platform of the redirect URI decides: Microsoft treats a Web redirect URI as a confidential web app's and asks it for a client_secret. So a public client registers the callback in step 3 as a custom redirect URI under Mobile and desktop applications, and skips step 4. Leave Allow public client flows, in Authentication, at No: that setting (isFallbackPublicClient in the manifest) is the client type Entra falls back on when a flow carries no redirect URI, and Yes turns on the password grant (ROPC), the device code flow and Windows integrated authentication for the whole registration. A public client exists to be shared with a cluster that accepts its ID tokens, so each of those flows would be one more way to get a token that cluster accepts. This setup hasn't been tried yet on a real tenant: check Microsoft's guides to redirect URIs and to the app manifest (isFallbackPublicClient, publicClient), and keep a confidential client if you can.

Okta

The issuer is your Okta domain, https://<your-org>.okta.com, or a custom authorization server such as https://<your-org>.okta.com/oauth2/default. Both publish their discovery document at the issuer followed by /.well-known/openid-configuration.

  1. In the Admin Console: Applications → Applications → Create App Integration (under Applications and Resources in newer consoles; choose the classic experience if it asks). Sign-in method: OIDC - OpenID Connect; Application type: Web Application; Next.
  2. Sign-in redirect URIs: <KUBELATCH_BASE_URL>/api/auth/oidc/callback. Grant type: Authorization Code. Under Assignments, choose who may use it. Save.
  3. In Client Credentials, copy the Client ID, and with Client authentication set to Client secret, the secret.

In kubelatch, issuer exactly as the discovery document has it, allowedDomains and displayName.

In the authorization code flow, Okta's ID token is "thin": it leaves out claims such as email_verified, which Okta serves from userinfo instead. kubelatch handles that by itself: when the ID token lacks the e-mail or its verification, it asks userinfo with the access token, checks that the answer is about the same sub, and takes the e-mail and its verification from there. There's nothing to configure, as long as the email scope is granted; with a custom authorization server, its access policy has to allow openid, email and profile.

Okta refuses someone not assigned to the application, and kubelatch shows them the denied message.

Keycloak

The issuer is https://<keycloak-host>/realms/<realm>, and its discovery document is that URL followed by /.well-known/openid-configuration.

  1. In the realm's admin console: Clients → Create client → Client type: OpenID Connect, with a Client ID such as kubelatch → Next. Turn Client authentication on for a confidential client and keep Standard flow checked → Next. Valid redirect URIs: <KUBELATCH_BASE_URL>/api/auth/oidc/callback → Save.
  2. Copy the secret from the client's Credentials tab.
  3. Keep the email scope among the client's Client scopes, where it is by default: it carries email and email_verified.

Keycloak has to consider the addresses verified:

  • A user created by hand needs Email verified on.
  • For users from LDAP or Active Directory, turn on Trust Email in the user federation provider (it's off by default), and check Email verified on an imported user afterwards. Otherwise their addresses never count as verified and every sign-in gets email_unverified.

Trust Email marks as verified whatever the directory's mail attribute holds, so it's safe only when administrators alone write mail. If people can edit their own entry, someone could put an administrator's address in theirs and, with OIDC_AUTOLINK=all, be linked to that administrator's account on their first sign-in. In that case set OIDC_AUTOLINK to off, as with a hard-coded verification in Authentik.

With a private CA, as many on-premises Keycloaks have, kubelatch has to trust it: put the CA in a ConfigMap and point SSL_CERT_FILE at it with extraEnv. kubelatch keeps trusting the public CAs as well.

kubectl --kubeconfig "$MGMT_KUBECONFIG" -n kubelatch create configmap idp-ca --from-file=ca.crt=<your-ca>.pem
extraEnv:
  - name: SSL_CERT_FILE
    value: /etc/kubelatch/idp-ca/ca.crt
extraVolumes:
  - name: idp-ca
    configMap:
      name: idp-ca
extraVolumeMounts:
  - name: idp-ca
    mountPath: /etc/kubelatch/idp-ca
    readOnly: true

extraEnv is for plain settings like this one. A secret such as OIDC_CLIENT_SECRET goes in the Secret, or in extraEnv only through valueFrom.secretKeyRef: a literal value would sit in clear text in the Deployment. Without the CA, kubelatch can't fetch the discovery document: the line in Users shows the certificate error and sign-ins fail with provider_unavailable.

Authentik

The issuer is https://<authentik-host>/application/o/<application-slug>/, with its trailing slash, and its discovery document is under it, at .well-known/openid-configuration.

  1. Applications → Applications → New Application. Give it a name and note its Slug, which goes in the issuer.
  2. Provider type: OAuth2/OpenID Connect. Client type: Confidential. Copy the Client ID and the Client Secret. Add a Redirect URI of type Strict (and Authorization, in the versions that ask for one): <KUBELATCH_BASE_URL>/api/auth/oidc/callback.
  3. Signing Key: choose a certificate with an RSA key, such as the self-signed one authentik creates. Without a signing key authentik signs with HS256, and with an ECDSA key it doesn't sign with RS256 either: kubelatch refuses both (token).
  4. Leave the provider's Issuer mode at its default, one issuer per provider: kubelatch looks for the discovery document under the issuer.

Since authentik 2025.10, the default email scope mapping sends email_verified as false, because authentik doesn't verify addresses, so every sign-in would get email_unverified. Authentik's guide has two ways to vouch for them, and both end in a scope mapping of your own (Customization → Property Mappings → New Property Mapping → Scope Mapping, Scope name email) that takes the place of the default email mapping in the provider's scopes:

  • If your users carry their verification in a boolean email_verified attribute, pass it on, and clear that attribute whenever the address changes, as the guide asks:

    return {
        "email": request.user.email,
        "email_verified": request.user.attributes.get("email_verified", False),
    }
    
  • If nothing verifies the addresses yet, have authentik verify them with an email stage: the guide gives the stage, the policy and the mapping.

The guide's other mapping hard-codes "email_verified": True. Use it only with OIDC_AUTOLINK=off, or at least invited. authentik itself warns against it when an address can be unverified or changed without verification, and that claim is kubelatch's only proof that the address belongs to the person: with all, anyone who can pick their own address in authentik, in an enrollment flow or in their user settings, could enroll as admin@acme.com and be linked to that administrator's account on their first sign-in. With off, an account links only through a link you send or from My account, where its person is already signed in; invited still leaves exposed the accounts that never signed in or hold a live link.

Auth0

The issuer is https://<tenant>.<region>.auth0.com/, with its trailing slash: https://, then the application's Domain, then /. With a custom domain, it's https://<your-domain>/. The discovery document is under it, at .well-known/openid-configuration.

  1. Applications → Applications → Create Application → Regular Web Applications.
  2. In Settings, copy the Domain, the Client ID and the Client Secret, and add <KUBELATCH_BASE_URL>/api/auth/oidc/callback to Allowed Callback URLs. Save.
  3. Keep the ID token signed with RS256, the default, in Advanced Settings → OAuth.

Auth0 marks an address verified only once the person has confirmed it (a database connection sends them an e-mail for that) or when a social provider vouches for it. Unverified sign-ups get email_unverified, and kubelatch has no switch to accept them.

When a sign-in fails

A failed sign-in brings the person back to the login screen with a message, and leaves a login.failure row in Audit → Control plane with via: oidc, the code as reason, and the e-mail and the provider's subject when they're known. Three exceptions write no row: server; state when the browser brings no valid state cookie (missing, expired after ten minutes, or from another session); and a click on Sign in with {name} while kubelatch can't get the discovery document, which comes straight back with provider_unavailable. The texts name the provider as OIDC_DISPLAY_NAME does ({name} below).

Code What the person reads What to do
rate_limited Too many attempts from this address. Wait a minute and try again. The per-IP limit of password logins also applies here. If everyone hits it at once behind an Ingress, kubelatch sees them all as one IP: set KUBELATCH_TRUSTED_PROXIES.
state Your {name} sign-in session has expired or is invalid. Try again. The sign-in took more than 10 minutes or came back to another browser. If it happens to everyone, people open kubelatch at a host other than the one in KUBELATCH_BASE_URL, and the cookie of the sign-in stays on that host.
denied You canceled the sign-in on {name}. The person canceled at the provider, or the provider refused them (in Okta, someone not assigned to the application).
oidc {name} didn't respond as expected. Try again in a moment. The provider sent back another error, or refused to exchange the code: a wrong client ID or secret, an expired secret, a public client the provider doesn't accept. The event's idp_error has the error the provider sent with the person, and the provider's sign-in log usually says more.
provider_unavailable kubelatch can't reach {name} right now. Try again in a few minutes; if it persists, tell an administrator. kubelatch couldn't reach the provider (the discovery document, the token endpoint, the keys or userinfo), or the discovery document isn't valid. Look at the line in Users and at the log: a network error, a CA kubelatch doesn't trust (Keycloak), or an issuer in the document that differs from OIDC_ISSUER.
token kubelatch couldn't verify the answer from {name}. Try again; if it persists, tell an administrator. The ID token failed a check (signature, which must be RS256; issuer, audience, azp, expiry, nonce) or lacks the subject claim. Usual causes: a token signed with HS256 (Authentik without a signing key), OIDC_SUBJECT_CLAIM=oid without the profile scope, or a server clock far off.
no_email {name} didn't share your e-mail address with kubelatch. Ask an administrator to check the configuration. Neither the ID token nor userinfo carried an e-mail: check that email is in OIDC_SCOPES and granted to the application (on Entra ID, the email optional claim), and that the person has an address at the provider.
email_unverified {name} doesn't vouch for your e-mail address, so kubelatch can't match it to an account. The claim in OIDC_EMAIL_VERIFIED_CLAIM isn't true. Entra ID: the xms_edov optional claim and the variable. Keycloak: Email verified, or Trust Email for LDAP. Authentik 2025.10 or later: the email mapping. Auth0: the person hasn't confirmed their address.
domain_not_allowed That {name} account isn't in a domain this kubelatch accepts. Did you pick a personal account? The domain of the address isn't in OIDC_ALLOWED_DOMAINS or, on Google, the account has no hd (a personal account) or another one. The screen offers Try another account, which asks the provider for its account picker. If the domain is yours, add it.
unknown_account No kubelatch account has the e-mail address of that {name} account. No account has that address, or the policy doesn't allow linking it. Outside the letters A to Z, case counts: Álvaro@acme.com doesn't match álvaro@acme.com (How accounts are linked). Create the account in Users with that e-mail in lower case, or send a linking link. The screen adds "To get an account, ask {contact}." when KUBELATCH_ACCESS_CONTACT is set, and Try another account.
ambiguous_account More than one kubelatch account has your e-mail address. Ask an administrator to fix it. The event's candidates lists the accounts. Leave the address on one enabled account: disable the other, or change its e-mail.
disabled Your account is disabled. The account, or every account with that address, is disabled. Enable it if the person should get in.
identity_mismatch Your kubelatch account is linked to another {name} identity. Ask an administrator to unlink it. The account the address finds already has another identity of this provider. The same person recreated at the provider: Unlink {name}. Someone else with a reused address: don't (When someone leaves, or an address comes back).
oidc_taken That {name} account is already linked to another kubelatch account. That identity already opens another account. Find it in Users, where Sign-in shows the address, and unlink it there first if the link is wrong.
already_linked Your kubelatch account is already linked to another {name} identity. Ask an administrator to unlink it first. From My account: the account already has an identity of this provider. Unlink {name} first, if the change is intended.
link_gone The link has expired or was already used; ask an administrator for a new one. Generate another invitation or linking link.
server Internal error completing the {name} sign-in. Try again. An internal error: look at the server log.

Check that it works

With the application registered and kubelatch restarted with the variables:

  • GET /api/auth/methods answers "oidc": true with your oidc_name, and the login screen shows Sign in with {name}.
  • In Users, the provider's line reads "{name}: discovery fetched {date}.".
  • The break-glass account signs in from Sign in with a password. The password of a normal account no longer works (a generic 401 and login.failure with reason: password_disabled).
  • In a private window, someone with an account and its address clicks Sign in with {name} and lands in Home, signed in. In Audit → Control plane there's an oidc.link with via: email and a login.success with via: oidc; in Users, their Sign-in reads "{name} · {email}".
  • They sign out and back in: a new login.success, and no second oidc.link.
  • On their computer, kubelatch login opens the browser, goes through the provider if needed and ends in the usual approval (The CLI).
  • With an account outside your domains, such as a personal Gmail, the screen reads "That {name} account isn't in a domain this kubelatch accepts. Did you pick a personal account?" and offers Try another account; for someone without an account, it reads "No kubelatch account has the e-mail address of that {name} account.". Both leave a login.failure with their reason.
  • GET /api/oidc/status (admin) shows configured, issuer, domains, autolink, public_client and discovery with its fetched_at.

If something else fails, see Troubleshooting.