HOWTO: Setup SAML2 For CloudBolt

Dependencies

Python Library Dependencies (Pre-CB v.7.4)

You'll have to first install a library and its dependencies on the CloudBolt appliance with pip. When prompted, type "y" to continuing during the uninstall process.

$ yum install xmlsec1 xmlsec1-openssl
#for CloudBolt prior to 9.0
$ pip install djangosaml2==0.16.11
#for CloudBolt 9.0 and later
$ pip install djangosaml2==0.17.1


CloudBolt Application Setup

Update customer_settings.py

All configuration changes should be made here:

/var/opt/cloudbolt/proserv/customer_settings.py

All the changes you see in this section should be made to this file. If the file doesn't exist on your CloudBolt server, you can create it yourself.

INSTALLED_APPS

The INSTALLED_APPS setting tells CloudBolt which Django applications should be loaded at startup. Since djangosaml2 is a Django app, we need to append it to the list of applications to be loaded:

from settings import INSTALLED_APPS

INSTALLED_APPS += ('djangosaml2',)

AUTHENTICATION_BACKENDS

This setting configures which authentication plugins are available to CloudBolt. In this case we're adding the SAML2 authentication backend.

from settings import AUTHENTICATION_BACKENDS

AUTHENTICATION_BACKENDS += ('djangosaml2.backends.Saml2Backend',)

LOGIN_REQUIRED_URLS_EXCEPTIONS

We need to instruct CloudBolt not to require login to the urls used for authenticating SAML users.

from settings import LOGIN_REQUIRED_URLS_EXCEPTIONS

LOGIN_REQUIRED_URLS_EXCEPTIONS += (r'^/saml2/',)

LOGIN_URL

This step is optional, and forces all user logins to use SAML2.

LOGIN_URL = '/saml2/login/'

Update customer_urls.py

The '/var/opt/cloudbolt/proserv/customer_urls.py' file is used to provide url mapping to application views. While the djangosaml2 application provides its own routing, we need to include this routing with the parent application routes by adding the following to '/var/opt/cloudbolt/proserv/customer_urls.py'.

Again, if this file doesn't already exist, simply create and edit. If you want to set the URL for the SAML path to something other than saml2/ you can do that by changing saml2/ to whatever you'd like; just keep you'll have to update the LOGIN_URL and LOGIN_REQUIRED_URLS_EXCEPTION settings in customer_settings.py.

Note: If the first two lines are already in the file, you do not need to add them again.

from django.conf.urls import include, url
from urls import urlpatterns


urlpatterns += [
   url(r'^saml2/', include('djangosaml2.urls')),
]

 

Service Provider (SP) Configuration (On CloudBolt Server)

Create saml2 directory

If the '/var/opt/cloudbolt/proserv/saml2/' and '/var/opt/cloudbolt/proserv/saml2/attribute-maps/' directories don't exist in '/var/opt/cloudbolt/proserv', please create them as this is the directory where all SAML certs, and IDP metadata will be stored.

 

Generate Self-Signed Key-Pair

These attribute maps are used to map standard identifiers for attributes provided by your IdP to local attributes that are mapped to the local system in the SAML_ATTRIBUTE_MAPPING variable. No changes should be required to the attribute map files.

Run the following commands and copy saml.key and saml.crt to /var/opt/cloudbolt/proserv/saml2:

$ openssl genrsa -out saml.key 2048

$ openssl req -new -key saml.key -out certreq.csr

$ openssl x509 -req -in certreq.csr -signkey saml.key -out saml.crt

If names other than saml.key and saml.crt are used, please update their references in the SAML_CONFIG settings in customer_settings.py.

Fetch IdP Metadata

SAML2 identity providers will often allow you to download their IdP metadata for use on your CloudBolt instance. In the configuration below, our metadata was downloaded from our IdP and stored at /var/opt/cloudbolt/proserv/saml2/remote_metadata.xml. You'll see this file is referenced in the example configuration file below.

Setup SAML_CONFIG in customer_settings.py

The following configuration is used to connect CloudBolt to our IdP. You'll want to refer to pysaml2 documentation at https://pythonhosted.org/pysaml2 for more information on the available directives and configuration options.

import os
import saml2

SAML2_DIR = os.path.join('/var/opt/cloudbolt/proserv/saml2')

SAML_CONFIG = {
   'xmlsec_binary': '/usr/bin/xmlsec1',
    'name': 'CloudBolt SP',
   'entityid': 'https://cloudbolt.company.com/saml2/metadata/',

   'service': {
       'sp': {
           'want_assertions_signed': False,
           'want_response_signed': False,
           'allow_unsolicited': True,
           'endpoints': {
               'assertion_consumer_service': [
                       ('https://cloudbolt.company.com/saml2/acs/', saml2.BINDING_HTTP_POST),
                   ],
               'single_logout_service': [
                       ('https://cloudbolt.company.com/saml2/ls/', saml2.BINDING_HTTP_REDIRECT),
               ],
           },
           'required_attributes': ['email'],
       },
   },
   'debug': 1,
   'key_file': os.path.join(SAML2_DIR, 'saml.key'),  # private part
   'cert_file': os.path.join(SAML2_DIR, 'saml.crt'),  # public part
   'allow_unknown_attributes': True,
   'attribute_map_dir': os.path.join(/usr/local/lib/python3.6/site-packages/saml2/attributemaps'),
   'metadata': {
       'local': [os.path.join(SAML2_DIR, 'remote_metadata.xml')],
   },
   'contact_person': [{
       'given_name': 'First',
       'sur_name': 'Last',
       'company': 'Company',
       'email_address': 'me@company.com',
       'contact_type': 'technical'
   }],
   'organization': {
       'name': 'Company',
       'display_name': 'Company',
       'url': 'http://www.company.com',
   },
   'valid_for': 24,  # how long is our metadata valid
'accepted_time_diff': 120, #seconds
}

SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'username'
SAML_CREATE_UNKNOWN_USER = True
SAML_ATTRIBUTE_MAPPING = {
       'email': ('email', ),
       'givenName': ('first_name', ),
       'sn': ('last_name', ),
       'uid': ('username', ),
}

In the above example, the key parameters are as follows:

Metadata URL / Entity ID: https://cloudbolt.company.com/saml2/metadata/

CloudBolt ACS URL: https://cloudbolt.company.com/saml2/acs/

CloudBolt SLS/Logout URL: https://cloudbolt.company.com/saml2/ls/

IDP Metadata file: remote_metadata.xml

Integrating LDAP Group Mappings for SAML2 SSO Auto-Created Users

When a user logs in via SSO and they’ve never logged in before, Django will auto create a user in the local user space in CloudBolt without an LDAPUtility assigned.  

Once logged in, the user might expect certain permissions assigned to their user account because they logged in with their LDAP/AD credentials, however, because they logged in via SAML SSO initially, they were not authenticated with an LDAP domain when logging on, so CloudBolt does not know that they should be assigned to any LDAP groups.

The following code modifies the default behavior of the django user create method and passes in the SAML “attributes” sent via SSO to the “External Users Sync” Orchestration Action.  

Save the following code as:

/var/opt/cloudbolt/proserv/customer/backends.py

from utilities.logger import ThreadLogger
from djangosaml2.backends import Saml2Backend
from cbhooks import run_hooks


logger = ThreadLogger(__name__)

class CustomSaml2Backend(Saml2Backend):
    def update_user(self, user, attributes, attribute_mapping,
                 force_save=False):
        """
            Custom SAML2 update_user method to handle group
            attributes that will map to CloudBolt groups
        """
        #logger.info(f'LDAP attributes = {attributes}')
        #logger.info(f'LDAP attribute_mapping: {attribute_mapping}')
        #Extends the functionality of the Saml2Backend to allow us to send SAML
        # Assertion data to the external users sync Orchestration Action
        # and map the ldap domain of the auto-created user correctly

        user = super(CustomSaml2Backend, self).update_user(
            user, attributes, attribute_mapping, force_save=False
)

        profile = user.userprofile

        run_hooks("external_users_sync", job=None,
              users=[profile], attributes=attributes)

        return user

 
Change the /var/opt/cloudbolt/proserv/customer_settings.py file for the following property (replace this entry: “djangosaml2.backends.Saml2Backend”) so that it looks like this:

AUTHENTICATION_BACKENDS += ('customer.backends.CustomSaml2Backend',)

Contact your account representative for an updated external users sync script, which calls the external users sync while possibly passing the SAML Attributes that will allow for any LDAP domain configured in CloudBolt to match the user’s login LDAP domain name.

 Here's an excerpt from said plugin:

def run(job, mapping=None, dry_run=False, **kwargs):
   logger.debug("Running hook {}".format(__name__))
   users = kwargs.get('users', None)
   attributes = kwargs.get('attributes', None)
   if attributes and not dry_run:
       setup_ldap_for_saml_logins(users=users, attributes=attributes)

NOTE: It is necessary to configure your SAML2 provider to send an extra attribute in the SAML Assertion data in order to correctly identify the user’s LDAP domain.  The plugin mentioned above assumes a Microsoft ADFS SAML2 provider, and has the configuration set for the appropriate property name in Microsoft’s SAML2 assertion “attributes” data.  You will need to modify this property based on the property name that your SAML2 provider is sending. UPN translates to UserPrincipalName.

UserPrincipalName follows this format: sAMAccountName@LDAPDomainName

i.e. username@domainname.com

UPN_ATTR = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn'

Okta, OnePoint and other SAML2 providers should be able to pass an extra attribute with that data, but the name is arbitrary so make sure to modify the “UPN_ATTR” to the name of the attribute being passed from the appropriate SAML2 provider.

After you’ve finished all this, we should now be able to restart the httpd service.

service httpd restart

You should now be able to test SAML2 auto-user creation and have the LDAP Domain configured appropriately on the CloudBolt user sent via SAML2, as well as the appropriate permissions based on your LDAP Group membership (as defined by the LDAPGroupMapping object).

Test the configuration, and look at the /var/log/cloudbolt/application.log if you have issues or if the website doesn’t properly load.

Encrypted SAML Responses

As a part of hardening the SAML integration from a security perspective, we can “encrypt” the SAML response object (all the data that the we use to create users in Django/CloudBolt)

This data includes:

  • LDAP username,
  • Email address,
  • GivenName/FirstName,
  • SurName/LastName,
  • LDAP groups the user is a member of (potentially),

SAML Response encryption requires updates on both the IdP and the customer_settings.py on the CloudBolt server in the form of a certificate to encrypt/decrpyt the SAML response data.  For the purposes of testing, you may use the SSL certificate that your dev CloudBolt server’s Apache service is running as.

The IdP will need the public key, and the CloudBolt server will need the public and private keys.

Without the configuration to encrypt saml responses, you will see a log entry in the cloudbolt application.log when a user logs in via a saml assertion:

2019-01-22 13:57:02,079 [DEBUG] saml2.response: ***Unencrypted assertion***

Response Encryption can be attained by adding this information to the /var/opt/cloudbolt/proserv/customer_settings.py in the SAML_CONFIG

    'encryption_keypairs': [{
           'key_file': '/etc/pki/tls/private/cloudbolt.name.net.key',
           'cert_file': '/etc/pki/tls/certs/cloudbolt.name.net.crt'
   }],

You can use a different key than apache’s SSL certificate.

Note: You will need to restart the httpd service in order for this configuration change to become active.

 Make sure that you configure the IdP to use the public key certificate that is referenced in the SAML_CONFIG[“encryption_keypairs”][“cert_file“] section in customer_settings.py, so that the SAML response can be sent encrypted with a certificate that SAML knows how to decrypt.

 Once the configuration is updated in both the IDP and the CloudBolt customer_settings.py (and httpd is restarted), on a new SSO login, you should see that the response is encrypted based on this log entry:

 2019-01-22 14:14:12,250 [DEBUG] saml2.response: ***Encrypted assertion/-s***

 ...and SAML2 authentication should process correctly.

 If you get an error in the browser similar to:

'NoneType' object has no attribute 'authn_statement'

...you at least know that the IdP is sending an encrypted response but the SAML_CONFIG in CloudBolt doesn’t know how to process it.

 This typically means one or more of the following conditions have happened:

  • The configuration change was not made to the customer_settings.py or it has typographical errors (correct file names?)
  • The httpd service was not restarted after making the configuration changes in the customer_settings.py

Example customer_settings.py (excerpt):

pasted_image_0.png

Common Identity Providers (“IdP”s)

No matter what SAML Identity Provider in use, all that is needed is the same information from any of them to correctly create a user in CloudBolt and attach that user to the correct LDAP Domain (for automatic placement of authenticated users into CloudBolt Groups).

  1. Username
  2. FirstName
  3. LastName
  4. Email
  5. UserPrincipalName
  6. User Group List [Distinguished Names] (OPTIONAL, but suggested for future enhancements to the external users sync script. LDAP-based lookups from CloudBolt work just fine without this)

Okta

Okta is a pretty straight-forward IdP and lots of customers have successfully implemented Okta to CloudBolt integration. About the only issue we tend to see is that you can customize the Attribute names and we need to know what the attribute names Okta is sending for each attribute:

 okta.png

From this example we would need the “FirstName”, “LastName”, “Email” and eventually we would need to define “UserName” and or “UserPrincipalName” as well, in order to set the customer_settings.py SAML_ATTRIBUTE_MAPPINGS settings correctly for CloudBolt.

This particular string could be used as a template to pass an appropriate value for the “UserPrincipalName” based on what is configured for user.login.  In the example below, the user had something similar to this in user.login username@emaildomainname.com (which didn’t match the LDAP domain name required).

Placing this string in the value of the ATTRIBUTE STATEMENTS in Okta for an Attribute named UPN allowed us to send the needed data in the format required to override the emaildomainname that was present in user.login and pass the valid LDAP domain name as part of this Attribute.

String.substringBefore(user.login, "@")+"@somedomain.com"

ADFS (Microsoft)

Microsoft’s IdP (Active Directory Federation Services) is a little harder to work with, because they customized it a lot and the attribute names are URLs not just names like “username”.

Additionally, differences in ADFS v2 (Windows 2012) and subsequent changes/improvements in ADFS v3 (Windows 2016) make implementations slightly different. Once a Relying Party Trust has been created, verify the following screenshots look similar to this.  Tabs that are not shown here, have nothing configured in them. See examples of the Attributes we can make use of in ADFS (screenshot from ADFS v3):

saml_6.pngsaml_5.png

saml_4.pngsaml_3.png

saml_2.pngsaml_1.png

After configuring the Relying Party Trust, the customer will need to “Edit Claim Issuance Policy” for this Relying Party Trust with the following settings.

adfs_1.pngadfs_2.png

adfs_3.png

NOTE: ADFS can pass the Group list that a user is a member of so that instead of querying LDAP, the SAML Response data can include the group membership list (as Group DistinguishedNames) from Active Directory.  In addition the only LDAP Attribute mapping missing fromthe image above is the “User-Principal-Name” with an “Outgoing Claim Type” of “UPN”.

 In ADFS, we can click on the “View Rule Language” button to see how these Attributes look with their actual attribute names (while noting that some of the URLs are prefixed with “http://schemas.microsoft.com” and others are prefixed with “http://schemas.xmlsoap.com”:

c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "AD AUTHORITY"]

=> issue(store = "Active Directory", types = ("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"), query = ";mail,sn,givenName,sAMAccountName,memberOf,userPrincipalName;{0}", param = c.Value);

NOTE: These URL attribute names are going to be the names we need to map in the customer_settings.py for CloudBolt in SAML_ATTRIBUTE_MAPPINGS.

 

APPENDIX:

Documentation for SAML2 configuration entries

https://pysaml2.readthedocs.io/en/latest/howto/config.html

Troubleshooting

Time Skew

Time Skew is a major factor when configuring SAML because SAML will refuse to authenticate if the system clocks are off by a particular amount.  Here’s an example from the CloudBolt logs where the CloudBolt server clock and the ADFS server clock are off by too much time. Notice that we are 1.5 minutes ~ outside of the acceptable window in order to use the SAML Response object. (the CloudBolt server is 4 hours off of “Z” aka UTC time.

Please consider configuring the CloudBolt server as an NTP client to correct clock skew.

2019-04-08 12:23:31,528 [DEBUG] saml2.response: conditions: <saml:Conditions xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NotBefore="2019-04-08T16:24:59.893Z" NotOnOrAfter="2019-04-08T17:24:59.893Z"><saml:AudienceRestriction><saml:Audience>https://cloudbolt.corp.tmnas.com/saml2/acs/</saml:Audience></saml:AudienceRestriction></saml:Conditions> 
2019-04-08 12:23:31,530 [ERROR] saml2.response: Exception on conditions: Can't use response yet: (now=2019-23-08T16:23:31Z + slack=0) <= notbefore=2019-04-08T16:24:59.893Z

Example ADFS configured CloudBolt customer_settings.py

If the “Test URL” button from ADFS doesn't work, but you can browse the metadata url from Internet Explorer, Chrome, et al, Check the following Registry Keys on the ADFS server to verify they exist and are set appropriately (in order to enable TLS 1.2 in 64-bit and 32-bit .Net Frameworks for v2 and v4).

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v2.0.50727]"SchUseStrongCrypto"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319]"SchUseStrongCrypto"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v4.0.30319]"SchUseStrongCrypto"=dword:00000001

[HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\v2.0.50727]"SchUseStrongCrypto"=dword:00000001

NOTE:  A Reboot of your AD FS Server is required after changing the SchUseStrongCrypto Values

Key points:  

  • want_response_signed: Response Signing ensures that the RP (CloudBolt) knows that the IdP sent the data because its signed with a certificate that CloudBolt can verify locally (enabled in this example)
  • encryption_keypairs: Response Encryption encrypts the data to ensure an extra layer of security - in addition to the Response being signed, We can further encrypt the data with another certificate (at IdP) and decode it with the private key in CloudBolt (enabled in this example) - not necessary for basic communication (only provides added security)
  • allow_unknown_attributes: Allows extra attributes to be sent in the SAML Response to the SP beyond what is expecting in the SAML_ATTRIBUTE_MAPPING
  • DEBUG = True (allow debug messages to get sent to the application.log - in conjunction with setting the SAML logging to debug via SAML_CONFIG[‘debug’])
  • AUTHENTICATION_BACKENDS (need to set this to backends2 in order to allow for SAML integrated Django user creation via SAML - in conjunction with external_users_sync modifications) 
import os
import saml2
from settings import INSTALLED_APPS
from settings import AUTHENTICATION_BACKENDS
from settings import LOGIN_REQUIRED_URLS_EXCEPTIONS

DEBUG = True

ADMINS = (
   ("admin", "someone@hhgttg.com"),
)
MANAGERS = ADMINS
BASE_EMAIL_URL = 'http://cloudbolt.hhgttg.com'
TIME_ZONE = 'America/New_York'
#ENQUEUE_JOBS = True

#SAML
INSTALLED_APPS += ('djangosaml2',)
AUTHENTICATION_BACKENDS += ('djangosaml2.backends2.CustomSaml2Backend',)
LOGIN_REQUIRED_URLS_EXCEPTIONS += (r'^/saml2/',)
#OPTIONAL: ALL LOGINS FROM AD REQUIRE SAML
LOGIN_URL = '/saml2/login/'
SAML2_DIR = os.path.join('/var/opt/cloudbolt/proserv/saml2')

SAML_CONFIG = {

   'xmlsec_binary': '/usr/bin/xmlsec1',
   'entityid': 'https://cloudbolt.hhgttg.com/saml2/metadata/',
   'service': {
       'sp': {
           'allow_unsolicited': True, #must be true
           'name': 'CloudBolt SP',
           'want_assertions_signed': True, #assertion signing (default=True)
           'want_response_signed': True, #is response signing required
           'name_id_format': None,
           'endpoints': {
               'assertion_consumer_service': [
                   ('https://cloudbolt.hhgttg.com/saml2/acs/',
                    saml2.BINDING_HTTP_POST),
               ], #url to where SSO needs to POST the authentication data
               'single_logout_service': [
                   ('https://cloudbolt.hhgttg.com/saml2/ls/',
                    saml2.BINDING_HTTP_REDIRECT),
               ], #url to where sso needs to redirect on logout
           },
       },
   },
   'debug': 1, #send saml logs to application log in debug mode
   'encryption_keypairs': [{
           'key_file': '/etc/pki/tls/private/cloudbolt.hhgttg.com.key',
           'cert_file': '/etc/pki/tls/certs/cloudbolt.hhgttg.com.crt'
   }], #this controls response encryption (if required by your customer or IdP)
   'key_file': os.path.join(SAML2_DIR, 'saml.key'), # private part
   'cert_file': os.path.join(SAML2_DIR, 'saml.crt'), # public part
   'allow_unknown_attributes': True, #allow extra attributes in response
   'attribute_map_dir': os.path.join(SAML2_DIR, 'attribute-maps'),
   'metadata': {
       'local': [os.path.join(SAML2_DIR,
                              'remote_metadata.xml')],
   }, #this is the metadata file from the IdP
   'contact_person': [{
       'given_name': 'Douglas',
       'sur_name': 'Adams',
       'company': 'HHGTTG',
       'email_address': 'admin@hhgttg.com',
       'contact_type': 'technical'
   }], #contact data - not integral to authentication
   'organization': {
       'name': 'hhgttg.com',
       'display_name': 'The HitchHikers Guide To The Galaxy',
       'url': 'http://www.somewhere.com',
   }, #contact data - not integral to authentication
   'valid_for': 42, #how long is this configuration information valid for (hours)
}
#what attribute should we create the username with: username is typically used
SAML_DJANGO_USER_MAIN_ATTRIBUTE = 'username'
SAML_CREATE_UNKNOWN_USER = True #create users that don’t exist: True | False

MS_CLAIMS = 'http://schemas.microsoft.com/ws/2008/06/identity/claims/'
ORG_WS_CLAIMS = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/'
SAML_ATTRIBUTE_MAPPING = {
   ORG_WS_CLAIMS + 'emailaddress': ('email', ),
   MS_CLAIMS + 'windowsaccountname': ('username', ),
   ORG_WS_CLAIMS + 'surname': ('last_name', ),
   ORG_WS_CLAIMS + 'givenname': ('first_name', ),
}#upn isn’t in this list and doesn’t need to be

Abbreviations

IdP

Identity Provider: Server that provides authentication data to CloudBolt

SP

Service Provider (CloudBolt)

LDAP

Lightweight Directory Access Protocol (Enterprise Directory for User data, commonly implemented as Microsoft Active Directory)

SAML2

Security Assertion Markup Language version 2 : the open-source backend protocol that makes Single Sign-on possible between an IdP and an SP

UPN

User Principal Name: The username for a particular LDAP domain in the following format username@ldapdomain

 

Have more questions? Submit a request

0 Comments

Please sign in to leave a comment.