Skip to content

Attach an LDAP Domain Backend to Keystone ​

This guide walks through attaching an LDAP/AD-backed identity domain to a running Keystone with the KeystoneIdentityBackend CRD: create the bind credentials Secret, apply one CR, watch the conditions converge, and verify an LDAP user can authenticate. The worked example uses the same docker-test-openldap fixture the e2e suite deploys (seeded dc=planetexpress,dc=com tree), so every value is copy-pasteable against a kind cluster.

For the full field reference, see the KeystoneIdentityBackend CRD API Reference.

Prerequisites ​

Devstack

This guide is written against the Quick Start devstack. Stand it up first:

bash
KIND_HOST_PORT=8443 make deploy-infra

Follow that tutorial through to its final Verify step, so a Keystone CR named keystone is Ready in the openstack namespace. Every resource name in the examples below is one that devstack produces.

  • A Keystone CR that reaches Ready=True; backends can be applied before the Keystone exists, but nothing is provisioned until its API is up.
  • An LDAP server reachable from the cluster, plus a bind DN allowed to search the user/group trees. On the kind devstack, Step 1 stands up the seeded fixture directory; on other clusters, substitute your own directory.
  • Service users stay SQL-backed. The backend is read-only by default, so OpenStack service accounts (and the bootstrap admin) must remain in the SQL-backed Default domain — the CRD hard-rejects attaching a backend to Default for this reason. Plan for humans in the LDAP domain and services in Default.

Step 1 — Deploy the seeded OpenLDAP fixture (kind devstack) ​

On the kind devstack, stand up the same OpenLDAP directory the e2e suite uses. It is a plain namespace-pinned manifest, so kubectl apply runs it verbatim:

bash
kubectl apply -f tests/e2e/keystone/ldap-domain-backend/00-openldap.yaml
kubectl -n openstack rollout status deploy/openldap

This ships, all in the openstack namespace:

  • a seeded dc=planetexpress,dc=com tree (users under ou=people with objectClass: inetOrgPerson, uid/mail attributes, and userPassword equal to the uid);
  • a Service openldap on port 10389 (ldap://openldap.openstack.svc.cluster.local:10389);
  • a bind-credentials Secret openldap-bind holding cn=admin,dc=planetexpress,dc=com / GoodNewsEveryone.

On a non-kind cluster, skip this step and point the CR (Step 3) at your own directory instead — every value below is one this fixture produces.

Step 2 — Create the bind credentials Secret ​

The projection reads two fixed data keys: username (the bind DN) and password:

bash
kubectl create secret generic corp-ldap-bind -n openstack \
  --from-literal=username='cn=admin,dc=planetexpress,dc=com' \
  --from-literal=password='GoodNewsEveryone'

Rotating this Secret later re-renders the per-domain config automatically — the operator watches it.

On the kind devstack

Step 1's fixture already ships an openldap-bind Secret with these exact credentials (it is the one the e2e suite's own backend CR binds through), so you can skip this kubectl create and set bindCredentialsSecretRef.name: openldap-bind in Step 3 instead.

Step 3 — Apply the backend CR ​

yaml
apiVersion: keystone.openstack.c5c3.io/v1alpha1
kind: KeystoneIdentityBackend
metadata:
  name: corp-ldap
  namespace: openstack
spec:
  keystoneRef:
    name: keystone
  domain:
    name: planetexpress
    mode: Manage           # the operator creates the domain
    deletionPolicy: Retain # keep the domain when this CR is deleted
  type: LDAP
  ldap:
    url: ldap://openldap.openstack.svc.cluster.local:10389
    bindCredentialsSecretRef:
      name: corp-ldap-bind
    suffix: dc=planetexpress,dc=com
    readOnly: true         # the default, spelled out: keystone never writes to LDAP
    users:
      treeDN: ou=people,dc=planetexpress,dc=com
      objectClass: inetOrgPerson
      idAttribute: uid
      nameAttribute: uid
      mailAttribute: mail

Pick mode: Adopt instead when the domain already exists in Keystone — Adopt resolves it by name and never mutates it (adopted domains are also always retained on deletion).

For ldaps:// endpoints, add certificate verification:

yaml
    tls:
      caBundleSecretRef:
        name: corp-ldap-ca   # data key must be "ca.crt"

Step 4 — Watch the conditions converge ​

bash
kubectl get keystoneidentitybackends -n openstack
NAME        READY   DOMAIN          KEYSTONE   AGE
corp-ldap   True    planetexpress   keystone   1m

kubectl describe keystoneidentitybackend corp-ldap -n openstack shows the progression: DomainReady=True (the domain exists in Keystone), then ConfigProjected=True (the rendered keystone.planetexpress.conf is mounted in the Keystone Deployment at /etc/keystone/domains/), then Ready=True. The Keystone CR itself reports the aggregate across all attached backends via its IdentityBackendsReady condition; a pending backend holds the Keystone's Ready at False so a half-attached domain never goes unnoticed.

The figure shows that order for every satellite kind. Its gate, CredentialsReady, is DomainReady on an identity backend. The handshake lists the five steps and what differs per kind.

The handshake between a satellite resource and the service it attaches to, in five numbered steps across two controllers. 1: the satellite controller checks the credentials and sets CredentialsReady on the satellite. 2: the aggregation step of the service controller reads only that condition. 3: it renders one section per satellite that passed into a Secret whose name carries a hash of its content. 4: the pod template of the service's Deployment mounts that Secret, and a new name rolls the pods. 5: the satellite controller finds its section in the mounted Secret and sets ConfigProjected. Ready turns True once both conditions are. An arrow marked never runs from Ready to the aggregation step: reading Ready there would deadlock, because Ready needs ConfigProjected, which needs that step. On a KeystoneIdentityBackend the gate is DomainReady, and ConfigProjected also waits until the rollout has finished.

The domains Secret is content-hashed, so the attach rolls the Keystone Deployment once; subsequent reconciles are no-ops until the spec or the bind credentials change.

Step 5 — Verify an LDAP user can authenticate ​

bash
# List the LDAP-backed users through the Keystone API (admin credentials):
openstack user list --domain planetexpress

# Issue a token as an LDAP user (docker-test-openldap seeds password == uid):
openstack --os-username professor --os-password professor \
  --os-user-domain-name planetexpress \
  --os-domain-name planetexpress token issue

Without the openstack CLI, the same flow works with two curl/urllib calls against POST /v3/auth/tokens — see step 4 of tests/e2e/keystone/ldap-domain-backend/chainsaw-test.yaml for a copy-pasteable in-cluster variant.

Deleting a backend ​

kubectl delete keystoneidentitybackend corp-ldap de-projects the config first (keystone never runs with config pointing at a dead domain) and then applies spec.domain.deletionPolicy:

Mode / policyEffect on the Keystone domain
Manage + Retain (default)Domain stays, including its users' assignments.
Manage + DeleteDomain is disabled, then deleted.
Adopt (any policy)Domain always stays — the operator never destroys what it did not create.

deletionPolicy is mutable, so you can decide at teardown time.

Troubleshooting ​

SymptomLikely cause
DomainReady=False/WaitingForKeystoneAPIThe referenced Keystone is not serving yet — check its KeystoneAPIReady condition.
DomainReady=False/DomainAlreadyExistsA same-named domain exists that this CR did not create. The operator never seizes foreign domains; use mode: Adopt to attach to it.
DomainReady=False/DomainNotFoundmode: Adopt but no domain with that name exists — Adopt never creates.
IdentityBackendsReady=False/WaitingForBackends with IdentityBackendSkipped Warning eventsThe bind Secret is missing or lacks the username/password keys; healthy sibling backends keep working.
LDAP users not listedCheck the tree/attribute mapping against your directory (user_tree_dn, user_objectclass, user_id_attribute) — unset fields fall back to keystone's defaults, which may not match your schema.
GET /v3/users returns HTTP 400 'enabled' is a required propertyThe projected user_enabled_invert/user_enabled_default fallback was suppressed by a user_enabled_* key in extraOptions that does not match the directory — with no matching attribute, keystone omits enabled from the user model and its own response validation rejects the reply. Align the user_enabled_* options with the directory schema (or drop them to restore the fallback).
Admission rejects the CRThe message names the exact rule: Default-domain protection, domain-name uniqueness per Keystone, the extraOptions denylist, or the fixed Secret data-key contract.

Tested by ​

Attaching the LDAP domain, watching the conditions converge, and authenticating an LDAP user against the seeded dc=planetexpress,dc=com directory are asserted end-to-end on the CI e2e kind cluster by this chainsaw suite:

bash
chainsaw test --test-dir tests/e2e/keystone/ldap-domain-backend
The backend CR the suite applies

The suite isolates its Keystone instance from the parallel suite pool, so its backend CR name (planetexpress-ldap) and keystoneRef (keystone-ldap) differ from the corp-ldap / keystone names used in the walkthrough above.

yaml
apiVersion: keystone.openstack.c5c3.io/v1alpha1
kind: KeystoneIdentityBackend
metadata:
  name: planetexpress-ldap
  namespace: openstack
spec:
  keystoneRef:
    name: keystone-ldap
  domain:
    name: planetexpress
    mode: Manage
    deletionPolicy: Delete
    description: Planet Express corporate directory
  type: LDAP
  ldap:
    url: ldap://openldap.openstack.svc.cluster.local:10389
    bindCredentialsSecretRef:
      name: openldap-bind
    suffix: dc=planetexpress,dc=com
    readOnly: true
    users:
      treeDN: ou=people,dc=planetexpress,dc=com
      objectClass: inetOrgPerson
      idAttribute: uid
      nameAttribute: uid
      mailAttribute: mail