For the complete documentation index, see llms.txt. This page is also available as Markdown.

LDAP / Kerberos SSO — Setup & Troubleshooting Runbook

Internal engineering runbook for standing up AD/LDAP + Kerberos (SPNEGO) silent SSO for the standard desktop app end-to-end, with the non-obvious gotchas that break it. Companion to the client-facing

End-to-end configuration for silent AD login: a domain-joined Windows client runs the standard app, which auto-discovers the tenant from a pre-provisioned zs-origin.json, opens the browser to Keycloak, the browser negotiates Kerberos (SPNEGO) with no prompt, and core-service JIT-provisions the user on first login.

Reproducible local rig (Samba AD DC + Keycloak federation + Windows join scripts) lives in java-apps/.deployment/ad-kerberos/. This page is the "why", the correct settings, and the failure modes.

The chain (what must line up)

domain-joined Windows client (domain-user session → Kerberos TGT)
   │  reads C:\Windows\System32\zs-origin.json  → tenant + apiKeycloakUrl + realm
   │  browser (Chrome/Edge) with SPNEGO allowlist for the Keycloak host

Keycloak (developer/tenant realm)
   │  LDAP user federation (LDAPS) + Kerberos, editMode = UNSYNCED
   │  SPN HTTP/<keycloak-host> + AES keytab

core-service  (JIT provisioning: /api/v2/onboarding/ensure-federated-identity)
   │  tenant StandardConfig.autoCreateUsers = true

We360 Identity created; identity_id claim written back to Keycloak

Keycloak federation — the settings that matter

Setting
Value
Why

Connection

ldaps://<dc>:636

Samba blocks cleartext simple binds; do not set ldap server require strong auth = no.

TLS cert

Must carry an IP SAN if Keycloak dials the DC by IP

Java LDAPS verifies the host; a DNS-only SAN fails on an IP dial.

Truststore

DC CA in Keycloak conf/truststores/

Loaded at startup only — restart Keycloak after adding.

editMode

UNSYNCED (not READ_ONLY)

READ_ONLY makes the imported user's federated storage non-writable, so the identity_id attribute can't be stored → JIT fails with "Federated storage is not writable".

importEnabled

true

The identity_id attribute lives on the imported (local) copy.

Mappers

username→sAMAccountName, email→mail, first name→givenName, last name→sn

The API-created federation is easy to leave missing the first-name mapper; then firstName is null and JIT fails validation "First name and last name cannot be empty".

Kerberos

allowKerberosAuthentication=true, serverPrincipal=HTTP/<host>@REALM, keyTab=<path>

<host> MUST equal the host in the manifest's apiKeycloakUrl (see below).

Kerberos SPN / keytab — must match the manifest host and be AES

  • The browser derives the SPN from the URL it opens: apiKeycloakUrl host → HTTP/<that-host>. The AD SPN, the exported keytab, and Keycloak's serverPrincipal must all use that exact host. A mismatch = Keycloak can't decrypt the ticket → silent fallback to the username/password form.

  • Use AES, not RC4. Set msDS-SupportedEncryptionTypes = 24 (AES128+AES256) on the service account, then re-export the keytab. JDK 17+/21 (Keycloak) rejects RC4 with "Encryption type RC4 with HMAC is not supported/enabled", which also falls back to the form. Verify the keytab: ktutil -k <file> list should show aes256-cts-hmac-sha1-96 / aes128-…, not arcfour-hmac.

  • After changing enctype, purge the client's cached ticket (klist purge in the domain-user session) so it fetches a fresh AES service ticket.

The Windows client

  1. Domain-joined and the interactive desktop session must be a domain user (e.g. DOMAIN\einstein) — a local account has no Kerberos TGT, so SPNEGO cannot happen regardless of everything else. Check with klist in that session.

  2. Browser SPNEGO allowlist (whitelist the Keycloak host). By default Chrome and Edge do not perform Kerberos/Negotiate for any site — they do not honour the IE "Local Intranet" zone for this. You must explicitly allowlist the Keycloak host via the AuthServerAllowlist policy. The host you list MUST be the host in the manifest's apiKeycloakUrl — the same host the SPN/keytab use (e.g. api.local.we360.ai). Listing the wrong host = no Negotiate = password prompt.

    What to allowlist. A comma-separated list of hosts; wildcards are allowed, so a whole domain works — e.g. api.local.we360.ai or *.we360.ai. Do not include a scheme or port (https://, :8080) — host only.

    Deploy via Group Policy (fleet). Import the Chrome/Edge ADMX templates, then set:

    • Chrome: Computer Configuration → Administrative Templates → Google → Google Chrome → HTTP authentication → Authentication server allowlist = <host or *.domain>

    • Edge: … → Microsoft Edge → Authentication server allowlist = <host or *.domain>

    Or set the registry directly (per-machine, /reg:64 on ARM64/64-bit Windows):

    Set the policy for every browser the app may open — the standard app launches the OS default browser, which is often Edge. Fully quit and relaunch the browser (kill all chrome.exe / msedge.exe) so it reloads the policy — a browser that was already running will ignore a freshly-set allowlist.

    AuthNegotiateDelegateAllowlist (same key) is only needed if the Keycloak host must delegate the user's ticket onward (constrained delegation); plain SSO does not need it — leave it unset unless you have that requirement.

    Firefox (if used) doesn't read these policies — set network.negotiate-auth.trusted-uris = <host or .domain> (via about:config or a policy) instead.

    Reading the symptom:

    • Basic-auth popup (browser's own username/password box) → host not allowlisted (or browser not restarted). Fix the allowlist.

    • Proper Keycloak login form → Negotiate was attempted but Keycloak rejected the ticket (server-side GSS: RC4 vs AES, or SPN/host mismatch — see above).

  3. Post-domain-join, the network profile switches to Domain — if OpenSSH (or any management) was firewalled to Private/Public only, it goes dark. Widen the rule to -Profile Any if you manage the box remotely.

zs-origin.json (pre-provisioned discovery)

  • Content is exactly the origin by-base-domain response — open in a browser: https://origin.in.we360.ai/discover/tenant_by_base_domain/?baseDomain=<baseDomain> and save the JSON verbatim. See Pre-Provisioned Origin Data for schema/trust.

  • Windows path: C:\Windows\System32\zs-origin.json.

  • WOW64 caveat (ARM64 Windows). The app reads the real System32 (it's a 64-bit/ARM64 process). A 32-bit shell (e.g. x86 PowerShell over OpenSSH) writing to C:\Windows\System32 is silently redirected to SysWOW64 — and a read-back in the same 32-bit shell also hits SysWOW64, so it looks placed but the app never sees it. Write via C:\Windows\Sysnative\zs-origin.json (or a native-arch shell) and verify from an ARM64/x64 process.

JIT provisioning (core-service)

  • Gated by the tenant's StandardConfig.autoCreateUsers (core_master.tenant_settings.standard.auto_create_users). Off → ensure-federated-identity returns FORBIDDEN.

  • On first login the endpoint creates the identity and writes identity_id back to Keycloak; the client refreshes its token to pick up the claim. When the identity already exists the endpoint is a fast no-op — it must not re-touch Keycloak.

  • AD user needs: sAMAccountName (username), mail (email, e.g. user@realm), givenName + sn (first/last — required), userPrincipalName.

Troubleshooting — symptom → cause → fix

Symptom
Cause
Fix

Browser shows basic-auth popup

Host not in the browser's SPNEGO allowlist

Set Chrome/Edge AuthServerAllowlist; restart browser

Browser shows Keycloak form (not silent)

SPNEGO reached Keycloak but GSS failed

Check keytab: RC4 (→ set AES) or SPN/host mismatch with apiKeycloakUrl

Keycloak log: RC4 with HMAC not supported

Service ticket is RC4; JDK rejects it

msDS-SupportedEncryptionTypes=24, re-export keytab, klist purge on client

Prompt even with policy set

Interactive session is a local user (no TGT)

Sign in as a domain user; verify klist

App hangs after login

ensure-federated-identity erroring/slow

Check core-service log (see below); usually one of the next three rows

First name and last name cannot be empty

Missing first-name mapper / no givenName

Add givenName→firstName mapper; set givenName+sn in AD

Federated storage is not writable

editMode = READ_ONLY

Set editMode = UNSYNCED, re-sync

User exists with same email (409)

KC user lookup by username, but federated username ≠ email

Fixed in core-service (searchByEmail); rebuild required

zs-origin.json "not found" by app

Written to SysWOW64 via a 32-bit shell

Write to C:\Windows\Sysnative\...; verify from ARM64 process

Getting core-service logs locally

core-service defaults to stdout-only JSON. For local debugging, enable the file appender with -Dcore.logToFile=true (path via -Dcore.logFile=…). ./dev.sh api sets this automatically and writes tmp/logs/core-service-app.log; for an IDE run, add the flag to the run configuration's VM options.

Last updated

Was this helpful?