Keycloak on Windows + LDAPS to FreeIPA
- FreeIPA CA is in the Windows Local Machine Root store.
- FreeIPA CA is in the Java truststore used by
kc.bat(this is the single most common source of failure — we will do it again here for safety). - DNS resolves
ipa.example.internal, and clocks are in sync with IPA.
Prereqs
- Windows 11 with local admin.
- Keycloak ZIP distribution extracted to
C:\keycloak. This page uses Keycloak 26.x conventions; earlier versions need minor path changes but the federation steps are identical. - A supported JDK for that Keycloak release (17 for 24.x, 21 for 26.x). Set
JAVA_HOMEto the JDK and add%JAVA_HOME%\bintoPATH. - FreeIPA server (
ipa.example.internal, realmEXAMPLE.INTERNAL) reachable on 636/TCP. - A dedicated FreeIPA bind account for Keycloak (recommended) — see below.
Create a dedicated IPA bind account
Do not point Keycloak at cn=Directory Manager. Create a low-privilege account that can read users and groups:
# On the IPA server (or via /freeipa-cli-windows/)
ipa user-add keycloak-bind --first=Keycloak --last=Bind --random
ipa user-show keycloak-bind --all | grep -i 'Random password'
# Optional: add to a read-only helper group you maintain
ipa group-add-member ipausers --users=keycloak-bind
Record the password (Keycloak will store it in its own DB). The bind DN will be:
uid=keycloak-bind,cn=users,cn=accounts,dc=example,dc=internal
Getting the IPA CA into the Keycloak truststore
The single most common failure mode ("PKIX path building failed") is Keycloak's JVM not trusting the IPA CA. There are two supported ways; use option B for anything beyond a throwaway lab because it survives JDK upgrades.
Option A: import into the JDK's cacerts
& "$env:JAVA_HOME\bin\keytool.exe" -importcert `
-trustcacerts -noprompt `
-alias freeipa-ca `
-file C:\certs\ipa-ca.crt `
-keystore "$env:JAVA_HOME\lib\security\cacerts" `
-storepass changeit
Option B (recommended): app-local truststore + env var
mkdir C:\keycloak\conf -Force | Out-Null
& "$env:JAVA_HOME\bin\keytool.exe" -importcert `
-trustcacerts -noprompt `
-alias freeipa-ca `
-file C:\certs\ipa-ca.crt `
-keystore C:\keycloak\conf\truststore.p12 `
-storetype PKCS12 `
-storepass changeit
Then make Keycloak use it. Create C:\keycloak\conf\keycloak.conf (Keycloak reads this at startup — no env-var juggling):
# C:\keycloak\conf\keycloak.conf
# ---- HTTP / hostname ----
http-enabled=true
http-port=8080
hostname=localhost
# For non-lab use, drop 'localhost' and set a real FQDN with TLS.
# ---- Truststore: this is how Keycloak trusts the FreeIPA CA ----
truststore-paths=C:/keycloak/conf/truststore.p12
tls-hostname-verifier=DEFAULT
# ---- Logging ----
log-level=info,org.keycloak.storage.ldap:debug
Note the forward slashes in truststore-paths — Keycloak tolerates either, but forward slashes avoid Windows backslash-escape problems if you ever migrate the config.
Launching Keycloak (kc.bat)
First-time build and development start
cd C:\keycloak
bin\kc.bat build
bin\kc.bat start-dev
The build step bakes in your keycloak.conf settings. The start-dev step launches with HTTP and relaxed hostname checks — useful while you are wiring up LDAPS, painful for production. Open http://localhost:8080, create the initial admin user, log in.
Production start
cd C:\keycloak
bin\kc.bat build
bin\kc.bat start --hostname=sso.example.internal --https-port=8443 ^
--https-certificate-file=C:\certs\sso.example.internal.crt ^
--https-certificate-key-file=C:\certs\sso.example.internal.key
For a real deployment, issue the sso.example.internal certificate from FreeIPA (ipa cert-request) so it chains to the same CA your federation is already trusting. That way browsers, curl, and Keycloak all validate with one CA.
Admin realm and a test realm
- Log in as the initial admin to
master. - Top-left realm picker → Create realm → name:
example. This is where IPA users will live. - Switch to the
examplerealm for all steps below.
User Federation: LDAPS to FreeIPA
User federation → Add provider → ldap. Use the values below exactly — almost every field that trips people up is in this table.
General options
| Field | Value |
|---|---|
| UI display name | freeipa-ldaps |
| Vendor | Red Hat Directory Server (FreeIPA is 389-DS under the hood) — or Other if 26.x hides the RHDS preset |
| Enabled | On |
| Import users | On (copies users into the Keycloak DB on first login; leave off for fully-on-demand reads) |
| Edit mode | READ_ONLY (IPA should be source of truth; flip to WRITABLE only if you also grant the bind account User Administrator) |
Connection and authentication
| Field | Value |
|---|---|
| Connection URL | ldaps://ipa.example.internal:636 |
| Bind type | simple |
| Bind DN | uid=keycloak-bind,cn=users,cn=accounts,dc=example,dc=internal |
| Bind credentials | (the random password you generated for keycloak-bind) |
| Test connection | Must return "Successfully connected to LDAP" |
| Test authentication | Must return "Successfully authenticated with LDAP" |
org.keycloak.storage.ldap:debug) will tell you which.
LDAP searching & user attributes
| Field | Value |
|---|---|
| Users DN | cn=users,cn=accounts,dc=example,dc=internal |
| Username LDAP attribute | uid |
| RDN LDAP attribute | uid |
| UUID LDAP attribute | ipaUniqueID |
| User object classes | inetOrgPerson, organizationalPerson, person, ipaObject |
| User LDAP filter | leave empty, or (memberOf=cn=sso-users,cn=groups,cn=accounts,dc=example,dc=internal) to gate by IPA group |
| Search scope | Subtree |
| Pagination | On |
Synchronization settings
- Import users: On
- Batch size:
1000 - Periodic full sync: enable, every 24h for small directories, off and use "on demand" for very large directories.
- Periodic changed-user sync: enable, every 5 min (IPA 389-DS supports
modifyTimestamp).
Save. You should see a toast with the federation provider ID. Go back to the provider and click Synchronize all users — logs will show imports. Users (left menu) → you should now see your IPA accounts.
Group mapper
On the federation provider page → Mappers → Create. Type group-ldap-mapper.
| Field | Value |
|---|---|
| Name | ipa-groups |
| LDAP Groups DN | cn=groups,cn=accounts,dc=example,dc=internal |
| Group Name LDAP Attribute | cn |
| Group Object Classes | ipaUserGroup, groupOfNames (ipaUserGroup is the canonical IPA class; groupOfNames lets non-IPA groups ride along) |
| Preserve Group Inheritance | On |
| Membership LDAP Attribute | member |
| Membership Attribute Type | DN |
| Membership User LDAP Attribute | uid |
| LDAP Filter | empty |
| Mode | READ_ONLY |
| User Groups Retrieve Strategy | LOAD_GROUPS_BY_MEMBER_ATTRIBUTE (or GET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTE if you rely on the IPA memberOf plugin; both work, the former is slightly slower but less finicky) |
| Drop non-existing groups during sync | On |
Save. Back on the provider → Action → Sync all users. Then check Groups (left menu) — IPA groups should appear with members.
Optional: Kerberos / SPNEGO SSO
If you want browsers on IPA-domain-joined Linux clients (or Windows hosts configured per Windows 11 + FreeIPA) to log into Keycloak-protected apps without typing a password, add Kerberos to the same federation provider.
Create a service principal and keytab on IPA
ipa service-add HTTP/sso.example.internal
ipa-getkeytab -s ipa.example.internal \
-p HTTP/sso.example.internal \
-k /tmp/sso.keytab
# Copy to Windows:
# scp /tmp/sso.keytab user@win11:/C:/keycloak/conf/sso.keytab
Wire it into Keycloak
Same federation provider → scroll to "Kerberos integration":
| Field | Value |
|---|---|
| Allow Kerberos authentication | On |
| Use Kerberos for password authentication | Off (leave LDAP simple bind handling passwords; Kerberos is for browser SSO) |
| Kerberos realm | EXAMPLE.INTERNAL |
| Server principal | HTTP/sso.example.internal@EXAMPLE.INTERNAL |
| Keytab | C:/keycloak/conf/sso.keytab |
Browser config
- Edge / Chrome: trusted by default for Kerberos to sites matching
AuthServerAllowlist. Set via Group Policy or registry:HKLM\SOFTWARE\Policies\Microsoft\Edge\AuthServerAllowlist = *.example.internal. - Firefox:
about:config→network.negotiate-auth.trusted-uris=.example.internal. - The browser must already have a Kerberos ticket (MIT
kinitor a successful SSPIklist).
Troubleshooting
| Symptom / log line | Root cause | Fix |
|---|---|---|
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target |
IPA CA is not in the Java truststore Keycloak is using. | Re-do Option B and restart kc.bat. keytool -list -v should show your freeipa-ca alias. Confirm the truststore path in keycloak.conf matches. |
javax.net.ssl.SSLHandshakeException: Hostname mismatch |
The cert SAN does not include the hostname in the Connection URL (common when using aliases like ipa vs ipa.example.internal). |
Use the real FQDN in the Connection URL. Verify SAN with openssl s_client -connect ipa.example.internal:636 -showcerts. |
LDAP: error code 49 - Invalid Credentials |
Bind DN or password wrong, or the bind account is locked. | Reproduce from WSL: ldapsearch -H ldaps://ipa.example.internal -D 'uid=keycloak-bind,cn=users,cn=accounts,dc=example,dc=internal' -W -x -b '' -s base. Fix in IPA. |
LDAP: error code 32 - No Such Object when syncing |
Wrong Users DN / Groups DN. |
Confirm with ipa user-show admin --all | grep -i 'dn:'. |
| User imports but groups show empty | Group mapper using uid vs DN mismatch, or wrong retrieval strategy. |
Try switching User Groups Retrieve Strategy. If IPA's memberOf plugin is disabled, use LOAD_GROUPS_BY_MEMBER_ATTRIBUTE. |
kc.bat starts but JAVA_OPTS_APPEND seems ignored |
The shell that launched kc.bat did not have the env var set. |
Prefer the keycloak.conf truststore-paths= approach — it is persistent and never depends on shell state. |
| Keycloak hangs for ~30s at startup before logging in | IPA LDAPS reachable by name but blocked by Windows firewall / VPN split-tunnel. | Test-NetConnection ipa.example.internal -Port 636 from the same user session that runs kc.bat. |
| Kerberos SSO: browser prompts for password instead of SSO-ing | Browser has no trusted-URI match, or no ticket. | Check klist; set the browser allowlist (see Kerberos section); confirm the SPN case: HTTP/ in upper case, FQDN in lower case. |
| Kerberos SSO works for one user, fails for another | The second user has no sso-users IPA group membership but you set a User LDAP filter. |
Either add them to the group, or drop the LDAP filter. |
Verification
- Create a client in the
examplerealm (Clients → Create client), type OpenID Connect, root URLhttp://localhost:9090/. Save. - Use the Keycloak account console (
http://localhost:8080/realms/example/account/) to log in as an IPA user. Password must work. - In Users → select your IPA user → Groups — the tab should list the IPA groups you expect.
- Change the user's password in IPA (
ipa passwd <uid>) and confirm the old password stops working in Keycloak within thePeriodic changed-user syncinterval. - If you configured Kerberos: from a pre-
kinit'd browser, hit the account console and expect no password prompt.
Running Keycloak as a Windows Service
For production, run Keycloak under a Windows Service using NSSM so it restarts on reboot.
nssm install Keycloak "C:\keycloak\bin\kc.bat" start
nssm set Keycloak AppDirectory "C:\keycloak"
nssm set Keycloak AppStdout "C:\keycloak\logs\keycloak.out.log"
nssm set Keycloak AppStderr "C:\keycloak\logs\keycloak.err.log"
nssm set Keycloak AppStopMethodConsole 30000
nssm set Keycloak ObjectName "NT SERVICE\Keycloak" ""
nssm start Keycloak
Then sc query Keycloak and Get-Content C:\keycloak\logs\keycloak.err.log -Wait give you Linux-style service lifecycle on Windows.
example.internal for your real domain and this same page drives ADFS-free, cloud-free, on-prem Keycloak that shares a single IPA identity for every service in your stack.