It is time to play with Keystone to Keystone Federation in Kilo!

After the debut of the feature in the Juno release, Keystone-to-Keystone (K2K) federation has received lots of improvements and bug fixes during the Kilo cycle. Here at the Distributed Systems Lab - UFCG we worked together with contributors from several companies and organizations like CERN, IBM, Rackspace, Yahoo, and HP in order to bring these advances in the Kilo release.

So... It is time to test everything again!

The first major change is the addition of the Service Provider API (/v3/OS-FEDERATION/service_providers). In Juno, the Keystone Service Provider (SP) was treated as a Region with an URL field in the Keystone Identity Provider (IdP). This URL was used to store the endpoint to send a SAML assertion to the SP. In Kilo, we dropped the URL field from the Region table and added the Service Provider object. This SP object contains the following attributes:

  • sp_url: represents the SP URL that will receive the SAML assertion generated by the Keystone IdP
  • auth_url: the URL which the Keystone IdP will request the unscoped token after the Keystone SP has accepted the SAML assertion
  • relay_state_prefix: the tag configured in the SP that handles the Relay State. By default, it has the ss:mem: value, which is the one used by Shibboleth (if you haven't changed it).

We also added the support to generate ECP wrapped SAML assertions (GET /v3/auth/OS-FEDERATION/saml2/ecp). In the Juno release we needed to manually perform this wrapping after retrieve the "pure" SAML assertion from Keystone. The support to retrieve such type of assertions is already supported in python-keystoneclient as well.

If you are not familiar with the SAML protocol and/or its profiles, you can find some useful information in Wikipedia.

Finally, in the deploy made in this tutorial we used two Ubuntu 14.04 LTS virtual machines running Devstack (stable/kilo release). In order to identify the addresses of the Keystone IdP and Keystone SP the following aliases are used: keystone.idp and keystone.sp. Additionally, some steps taken in the Juno version of the tutorial haven't changed, but they will be repeated here. Although you can deploy Keystone with Eventlet, by default it uses Apache HTTP Server - which is required for the federation feature.

Keysonte as an IdP

We no longer need to enable the Federation extension - it is alreay part of the core Keystone features, but we may need to install the xmlsec1 and pysaml2 packages:

sudo apt-get install xmlsec1  
sudo pip install pysaml2  

1. Add the capabilities to sign/generate SAML assertions

The configurations should be added within the [saml] area of the keystone.conf file. The mandatory ones are:

[saml]
certfile=/etc/keystone/ssl/certs/ca.pem  
keyfile=/etc/keystone/ssl/private/cakey.pem  
idp_entity_id=http://keystone.idp/v3/OS-FEDERATION/saml2/idp  
idp_sso_endpoint=http://keystone.idp/v3/OS-FEDERATION/saml2/sso  
idp_metadata_path=/etc/keystone/keystone_idp_metadata.xml  

Also, there are some optional configurations regarding the organization that can be skipped, but it's advised to provide them:

idp_organization_name=rodrigods  
idp_organization_display_name=rodrigods  
idp_organization_url=rodrigods.com  
idp_contact_company=rodrigods  
idp_contact_name=Rodrigo  
idp_contact_surname=Duarte  
idp_contact_email=rodrigodsousa@gmail.com  
idp_contact_telephone=555-55-5555  
idp_contact_type=technical  

2. Generate the IdP metadata

To be able to perform federated requests, we need to generate the IdP metadata via the keystone-manage CLI and provide it to the SP. Details on how to do the second part will be shown at the Keystone as an SP section.

keystone-manage saml_idp_metadata > /etc/keystone/keystone_idp_metadata.xml  

The output file should be the one pointed at the idp_metadata_path config.

3. Restart Keystone server

Once everything is in place, we can restart the Keystone server:

sudo service apache2 restart  

Keystone as a SP

In order to provide federation support as a Service Provider, Keystone consumes SAML assertions issued by external Identity Providers (which will be another Keystone in the case of K2K federation). This is done via a third party SP software, in this guide we will be using Shibboleth (mod_shib).

Enable saml2 authentication method

In order to Keystone accept federated identities, we need to enable the authentication method in the keystone.conf file. Since K2K federation only work with SAML2 we add the saml2 entry under the [auth] session:

[auth]
methods = external,password,token,oauth1,saml2

saml2 = keystone.auth.plugins.mapped.Mapped  

Mapped is the plugin responsible for federated authentication.

Install and configure Shibboleth SP

First we need to install the Shibboleth package:

sudo apt-get install libapache2-mod-shib2  

Now we need to configure the Keystone virtual host to properly handle SAML2 workflow. The file where these configurations can be found is /etc/apache2/sites-available/keystone.conf.

First, we add WSGIScriptAliasMatch ^(/v3/OS-FEDERATION/identity_providers/.*?/protocols/.*?/auth)$ /var/www/keystone/main/$1 under the <VirtualHost *:5000> section
(the port 5000 is being used, otherwise it should be under <VirtualHost *:35357>, or both):

<VirtualHost *:5000>  
    WSGIScriptAliasMatch ^(/v3/OS-FEDERATION/identity_providers/.*?/protocols/.*?/auth)$ /var/www/keystone/main/$1

    ...

We also need to append the following lines to the end of the file:

<Location /Shibboleth.sso>  
    SetHandler shib
</Location>

<LocationMatch /v3/OS-FEDERATION/identity_providers/.*?/protocols/saml2/auth>  
    ShibRequestSetting requireSession 1
    AuthType shibboleth
    ShibExportAssertion Off
    Require valid-user
</LocationMatch>  

Now we edit the Attribute Map file (/etc/shibboleth/attribute-map.xml) to add the attributes used by Keystone IdP:

<Attribute name="openstack_user" id="openstack_user"/>  
<Attribute name="openstack_roles" id="openstack_roles"/>  
<Attribute name="openstack_project" id="openstack_project"/>  

If you want support for the two new attributes, you also need to add the following lines at the Attribute Map file:

<Attribute name="openstack_user_domain" id="openstack_user_domain"/>  
<Attribute name="openstack_project_domain" id="openstack_project_domain"/>  

Then, we edit the /etc/shibboleth/shibboleth2.xml file to add the Keystone IdP entityID and MetadataProvider:

<SSO entityID="https://keystone.idp/v3/OS-FEDERATION/saml2/idp">  
    SAML2 SAML1
</SSO>

<MetadataProvider type="XML" uri="https://keystone.idp/v3/OS-FEDERATION/saml2/metadata"/>  

Finally, we generate Shibboleth's key-pair and restart Apache:

sudo shib-keygen  
sudo service apache2 restart  
  • Note: to check if Shibboleth's module (shib2) is enabled we can run the following command:
sudo a2enmod shib2  

Make Keystone IdP and SP know each other

Now the fun part starts. First we setup the SP to accept SAML assertions generated by the Keystone IdP we are using. Than we do the equivalent set up in the Keystone IdP side.

The following examples are using the python-keystoneclient. To execute most of the actions needed here it's necessary to have an admin scoped token. For a regular Devstack setup, the following method should be enough to build a client for this admin user:

import os

from keystoneclient import session as ksc_session  
from keystoneclient.auth.identity import v3  
from keystoneclient.v3 import client as keystone_v3

try:  
    # Used for creating the ADMIN user
    OS_PASSWORD = os.environ['OS_PASSWORD']
    OS_USERNAME = os.environ['OS_USERNAME']
    # This will vary according to the entity:
    # the IdP or the SP
    OS_AUTH_URL = os.environ['OS_AUTH_URL']
    OS_PROJECT_NAME = os.environ['OS_PROJECT_NAME']
    OS_DOMAIN_NAME = os.environ['OS_DOMAIN_NAME']
except KeyError as e:  
    raise SystemExit('%s environment variable not set.' % e)

def client_for_admin_user():  
    auth = v3.Password(auth_url=OS_AUTH_URL,
                       username=OS_USERNAME,
                       password=OS_PASSWORD,
                       user_domain_name=OS_DOMAIN_NAME,
                       project_name=OS_PROJECT_NAME,
                       project_domain_name=OS_DOMAIN_NAME)
    session = ksc_session.Session(auth=auth)
    return keystone_v3.Client(session=session)

# Used to execute all admin actions
client = client_for_admin_user()  
  • Note: the scripts in this session should be executed in the correct virtual machine: the first step in the Keystone SP Devstack and the second step in the Keystone IdP Devstack. Also, many thanks to Iury Gregory (iurygregory at irc.freenode.net) for helping testing and writing all the scripts used in this guide.

1. Setting up the Keystone IdP in the Keystone SP

  • Note: we have lots of new features in the process of recognizing entities and attributes from an IdP in the SP side. In this setup we will keep them very basic, but we address this new features in the end of this guide in the Other updates to Federation session.

When we have a federated user accessing a Keystone SP, this user is mapped into local entities. In this tutorial we will map the users coming from the Keystone IdP into a specific domain and group:

def create_domain(client, name):  
    try:
         d = client.domains.create(name=name)
    except:
         d = client.domains.find(name=name)
    return d

def create_group(client, name, domain):  
    try:
         g = client.groups.create(name=name, domain=domain)
    except:
         g = client.groups.find(name=name)
    return g

def create_role(client, name):  
    try:
        r = client.roles.create(name=name)
    except:
        r = client.roles.find(name=name)
    return r

print('\nCreating domain1')  
domain1 = create_domain(client, 'domain1')

print('\nCreating group1')  
group1 = create_group(client, 'group1', domain1)

print('\nCreating role Member')  
role1 = create_role(client, 'Member')

print('\nGrant role Member to group1 in domain1')  
client.roles.grant(role1, group=group1, domain=domain1, os_inherit_extension_inherited=True)

print('\nList group1 role assignments')  
client.role_assignments.list(group=group1)  

Once we have created the necessary entities, we can register the mapping rules. In this example, we are going to map the openstack_user attribute from the SAML assertion to group1. To create a mapping we need to provide a mapping_id and a list containing the mapping rules.

def create_mapping(client, mapping_id, rules):  
    try:
        m = client.federation.mappings.create(
            mapping_id=mapping_id, rules=rules)
    except:
        m = client.federation.mappings.find(
            mapping_id=mapping_id)
    return m

print('\nCreating mapping')  
rules = [  
{
    "local": [
        {
            "user": {
                "name": "federated_user"
            },
            "group": {
                "id": group1.id
            }
        }
    ],
    "remote": [
        {
            "type": "openstack_user",
            "any_one_of": [
                "user1",
                "admin"
            ]
        }
    ]
}
]

mapping1 = create_mapping(client, mapping_id='keystone-idp-mapping', rules=rules)  

Now we can create the IdP and link it with the mapping created above using a protocol. We are going to call the IdP keystone-idp and protocol saml2. A new feature of the Kilo release is the possibility to add the remote_ids fields for the IdP. This field is used to limit the access to assertions generated only by the registered remote ids, you can find more information about it in this great email written by Nathan Kinder (nkinder on IRC).

After everything is registered, it's important to store the IdP and protocol ids because they are going to be part of the SP auth_url field.

def create_idp(client, id, remote_id):  
    idp_ref = {'id': id,
               'remote_ids': [remote_id],
               'enabled': True}
    try:
        i = client.federation.identity_providers.create(**idp_ref)
    except:
        i = client.federation.identity_providers.find(id=id)
    return i

def create_protocol(client, protocol_id, idp, mapping):  
    try:
        p = client.federation.protocols.create(protocol_id=protocol_id,
                                               identity_provider=idp,
                                               mapping=mapping)
    except:
        p = client.federation.protocols.find(protocol_id=protocol_id)
    return p


print('\nRegister keystone-idp')  
idp1 = create_idp(client, id='keystone-idp',  
                  remote_id='https://keystone.idp/v3/OS-FEDERATION/saml2/idp')

print('\nRegister protocol')  
protocol1 = create_protocol(client, protocol_id='saml2', idp=idp1,  
                            mapping=mapping1)

2. Setting up the Keystone SP in the Keystone IdP

In order to setup the Keystone SP, we need to create a SP object. In our example, we are going to use the following values:

  • sp_url: http://keystone.sp/Shibboleth.sso/SAML2/ECP
    • This URL can be found in the SP's metadata. In K2K federation, the Enhanced Client or Proxy (ECP) profile is used. This profile is then one used to execute SAML without browsers (via command line).
  • auth_url: http://keystone.sp/v3/OS-FEDERATION/identity_providers/keystone-idp/protocols/saml2/auth
    • This is the protocol URL in the Keystone SP created in the previous session plus the auth field.

Now we can create a SP in the Keystone IdP using keystone.sp as id:

def create_sp(client, sp_id, sp_url, auth_url):  
        sp_ref = {'id': sp_id,
                  'sp_url': sp_url,
                  'auth_url': auth_url,
                  'enabled': True}
        return client.federation.service_providers.create(**sp_ref)

print('\nCreate SP')  
create_sp(client,  
          'keystone.sp',
          'http://keystone.sp/Shibboleth.sso/SAML2/ECP',
          'http://keystone.sp/v3/OS-FEDERATION/identity_providers/'
          'keystone-idp/protocols/saml2/auth')

Get a unscoped token from the SP using a SAML assertion generated by the Keystone IdP

Now we have everything configured and should be able to request an unscoped token from the Keystone SP using an assertion generated by the Keystone IdP.

We implemented a K2KClient class to encapsulate all the steps taken in this process:

import json  
import os

from keystoneclient import session as ksc_session  
from keystoneclient.auth.identity import v3  
from keystoneclient.v3 import client as keystone_v3


class K2KClient(object):  
    def __init__(self):
        self.sp_id = os.environ.get('OS_SP_ID')
        self.token_id = os.environ.get('OS_TOKEN')
        self.auth_url = os.environ.get('OS_AUTH_URL')
        self.project_id = os.environ.get('OS_PROJECT_ID')
        self.username = os.environ.get('OS_USERNAME')
        self.password = os.environ.get('OS_PASSWORD')
        self.domain_id = os.environ.get('OS_DOMAIN_ID')

    def v3_authenticate(self):
        auth = v3.Password(auth_url=self.auth_url,
                           username=self.username,
                           password=self.password,
                           user_domain_id=self.domain_id,
                           project_id=self.project_id)
        self.session = ksc_session.Session(auth=auth, verify=False)
        self.session.auth.get_auth_ref(self.session)
        self.token = self.session.auth.get_token(self.session)

    def _generate_token_json(self):
        return {
            "auth": {
                "identity": {
                    "methods": [
                        "token"
                    ],
                    "token": {
                        "id": self.token
                    }
                },
                "scope": {
                    "service_provider": {
                        "id": self.sp_id
                    }
                }
            }
        }

    def _check_response(self, response):
        if not response.ok:
            raise Exception("Something went wrong, %s" % response.__dict__)

    def get_saml2_ecp_assertion(self):
        token = json.dumps(self._generate_token_json())
        url = self.auth_url + '/auth/OS-FEDERATION/saml2/ecp'
        r = self.session.post(url=url, data=token, verify=False)
        self._check_response(r)
        self.assertion = str(r.text)

    def _get_sp(self):
        url = self.auth_url + '/OS-FEDERATION/service_providers/' + self.sp_id
        r = self.session.get(url=url, verify=False)
        self._check_response(r)
        sp = json.loads(r.text)[u'service_provider']
        return sp

    def _handle_http_302_ecp_redirect(self, response, location, **kwargs):
        return self.session.get(location, authenticated=False, **kwargs)

    def exchange_assertion(self):
        """Send assertion to a Keystone SP and get token."""
        sp = self._get_sp()

        r = self.session.post(
            sp[u'sp_url'],
            headers={'Content-Type': 'application/vnd.paos+xml'},
            data=self.assertion,
            authenticated=False,
            redirect=False)

        self._check_response(r)

        r = self._handle_http_302_ecp_redirect(r, sp[u'auth_url'],
                                               headers={'Content-Type':
                                               'application/vnd.paos+xml'})
        self.fed_token_id = r.headers['X-Subject-Token']
        self.fed_token = r.text


def main():  
    client = K2KClient()
    client.v3_authenticate()
    client.get_saml2_ecp_assertion()
    print('ECP wrapped SAML assertion: %s' % client.assertion)
    client.exchange_assertion()
    print('Unscoped token id: %s' % client.fed_token_id)


if __name__ == "__main__":  
    main()

Scope the unscoped token

Finally, in order to use resources from the SP cloud, we need to scope the unscoped token acquired in the previous step. We can list the projects we have access using the GET /v3/OS-FEDERATION/projects call:

    def list_federated_projects(self):
        url = 'https://keystone.sp/v3/OS-FEDERATION/projects'
        headers = {'X-Auth-Token': self.fed_token_id}
        r = self.session.get(url=url, headers=headers, verify=False)
        self._check_response(r)
        return json.loads(str(r.text))

Now we use one of the IDs listed above and request the scoped token:

    def _get_scoped_token_json(self, project_id):
        return {
            "auth": {
                "identity": {
                    "methods": [
                        "token"
                    ],
                    "token": {
                        "id": self.fed_token_id
                    }
                },
                "scope": {
                    "project": {
                        "id": project_id
                    }
                }
            }
        }

    def scope_token(self):
        # project_id can be select from the list in the previous step
        token = json.dumps(self._get_scoped_token_json({project_id}))
        url = 'https://keystone.sp/v3/auth/tokens'
        headers = {'X-Auth-Token': self.fed_token_id,
                   'Content-Type': 'application/json'}
        r = self.session.post(url=url, headers=headers, data=token,
                              verify=False)
        self._check_response(r)
        self.scoped_token_id = r.headers['X-Subject-Token']
        self.scoped_token = str(r.text)

Other updates to Federation

Besides the advances in the K2K feature, there were several improvements in the overall Identity Federation feature of Keystone:

  • Added the support to use SAML WebSSO profile, also with supported by Horizon. This means that now it is possible to use a OpenStack cloud via browser using an identity from a third party IdP.
  • It is possible to map federatated users to local entities. With this change, ephemeral users are mapped to a federated domain and it is possible to map to existing users.
  • Now, groups can be specified by their names and domains in the local part of mapping rules.
  • Added the possibility to map various groups to local ones using the blacklist and whitelist rules.
  • Support of the OpenID Connect protocol as a federated identity authentication mechanism. You can find a tutorial on how to setup a Keystone SP to work with such protocol here. Unfortunatelly, it is not possible to use K2K federation with it, since the Keystone IdP currently understands only SAML.
Do you want to know more about Federation in OpenStack? Come and join us in the OpenStack Summit in Vancouver! We will be giving a presentation to highlight some use cases and the improvements achieved in the Kilo release.
comments powered by Disqus