Openstack Keystone LDAP Redux

A recent change in the structure of the Openstack Keystone architecture resulted in the loss of support for an LDAP Backend. I’ve been working to rectify that.  Here’s my set up and the design decisions I’ve made so far.  Since this code is not yet submitted for code review,  there is a good chance that it will change prior to deployment.

Users will be stored in a flat collection. ou=Users,$SUBTREE and be based on the standard LDAP objectClass inetOrgPerson which is defined in /etc/openldap/schema/inetorgperson.ldif. Currently, only two fields are used: cn and sn. cn is used for the bind call, and is the id field in the user object.

Tenants are in a collection that is a peer to Users. Tenants are instancs of the groupOfNames object class defined in /etc/openldap/schema/core.ldif. Tenant membership is indicated by the presence of the User’s DN in the tenant’s members attribute.

Roles are instances of the LDAP object class organizationalRole defined in /etc/openldap/schema/core.ldif. Role assignment is indicated by the presence of the User’s DN in the roleOccupant attribute.

Configuration of LDAP for the Keystone server is provided by the [LDAP] stanza in the appropriate keystone.conf file. Here are the supported values

  • url
  • user
  • password
  • suffix
  • use_dumb_member
  • user_tree_dn
  • tenant_tree_dn
  • role_tree_dn

And an example of what my config file looks like:

[ldap]
url = ldap://localhost
tree_dn = dc=younglogic,dc=com
user_tree_dn = ou=Users,dc=younglogic,dc=com
role_tree_dn = ou=Roles,dc=younglogic,dc=com
tenant_tree_dn = ou=Groups,dc=younglogic,dc=com
user = dc=Manager,dc=younglogic,dc=com
password = freeipa4all
backend_entities = ['Tenant', 'User', 'UserRoleAssociation', 'Role']
suffix =cn=younglogic,cn=com

[identity]
driver = keystone.identity.backends.ldap.Identity

Not all of these fields need to be specified. It is expected that the user will supply simply the suffix field, and not override the values of user_tree_dn,role_tree_dn, or tenant_tree_dn.

backend_entities is not currently honored. It is expected that LDAP will instead either manage all of these or non e of them, with token management handled by a different backend provider.

use_dumb_member is still honored from the previous incarnation, but has not been tested, nor do I understand the intention of this code.

The unit tests for the LDAP code use a common code sournce with the other Identity management backends. To run just the LDAP unit tests, from the Keystone directory, run

 python ./run_tests.py  test_backend_ldap

Additionally, the unit tests can be run against a live OpenLDAP server by running.

 python ./run_tests.py  _ldap_livetest

All tests pass successfully on my development machine as of this posting.

 

I’m running Fedora 16, which supports OpenLDAP. Specifically I am running openldap-servers-2.4.26-5.fc16.x86_64. To start the service, run

sudo service slapd start

To configure the server, I use a file I call manager.ldif:

dn:  olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcSuffix
olcSuffix: dc=younglogic,dc=com
-
replace: olcRootDN
olcRootDN: dc=Manager,dc=younglogic,dc=com
-
add: olcRootPW
olcRootPW: {SSHA}lBDIdfwvZkITal0k9tdhiCUolxpf6anu

You should modify the suffix for your organization.
Execute the configuration with:

 sudo ldapmodify -Y EXTERNAL -H ldapi:///  -f ./manager.ldif

And test that you can now do a simple bind to the localhost server.

ldapsearch -x -D "dc=Manager,dc=younglogic,dc=com" -H ldap://localhost  -w freeipa4all  -b ou=Groups,dc=younglogic,dc=com "(objectClass=*)"

Now set up the subtree for Keystone. I use file I call org.ldif

dn: dc=younglogic,dc=com
dc: younglogic
objectClass: dcObject
objectClass: organizationalUnit
ou: younglogic

dn: ou=Groups,dc=younglogic,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups

dn: ou=Users,dc=younglogic,dc=com
objectClass: top
objectClass: organizationalUnit
ou: users

dn: ou=Roles,dc=younglogic,dc=com
objectClass: top
objectClass: organizationalUnit
ou: users

Technically, the Roles ou is not required. My original thought was that this collection would contain the superset of roles possible for all of the Tenants. However, I have not implemented that.

Current code is commited to Github I will update this link if I rebase the branch.

Update: fixed typos in the config file segment. user_tree_dn etc should start with ou, not cn.

25 thoughts on “Openstack Keystone LDAP Redux

  1. Great post Adam, I love the way you’re explaining what you’rer doing in the open and asking for feedback! That is totally what we need in the Keystone project, and Im really glad your contributing with us.

    The backend entities seems like a slight hold-over from the older code base, where the new baseline is oriented around internal APIs on four key services: identity, catalog, token, and policy. What you’re doing looks to match beautifully to an LDAP backend for the “identity” service internal to keystone (“service” used here is meant to indicate an internal, loadable module that is defined in the keystone.conf file)

    I like the structure for the rest, and while I havent had a chance to read through all your code on github, Im looking forward to doing so.

    – joe

  2. Joe,

    Yes, I pulled the LDAP code over from the prior incarnation of Keystone intact. My goal was to first make it work, and then to refactor it into a simpler mechanism, more aligned with the SQL Backend. However, the SQL Code has the benefit of SQL Alchemy to build on, where-as there is no comparable library for LDAP, so we will have to write more marshalling code for the LDAP side regardless. If we can make the LDAP marshalling elegant enough, we might be able to extract it in to Openstack-common or a stand alone project like LDAP Alchemy….

  3. Adam
    Can you add a section on how to add users and groups? What would a user or group ldif look like? Thanks.

  4. askstack a trivial implementation would be something along the lines of what I have below. It depends on your LDAP set up for what fields are required and so on in the real world.

    dn: cn=foo,ou=Users,dc=younglogic,dc=com
    objectclass: person
    objectclass: inetOrgPerson
    objectclass: top
    objectclass: organizationalPerson
    cn: Fozzie Bear
    sn: Fozzie
    uid: fbear
    userpassword: freeipa4all

    dn: cn=kfrog,ou=users,dc=younglogic,dc=com
    objectclass: person
    objectclass: inetOrgPerson
    objectclass: top
    objectclass: organizationalPerson
    cn: Kermit Frog
    sn: Kermit
    uid: kfrog
    userpassword: freeipa4all

  5. Hi ! are you saying that if i try to use ldap backend today with the keystone shipped with ubuntu 12.04 is not gonna work ?

  6. It will work. The LDAP code shipped with Ubuntu 12.04 is 2012.1-0ubuntu2.1, which is the same as Fedora/EPEL openstack-nova-2012.1-4.el6. This is the Essex release.

  7. This all looks pretty good, except the initial query seems to be on sn instead of cn, and I dont see a way to configure that. Subsequent queries search on cn. So if your last name doesn’t match your username, I keep running into failures. From the logs (sorry in advance for the formatting):

    2012-05-08 20:26:10 DEBUG [keystone.common.wsgi] arg_dict: {}
    2012-05-08 20:26:10 DEBUG [keystone.common.ldap.core] LDAP init: url=ldap://ldap.blah.com
    2012-05-08 20:26:10 DEBUG [keystone.common.ldap.core] LDAP bind: dn=cn=manager,dc=blah,dc=com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP search: dn=ou=People,dc=blah,dc=com, scope=1, query=(&(sn=me)(objectClass=inetOrgPerson))
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP init: url=ldap://ldap.blah.com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP bind: dn=cn=manager,dc=blah,dc=com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP search: dn=cn=me,ou=People,dc=blah,dc=com, scope=0, query=(objectClass=inetOrgPerson)
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP init: url=ldap://ldap.blah.com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP bind: dn=cn=me,ou=People,dc=blah,dc=com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP init: url=ldap://ldap.blah.com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP bind: dn=cn=manager,dc=blah,dc=com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP search: dn=cn=blah,cn=com,ou=Groups, scope=1, query=(&(member=cn=me,ou=People,dc=blah,dc=com)(objectClass=groupOfNames))
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP init: url=ldap://ldap.blah.com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP bind: dn=cn=manager,dc=blah,dc=com
    2012-05-08 20:26:11 DEBUG [keystone.common.ldap.core] LDAP search: dn=cn=None,cn=blah,cn=com,ou=Groups, scope=0, query=(objectClass=groupOfNames)

    The “query=(&(sn=me)” is the problematic section.

    I would think that https://github.com/openstack/keystone/blob/master/keystone/identity/backends/ldap/core.py#L251 should have ‘name : cn’, as this is talking about username, not surname.

    Thanks for your work on this Adam – it’s a highly necessary feature.

  8. Hmm, I know it works….BUt the sn field is defined in keystone/identity/backend/ldap/core.py line 254: the attribute mapping is name to sn.

    In my ldif I have both
    cn: Admin
    sn: Admin

    So I might have had a false sense of security. You can change that to be ‘name’: ‘cn’ and see if it fixes things for you. Please log a bug with as much info and as detailed a description as you can make it.

  9. Yeah, that’s not a good general example testcase 🙂 . If I change my surname to match my username, then indeed everything works, and conversely, changing the code to use cn is fine as well, but the fragility of that solution is obviously not ideal. All other ldap implementations I use have this as an option, which just needs to be done here as well.

    https://bugs.launchpad.net/keystone/+bug/997700

  10. OK…Looking through the LDAP Schema’s it turns out that the only two fields that are MUST fields are cn and sn. UID is only a must for a Posix account. So That is why cn for ID and sn for name. Looks like the right solution is to leave that as default, but to provide an override in the config.

  11. We are in the need of using the UID and not SN and CN. Thus, it would be important to have a configuration parameter.

    Why do we need that? Our users do not just have access to OpenStack but also to traditional HPC resources that use a Posix account. This way we have the same account name between the systems.

  12. I’ve attached code code to the ticket, allthough I am not ready to submit it for review:

    While I have run it through the unit tests, I have not tested it against a live LDAP server. I will do so shortly, but need to set it up.

    IN the [LDAP] section of the config, you should be able to specify

    user_id_attribute = uid
    user_name_attribute = cn

    Or some other variation. Please let me know if this meets your needs.

    https://launchpadlibrarian.net/105463166/keystone-admiyo-0005-configuration-option-for-user-name.patch

    Original ticket it

    https://bugs.launchpad.net/keystone/+bug/997700

  13. There seems to be in the current essex code some issue with documentation in regards to the supported LDAP integration features. Looking at

    https://github.com/openstack/keystone/blob/stable/essex/keystone/common/ldap/core.py

    It looks like that the values are specified in conf.ldap. Most interestingly is also the
    %s_id_attribute which probably solves our issue (but we have not yet tried it).

    One problem we had is that we first overlooked this as we die not find it in the documentation . We actually looked into the code to find this. In general, it would be great if the keystone team could spend some time towards improving the documentation on LDAP and its support within Essex and upcoming versions so that looking at the code becomes less needed.

    Here the code we find interesting, especially %s_id_attribute:

    class BaseLdap(object):
    DEFAULT_SUFFIX = “dc=example,dc=com”
    DEFAULT_OU = None
    DEFAULT_STRUCTURAL_CLASSES = None
    DEFAULT_ID_ATTR = ‘cn’
    DEFAULT_OBJECTCLASS = None
    DUMB_MEMBER_DN = ‘cn=dumb,dc=nonexistent’

    def __init__(self, conf):

    dn = ‘%s_tree_dn’ % self.options_name
    self.tree_dn = (getattr(conf.ldap, dn)
    or ‘%s,%s’ % (self.suffix, self.DEFAULT_OU))

    idatt = ‘%s_id_attribute’ % self.options_name

  14. Hi Adam;

    What is backend_entities and suffix stand for. Where to find the
    run_tests.py script ?

  15. backend_entities is the domain objects like user and groups. Suffix is what is now subtree. These are vestiges of the LDAP code from before, and no longer are used. You can safely ignore them.

  16. Hi Adam;

    I was trying to configure ldap + keystone but it seems not working. I feel like authentication is successful but horizon return me python error. Im unable to trace as its does not give any detail. In following link I have attached the error, ldap dump, keystone config. I would really appreciate if you can note me down any configuration error.

    If its wrong ldap password it returns, “Invalid user name or password.” When type correct credentials but user not in the any of “ou=Groups,dc=example,dc=com” it return “You are not authorized for any projects.” When type correct credentials and the user is a member of a group (eg: cn=demo,ou=Groups,dc=example,dc=com), It returns the error.

    Please check follow link for error, ldap dump and keystone config, [ its not allowed to copy as a comment ]

    http://openstack9.blogspot.com/2012/12/error.html

    Thanks inadvance.

  17. I want to use keystone with a legacy LDAP directory where I dont have the luxury of adding any new objects to the schema. eg it doesnt have the concept of Tenants or Roles and I cant add them.
    Is it possible to configure keystone to work with such a directory just for the Users & Groups piece, and then pick up the Roles and Tenants information from some other augmenting data source?
    Sorry if this is a basic question answered elsewhere. Would be great to see a list somewhere of the pre-requisites that an LDAP directory must be able to meet in order to be compatible with keystone.

  18. We’re getting there, but we are not there yet. Your bet bet right now is to set up an intermediate LDAP server that can serve as a backend to Keystone, and that can talk to your corporate LDAP server on a synchronization schedule that you control. You still need a local store for Keystone, as you are going to have service users, groups, Tenants, and roles, and you will not be able to store those in the centralized LDAP. Might I suggest FreeIPA and a cross domain trust?

  19. Hmm. Not sure about the synchronization approach as with such a large directory as we have, it could introduce unacceptable risks around dirty data.
    When you say you are working on it, but not there yet, what solution are you working towards? Something that was able to leave the authentication to LDAP and manage the authorization and other Keystone specifics in the local datastore would sound like a reasonable approach, although there would still be issues of distributed user & rights administration to worry about.

  20. Andy,
    In Grizzly, we have the concept of authentication plugins. Right now, we only have two, one for token and and one for userid password that checks in the indentity backend. However, a UID/PW plugin that checks against LDAP is certainly possible, but not written yet. They are stackable, so you will be able to deploy the LDAP plugin before (or after) the Identity backend plugin to handle both remote auth against LDAP as well as local services.

  21. Adam,

    Have you ever had the chance to touch the Keystone Grizzly version? In fact, today, I just set up Keystone Grizzly and want to migrate my Folsom implementation to Grizzly version. My backend identity service is OpenLDAP(stores User, Tenant, Role information). After configuring the keystone.conf file for Grizzly and issue the command get http://146.89.7.107:35357/v3/users, I am returned back with a list of user with info ->

    ########
    {
    “users”: [
    {
    “password”: “secrete”,
    “id”: “nsoadmin-cn-1”,
    “links”: {
    “self”: “http://localhost:5000/v3/users/nsoadmin-cn-1”
    },
    “name”: “nsoadmin-1”
    },
    {
    “password”: “secrete”,
    “id”: “nsoadmin-cn-2”,
    “links”: {
    “self”: “http://localhost:5000/v3/users/nsoadmin-cn-2”
    },
    “name”: “nsoadmin-2”
    },
    ….
    ########

    However, from the official doc, it should be returned with more information such like ->

    ########
    [
    {
    “default_project_id”: “–default-project-id–“,
    “description”: “a user”,
    “domain_id”: “1789d1”,
    “email”: “…”,
    “enabled”: true,
    “id”: “–user-id–“,
    “links”: {
    “self”: “http://identity:35357/v3/users/–user-id–”
    },
    “name”: “admin”
    },
    {
    “default_project_id”: “–default-project-id–“,
    “description”: “another user”,
    “domain_id”: “1789d1”,
    “email”: “…”,
    “enabled”: true,
    “id”: “–user-id–“,
    “links”: {
    “self”: “http://identity:35357/v3/users/–user-id–”
    },
    “name”: “someone”
    }
    ]
    ########

    ,which is with more information such like domain, project(tenant) association with the user defined in OpenLDAP.

    Also, when I issue the command wanting to get a scoped token from the user ID/PW/Tenant info by the legacy V2.0 API version ->
    ########
    curl -X POST -d ‘{“auth”:{“tenantName”: “admin-tenant”, “passwordCredentials”:{“username”: “nsoadmin-1”, “password”: “secrete”}}}’ -H “Content-type: application/json” http://146.89.7.95:5000/v2.0/tokens | python -m json.tool
    ########

    Then I faced with the error message->
    “message”: “An unexpected error prevented the server from fulfilling your request. ‘domain_id'”,
    “title”: “Internal Server Error”

    Not sure if you can give me an insight here?

    Thank You.

  22. If I have only one level in LDAP without ou=roles and ou=users
    how can I set the keystone.conf

    the LDAP information is like this:
    basedn:dc=example,dc=com
    version:3
    adminuser:uid=icemonitor,ou=people,dc=example,dc=com
    adminpassword:123456

  23. I think you will want to use the latest code on Keystone. We’ve split the Identity backend such that you don’t store role or project data in there. Thus, if LDAP only has users and groups, it is much easier to now map it to Keystone. The rest of the data goes into SQL.

  24. can you give me a sample only use group and users, store the rest information into sql. In identity section ,there seems only one driver parameter

    [identity]
    driver = keystone.identity.backends.sql.Identity

  25. Hi Adam,

    I used to follow your ldap sample data but it seems get me error on glance and in nova for example. The logs says /var/log/keystone.log when I issue command nova image-list, glance image-list for example.

    DEBUG dogpile.core.dogpile [-] Released creation lock _enter_create /usr/lib/python2.6/site-packages/dogpile/core/dogpile.py:154
    8573 WARNING keystone.common.wsgi [-] Authorization failed. Invalid user / password from 192.168.1.1
    INFO access [-] 192.168.217.195 – – [31/Mar/2014:04:04:11 +0000] “POST http://192.168.217.195:35357/v2.0/tokens HTTP/1.0″ 401 87

    I put in the /etc/glance/glance-api.conf, glance-registry.conf, and glance-registry-paste.ini
    auth_host=192.168.1.1
    auth_port=35357
    auth_protocol=http
    admin_tenant_name=service
    admin_user=glance
    admin_password=passwd_in_ldap

    While admin_user=nova (for specific nova service) for /etc/nova/nova.conf. This is my ldap structure. see this link https://www.dropbox.com/s/322y64b8ph5px0v/ldapStructure.PNG and for the list of members of service https://www.dropbox.com/s/5wqbrqq5gynop12/ServicesMember.jpg

    I can use mysql without any problems but not on ldap.

    It seems my ldap working as I can issue the command
    ldapsearch -LLxWD cn=admin,dc=ldap,dc=example,dc=net objectClass=*

    Hope you can give hints of this problems.

Leave a Reply

Your email address will not be published. Required fields are marked *