Edit: This tutorial is for the OpenStack Juno release, in Kilo we have several modifications and improvements.
One of the coolest features from Juno release for Keystone was the ability to Federate Multiple Keystones or Keystone-to-Keystone (K2K) federation. With this feature, two (or more) cloud providers will be able to share resources between them.
As an addition to the Federation concept from the Icehouse release, now Keystone has the ability to be an Identity Provider (IdP) to another Keystone, that would be the Service Provider (SP). Turning Keystone into an IdP means that it will have to recognize an external Keystone as SP, and will also need the ability to generate SAML assertions. An external cloud (Keystone SP) is represented as a region; for that, an url field was added to the well known region table.
Although it was released as experimental, here at the Distributed Systems Lab - UFCG we decided to try to make a toy deployment using Devstack. The following alias will be used to represent both the Devstack with the Keystone IdP and with the Keystone SP: keystone.idp and keystone.sp. Both installations were made over Ubuntu 14.04 LTS virtual machines.
We also need to be aware about some SAML 2.0 SSO profiles. In the moment this tutorial was written, for K2K federation only the Enhanced Client or Proxy (ECP) profile could be used for authentication, handling SAML assertions and getting a OpenStack token.
Keystone as an IdP
Since the SP part remained the same as it was in the Icehouse Federation, this is the experimental phase.
First, we need to change some configurations at the keystone.conf
file (usually located at /etc/keystone/keystone.conf
):
1. Enable federation (OS-FEDERATION
) extension
2. 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
3. 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.
4. Restart Keystone server
Once everything is in place, we can restart the Keystone server:
sudo service apache2 restart
Or you can use the rejoin_stack.sh
script (devstack/rejoin_stack.sh
).
Keystone as a SP
The steps described here do not differ from a regular Icehouse federation setup.
In order to provide federation support as a Service Provider, Keystone consumes SAML assertions issued by external Identity Providers (for this guide, the IdP is another Keystone). This is done via Shibboleth SP, a single sign-in or logging-in system.
There are two main steps:
1. Install and configure Shibboleth SP
2. Enable the federation extension (same as for Keystone IdP)
Install and configure Shibboleth SP
Install 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 are placed 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). Also, we 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>
- Note: there are two configurations (ShibRequireAll and ShibRequireSession) from the regular tutorial that are omitted due conflicts with the version 2.4 from Apache (the one that comes with Ubuntu 14.04 LTS).
Also, we need to 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"/>
To configure Shibboleth we first need to generate a key-pair using the command below:
sudo shib-keygen
Now we need to 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" file="/etc/shibboleth/keystone_idp_metadata.xml"/>
- Note: on the date on which this tutorial was written, there was a bug already with a fix in review that prevented the SP from automatically getting the IdP metadata via GET
https://keystone.idp/v3/OS-FEDERATION/saml2/metadata
. For this reason, the IdP metadata (/etc/keystone/keystone_idp_metadata.xml
) was manually copied into the SP host.
We also need to remove the REMOTE_USER
entry in the shibboleth2.xml
file since it implies external authentication on Keystone:
sudo sed -r 's/REMOTE_USER="\w*"//' -i /etc/shibboleth/shibboleth2.xml
Finally, ensure that shib2
module is enabled and restart Apache:
sudo a2enmod shib2
sudo service apache2 restart
Make Keystone IdP and SP know each other
For the Keystone IdP side, the Keystone SP appears as an external region. We should add the new region using the Keystone SP authentication URL, which is present at the SP's metadata and for the ECP profile it's usually something like http://keystone.sp/Shibboleth.sso/ECP
.
For the SP, we need to add the IdP, create the mapping rules and register a protocol.
The examples in this post are using the python-keystoneclient
. To perform 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.v3 import client
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():
return client.Client(auth_url=OS_AUTH_URL,
username=OS_USERNAME,
password=OS_PASSWORD,
project_name=OS_PROJECT_NAME,
project_domain_name=OS_DOMAIN_NAME)
# Used to execute all admin actions
client = client.client_for_admin_user()
Add the Keystone SP region in the IdP
To create a region to represent the Keystone SP we need to provide an id
and a url
:
def create_region(client, id, url):
try:
r = client.regions.create(id=id, url=url)
except:
r = client.regions.find(id=id)
return r
sp_region = create_region(client, 'keystone.sp', 'http://keystone.sp/Shibboleth.sso/SAML2/ECP')
Setup the Keystone IdP in the SP
When we have a federated user accessing a Keystone SP, this user is mapped into a local domain and group. So we need to create the domain, the group and the role which the federated users comming from the Keystone IdP will be mapped to.
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)
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('Creating 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
. It's important to remember their ids because it is going to be part of the auth URL in the federation workflow.
def create_idp(client, id):
try:
i = client.federation.identity_providers.create(id=id, enabled=True)
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('Register keystone-idp')
idp1 = create_idp(client, id='keystone-idp')
print('\nRegister protocol')
protocol1 = create_protocol(client, protocol_id='saml2', idp=idp1,
mapping=mapping1)
Get a unscoped token from the SP using a SAML assertion generated by the Keystone IdP
Now that everything is in place, we need to be able to ask the Keystone IdP for a SAML assertion and use it to get an unscoped token from the SP. To ask for a SAML assertion we need to be authenticated and provide the token_id
along with the region_id
from the SP and do a GET http://keystone.idp/auth/OS-FEDERATION/saml2
. The following script uses a recent concept added to python-keystoneclient
called Session Objects:
import json
import os
import requests
from keystoneclient.auth.identity import v3
from keystoneclient import session
class K2KClient(object):
def __init__(self):
self.token_id = os.environ.get('OS_TOKEN')
# Keystone SP region
self.region_id = os.environ.get('OS_REGION')
# Keystone IdP auth URL
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.session = requests.Session()
self.verify = False
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.auth_session = session.Session(session=requests.session(),
auth=auth, verify=self.verify)
auth_ref = self.auth_session.auth.get_auth_ref(self.auth_session)
self.token = self.auth_session.auth.get_token(self.auth_session)
def _generate_token_json(self):
return {
"auth": {
"identity": {
"methods": [
"token"
],
"token": {
"id": self.token
}
},
"scope": {
"region": {
"id": self.region_id
}
}
}
}
def get_saml2_assertion(self):
token = json.dumps(self._generate_token_json())
url = self.auth_url + '/auth/OS-FEDERATION/saml2'
r = self.session.post(url=url,
data=token,
verify=self.verify)
if not r.ok:
raise Exception("Something went wrong, %s" % r.__dict__)
self.assertion = r.text
if __name__ == "__main__":
client = K2KClient()
client.v3_authenticate()
client.get_saml2_assertion()
print('SAML assertion: %s' % client.assertion)
Since we are using ECP, we need to send a SOAP envelope to the SP containing the SAML assertion generated above. We are going to add a transform_assertion_into_ecp()
method in the K2KClient object:
def transform_assertion_into_ecp(self):
TEMPLATE = """<soap11:Envelope
xmlns:soap11="http://schemas.xmlsoap.org/soap/envelope/"><soap11:Header><ecp:Relay
State
xmlns:ecp="urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next"
soap11:mustUnderstand="1">ss:mem:f88cd8ad5aeee3456e74900b306b5ed54ec9fb23c614f9fa7
3ece1c97ec004ed</ecp:RelayState><samlec:GeneratedKey
xmlns:samlec="urn:ietf:params:xml:ns:samlec"
soap11:actor="http://schemas.xmlsoap.org/soap/actor/next">yvYbdh49qSJ7LqjFv+rfB8SR
97hPWMwQkL0KKOgSkhY=</samlec:GeneratedKey></soap11:Header>
<soap11:Body>%(response)s</soap11:Body></soap11:Envelope>"""
assertion = '\n'.join(self.assertion.split('\n')[1:])
assertion = assertion.replace('\n', '')
self.ecp_assertion = TEMPLATE % {'response': assertion}
...
client.transform_assertion_into_ecp()
print("ECP assertion: %s" % client.ecp_assertion)
- Note: Many thanks to Marek Denis (marekd on
irc.freenode.net
) for providing the code to handle SAML assertions + ECP part.
The final step is to send the ECP assertion to the Keystone SP URL: POST http://keystone.sp/Shibboleth.sso/ECP
, it should respond with a redirect (302) and then the IdP client should do a GET http://keystone.sp/v3/OS-FEDERATION/identity_providers/kestone-idp/protocols/saml2/auth
. In this last step, the SP whould respond with the unscoped token: header X-Subject-Token
with the token_id
and a JSON body containing the full token:
def _get_sp_url(self):
url = self.auth_url + '/regions/' + self.region_id
r = self.auth_session.get(
url=url,
verify=self.verify)
if not r.ok:
raise Exception("Something went wrong, %s" % r.__dict__)
region = json.loads(r.text)[u'region']
return region[u'url']
def _handle_http_302_ecp_redirect(self, response, method, **kwargs):
location = os.environ.get('OS_SP_AUTH')
# We are not following the redirect URL, but the one at OS_SP_AUTH,
# for our example is
# http://keystone.sp/v3/OS-FEDERATION/identity_providers/kestone-idp/protocols/saml2/auth
return self.auth_session.request(location, method, authenticated=False, **kwargs)
def exchange_assertion(self):
"""Send assertion to a Keystone SP and get token."""
self.sp_url = self._get_sp_url()
r = self.auth_session.post(
self.sp_url,
headers={'Content-Type': 'application/vnd.paos+xml'},
data=self.ecp_assertion,
authenticated=False, redirect=False)
r = self._handle_http_302_ecp_redirect(r, 'GET',
headers={'Content-Type': 'application/vnd.paos+xml'})
self.fed_token_id = r.headers['X-Subject-Token']
self.fed_token = r.text
...
client.exchange_assertion()
print('Unscoped token_id: %s' % client.fed_token_id)
print('Unscoped token body:\n%s' % client.fed_token)
Here is an example of such unscoped token:
Unscoped token_id: 8589481025d043d2a6233684014df374
Unscoped token body:
{
"token":{
"methods":[
"saml2"
],
"expires_at":"2014-11-03T21:09:02.283467Z",
"extras":{
},
"user":{
"OS-FEDERATION":{
"identity_provider":{
"id":"keystone-idp"
},
"protocol":{
"id":"saml2"
},
"groups":[
{
"id":"f91e5cd3480b4ca8b53a5cc2f9f3c37e"
}
]
},
"id":"%7B0%7D",
"name":"{0}"
},
"audit_ids":[
"KXStRZkySRCkoXQncw2VNg"
],
"issued_at":"2013-11-04T20:09:02.283506Z"
}
}
- Note: During the tests made, the Keystone SP wasn't able to validate the Keystone IdP certificate (self-signed). For this reason, since we were deploying a test enviroment, the NullSecurity rule was used in the Security Policy file from Shibboleth (this file is usually located at
/etc/shibboleth/security-policy.xml
):
<SecurityPolicies xmlns="urn:mace:shibboleth:2.0:native:sp:config">
<Policy id="default" validate="false">
<PolicyRule type="NullSecurity"/>
</Policy>
</SecurityPolicies>
- Note: Thanks to Guang Yee (gyee on
irc.freenode.net
), this bug was fixed and it is not necessary to disable Shibboleth's Security Policy in order to make this work: https://review.openstack.org/#/c/150190/
After getting an unscoped token, we can get an scoped one by using the regular federation approach (Icehouse), than we can use it to request resources from the Keystone SP.