LabHub

Blog

Keycloak LDAP/Active Directory Integration — A Practical Guide to User Federation

한국어English日本語

Introduction

When adopting Keycloak in an enterprise environment, the very first challenge you run into is: "What do we do with the user directory we already have?" Most organizations operate a directory service such as Active Directory (AD) or OpenLDAP that has been running for years or even decades, holding thousands to hundreds of thousands of user accounts, groups, and password policies.

As of 2026, Keycloak has evolved to the 26.6.x line (26.6.2 as of May 2026), gaining modern capabilities such as passkeys integrated into the login form, FAPI 2.0 Security Profile Final support, and Workflows for realm management automation. Yet the success of an enterprise rollout is still decided by the quality of integration with the legacy directory. Even in an era where Zero Trust and identity-first security are the default, everything starts from "a single trustworthy user store."

This article covers Keycloak User Federation from architecture to the details of LDAP provider configuration, AD-specific options, attribute and group mapping, performance tuning for large directories, troubleshooting sync failures, and finally a migration strategy for eventually retiring LDAP in favor of Keycloak built-in storage — all from an operational perspective.

Understanding the User Federation Architecture

Basic structure

Keycloak User Federation is the layer that "links" an external user store to the Keycloak user model. The key point is that Keycloak does not replace the external store; instead, it performs delegation and caching through the User Storage SPI.

+-------------------+        +----------------------------+
|   Application     |        |         Keycloak           |
|  (OIDC / SAML)    +------->+  +----------------------+  |
+-------------------+        |  |  Authentication Flow  |  |
                             |  +----------+-----------+  |
                             |             |              |
                             |  +----------v-----------+  |
                             |  |   User Cache (L1)    |  |
                             |  +----------+-----------+  |
                             |             |              |
                             |  +----------v-----------+  |
                             |  |  User Storage SPI    |  |
                             |  +---+-------------+----+  |
                             |      |             |       |
                             +------+-------------+-------+
                                    |             |
                          +---------v---+   +-----v--------+
                          |  Local DB   |   | LDAP / AD    |
                          | (federated  |   | Provider     |
                          |  link data) |   +-----+--------+
                          +-------------+         |
                                            +-----v--------+
                                            |  Directory   |
                                            |  (AD/LDAP)   |
                                            +--------------+

The flow of operations is as follows.

  1. When a user attempts to log in, Keycloak first looks the user up in its internal cache and local DB.
  2. If not found, it queries the registered User Storage providers (the LDAP provider) in priority order.
  3. If the user is found in LDAP, Keycloak creates a "federated user" entry in its local DB. This entry keeps a link to the LDAP entry (the original DN and provider ID).
  4. Password verification is delegated to an LDAP bind or handled internally by Keycloak, depending on the configuration.

The important thing to understand is that a federated user is a link, not a mirror of the LDAP entry. Which attributes are copied locally and where write operations go are entirely determined by the edit mode and mapper configuration.

Import mode vs. no-import mode

The importEnabled option of the LDAP provider significantly changes operational characteristics.

ModeBehaviorProsCons
import on (default)Copy users into the local DB and keep a linkFast lookups/searches, stable offline tokensSync required, DB growth
no-importQuery LDAP directly on every requestNo sync needed, always freshHigher LDAP load, some feature limitations

For large organizations (50,000+ users), import mode plus periodic sync is the norm. No-import works well when the directory is fast and close, the user population is small, and "LDAP must always be the source of truth."

LDAP Provider Configuration in Detail

Basic connection settings

You can configure everything through the Admin Console under User Federation, but for reproducible infrastructure we recommend codifying it with the kcadm.sh CLI or Terraform (the keycloak provider).

# Create the LDAP provider (kcadm.sh)
./kcadm.sh create components -r myrealm \
  -s name=corp-ldap \
  -s providerId=ldap \
  -s providerType=org.keycloak.storage.UserStorageProvider \
  -s 'config.enabled=["true"]' \
  -s 'config.priority=["0"]' \
  -s 'config.editMode=["READ_ONLY"]' \
  -s 'config.vendor=["ad"]' \
  -s 'config.connectionUrl=["ldaps://ad01.corp.example.com:636"]' \
  -s 'config.usersDn=["OU=Employees,DC=corp,DC=example,DC=com"]' \
  -s 'config.bindDn=["CN=svc-keycloak,OU=ServiceAccounts,DC=corp,DC=example,DC=com"]' \
  -s 'config.bindCredential=["CHANGE_ME"]' \
  -s 'config.usernameLDAPAttribute=["sAMAccountName"]' \
  -s 'config.rdnLDAPAttribute=["cn"]' \
  -s 'config.uuidLDAPAttribute=["objectGUID"]' \
  -s 'config.userObjectClasses=["person, organizationalPerson, user"]' \
  -s 'config.searchScope=["2"]' \
  -s 'config.useTruststoreSpi=["always"]' \
  -s 'config.connectionPooling=["true"]' \
  -s 'config.pagination=["true"]'

Key points regarding the connection:

Edit Mode — READ_ONLY, WRITABLE, UNSYNCED

The edit mode is the single most important setting: it determines "where write operations go."

Edit ModeProfile updatesPassword changesBest-fit scenario
READ_ONLYNot allowed (throws)Not allowedAD is the sole source of truth, HR system manages AD
WRITABLEWritten directly to LDAPWritten to LDAPKeycloak used as a self-service portal
UNSYNCEDWritten only to the Keycloak local DBStored locally onlyLDAP is read-only seed, gradually becoming independent

Let us look at the operational implications of each mode.

READ_ONLY is the safest default. If a user tries to edit their profile in the Keycloak Account Console, an error occurs, so you should disable those features in the Account Console or adjust required actions. Password change requests are rejected, so you will need guidance like "Please change your password in the corporate portal."

WRITABLE turns Keycloak into a writing client of LDAP. The bind service account now needs write permissions, and with AD, LDAPS becomes mandatory for password changes (AD refuses unicodePwd modifications over insecure channels). Also note that Keycloak required actions (e.g., forced password update on first login) will change the actual AD password, so conflicts with the AD-side password policy must be reviewed carefully.

UNSYNCED is an interesting middle ground. Users are read from LDAP, but subsequent changes are stored only in the Keycloak local store. It is effectively a "gradual migration mode that uses LDAP as seed data" and the core tool of the migration strategy discussed later. Beware that in this mode LDAP and Keycloak data diverge over time, so you must define a clear operational policy about which side is the truth.

Sync strategies — full sync and changed users sync

In import mode, you need synchronization to propagate LDAP changes into the Keycloak local DB.

# Trigger a full sync (use the component ID returned at creation time)
./kcadm.sh create user-storage/COMPONENT_ID/sync?action=triggerFullSync -r myrealm

# Trigger a changed-users sync
./kcadm.sh create user-storage/COMPONENT_ID/sync?action=triggerChangedUsersSync -r myrealm

Periodic settings live in the provider config.

./kcadm.sh update components/COMPONENT_ID -r myrealm \
  -s 'config.fullSyncPeriod=["604800"]' \
  -s 'config.changedSyncPeriod=["3600"]'
StrategyRecommended periodBehaviorCaveat
Full syncWeekly (604800 s)Re-fetch and refresh all usersCan take tens of minutes on large directories
Changed users syncHourly (3600 s)Incremental fetch based on modification timestampsCannot detect deletions

Changed sync fetches only the entries modified since the last run, based on the LDAP modifyTimestamp attribute (AD: whenChanged). There are two traps people commonly fall into.

  1. Deletions are not detected. Users deleted in LDAP do not disappear through changed sync. They are removed during full sync (the "periodic full sync should remove non-existent users" behavior) or require a separate cleanup job. Leaver accounts surviving in Keycloak is a classic security incident, so design a deactivation policy (disable in Keycloak when disabled in AD) along with the full sync schedule.
  2. Timestamps are directory-server time. If the clocks of the Keycloak server and the directory server drift apart, changes can be missed. Verify NTP synchronization.

Active Directory-Specific Configuration

MSAD User Account Control Mapper

Unlike standard LDAP, AD manages account state via a bit-flag attribute called userAccountControl. Keycloak's MSAD user account control mapper interprets these flags and links them to Keycloak user state.

Key userAccountControl bit flags
---------------------------------------------
0x0002  ACCOUNTDISABLE     account disabled
0x0010  LOCKOUT            account locked out
0x0020  PASSWD_NOTREQD     no password required
0x10000 DONT_EXPIRE_PASSWD password never expires
0x800000 PASSWORD_EXPIRED  password expired

e.g.) 512 (0x200)  = normal account
      514 (0x202)  = normal account, disabled
      66048        = normal + password never expires

With this mapper enabled, the following becomes possible.

Another tip for AD integrations: adjust the search filter so that computer objects do not slip in through userObjectClasses, because AD applies the user objectClass to computer accounts as well.

Example additional user LDAP filter (custom user LDAP filter)
(&(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))

This filter returns only accounts in the person category whose disabled bit is not set. 1.2.840.113556.1.4.803 is the OID of AD's bitwise-AND matching rule.

Kerberos / SPNEGO integration

To let users who are domain-logged-in on corporate Windows PCs reach Keycloak in the browser without typing a password, configure Kerberos/SPNEGO integration.

First, register an SPN (Service Principal Name) for the Keycloak service account in AD and issue a keytab.

# On the AD domain controller (admin privileges)
setspn -A HTTP/sso.corp.example.com svc-keycloak-krb

ktpass -princ HTTP/sso.corp.example.com@CORP.EXAMPLE.COM \
  -mapuser CORP\svc-keycloak-krb \
  -crypto AES256-SHA1 -ptype KRB5_NT_PRINCIPAL \
  -pass SERVICE_ACCOUNT_PASSWORD \
  -out keycloak.keytab

On the Keycloak server side, place krb5.conf and enable the Kerberos integration options on the LDAP provider.

# /etc/krb5.conf
[libdefaults]
  default_realm = CORP.EXAMPLE.COM
  dns_lookup_kdc = true
  forwardable = true

[realms]
  CORP.EXAMPLE.COM = {
    kdc = ad01.corp.example.com
    admin_server = ad01.corp.example.com
  }
./kcadm.sh update components/COMPONENT_ID -r myrealm \
  -s 'config.allowKerberosAuthentication=["true"]' \
  -s 'config.kerberosRealm=["CORP.EXAMPLE.COM"]' \
  -s 'config.serverPrincipal=["HTTP/sso.corp.example.com@CORP.EXAMPLE.COM"]' \
  -s 'config.keyTab=["/opt/keycloak/conf/keycloak.keytab"]' \
  -s 'config.useKerberosForPasswordAuthentication=["false"]'

Finally, enable the Kerberos execution in the browser authentication flow as ALTERNATIVE or REQUIRED. A few operational tips:

Configuring Attribute Mappers

Mappers connect LDAP attributes to Keycloak user attributes and model fields. When the provider is created, vendor-appropriate default mappers (username, email, first name, last name, etc.) are generated automatically, and you add more as needed.

Mapper typePurposeExamples
user-attribute-ldap-mapperLDAP attribute to user attributemobile, department, employeeNumber
full-name-ldap-mapperSplit/join cn or displayName into first/last nameAD displayName
hardcoded-attribute-mapperInject a fixed valuesource=ldap
group-ldap-mapperLDAP groups to Keycloak groupssee section below
role-ldap-mapperLDAP groups/entries to roleslegacy role schemes
msad-user-account-control-mapperAD account state linkagesee section above

An example that pulls department information into a user attribute:

./kcadm.sh create components -r myrealm \
  -s name=department-mapper \
  -s providerId=user-attribute-ldap-mapper \
  -s providerType=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \
  -s parentId=COMPONENT_ID \
  -s 'config."user.model.attribute"=["department"]' \
  -s 'config."ldap.attribute"=["department"]' \
  -s 'config."read.only"=["true"]' \
  -s 'config."always.read.value.from.ldap"=["true"]' \
  -s 'config."is.mandatory.in.ldap"=["false"]'

Attributes imported this way can be exposed as token claims via protocol mappers in a client scope. That completes the pipeline: "AD department attribute → Keycloak user attribute → JWT claim." Points to watch:

Group and Role Mapping

group-ldap-mapper

Configuration that imports AD security groups into the Keycloak group tree:

./kcadm.sh create components -r myrealm \
  -s name=ad-groups \
  -s providerId=group-ldap-mapper \
  -s providerType=org.keycloak.storage.ldap.mappers.LDAPStorageMapper \
  -s parentId=COMPONENT_ID \
  -s 'config."groups.dn"=["OU=Groups,DC=corp,DC=example,DC=com"]' \
  -s 'config."group.name.ldap.attribute"=["cn"]' \
  -s 'config."group.object.classes"=["group"]' \
  -s 'config."membership.ldap.attribute"=["member"]' \
  -s 'config."membership.attribute.type"=["DN"]' \
  -s 'config."membership.user.ldap.attribute"=["sAMAccountName"]' \
  -s 'config."mode"=["READ_ONLY"]' \
  -s 'config."user.roles.retrieve.strategy"=["LOAD_GROUPS_BY_MEMBER_ATTRIBUTE"]' \
  -s 'config."preserve.group.inheritance"=["true"]' \
  -s 'config."drop.non.existing.groups.during.sync"=["false"]'

Key options explained:

Role mapping strategies

After importing groups, there are two patterns for granting authorization.

  1. Assign realm/client roles to groups: map the client role app-admin to the Keycloak group "AD-App-Admins". AD group membership directly becomes application authorization. This is the most common and recommended pattern.
  2. Create roles directly with role-ldap-mapper: convert LDAP groups straight into Keycloak roles. Suitable for simple cases where you only need roles and no group tree, but mixing groups and roles becomes hard to manage — standardize on one pattern.

When putting group information into tokens, add the group membership protocol mapper to a client scope and agree with the applications on whether to use the full path (slash-delimited hierarchy) to avoid parsing incidents.

Password Policy Conflicts

In WRITABLE mode, the most frequent problem is password policy conflicts. Keycloak realms have a password policy, and AD has its own domain password policy (complexity, minimum length, history).

User → Keycloak password change form
        |
        | (1) Keycloak realm policy check — passes
        v
      LDAP modify (unicodePwd)
        |
        | (2) AD domain policy check — fails!
        v
      LDAPException: WILL_NOT_PERFORM (error code 53)

If Keycloak's policy passes but AD rejects, the user sees only an opaque error. Practical guidelines:

Also, if AD account lockout and Keycloak brute force detection both operate, the user experience becomes confusing. The clean division of labor is usually: "delegate lockout to AD, use Keycloak brute force detection as a supplementary alerting signal."

Performance Tuning for Large Directories

Things to consider when federating directories with 100,000+ users.

Pagination and search scope

./kcadm.sh update components/COMPONENT_ID -r myrealm \
  -s 'config.pagination=["true"]' \
  -s 'config.batchSizeForSync=["1000"]'

Connection pool and timeouts

./kcadm.sh update components/COMPONENT_ID -r myrealm \
  -s 'config.connectionPooling=["true"]' \
  -s 'config.connectionTimeout=["5000"]' \
  -s 'config.readTimeout=["10000"]'

Cache policies

User Storage providers support cache policies.

PolicyBehaviorBest for
DEFAULTUse the standard user cacheMost cases
EVICT_DAILYInvalidate at a fixed time dailyDirectories refreshed by nightly batches
EVICT_WEEKLYInvalidate weekly at a fixed day/timeRarely changing environments
MAX_LIFESPANInvalidate after a duration (ms)Clear freshness requirements
NO_CACHENo cachingDebugging, extreme freshness needs

Caching reduces LDAP lookups, but it is also the cause of "we disabled the account in AD but they can still log into Keycloak." In security-sensitive environments, set MAX_LIFESPAN short (e.g., 5 minutes) and tune while monitoring LDAP load. The cache can also be invalidated manually via the Admin REST API clear-user-cache endpoint.

Troubleshooting Sync Failures

Common failure patterns seen in production and how to respond.

First step in diagnosis — log levels

# Add debug logging for the LDAP category to the Keycloak start options
bin/kc.sh start \
  --log-level=INFO,org.keycloak.storage.ldap:DEBUG \
  --spi-connections-http-client-default-connection-pool-size=128

Checklist by symptom

SymptomLikely causeHow to verify
Intermittent login failuresConnection pool exhaustion, one DC downPer-LDAP-server response times, pool settings
Full sync stops at 1000 usersPagination disabledpagination setting, AD MaxPageSize
Duplicate users createduuidLDAPAttribute changed/misconfiguredVerify objectGUID mapping
Leavers can still log inOnly changed sync runs, cache remainsFull sync period, cache policy
Password change fails (code 53)Plaintext channel, AD policy violationLDAPS in use, domain policy
Sync never finishesFull scan of a huge OU, unindexed filterCustom filter, AD indexed attributes
TLS handshake failureCorporate CA missing from truststoreuseTruststoreSpi, truststore contents

Reproducing outside Keycloak with ldapsearch

The fastest way to determine whether the problem is Keycloak configuration or the directory itself is an ldapsearch with identical conditions.

# Manually reproduce the same search Keycloak performs
ldapsearch -H ldaps://ad01.corp.example.com:636 \
  -D "CN=svc-keycloak,OU=ServiceAccounts,DC=corp,DC=example,DC=com" \
  -W \
  -b "OU=Employees,DC=corp,DC=example,DC=com" \
  -s sub \
  -E pr=1000/noprompt \
  "(&(objectCategory=person)(sAMAccountName=jdoe))" \
  sAMAccountName mail userAccountControl whenChanged

If this command returns quickly, the problem is on the Keycloak side; if it is already slow here, the problem is in the directory or the network. Filtering on unindexed AD attributes causes full scans, so check with your AD administrators that the attributes used in filters (sAMAccountName, mail, etc.) are indexed.

High availability

You can specify multiple servers in connectionUrl, separated by spaces.

ldaps://ad01.corp.example.com:636 ldaps://ad02.corp.example.com:636

This is only simple failover, though. In practice it is more robust to place the directory behind DNS round-robin or an LDAP proxy (e.g., the DC locator of the domain DNS) and handle health checking at the infrastructure layer.

Hybrid Scenario — AD Employees + Social External Users

Real-world services often have the mixed requirement of "employees via AD, external partners or customers via social/email signup." In Keycloak you can combine the following within a single realm.

                      +---------------------------+
                      |       Realm: company      |
                      |                           |
  Employees --------->| User Federation (LDAP/AD) |
  (corp laptop,       |                           |
   Kerberos SSO)      | Identity Providers        |
  Partners ---------->|  - Google / GitHub        |
  Customers --------->|  - Apple / Kakao          |
                      |                           |
                      | Local users (self-reg)    |
                      +---------------------------+

Design points:

Migration Strategy — from LDAP to Keycloak Built-in Storage

LDAP federation is often a transition, not a destination. Here is a migration scenario for cutting the directory dependency and making Keycloak (and the database behind it) the source of truth.

Phased strategy

Phase 1            Phase 2              Phase 3              Phase 4
READ_ONLY        → switch to UNSYNCED → progressively      → remove LDAP
federation         (writes become       capture credentials  (detach the
(status quo)       independent)         (store local hash     federation link)
                                        at login)
  1. Phase 1 — stabilize with READ_ONLY: with import mode plus full/changed sync, ensure every user exists as a federated entry in the Keycloak local DB. Consolidate all application authentication onto Keycloak.
  2. Phase 2 — switch to UNSYNCED: profile changes start landing only in the local store. From this point, switch user lifecycle events (HR integration, etc.) to flow directly through the Keycloak Admin API (or a SCIM bridge).
  3. Phase 3 — capture credentials: the trickiest part. LDAP password hashes usually cannot be extracted (impossible with AD), so let Keycloak store a local hash (argon2 by default) of passwords that validate successfully at login. Leverage the UNSYNCED behavior where password changes are stored locally, and/or run a campaign after a set period requiring all users to reset passwords or register passkeys. Folding a "passkey enrollment campaign" into this phase achieves migration and passwordless transition in one move.
  4. Phase 4 — detach the link: once all users (or a threshold share) have local credentials, remove the LDAP provider. Removing the provider severs the federated links, but the imported user entries remain. Always rehearse the provider removal in a staging realm beforehand and verify user state (credential presence, required actions).

Migration checklist

Summary of Operational Best Practices

Closing

User Federation is among the "oldest" features of Keycloak, yet in enterprise practice it still decides whether an adoption succeeds. Once you precisely understand the two axes of edit mode and sync strategy, most design decisions become obvious; and if you take care of the three operational settings — pagination, cache, timeouts — it runs reliably even at large scale.

In the long run, treat LDAP federation as a transition rather than a permanent state, and draw a roadmap toward passkeys and modern user lifecycle management (SCIM, Workflows) alongside it. In the next article we will cover fine-grained authorization with Keycloak Authorization Services.

References

Comments

No comments yet.

Sign in to leave a comment