Skip to content

Enterprise SSO (SAML / OIDC / LDAP)

Enterprise tier

SSO is an Enterprise feature. Enabling SSO requires an Enterprise license. Once configured, the login flow keeps authenticating users even if the license later lapses — a lapsed license must never lock existing SSO users out.

KubeManta authenticates operators through your own identity provider via three protocols on one pluggable engine, all configured through the same wizard:

  • SAML 2.0 — Azure AD, Okta, Google Workspace, Cognito, Auth0, Ping, ADFS
  • OIDC — authorization-code flow with PKCE against any standards-compliant provider (discovery-document based)
  • LDAP / Active Directory — direct-bind over LDAPS or StartTLS

Local login is retained by default as a break-glass path, so an IdP outage does not lock you out of your own cluster. Once SSO is proven you can switch it off with sso.local_login: off; recovery is then performed from inside the cluster rather than through the login form — see Break-glass local login below.


How it works

One pipeline handles every protocol — the provider validates the login and produces an Identity, and everything downstream is shared:

flowchart LR
  user["Operator<br/>(browser)"] -->|1. login| idp["Your IdP<br/>(SAML / OIDC / LDAP)"]
  idp -->|2. validated identity| km["KubeManta Agent"]
  km -->|3. validate + JIT provision| users[("user directory")]
  km -->|4. group → role map| role{"admin?"}
  role -->|5. issue session| user
  1. The operator signs in — a browser redirect to your IdP (SAML, SP-initiated; OIDC, authorization-code + PKCE) or a direct credential bind (LDAP).
  2. The provider returns a validated identity: a signed SAML assertion to the ACS endpoint, a verified OIDC ID token via the callback, or a successful LDAP bind plus attribute lookup.
  3. KubeManta just-in-time provisions the user, keyed on (provider, subject) — no pre-creation required.
  4. The IdP group claims are mapped to a KubeManta role (see below).
  5. KubeManta issues its normal signed session cookie. From that point the user is an ordinary authenticated session — session validation, admin gating, and RBAC are unchanged.

OIDC

The OIDC provider runs the authorization-code flow with PKCE. Endpoints are resolved from your issuer's discovery document (/.well-known/openid-configuration), the ID token's signature is verified against the issuer's JWKS (weak / none algorithms rejected before verification), and the state, nonce, and PKCE verifier are carried in a signed, short-lived state cookie. Outbound discovery/token/JWKS fetches are SSRF-guarded and DNS-rebind-pinned, with size caps and short timeouts.

LDAP

LDAP is a template direct-bind flow: the user binds with their own username + password — KubeManta stores no service-account bind credential. The connection must be ldaps:// or ldap:// + StartTLS (plaintext LDAP is refused — it would be a credential-capture vector); an optional PEM CA supports private/enterprise CAs. Empty passwords are rejected before any network bind (the LDAP "unauthenticated bind" bypass). After a successful bind, email and group memberships are read for JIT provisioning and role mapping. An insecure "skip TLS verification" toggle exists for development only and is clearly surfaced as unsafe.


JIT user provisioning

Users are created on first successful login, keyed on the identity provider plus the IdP's stable subject identifier. There is no separate "invite" or "create user" step. Deactivating a user is enforced per-request: a revoked SSO user is rejected within about 30 seconds. Password and API-key sessions carry no email claim and are unaffected.

Manage provisioned users in Admin → Auth (SSO).


Role mapping

KubeManta has two roles: admin and viewer.

Your directory decides who is in a group. KubeManta decides what a group grants. Groups are not typed in — they appear in Admin → Groups the first time somebody signs in carrying them, and you assign a role to one you can see. A new group grants viewer until you say otherwise.

A user becomes admin if either of these matches, and viewer otherwise:

  • one of the groups they are currently in is set to admin in Admin → Groups, or
  • their email address is in Admin users (by email).

If they are in several groups, the highest role wins.

Everything downstream — write gating, Expert Mode, Resource Builder Apply — keys off this role exactly as it does for local accounts.

Pick the approach that matches your directory:

Your IdP emits group names

Have one person sign in, then open Admin → Groups and set that group to admin. Everyone already in it becomes an admin immediately, and so does anyone your directory adds later.

Removing somebody from the group at your IdP demotes them at their next sign-in — KubeManta re-reads membership from the claim on every login, so your directory stays the place you deprovision.

Microsoft Entra ID — use App Roles, not security groups

This is the recommended path for Entra, and it avoids two problems that make group-based mapping unreliable there: the group claim carries object GUIDs rather than names, and Entra drops the claim entirely once a user belongs to more than roughly 150 groups ("group overage") — so an admin silently becomes a viewer as their group membership grows.

  1. In App registrations → your app → App roles, create a role with the value KubeManta.Admin.
  2. In Enterprise applications → your app → Users and groups, assign that role to the people (or a group) who should administer KubeManta.
  3. In the KubeManta wizard, set Groups attribute to roles.
  4. Have somebody sign in, then set KubeManta.Admin to admin in Admin → Groups.

Role values come through as the literal string you chose, there is no overage limit, and un-assigning the role demotes the user at their next sign-in.

Your directory has no usable groups

Some IdPs emit no group claim at all, and plenty of smaller organisations do not use groups. List the administrators under Admin users (by email) instead — matching is case-insensitive, and it works whether or not any group ever appears. This path also covers Entra group overage, where the claim vanishes precisely for the people who are in the most groups.

Default role covers the remaining case: set it to admin only for a trusted single-tenant IdP where every person who can authenticate should administer the cluster. It defaults to viewer.

Nothing appears under Groups

Groups only appear once a login carries them, so sign in first. If people are signing in and the list stays empty, the group claim is not reaching KubeManta — check Groups attribute in the SSO settings. On Entra that is roles when using App Roles; a directory with no groups at all should use Admin users (by email) instead.

Promoting one person by hand

An admin can change any user's role directly in Admin → Users. A role set this way is pinned: it survives the user signing in again, and the IdP will not overwrite it. The Users table shows which users are pinned and which still track your directory, and you can hand a user back to the IdP at any time.

Role changes — from the IdP or by hand — apply to sessions already signed in, so a demotion takes effect immediately rather than when the person next logs in.


Break-glass local login

The sso.local_login policy controls how the local password login coexists with SSO:

Value Behavior
all (default) Both SSO and local password login are available.
admin_only The login page prefers SSO; local password login is restricted to the admin break-glass account.
off Password login is fully disabled — the form is not rendered and the endpoint is gone. Requires at least one SSO method to be enabled.

off is one-way from the browser

Nothing in the UI turns password login back on. That is deliberate: it means a stolen SSO admin session cannot quietly re-open a password door. Recovery is a command on the cluster, so anyone with kubectl exec can always get back in if your IdP breaks:

kubectl exec -n kubemanta-system deploy/kubemanta-agent -- \
  python3 -m app.enable_local_login [--admin-only]

Re-enabling is recorded in the audit trail.

If you are locked out and cannot tell why — wrong password, lost second factor, deactivated account, an IP ban, or password login switched off — every one of those looks identical at the login form. km-recover clears all of them in one run:

kubectl exec -it -n kubemanta-system deploy/kubemanta-agent -- \
  python3 -m app.recover_admin --username <you> --password-stdin

Disabling SSO, which turns off enforcement entirely, is also always available to an admin.


Assertion hardening

The SAML SP is configured for a modern security posture out of the box:

  • RSA-SHA1 rejected. An RSA-SHA1-signed response is refused (per the OWASP SAML cheat-sheet).
  • SHA-256 pinned. The SP-side signature and digest algorithms are pinned to SHA-256.
  • Clock drift. NotBefore / NotOnOrAfter are validated with a fixed 5-minute allowed clock drift.
  • Replay protection. Each assertion ID is recorded once and rejected on reuse.
  • InResponseTo binding. SP-initiated login binds each response to the request that started it, and requires the response's InResponseTo to match. A response with no InResponseTo (unsolicited / IdP-initiated) is rejected explicitly.
  • Signed AuthnRequests & encrypted assertions (optional). Generate an SP keypair from the admin wizard to turn on signed AuthnRequests and/or encrypted assertions for IdPs that require them.
  • Single Logout. SP-initiated SLO is supported when your IdP advertises a SingleLogoutService URL; the SLO callback requires signed SLO messages — an unsigned LogoutRequest/LogoutResponse is rejected.

SP-initiated login only

SAML login is SP-initiated only (the user starts at KubeManta). Unsolicited / IdP-initiated responses are explicitly rejected via the InResponseTo binding above.

RelayState / CSRF

RelayState carries a signed CSRF nonce plus the return path in a short-lived, SameSite=Lax cookie. The return path is open-redirect-guarded to the same origin. The session cookie set after a successful SSO login uses SameSite=Lax so the fresh session survives the IdP's cross-site top-level redirect; local password login stays SameSite=Strict.


Integration endpoints

These are the public endpoints your IdP interacts with. The admin wizard shows you the exact URLs to register (ACS URL and SP Entity ID for SAML, redirect URI for OIDC).

Endpoint Purpose
GET /auth/saml/login Start SP-initiated SAML login (redirect to IdP).
POST /auth/saml/acs Assertion Consumer Service — receives the IdP's signed response.
GET /auth/saml/metadata SP metadata XML for your IdP to consume.
GET /auth/saml/logout Logout (starts SP-initiated SLO when the IdP advertises it; otherwise local).
GET /auth/oidc/login Start the OIDC authorization-code + PKCE flow.
GET /auth/oidc/callback OIDC redirect URI — code exchange + ID-token verification.
POST /auth/ldap/login LDAP direct-bind login (username + password).

Configuring, testing, and enabling SSO — and managing provisioned users — are all done from Admin → Auth (SSO) (admin-only). The audited actor for an SSO session is derived only from a cryptographically verified session, so a forged request on a public auth route cannot spoof the audited actor.


Admin wizard

Admin → Auth (SSO) is a guided setup:

  1. Pick the protocol (SAML / OIDC / LDAP) and an IdP preset.
  2. KubeManta shows the values your IdP needs — for SAML the ACS URL and SP Entity ID (metadata), for OIDC the redirect URI.
  3. Point at the IdP: upload/fetch SAML metadata, enter the OIDC issuer + client credentials, or the LDAP server URI + bind-DN template.
  4. Sign in once, then set the group to admin in Admin → Groups.
  5. Test the connection, then Enable.

The wizard's external base URL (the ACS / EntityID base, e.g. https://kubemanta.example.com) can be pre-filled from the Helm value sso.publicUrl — but the admin-entered configuration is always authoritative.

# values.yaml — optional pre-fill only
sso:
  publicUrl: "https://kubemanta.example.com"

Configuration storage & audit

  • The full SSO configuration is AES-256-GCM encrypted at rest, alongside your other stored credentials.
  • SSO login, config changes, and user deactivation are all audited through the normal write-audit path. For SSO sessions the audited actor is email-aware (email@ip).

Session revocation

Deactivating an SSO user takes effect within about 30 seconds cluster-wide: every request from an email-bearing session re-checks that the user is still active and rejects a revoked session inside that window.