What this is about
In most companies, Active Directory is the source of truth for identities, while modern applications expect OpenID Connect or SAML. Keycloak bridges that gap: it federates the existing AD over LDAPS, becomes the central identity provider for every application and adds MFA on top, without touching AD itself.
It gets interesting when you onboard not three applications but three hundred. Then success no longer depends on the LDAP configuration, but on how teams get their OIDC clients quickly and safely, without opening a ticket with the IAM team for every single one. This guide covers both: the AD integration the way I build it for production, and a client factory through which teams manage their own clients. All paths refer to Keycloak 26.
Key takeaways
- One central realm with AD integration, MFA and a role model, built properly once and used by every application.
- Resilient: LDAPS against at least two domain controllers, with nested groups resolved by the DC itself.
- MFA without changing AD: OTP, security keys or passkeys, controlled through AD groups.
- Self-service instead of tickets: teams describe their OIDC clients in Git, and a pipeline creates them with Terraform.
- Guardrails in the realm: client policies enforce PKCE, allowed redirect domains and lean tokens, no matter who creates the client.
Architecture at a glance
- Applications talk only OIDC or SAML to Keycloak. LDAP and AD stay invisible to them.
- Keycloak authenticates, runs the authentication flows including MFA and issues tokens with the right claims.
- Active Directory remains the user directory. Keycloak verifies passwords with an LDAP bind directly against the DC and never imports them. An IGA system such as Saviynt, SailPoint or OpenText Identity Manager can own the identity lifecycle.
Connecting Active Directory: five decisions that matter
The LDAP provider is configured in an hour. Whether it runs reliably for years depends on these five points.
1. LDAPS against at least two domain controllers
User federation → Add LDAP providers
Vendor: Active Directory
Connection URL: ldaps://dc1.example.com:636 ldaps://dc2.example.com:636
Use Truststore SPI: Always
Connection pooling: On
Connection timeout: 3000
Bind DN: CN=svc-keycloak,OU=Service Accounts,DC=example,DC=com
Bind credentials: ${vault.ldap_credential}
- Separate DCs with spaces only. Keycloak tries them from left to right, and the
Connection timeoutapplies per unreachable DC. At 3000 ms, failover stays quick. - Truststore: put the AD root CA as PEM into
conf/truststoresor add it viaKC_TRUSTSTORE_PATHS. The FQDN in the connection URL must be in the SAN of the DC certificates. - Test first: whatever works with
ldapsearchfrom the Keycloak host works in Keycloak too.
LDAPTLS_CACERT=/etc/pki/ad-root-ca.pem ldapsearch -LLL -H ldaps://dc1.example.com:636 \
-D 'CN=svc-keycloak,OU=Service Accounts,DC=example,DC=com' -W \
-b 'OU=Users,DC=example,DC=com' '(sAMAccountName=jdoe)' mail memberOf
2. Vendor, edit mode and username before the first save
Vendor Active Directory is the most important click of the whole setup. It sets the AD defaults (objectGUID, object classes) and creates the matching mappers, above all MSAD account controls. Because the mappers are derived from vendor, edit mode and username attribute, these three values belong in the first save:
| Edit mode | When | Password changes |
|---|---|---|
READ_ONLY | An IGA or an existing self-service portal owns the lifecycle. | outside Keycloak |
WRITABLE | The Keycloak account console becomes the self-service. | over LDAPS straight to AD |
As the Username LDAP attribute, use sAMAccountName for logins like jdoe and userPrincipalName for email-style logins.
3. Cut the scope with an LDAP filter
Instead of importing half the directory, a filter fetches exactly the users who should have access, including nested groups:
(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=APP-Keycloak-Users,OU=Groups,DC=example,DC=com))
1.2.840.113556.1.4.1941 is LDAP_MATCHING_RULE_IN_CHAIN: the DC resolves transitive membership on the server side. Add Search scope: Subtree and Pagination: On, because AD returns at most 1000 entries per request.
4. Sync and cache
Periodic changed users sync: 3600 # hourly
Periodic full sync: 86400 # nightly
Cache policy: MAX_LIFESPAN
Max lifespan: 180000 # 3 minutes
The nightly full sync has a concrete reason: when a user is added to a group in AD, the group object changes, not the user object. Anyone who newly falls into the scope through the memberOf filter therefore appears with the full sync or at first login. A three-minute cache keeps the load on the DCs low while changes still show up within a predictable time.
5. Reliably lock disabled accounts
The MSAD account controls mapper translates userAccountControl into Keycloak behaviour: an account disabled in AD is disabled in Keycloak. With Always Read Enabled Value From LDAP: On, this also applies to logins without a password bind, such as passkeys. In WRITABLE, an expired AD password leads straight to the required action Update Password.
Groups and roles from AD
A group-ldap-mapper imports the relevant AD groups into /ad:
LDAP Groups DN: OU=Groups,DC=example,DC=com
LDAP Filter: (cn=APP-*)
Preserve Group Inheritance: Off
User Groups Retrieve Strategy: LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY
Groups Path: /ad
Mode: READ_ONLY
The recursive strategy resolves nested groups directly on the DC. Preserve Group Inheritance: Off keeps groups with multiple parents from breaking the sync, and the LDAP Filter keeps thousands of irrelevant groups out.
The result is a clean role model: the AD group APP-Admins appears as /ad/APP-Admins, and the role app-admin is attached to that group. Membership is maintained in AD and its existing approval processes, permissions in Keycloak. In the token, roles end up in realm_access.roles, and groups via a groups client scope with a group membership mapper:
{
"preferred_username": "jdoe",
"groups": ["APP-Admins", "APP-Users"],
"realm_access": { "roles": ["app-admin"] }
}
MFA, controlled through AD groups
This is where Keycloak in front of AD adds the most value: one MFA layer for every application, OIDC or SAML, without MFA infrastructure in AD.
Authentication → browser → Action → Duplicateasbrowser-mfa.- In the
formssubflow, add a subflowMFA enforcement(Conditional) withCondition - User Roleon the rolemfa-required,Condition - sub-flow executedon the 2FA subflow withnot-executed, and below themOTP Formas Required. The second condition keeps users who already have OTP from being asked twice. - Import the AD group
APP-MFA-Requiredwith the group mapper and bind it tomfa-required. Action → Bind flow → Browser flow.
The MFA rollout now follows AD group maintenance, department by department, without a single click in Keycloak. Add passkeys (Webauthn Register Passwordless) and recovery codes as a fallback in the same flow. Set brute force detection below the AD lockout threshold, so password spraying through the login page cannot lock AD accounts.
Client factory: teams manage their own OIDC clients
A central realm only pays off when new applications can connect quickly. The classic way is a ticket to the IAM team, manual setup in the admin console and a client secret sent over chat. That does not scale, and it is the opposite of secure.
The answer is a client factory. The realm with AD integration, MFA and role model is built once, centrally, and teams manage their OIDC clients in it themselves, through pull requests and within firm guardrails.
Teams in the admin console: their own clients only
With fine-grained admin permissions v2 (Keycloak 26.2 and later, Realm settings → Admin Permissions), team admins see and manage exactly their own clients in the admin console. This comes from the factory as well: the resource keycloak_openid_client_admin_permissions grants each team the scopes view and manage on its clients, bound to the team’s AD group.
The AD integration as code, too
What applies to the clients applies to the realm itself: the Terraform provider keycloak/keycloak describes the complete AD integration declaratively, and terraform plan shows drift immediately.
resource "keycloak_ldap_user_federation" "ad" {
name = "active-directory"
realm_id = data.keycloak_realm.corp.id
vendor = "AD"
edit_mode = "READ_ONLY"
import_enabled = true
connection_url = "ldaps://dc1.example.com:636 ldaps://dc2.example.com:636"
use_truststore_spi = "ALWAYS"
connection_timeout = "3s"
bind_dn = "CN=svc-keycloak,OU=Service Accounts,DC=example,DC=com"
bind_credential_wo = var.ad_bind_password
bind_credential_wo_version = 1
users_dn = "OU=Users,DC=example,DC=com"
username_ldap_attribute = "sAMAccountName"
uuid_ldap_attribute = "objectGUID"
user_object_classes = ["person", "organizationalPerson", "user"]
custom_user_search_filter = "(&(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=CN=APP-Keycloak-Users,OU=Groups,DC=example,DC=com))"
search_scope = "SUBTREE"
pagination = true
full_sync_period = 86400
changed_sync_period = 3600
cache {
policy = "MAX_LIFESPAN"
max_lifespan = "3m"
}
}
bind_credential_wo is a write-only argument (Terraform 1.11 and later): the bind password never ends up in the state. vendor = "AD" creates the same default mappers as the UI, including MSAD account controls.
Troubleshooting: the five most common errors
| Symptom | Cause | Fix |
|---|---|---|
PKIX path building failed | AD CA missing from the truststore | CA into conf/truststores or KC_TRUSTSTORE_PATHS, restart |
No subject alternative DNS name matching … | connection URL uses an IP or alias | use the FQDN from the DC certificate |
error code 49 … data 52e | wrong bind DN or password | verify with ldapsearch |
| sync returns at most 1000 users | Pagination is off | Pagination: On |
| nested groups missing | non-recursive strategy in the group mapper | LOAD_GROUPS_BY_MEMBER_ATTRIBUTE_RECURSIVELY |
For deeper analysis, --log-level="INFO,org.keycloak.storage.ldap:trace" logs every LDAP query.
Production checklist
- LDAPS with at least two DCs, AD CA in the truststore, bind credential in the vault
- Vendor, edit mode and username attribute set in the first save
- User scope via LDAP filter,
Pagination: On, hourly and nightly sync - Recursive group mapper with filter and
Groups Path, roles bound to groups - MFA flow bound, recovery codes enabled, brute force below the AD lockout threshold
- Client factory with pull request review, guardrails via client policies and secrets straight into the secret store
- Complete configuration in Git via Terraform, plus a local emergency admin independent of AD
Conclusion
Keycloak in front of Active Directory quickly delivers what AD alone never will: OIDC and SAML for every application, MFA without changes to AD and a role model built on existing AD processes. The client factory turns that into a platform: teams connect in minutes, the IAM team keeps the guardrails, and every change is traceable in Git.
Planning a Keycloak rollout, or want to move an existing identity platform to self-service? Get in touch: david@picillo.de