Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

LDAP Identity Backend

The LDAP identity backend (crates/identity-driver-ldap) is a read-only IdentityBackend implementation that reads users and groups directly from an LDAP directory (OpenLDAP, FreeIPA, Active Directory, etc.). Its [ldap] config section is field-for-field compatible with Python Keystone’s keystone.conf.ldap options, so a config file already in use with Python Keystone works against keystone-rs largely unmodified — enabling a config-only driver = sqldriver = ldap switch during a rolling upgrade. See ADR-0027 for the full design rationale and the Python-compatibility findings from its implementation review.

Enabling the backend

[identity]
driver = ldap
default_domain_id = default

[ldap]
url = ldaps://ldap.example.com
user = cn=service,dc=example,dc=com
password = servicepassword
suffix = dc=example,dc=com
user_tree_dn = ou=Users,dc=example,dc=com
group_tree_dn = ou=Groups,dc=example,dc=com

[identity] driver is a single, deployment-wide setting — there is no per-domain driver selection yet, so a keystone-rs deployment runs either fully against LDAP or fully against SQL, not a mix. All LDAP users and groups are exposed under the single default_domain_id domain, since the LDAP directory itself has no concept of Keystone domains.

LdapBackend::new() performs one real bind against url/user/password at startup and fails fast if it cannot reach the directory — a misconfigured [ldap] section prevents the server from starting rather than surfacing as runtime errors on the first request.

Read-only

Every mutating IdentityBackend method (create_user, update_user, delete_user, create_group, add_user_to_group, update_user_password, etc.) returns IdentityProviderError::Readonly, mapped to an HTTP 403. Manage users and groups directly in the directory (or via whatever tool your organization already uses for that) — Keystone only reads from it.

A few capabilities that don’t exist in an LDAP directory at all return IdentityProviderError::NotImplemented (mapped to 500) rather than Readonly, since there’s no LDAP concept to even be read-only about: create_service_account, find_federated_user, and the expiring-group- membership methods. get_service_account returns Ok(None) unconditionally.

Configuration reference

Connection

OptionDefaultNotes
urlldap://localhostOne or more server URLs, comma/whitespace-separated (matches Python’s re.split(r'[\s,]+', ...)). Each candidate is tried in turn on connect/bind failure.
userunsetService bind DN, used for all read queries.
passwordunsetService bind password.
use_tlsfalseUse StartTLS on the plain ldap:// connection. Use ldaps:// in url for implicit TLS instead.
tls_cacertfile / tls_cacertdirunsetParsed for config compatibility but not applied — see Known limitations.
tls_req_certdemanddemand | allow | try | never.
connection_timeout-1 (no timeout)Seconds.
randomize_urlsfalseShuffle the candidate list from url before trying servers (matches Python’s random.shuffle).
pool / pool_size / pool_retry_max / pool_retry_delay / pool_connection_timeout / pool_connection_lifetimetrue / 10 / 3 / 0.1 / -1 / 600Service (read) connection pool.
auth_pool / auth_pool_size / auth_pool_connection_lifetimetrue / 100 / 60Dedicated pool for end-user password-auth binds, isolated from the service pool so a burst of failed logins can’t starve directory reads.

Query

OptionDefaultNotes
query_scopeoneone (single-level under the tree DN) or sub (whole subtree). Governs the scope of every read search — get, get_by_name, and list — not just id/DN resolution.
page_size0 (disabled)Entries per page (RFC 2696) for list_users/list_groups.
alias_dereferencingdefaultParsed but not applied — see Known limitations.
chase_referralsunset
debug_level0

User mapping

OptionDefault
user_tree_dn"" (must be set)
user_objectclassinetOrgPerson
user_id_attributecn
user_name_attributesn
user_mail_attributemail
user_description_attributedescription
user_pass_attributeuserPassword
user_enabled_attributeenabled
user_enabled_maskunset
user_enabled_invertfalse
user_enabled_default"True"
user_additional_attribute_mapping{}
user_filterunset
user_attribute_ignore{default_project_id}
user_enabled_emulationfalse
user_enabled_emulation_dnunset
user_enabled_emulation_use_group_configfalse

user_id_attribute falls back to the entry’s DN (with a logged warning) when the configured attribute is absent or multi-valued on a given entry, rather than failing the whole request.

Enabled-attribute strategies

user_enabled_attribute is interpreted by one of four strategies, evaluated in the same precedence order as Python Keystone:

  1. Group-membership emulation (user_enabled_emulation = true): a user is enabled iff they’re a member of user_enabled_emulation_dn.
  2. Bitmask (user_enabled_mask set): enabled = (raw_value & mask) != mask — the user is enabled unless all masked bits are set. This is the Active Directory userAccountControl convention, where mask = 2 corresponds to the ACCOUNTDISABLE bit. user_enabled_invert is ignored entirely once a mask is configured, matching Python.
  3. Invert (user_enabled_invert = true, no mask): the raw boolean value is negated.
  4. Plain boolean: the raw attribute value is parsed directly.

When user_enabled_attribute is absent from an entry, the backend falls back to parsing user_enabled_default, which is why that option is a string rather than a boolean — under the bitmask strategy it’s expected to hold an integer literal (e.g. "512") instead of "True"/"False".

Group mapping

OptionDefault
group_tree_dn"" (must be set)
group_objectclassgroupOfNames
group_id_attributecn
group_name_attributeou
group_desc_attributedescription
group_member_attributemember
group_members_are_idsfalse
group_attribute_ignore{}
group_additional_attribute_mapping{}
group_filterunset
group_ad_nestingfalse

group_members_are_ids supports directories where group_member_attribute holds Keystone user IDs directly instead of member DNs — most commonly posixGroup’s memberUid, which stores POSIX usernames rather than distinguished names. Leave it false for the default groupOfNames + member (DN-valued) convention.

group_ad_nesting resolves nested Active Directory group membership using the LDAP_MATCHING_RULE_IN_CHAIN (1.2.840.113556.1.4.1941) matching rule in list_groups_of_user.

General

OptionDefault
suffixcn=example,cn=com

Known limitations

  • tls_cacertfile / tls_cacertdir are parsed but not applied. The ldap3 crate has no ready-made hook for a custom CA trust root short of manually constructing a rustls::ClientConfig; TLS verification always uses the platform/rustls default trust store instead. A startup warning is logged when either option is set. Work around this by installing the directory’s CA into the system trust store, or by setting tls_req_cert = never for a test/lab directory.
  • alias_dereferencing is parsed but not applied. The ldap3 crate (v0.11) has no per-search or per-connection alias-dereferencing control; every search uses the underlying client library’s default behavior regardless of this setting.
  • No per-domain driver selection. [identity] driver is global; running some domains against LDAP and others against SQL simultaneously isn’t supported yet.

Testing

Beyond crate-level unit tests (cargo test -p openstack-keystone-identity-driver-ldap), the crate ships a live_tests module that exercises the driver’s read and authentication paths against a real OpenLDAP (slapd) instance — real network round trips, real schema validation, real bind/search behavior, not mocks.

Run it via the dedicated nextest profile:

cargo nextest run -p openstack-keystone-identity-driver-ldap --profile ldap

This uses a nextest setup script (tools/start-ldap-test.sh) to start a throwaway local slapd on port 3890, seed it from crates/identity-driver-ldap/tests/fixtures/base.ldif, and export KEYSTONE_LDAP_TEST_URL/_BASE_DN/_ADMIN_DN/_ADMIN_PW for the test binary to pick up. tools/teardown-ldap-test.sh stops it afterwards (nextest does not run teardown scripts automatically, so run it by hand after a manual cargo nextest run --profile ldap).

Under any other profile, live_tests’ functions skip themselves gracefully (printing a skip message) rather than failing, since KEYSTONE_LDAP_TEST_URL is only set by the ldap/ci-ldap profiles — plain cargo test for the crate never requires a live directory.

The fixture directory (base.ldif) seeds users and groups covering the enabled/disabled cases, groupOfNames membership, and a posixGroup for exercising group_members_are_ids; see the file itself for the full seed data.