After I setup the keystone federation feature, it’s nice to go with the examples in official docs. However, I want to see more from it. :) First thing is that how do the rules to map federation protocol attributes to Identity API objects and how does SP manage the mapping users.

What’s Mapping

A mapping is a set of rules to map federation protocol attributes to Identity API objects. An Identity Provider can have a single mapping specified per protocol. A mapping is simply a list of rules.

As a simple example, if keystone is your IdP, you can map a few known remote users to the group you already created:

$ cat > rules.json <<EOF
[
{
"local": [
{
"user": {
"name": "{0}"
},
"group": {
"domain": {
"name": "Default"
},
"name": "federated_users"
}
}
],
"remote": [
{
"type": "openstack_user",
"any_one_of": [
"demo",
"alt_demo"
]
}
]
}
]
EOF
$ openstack mapping create --rules rules.json myidp_mapping

Value Setting in Mapping rules

A rule hierarchy looks as follows:

{
"rules": [
{
"local": [
{
"<user> or <group>"
}
],
"remote": [
{
"<condition>"
}
]
}
]
}
  • rules: top-level list of rules.
  • local: a rule containing information on what local attributes will be mapped.
  • remote: a rule containing information on what remote attributes will be mapped.
  • condition: contains information on conditions that allow a rule, can only be set in a remote rule.

Note: You can not set value arbitrary in remote rule. All the value must follow federation protocol attributes and the key should be type.

What’s Federation Protocol Attributes

Federation protocol attributes is the assertion sent by IdP. Normally You can see it in SP logs, and you can find openstack_user inside. It’s why we have to have “type”: “openstack_user” in the rule. Please refer to this if you want to have other values, such as SERVER_NAME, SERVER_PORT, etc.

2016-12-30 08:45:48.234 13923 DEBUG keystone.federation.utils [req-2d12dd32-a563-409d-92b3-f84d20c817c4 - - - - -] assertion: {'AUTH_TYPE': [u'shibboleth'], 'mod_wsgi.listener_port': [u'5000'], 'HTTP_COOKIE': [u'_shibsession_64656661756c74687474703a2f2f3137322e31362e34302e3131322f73686962626f6c657468=_d88c6c214f5deb51aa78e4a3e0062d75'], 'CONTEXT_DOCUMENT_ROOT': [u'/var/www'], 'SERVER_SOFTWARE': [u'Apache/2.4.7 (Ubuntu)'], 'SCRIPT_NAME': [u'/v3'], 'mod_wsgi.enable_sendfile': [u'0'], 'mod_wsgi.handler_script': [u''], 'SERVER_SIGNATURE': [u'<address>Apache/2.4.7 (Ubuntu) Server at 172.16.40.112 Port 5000</address>\\n'], 'REQUEST_METHOD': [u'GET'], 'PATH_INFO': [u'/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth'], 'SERVER_PROTOCOL': [u'HTTP/1.1'], 'QUERY_STRING': [u''], 'openstack_user': [u'mike'], 'HTTP_USER_AGENT': [u'python-keystoneclient'], 'HTTP_CONNECTION': [u'keep-alive'], 'SERVER_NAME': [u'172.16.40.112'], 'REMOTE_PORT': [u'45344'], 'mod_wsgi.queue_start': [u'1483087544870780'], 'Shib-AuthnContext-Class': [u'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'], 'mod_wsgi.request_handler': [u'wsgi-script'], 'wsgi.url_scheme': [u'http'], 'Shib-Authentication-Method': [u'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'], 'openstack_user_domain': [u'Default'], 'PATH_TRANSLATED': [u'/usr/local/bin/keystone-wsgi-public/v3/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth'], 'SERVER_PORT': [u'5000'], 'mod_wsgi.input_chunked': [u'0'], 'openstack_roles': [u'admin'], 'SERVER_ADDR': [u'172.16.40.112'], 'DOCUMENT_ROOT': [u'/var/www'], 'mod_wsgi.process_group': [u'keystone-public'], 'Shib-Authentication-Instant': [u'2016-12-30T08:45:44Z'], 'SCRIPT_FILENAME': [u'/usr/local/bin/keystone-wsgi-public'], 'openstack_project_domain': [u'Default'], 'SERVER_ADMIN': [u'[no address given]'], 'REMOTE_USER': [u''], 'HTTP_HOST': [u'172.16.40.112:5000'], 'CONTEXT_PREFIX': [u''], 'mod_wsgi.callable_object': [u'application'], 'Shib-Session-Index': [u'd7371f4d5d3547c3b61560c80fc0bc05'], 'REQUEST_URI': [u'/v3/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth'], 'HTTP_ACCEPT': [u'*/*'], 'openstack.request_id': [u'req-2d12dd32-a563-409d-92b3-f84d20c817c4'], 'Shib-Application-ID': [u'default'], 'GATEWAY_INTERFACE': [u'CGI/1.1'], 'REMOTE_ADDR': [u'172.16.40.115'], 'mod_wsgi.listener_host': [u''], 'REQUEST_SCHEME': [u'http'], 'Shib-Identity-Provider': [u'http://172.16.40.115/v3/OS-FEDERATION/saml2/idp'], 'openstack_project': [u'demo'], 'CONTENT_TYPE': [u'application/vnd.paos+xml'], 'mod_wsgi.application_group': [u''], 'Shib-Session-ID': [u'_d88c6c214f5deb51aa78e4a3e0062d75'], 'mod_wsgi.script_reloading': [u'1'], 'HTTP_ACCEPT_ENCODING': [u'gzip, deflate']} process /opt/stack/keystone/keystone/federation/utils.py:489

How does keystone process mappings?

The main entry is from ‘keystone/federation/core.py’ and ‘keystone/federation/utils.py’ finishes the jobs. Take a look at the process function of RuleProcessor class, _verify_all_requirements function and _update_local_mapping function in utils.py.

why we have to use ‘type’? keystone will use ‘type’ as the key to get the value from assertion. If it’s None, it will cause a final failed in _transform function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
def _verify_all_requirements(self, requirements, assertion):
    """Compare remote requirements of a rule against the assertion.

    If a value of ``None`` is returned, the rule with this assertion
    doesn't apply.
    If an array of zero length is returned, then there are no direct
    mappings to be performed, but the rule is valid.
    Otherwise, then it will first attempt to filter the values according
    to blacklist or whitelist rules and finally return the values in
    order, to be directly mapped.

    :param requirements: list of remote requirements from rules
    :type requirements: list

    Example requirements::

        [
            {
                "type": "UserName"
            },
            {
                "type": "orgPersonType",
                "any_one_of": [
                    "Customer"
                ]
            },
            {
                "type": "ADFS_GROUPS",
                "whitelist": [
                    "g1", "g2", "g3", "g4"
                ]
            }
        ]

    :param assertion: dict of attributes from an IdP
    :type assertion: dict

    Example assertion::

        {
            'UserName': ['testacct'],
            'LastName': ['Account'],
            'orgPersonType': ['Tester'],
            'Email': ['testacct@example.com'],
            'FirstName': ['Test'],
            'ADFS_GROUPS': ['g1', 'g2']
        }

    :returns: identity values used to update local
    :rtype: keystone.federation.utils.DirectMaps or None

    """
    direct_maps = DirectMaps()

    for requirement in requirements:
        requirement_type = requirement['type']
        direct_map_values = assertion.get(requirement_type)
        regex = requirement.get('regex', False)

        if not direct_map_values:
            return None

        any_one_values = requirement.get(self._EvalType.ANY_ONE_OF)
        if any_one_values is not None:
            if self._evaluate_requirement(any_one_values,
                                          direct_map_values,
                                          self._EvalType.ANY_ONE_OF,
                                          regex):
                continue
            else:
                return None

        not_any_values = requirement.get(self._EvalType.NOT_ANY_OF)
        if not_any_values is not None:
            if self._evaluate_requirement(not_any_values,
                                          direct_map_values,
                                          self._EvalType.NOT_ANY_OF,
                                          regex):
                continue
            else:
                return None

        # If 'any_one_of' or 'not_any_of' are not found, then values are
        # within 'type'. Attempt to find that 'type' within the assertion,
        # and filter these values if 'whitelist' or 'blacklist' is set.
        blacklisted_values = requirement.get(self._EvalType.BLACKLIST)
        whitelisted_values = requirement.get(self._EvalType.WHITELIST)

        # If a blacklist or whitelist is used, we want to map to the
        # whole list instead of just its values separately.
        if blacklisted_values is not None:
            direct_map_values = [v for v in direct_map_values
                                 if v not in blacklisted_values]
        elif whitelisted_values is not None:
            direct_map_values = [v for v in direct_map_values
                                 if v in whitelisted_values]

        direct_maps.add(direct_map_values)

        LOG.debug('updating a direct mapping: %s', direct_map_values)

    return direct_maps

Reference

Environment

In the previous post, I elaborated how to setup keystone to keystone federation with two devstacks. In this post, I’ll use the similar environment except change the token from fernet to uuid for simplicity.

1
2
3
4
5
+-------------------+     +------------------+
|                   |     |                  |
| IdP:172.16.40.113 |     | SP:172.16.40.114 |
|                   |     |                  |
+-------------------+     +------------------+

Demo step

  1. Get an unscoped token.1
  2. Scope the unscoped token with specific domain and project name.
  3. Use the scoped token2 to achieve user list in SP.

Get an unscoped token.

You have to use python client to get a unscoped token right now.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ cat v3rc
export OS_IDENTITY_API_VERSION=3
export OS_AUTH_URL=http://172.16.40.113:35357/v3
export OS_PROJECT_DOMAIN_ID=default
export OS_USER_DOMAIN_ID=default
export OS_DEFAULT_DOMAIN=default
export OS_USERNAME=admin
export OS_PASSWORD=password
export OS_PROJECT_NAME=admin
export OS_PROJECT_ID=48614c49a38b4dfa919ab8fe3db7e702

$ source v3rc
$ python get-unscope-token.py

...
...
...

Unscoped token id: 114350fd00434c1b83838c65464aa7c8
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):
# os sp id need set manually
#self.sp_id = os.environ.get('OS_SP_ID')
self.sp_id = 'mysp'
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')
self.domain = os.environ.get('OS_DOMAIN')


def v3_authenticate(self):
auth = v3.Password(auth_url=self.auth_url,
username=self.username,
password=self.password,
user_domain_id='default',
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
#"id": "23fd45092e434d529bc7bb5fa9bdb711"
}
},
"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):
""" Exchange a scoped token for an ECP assertion. """
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, session, response, location, method, **kwargs):
#return session.get(location, authenticated=False, data=self.assertion, **kwargs)
return session.get(location, authenticated=False, **kwargs)
#return session.request(location, method, authenticated=False,
# **kwargs)


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

# import pdb
# pdb.set_trace()

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

#r = self._handle_http_302_ecp_redirect(r, sp[u'auth_url'],
# headers={'Content-Type':
# 'application/vnd.paos+xml'})
r = self._handle_http_302_ecp_redirect(self.session, response, sp[u'auth_url'],
method='GET',
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 with specific domain and project name.

$ curl -X POST -H "Content-Type: application/json" -d '{"auth":{"identity":{"methods":["token"],"token":{"id":"114350fd00434c1b83838c65464aa7c8"}},"scope":{"project":{"domain": {"name": "Default"},"name":"admin"}}}}' -D - http://172.16.40.114:5000/v3/auth/tokens

And you’ll get something that looks like this:

HTTP/1.1 201 Created
Date: Tue, 20 Dec 2016 10:20:14 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Subject-Token: 5f8e00d777754986a63dbab3431aa867
Vary: X-Auth-Token
x-openstack-request-id: req-cb7a71ec-4527-44d5-9ded-876b9166e951
Content-Length: 5659
Content-Type: application/json

{
"token": {
"audit_ids": [
"5YphXnNoQj64-aaXBvDxcg"
],
"catalog": [
{
"endpoints": [
{
"id": "0610fd8c59614b48a1984ea32ddaf534",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114/identity"
},
{
"id": "7c6ac5589b12416e894266246379a403",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114/identity"
},
{
"id": "943df2eaacee4062b0849294099fdde8",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114/identity_admin"
}
],
"id": "463f1970c326420997bc16b0e9631eb2",
"name": "keystone",
"type": "identity"
},
{
"endpoints": [
{
"id": "0535350a500c4d47b0414293ea7af1b7",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v1/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "660c6fba4d4f47bbacad890d5a87fde0",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v1/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "7123c5f93d0b4691aaae599bfa990c44",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v1/df704be2e4d344519b674c6621b42ed3"
}
],
"id": "481215d6de764c1c993633545c9ad0b7",
"name": "cinder",
"type": "volume"
},
{
"endpoints": [
{
"id": "5cc44f18848b49808662ae491829502c",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9696/"
},
{
"id": "5f0e5b329e93492cbfa7a268ee4cee63",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9696/"
},
{
"id": "dab46ac4d6084ec0a945c29c7a3761de",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9696/"
}
],
"id": "558555396b0447a7b005c43ad56988f6",
"name": "neutron",
"type": "network"
},
{
"endpoints": [
{
"id": "005b6eca401b4e8386e733d66db8abf3",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9292"
},
{
"id": "ad723da65ca747a49b34f73ebffda192",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9292"
},
{
"id": "f93a6d6820ff4b1ebda8f4a0d0cd0cd9",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:9292"
}
],
"id": "599201c7e5aa4feeab5cee3f0232f728",
"name": "glance",
"type": "image"
},
{
"endpoints": [
{
"id": "8504d494189d42659b8810f036a99ed5",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "8eaa24f979a84d0788b54d2eb45ee22c",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "b99163261e564521ae9de32381c6f1dd",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2/df704be2e4d344519b674c6621b42ed3"
}
],
"id": "6118a293edca4afd90df8a1ad034745f",
"name": "nova_legacy",
"type": "compute_legacy"
},
{
"endpoints": [
{
"id": "166ebfdd8f7648ac87ea77696d0ec2a8",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v2/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "3ddc632d3783491a99524170d0f9f4a5",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v2/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "eb5432a5d8a74845a1e387189c43e81c",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v2/df704be2e4d344519b674c6621b42ed3"
}
],
"id": "7473883c655f4f7c8f2154e76329bcb0",
"name": "cinderv2",
"type": "volumev2"
},
{
"endpoints": [
{
"id": "6c8eaf362f9344388a1acd09c5852b36",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v3/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "85811b68a145489695857735c7681c49",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v3/df704be2e4d344519b674c6621b42ed3"
},
{
"id": "b8a087d574f74f88a19a3f939082e286",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8776/v3/df704be2e4d344519b674c6621b42ed3"
}
],
"id": "87a91a84d7c84c6aa0d156cbc1597e16",
"name": "cinderv3",
"type": "volumev3"
},
{
"endpoints": [
{
"id": "308a0278d62e4b14bd873416b18957d2",
"interface": "internal",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2.1"
},
{
"id": "6ae89f4a60f94b6ba9616a39d748fabd",
"interface": "admin",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2.1"
},
{
"id": "be0d82d66d374497bca3474f23593926",
"interface": "public",
"region": "RegionOne",
"region_id": "RegionOne",
"url": "http://172.16.40.114:8774/v2.1"
}
],
"id": "8bfdba065fae4298868453592b7d171f",
"name": "nova",
"type": "compute"
}
],
"expires_at": "2016-12-20T11:20:18.000000Z",
"is_domain": false,
"issued_at": "2016-12-20T10:20:18.000000Z",
"methods": [
"token"
],
"project": {
"domain": {
"id": "default",
"name": "Default"
},
"id": "df704be2e4d344519b674c6621b42ed3",
"name": "admin"
},
"roles": [
{
"domain_id": null,
"id": "0c2aa16148a64bc4b918f7b16afa8bf5",
"name": "admin"
},
{
"domain_id": null,
"id": "0c2aa16148a64bc4b918f7b16afa8bf5",
"name": "admin"
}
],
"user": {
"OS-FEDERATION": {
"groups": [
{
"id": "0c68765dc5764a30b673f32370e01983"
}
],
"identity_provider": {
"id": "myidp"
},
"protocol": {
"id": "saml2"
}
},
"domain": {
"id": "Federated",
"name": "Federated"
},
"id": "1933c49872d144bca5d9ce5d86cebae8",
"name": "admin"
}
}
}

You can copy the scoped token from X-Subject-Token at the response header and use it in the next step.

Use the scoped token to achieve user list in SP.

Use the scoped token to interact with SP endpoints.

$ curl -g  -X GET http://172.16.40.114:5000/v3/users -H "Accept: application/json" -H "X-Auth-Token: 5f8e00d777754986a63dbab3431aa867" | python -m json.tool

{
"links": {
"next": null,
"previous": null,
"self": "http://172.16.40.114/identity/v3/users"
},
"users": [
{
"domain_id": "default",
"email": "alt_demo@example.com",
"enabled": true,
"id": "08e3136348284f218f841742bc12b7fd",
"links": {
"self": "http://172.16.40.114/identity/v3/users/08e3136348284f218f841742bc12b7fd"
},
"name": "alt_demo",
"password_expires_at": null
},
{
"domain_id": null,
"enabled": true,
"id": "1933c49872d144bca5d9ce5d86cebae8",
"links": {
"self": "http://172.16.40.114/identity/v3/users/1933c49872d144bca5d9ce5d86cebae8"
},
"name": "admin",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "1df12ac7f6114779b6b68c4be30ae162",
"links": {
"self": "http://172.16.40.114/identity/v3/users/1df12ac7f6114779b6b68c4be30ae162"
},
"name": "neutron",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "9ca3f32b99c24eb6933c95afd13ff140",
"links": {
"self": "http://172.16.40.114/identity/v3/users/9ca3f32b99c24eb6933c95afd13ff140"
},
"name": "glance",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "a04f9ee0b24d4dceb695f2bd38e8cd7d",
"links": {
"self": "http://172.16.40.114/identity/v3/users/a04f9ee0b24d4dceb695f2bd38e8cd7d"
},
"name": "admin",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "ad826ed2897f45b8858f953612a9588e",
"links": {
"self": "http://172.16.40.114/identity/v3/users/ad826ed2897f45b8858f953612a9588e"
},
"name": "user_in_sp",
"password_expires_at": null
},
{
"domain_id": "default",
"email": "demo@example.com",
"enabled": true,
"id": "b39ec9e44b784c13aae015c7f49baccd",
"links": {
"self": "http://172.16.40.114/identity/v3/users/b39ec9e44b784c13aae015c7f49baccd"
},
"name": "demo",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "e063a425d0c341b98ce449dd84f08a26",
"links": {
"self": "http://172.16.40.114/identity/v3/users/e063a425d0c341b98ce449dd84f08a26"
},
"name": "nova",
"password_expires_at": null
},
{
"domain_id": "default",
"enabled": true,
"id": "ec7df3dc3fe14b78bf2351994eca1a54",
"links": {
"self": "http://172.16.40.114/identity/v3/users/ec7df3dc3fe14b78bf2351994eca1a54"
},
"name": "cinder",
"password_expires_at": null
}
]
}

Reference

Companies are on longer asking if they should move to the cloud. They’re asking how to migrate to hybrid cloud with OpenStack. A hybrid cloud can offer a company the best of both public and private cloud, meet the requirements such as Cloud bursting, Disaster Recovery, Lifecycle-based Deployment, etc. But not if you slip into one of these pitfalls.

  • Visibility: how to manage clouds with a single pane?
  • Access Control: how to handle different credentials with security manner?
  • Network Connectivity: how to manage network configurations and maintain connectivity across clouds?
  • Outage & DR: how to ensure hybrid applications stay up during one cloud outages?
  • Audit & Compliance: how to collect audit data and comply with regulations?

Some OpenStack projects/features like tacker, tricircle, freezer, federated identity, sso, etc have collaborated to address these problems. In this session, I’ll share1:

  • The pitfalls in OpenStack
  • What are different teams working on
  • Solutions and mitigations

Summary

  • Keystone as a Service Provider
    • Install and configure apache to use a federation capable authentication method. If you choose SAML, setup Shibboleth.
    • Configure federation in keystone
      • Configure authentication drivers in keystone.conf
      • Create keystone groups and assign roles
      • Add Identity Provider(s), Mapping(s), and Protocol(s)
    • Let SP know IdP.
  • Keystone as a Identity Provider
    • Install xmlsec1 tool
    • Configure federation in keystone
    • Generate Metadata
    • Let IdP know SP.

Environment

1
2
3
4
5
+-------------------+     +------------------+
|                   |     |                  |
| IdP:172.16.40.115 |     | SP:172.16.40.112 |
|                   |     |                  |
+-------------------+     +------------------+
  1. I setup the keystone to keystone federation with two devstacks. Please pay attention that this guide is based on devstack which assumes keystone is running under Apache already.
  2. Use SAML2 as the federation protocol.
  3. It only works in CLI. No horizon SSO enabled in this guide right now.
  4. Software Versions
Software Version Description
OS Ubuntu 14.04.3 LTS  
libapache2-mod-shib2 2.5.2+dfsg-2 Federated web single sign-on system (Apache module)
liblog4shib1:amd64 1.0.8-1 log4j-style configurable logging library for C++ (runtime)
libshibsp6:amd64 2.5.2+dfsg-2 Federated web single sign-on system (runtime)
shibboleth-sp2-schemas 2.5.2+dfsg-2 Federated web single sign-on system (schemas)
xmlsec1 1.2.18-2ubuntu1 XML security command line processor
libxmlsec1 1.2.18-2ubuntu1 XML security library
libxmlsec1-openssl 1.2.18-2ubuntu1 Openssl engine for the XML security library

Keystone as a Service Provider (SP)

Finish the following configuration in SP:172.16.40.112.

  1. Setup Shibboleth
  2. Configure Federation in Keystone

After the configuration, the total changes in my /etc is shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ubuntu@shuquan-devstack-sp:/etc$ sudo git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   apache2/sites-available/keystone.conf
	modified:   keystone/keystone.conf
	modified:   shibboleth/attribute-map.xml
	modified:   shibboleth/shibboleth2.xml

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	shibboleth/sp-cert.pem
	shibboleth/sp-key.pem

no changes added to commit (use "git add" and/or "git commit -a")

Setup Shibboleth

Just follow the instruction of the official docs and nothing special. :) My changes are shown below.

  • /etc/shibboleth/shibboleth2.xml
ubuntu@shuquan-devstack-sp:/etc$ sudo git diff shibboleth/shibboleth2.xml
diff --git a/shibboleth/shibboleth2.xml b/shibboleth/shibboleth2.xml
index 1a4b4b8..c8268e1 100644
--- a/shibboleth/shibboleth2.xml
+++ b/shibboleth/shibboleth2.xml
@@ -20,8 +20,7 @@
-->

<!-- The ApplicationDefaults element is where most of Shibboleth's SAML bits are defined. -->
- <ApplicationDefaults entityID="https://sp.example.org/shibboleth"
- REMOTE_USER="eppn persistent-id targeted-id">
+ <ApplicationDefaults entityID="http://172.16.40.112/shibboleth">

<!--
Controls session lifetimes, address checks, cookie handling, and the protocol handlers.
@@ -41,8 +40,7 @@
(Set discoveryProtocol to "WAYF" for legacy Shibboleth WAYF support.)
You can also override entityID on /Login query string, or in RequestMap/htaccess.
-->

- <SSO entityID="https://idp.example.org/idp/shibboleth"
- discoveryProtocol="SAMLDS" discoveryURL="https://ds.example.org/DS/WAYF">
+ <SSO entityID="http://172.16.40.115/v3/OS-FEDERATION/saml2/idp">
SAML2 SAML1
</SSO>

@@ -78,6 +76,7 @@
<MetadataFilter type="Signature" certificate="fedsigner.pem"/>
</MetadataProvider>
-->
+ <MetadataProvider type="XML" uri="http://172.16.40.115:5000/v3/OS-FEDERATION/saml2/metadata"/>

<!-- Example of locally maintained metadata. -->
  • /etc/shibboleth/attribute-map.xml
ubuntu@shuquan-devstack-sp:/etc$ sudo git diff shibboleth/attribute-map.xml
diff --git a/shibboleth/attribute-map.xml b/shibboleth/attribute-map.xml
index 8dd4073..7a6bd93 100644
--- a/shibboleth/attribute-map.xml
+++ b/shibboleth/attribute-map.xml
@@ -140,5 +140,9 @@
<Attribute name="urn:oid:2.5.4.15" id="businessCategory"/>
<Attribute name="urn:oid:2.5.4.19" id="physicalDeliveryOfficeName"/>
-->
-
+ <Attribute name="openstack_user" id="openstack_user"/>
+ <Attribute name="openstack_roles" id="openstack_roles"/>
+ <Attribute name="openstack_project" id="openstack_project"/>
+ <Attribute name="openstack_user_domain" id="openstack_user_domain"/>
+ <Attribute name="openstack_project_domain" id="openstack_project_domain"/>
</Attributes>
  • /etc/apache2/sites-available/keystone.conf
ubuntu@shuquan-devstack-sp:/etc$ sudo git diff apache2/sites-available/keystone.conf
diff --git a/apache2/sites-available/keystone.conf b/apache2/sites-available/keystone.conf
index 9c347c5..6015bd0 100644
--- a/apache2/sites-available/keystone.conf
+++ b/apache2/sites-available/keystone.conf
@@ -1,12 +1,14 @@
Listen 5000
Listen 35357
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D(us)" keystone_combined
+ServerName 172.16.40.112

<Directory /usr/local/bin>
Require all granted
</Directory>

<VirtualHost *:5000>
+ WSGIScriptAliasMatch ^(/v3/OS-FEDERATION/identity_providers/.*?/protocols/.*?/auth)$ /usr/local/bin/keystone-wsgi-public/$1
WSGIDaemonProcess keystone-public processes=5 threads=1 user=ubuntu display-name=%{GROUP}
WSGIProcessGroup keystone-public
WSGIScriptAlias / /usr/local/bin/keystone-wsgi-public
@@ -59,3 +61,19 @@ Alias /identity_admin /usr/local/bin/keystone-wsgi-admin
WSGIApplicationGroup %{GLOBAL}
WSGIPassAuthorization On
</Location>
+
+<Location /Shibboleth.sso>
+ SetHandler shib
+</Location>
+
+<Location /v3/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth>
+ ShibRequestSetting requireSession 1
+ AuthType shibboleth
+ ShibExportAssertion Off
+ Require valid-user
+
+ <IfVersion < 2.4>
+ ShibRequireSession On
+ ShibRequireAll On
+ </IfVersion>
+</Location>

Configure Federation in Keystone

Please pay attention to idp_entity_id. It has to be identical in SP & IdP. You will use it when you config the Identity Provider in Keystone’s [saml]/idp_entity_id option in IdP.

idp_entity_id is the unique identifier for the Identity Provider in Keystone’s [saml]/idp_entity_id option in IdP. This value should be the same in SSO entityID in /etc/shibboleth/shibboleth2.xml and use this command

1
openstack identity provider create --remote-id https://myidp.example.com/v3/OS-FEDERATION/saml2/idp myidp
when you create idp in SP.

  • /etc/keystone/keystone.conf
ubuntu@shuquan-devstack-sp:/etc$ sudo git diff keystone/keystone.conf
diff --git a/keystone/keystone.conf b/keystone/keystone.conf
index f80907f..a6d8745 100644
--- a/keystone/keystone.conf
+++ b/keystone/keystone.conf
@@ -398,7 +398,7 @@ driver = sql
#

# Allowed authentication methods. (list value)
-#methods = external,password,token,oauth1
+methods = external,password,token,oauth1,saml2

# Entry point for the password auth plugin module in the
# `keystone.auth.password` namespace. You do not need to set this unless you
@@ -864,7 +864,7 @@ connection = mysql+pymysql://root:password@127.0.0.1/keystone?charset=utf8
# environment. For `mod_shib`, this would be `Shib-Identity-Provider`. For For
# `mod_auth_openidc`, this could be `HTTP_OIDC_ISS`. For `mod_auth_mellon`,
# this could be `MELLON_IDP`. (string value)
-#remote_id_attribute = <None>
+remote_id_attribute = Shib-Identity-Provider

# An arbitrary domain name that is reserved to allow federated ephemeral users
# to have a domain concept. Note that an admin will not be able to create a
@@ -2790,3 +2790,7 @@ provider = fernet
# Keystone only provides a `sql` driver, so there is no reason to change this
# unless you are providing a custom entry point. (string value)
#driver = sql
+
+[saml2]
+
+remote_id_attribute = Shib-Identity-Provider

Create keystone groups and assign roles

Make sure you’re using v3 right now.

ubuntu@shuquan-devstack-sp:~$ cat v3rc
# OS_AUTH_URL must point to /v3 not /v2.0
export OS_AUTH_URL=http://172.16.40.112:5000/v3
# OS_PROJECT_NAME instead of OS_TENANT_NAME
export OS_PROJECT_NAME=admin
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_DOMAIN_NAME=Default
export OS_IDENTITY_API_VERSION=3
export OS_USERNAME=admin
export OS_PASSWORD=password
$ openstack domain create federated_domain
$ openstack project create federated_project --domain federated_domain
$ openstack group create federated_users
$ openstack role add --group federated_users --domain federated_domain Member
$ openstack role add --group federated_users --project federated_project Member

Add Identity Provider(s), Mapping(s), and Protocol(s)

$ openstack identity provider create --remote-id http://172.16.40.115/v3/OS-FEDERATION/saml2/idp myidp
$ cat > rules.json <<EOF
[
{
"local": [
{
"user": {
"name": "{0}"
},
"group": {
"domain": {
"name": "Default"
},
"name": "federated_users"
}
}
],
"remote": [
{
"type": "openstack_user"
}
]
}
]
EOF
$ openstack mapping create --rules rules.json myidp_mapping
$ openstack federation protocol create saml2 --mapping myidp_mapping --identity-provider myidp

Note: The name you give the protocol is not arbitrary. It must match the method name you gave in the [auth]/methods config option. When authenticating it will be referred to as the protocol_id.

Keystone as an Identity Provider (IdP)

Finish the following configuration in IdP:172.16.40.115.

  1. Package Installation.
  2. Configure Federation in Keystone

After the configuration, the total changes in my /etc is shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@shuquan-devstack-idp:/etc$ sudo git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   apache2/sites-available/default-ssl.conf
	modified:   apache2/sites-available/keystone.conf
	modified:   keystone/keystone.conf

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	keystone/saml2_idp_metadata.xml
	keystone/ssl/

no changes added to commit (use "git add" and/or "git commit -a")

Package Installation

The only package need to install is xmlsec1.

1
$ apt-get install xmlsec1

Configure Federation in Keystone

1.Enable IdP is easier because you don’t need to deal with Shibboleth. Before following the official documentation, you should generate a self-signed cert-key pair for signing in the future and configure it properly in keystone and apache configure file.

$ openssl req -x509 -newkey rsa:2048 -keyout /etc/keystone/ssl/private/signing_key.pem -out /etc/keystone/ssl/certs/signing_cert.pem -days 9999 -nodes

2.Generate Metadata.To create metadata for your keystone IdP, run the keystone-manage command and redirect the output to a file. For example:

$ keystone-manage saml_idp_metadata > /etc/keystone/saml2_idp_metadata.xml

3.Please pay attention to the SP creation. I made a mistake here and spent some time on debugging. The key is that you don’t need to use entityID of shibboleth2.xml in SP for –service-provider-url setting. http://172.16.40.112/Shibboleth.sso/SAML2/ECP is fine because IdP will send SAML assertion to this link and the entityID may not resolve to anything. Surely, you can set these two value identical.

  • /etc/apache2/sites-available/default-ssl.conf
ubuntu@shuquan-devstack-idp:/etc$ sudo git diff apache2/sites-available/default-ssl.conf
diff --git a/apache2/sites-available/default-ssl.conf b/apache2/sites-available/default-ssl.conf
index 432b965..00e3b92 100644
--- a/apache2/sites-available/default-ssl.conf
+++ b/apache2/sites-available/default-ssl.conf
@@ -1,5 +1,6 @@
<IfModule mod_ssl.c>
<VirtualHost _default_:443>
+ ServerName 172.16.40.115
ServerAdmin webmaster@localhost

DocumentRoot /var/www/html
@@ -29,8 +30,8 @@
# /usr/share/doc/apache2/README.Debian.gz for more info.
# If both key and certificate are stored in the same file, only the
# SSLCertificateFile directive is needed.
- SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
- SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
+ SSLCertificateFile /etc/keystone/ssl/certs/signing_cert.pem
+ SSLCertificateKeyFile /etc/keystone/ssl/private/signing_key.pem

# Server Certificate Chain:
# Point SSLCertificateChainFile at a file containing the
  • /etc/apache2/sites-available/keystone.conf
ubuntu@shuquan-devstack-idp:/etc$ sudo git diff apache2/sites-available/keystone.conf
diff --git a/apache2/sites-available/keystone.conf b/apache2/sites-available/keystone.conf
index 9c347c5..b44aa67 100644
--- a/apache2/sites-available/keystone.conf
+++ b/apache2/sites-available/keystone.conf
@@ -1,6 +1,7 @@
Listen 5000
Listen 35357
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\" %D(us)" keystone_combined
+ServerName 172.16.40.115

<Directory /usr/local/bin>
Require all granted
  • /etc/keystone/keystone.conf
ubuntu@shuquan-devstack-idp:/etc$ sudo git diff keystone/keystone.conf
diff --git a/keystone/keystone.conf b/keystone/keystone.conf
index e1fd8d8..21ef281 100644
--- a/keystone/keystone.conf
+++ b/keystone/keystone.conf
@@ -2371,24 +2371,24 @@ driver = sql

# Absolute path to the public certificate file to use for SAML signing. The
# value cannot contain a comma (`,`). (string value)
-#certfile = /etc/keystone/ssl/certs/signing_cert.pem
+certfile = /etc/keystone/ssl/certs/signing_cert.pem

# Absolute path to the private key file to use for SAML signing. The value
# cannot contain a comma (`,`). (string value)
-#keyfile = /etc/keystone/ssl/private/signing_key.pem
+keyfile = /etc/keystone/ssl/private/signing_key.pem

# This is the unique entity identifier of the identity provider (keystone) to
# use when generating SAML assertions. This value is required to generate
# identity provider metadata and must be a URI (a URL is recommended). For
# example: `https://keystone.example.com/v3/OS-FEDERATION/saml2/idp`. (uri
# value)
-#idp_entity_id = <None>
+idp_entity_id = http://172.16.40.115/v3/OS-FEDERATION/saml2/idp

# This is the single sign-on (SSO) service location of the identity provider
# which accepts HTTP POST requests. A value is required to generate identity
# provider metadata. For example: `https://keystone.example.com/v3/OS-
# FEDERATION/saml2/sso`. (uri value)
-#idp_sso_endpoint = <None>
+idp_sso_endpoint = http://172.16.40.115/v3/OS-FEDERATION/saml2/sso

# This is the language used by the identity provider's organization. (string
# value)
@@ -2432,7 +2432,7 @@ driver = sql
# Absolute path to the identity provider metadata file. This file should be
# generated with the `keystone-manage saml_idp_metadata` command. There is
# typically no reason to change this value. (string value)
-#idp_metadata_path = /etc/keystone/saml2_idp_metadata.xml
+idp_metadata_path = /etc/keystone/saml2_idp_metadata.xml

# The prefix of the RelayState SAML attribute to use when generating enhanced
# client and proxy (ECP) assertions. In a typical deployment, there is no
  • Service Provider (SP) Creation.
ubuntu@shuquan-devstack-idp:~$ openstack service provider create --service-provider-url 'http://172.16.40.112/Shibboleth.sso/SAML2/ECP' --auth-url http://172.16.40.112:5000/v3/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth mysp
+--------------------+------------------------------------------------------------------------------------------+
| Field | Value |
+--------------------+------------------------------------------------------------------------------------------+
| auth_url | http://172.16.40.112:5000/v3/OS-FEDERATION/identity_providers/myidp/protocols/saml2/auth |
| description | None |
| enabled | True |
| id | mysp |
| relay_state_prefix | ss:mem: |
| sp_url | http://172.16.40.112/Shibboleth.sso/SAML2/ECP |
+--------------------+------------------------------------------------------------------------------------------+

Validation

  1. Get an unscoped token.1
  2. Scope the unscoped token with specific domain and project name.
  3. Use the scoped token2 to achieve user list in SP.

I’ll elaborate it in next post.

Reference

This is Ironic standalone demo.