Establishing an LDAP connection#

The Lightweight Directory Access Protocol (LDAP) is a standardized set of rules that governs how information is transmitted and handled between a directory server and other servers on a network. Directory servers are external storage that contains information about users’ identities. This could include their first and last name, email address, and employee ID number. Directory servers also often contain sensitive information, such as account passwords. For more information about LDAP, see the official LDAP documentation.

Before you begin#

Anaconda highly recommends you have knowledge of your LDAP server and organizational structure to complete this procedure. Configuring identity and access management is complex, and each enterprise has a unique LDAP directory structure.

While your implementation will be based on the specific structure and needs of your organization, the principals and processes described here will enable you to:

  • Gather directory information about your LDAP server.

  • Establish a connection to your LDAP server in Keycloak.

  • Reduce the number of users that need to be mapped into Anaconda through the use of groups and roles.

  • Reduce the number of groups that need to be mapped into Anaconda by filtering groups.

  • Automate importing new groups for team memberships based on filters.

  • Automate the provisioning of permissions to users based on group membership.

Prerequisites#

You must have credentials for the “bind user” service account to perform this task. If you do not have the proper credentials, get them from your LDAP Server Administrator.

Gathering directory structure information#

In order to configure Keycloak to validate credentials from your external LDAP server, you need to gather some information about your LDAP directory structure. You’ll use this information to link user attributes (object classes, email, user ID, password, etc.) for use in Anaconda.

While you cannot discern a complete picture of your LDAP directory structure from the bind user credentials, you can make some general assumptions about it based on them. For example, if the bind user distinguished name (DN) is:

uid=binduser,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io

We can see the “root” or “base” of the directory tree is dc=tools,dc=anaconda,dc=io. From there we can discern the rest of the tree structure. In this example, we can see that the uid attribute is stored in the users folder, which is stored in the accounts folder.

If you prefer, tools are available to aid in the visualization, navigation, and updating of your organization’s LDAP directory server, such as phpldapadmin, which was used to generate the following view. This provides additional information about the LDAP structure that you can’t discern from just looking at the bind credentials, such as the location of groups, which is also stored in the accounts folder.

Now that you better understand how to discern your LDAP directory structure, you are ready to use the ldapsearch tool, along with the bind user credentials, to learn details about an individual user based on their User ID. For more information about the ldapsearch tool, see the official documentation.

Gather the information you’ll need to configure user federation within Keycloak by running the following command against a known user ID:

# Replace <BIND_USER> with your bind user address
# Replace <LDAP_ADDRESS> with the address of your LDAP server
# Replace <DIRECTORY_ROOT> with the root of your LDAP server
# Replace <USER_ID> with the ID of a user on your LDAP sever
ldapsearch -D '<BIND_USER>' -W -H <LDAP_ADDRESS> -b <DIRECTORY_ROOT> "(<USER_ID>)"

Following the example for the bind user DN, to find information about user User1 the command would look like this:

ldapsearch -D 'uid=binduser,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io' -W -H ldap://ipa.tools.anaconda.io -b dc=tools,dc=anaconda,dc=io "(uid=User1)"

The return from the command will look like this:

# User1, users, compat, tools.anaconda.io
dn: uid=User1,cn=users,cn=compat,dc=tools,dc=anaconda,dc=io
objectClass: posixAccount
objectClass: ipaOverrideTarget
objectClass: top
gecos: User1
cn: User1
uidNumber: 1666600031
gidNumber: 1666600031
loginShell: /bin/sh
homeDirectory: /home/User1
ipaAnchorUUID:: OklQQTp0b29scy5jb250aW51dW0uaW86OTEyYTMwNjgtZDhmYy0xMWU4LTgzYTUtMTIyYTE3YWNlMzJh
uid: User1

# User1, users, accounts, tools.anaconda.io
dn: uid=User1,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
displayName: User1
uid: User1
krbCanonicalName: User1@TOOLS.anaconda.IO
objectClass: top
objectClass: person
objectClass: organizationalperson
objectClass: inetorgperson
objectClass: inetuser
objectClass: posixaccount
objectClass: krbprincipalaux
objectClass: krbticketpolicyaux
objectClass: ipaobject
objectClass: ipasshuser
objectClass: ipaSshGroupOfPubKeys
objectClass: mepOriginEntry
loginShell: /bin/sh
initials: U1
gecos: User1
sn: LastName
homeDirectory: /home/User1
mail: User1@tools.anaconda.io
krbPrincipalName: User1@TOOLS.anaconda.IO
givenName: User1
cn: User1
ipaUniqueID: 912a3068-d8fc-11e8-83a5-122a17ace32a
uidNumber: 1666600031
gidNumber: 1666600031
krbPasswordExpiration: 20181026085310Z
krbLastPwdChange: 20181026085310Z
memberOf: cn=ipausers,cn=groups,cn=accounts,dc=tools,dc=anaconda,dc=io
memberOf: cn=grp-anaconda-data-scientist,cn=groups,cn=accounts,dc=tools,dc=anaconda,dc=io
memberOf: cn=grp-anaconda-users,cn=groups,cn=accounts,dc=tools,dc=anaconda,dc=io
memberOf: cn=grp-finance,cn=groups,cn=accounts,dc=tools,dc=anaconda,dc=io

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

Remember, the information you need to configure Keycloak to authenticate your users can be found in these results. Save this information and keep it somewhere accessible for reference.

Setting up LDAP user federation#

Now that you have gathered information about your directory, you need to tell Keycloak how to interpret that information, so it can use it with Anaconda software.

Note

These attributes can be remapped later, if necessary.

  1. Log in to your Keycloak console using an account that has administrator privileges.

  2. Select User federation from the left-hand navigation menu.

  3. Select ldap from the Add provider… dropdown menu.

Let’s take a look at the Add LDAP provider page. Keep in mind that entries shown here are in alignment with the example provided above.

  1. Console Display Name

    Enter a name for Keycloak to display for the LDAP server.

  2. Vendor

    Select your LDAP server vendor from the dropdown menu. If you do not know your vendor, ask your LDAP server administrator.

  3. Connection URL

    Enter the LDAP server’s URL here.

  4. Bind User information

    Enter the bind user information here.

  5. Edit Mode

    Set the edit mode to READ_ONLY so you can view and import user information but don’t have to worry about making unwanted changes to your LDAP server.

  6. Users DN

    Enter the LDAP directory location for your users here.

  7. Username LDAP attribute

    Get this information from your ldapsearch return. This attribute determines what is displayed as your user’s name when they sign into Anaconda. In this example, the username attribute is uid.

    Caution

    Usernames cannot be all numeric values. All numeric usernames are indistinguishable from UUID’s, and will break managed persistence if implemented.

  8. RDN LDAP attribute

    Get this information from your ldapsearch return. Usually, the relative distinguished name (RDN) attribute is the same as the username attribute, but this field may default to something else depending on your vendor.

  9. UUID LDAP attribute

    Get this information from your ldapsearch return. Your users’ unique identifiers (UUID).

  10. User object classes

    Get this information from your ldapsearch return. Generally, the user object classes field will have more than one entry, separated by a comma.

  11. User LDAP filter

    The user LDAP filter restricts which users are returned from your LDAP directory. In the example, we only want users with the attribute objectClass=person that also have a uid and are in the group cn=grp-anaconda-users.

    Because users must explicitly be added to the group, unauthorized access is prevented, and license management is simplified.

    Filters also limit the need to synchronize a large number of objects from LDAP, which will help prevent out-of-memory errors in the auth pod.

    Caution

    Avoid the temptation to add new groups into the Custom User LDAP Filter! ldapsearch utilizes regular expressions and is notorious for its complexity. If implemented incorrectly, a custom filter could cause all users to have their access suspended or be functionally disabled.

  12. Test buttons

    Use the Test connection and Test authentication buttons to verify that Anaconda can connect to the provider with the credentials provided. You’ll need to resolve any errors before continuing.

    Tip

    This is a good method for making sure your certifications are in the correct place if you are using LDAPS.

    By default, users will not be synced from LDAP until they log in. To test whether the custom user LDAP Filter is working correctly, add or remove users in LDAP, then enable the sync settings to see if your changes are picked up and user authentication works as expected.

After you save your configurations, your LDAP server can be viewed on the User Federation page.

Caution

If you are using LDAPS, you must also complete the steps provided here.

Once you’ve saved your changes, you can navigate to Users in the left-hand navigation, then select View all users to verify that your users’ information was imported correctly.

Configuring Group Mappers#

Once user federation is established, you can set up a group mapper for Keycloak to import your LDAP server’s groups automatically for you. Once again, use the ldapsearch tool to gather information about your LDAP directory, only this time, look for information pertaining to your organizations groups.

To gather information about groups in your LDAP directory, run the following command against a known group DN:

# Replace <BIND_USER> with your bind user address
# Replace <LDAP_ADDRESS> with the address of your LDAP server
# Replace <DIRECTORY_ROOT> with the root of your LDAP server
# Replace <GROUP_DN> with the group's distinguished name
ldapsearch -D '<BIND_USER>' -W -H <LDAP_ADDRESS> -b <DIRECTORY_ROOT> "(<GROUP_DN>)"

Following with the earlier example, the command looks like this:

ldapsearch -D 'uid=binduser,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io' -W -H ldap://ipa.tools.anaconda.io -b dc=tools,dc=anaconda,dc=io "(cn=grp-anaconda-users)"

The return from the command will look like this:

# grp-anaconda-users, groups, compat, tools.anaconda.io
dn: cn=grp-anaconda-users,cn=groups,cn=compat,dc=tools,dc=anaconda,dc=io
objectClass: posixGroup
objectClass: ipaOverrideTarget
objectClass: ipaexternalgroup
objectClass: top
gidNumber: 1666600026
memberUid: User1
memberUid: User2
memberUid: User3
memberUid: User4
memberUid: User5
memberUid: User6
memberUid: User7
memberUid: User8
memberUid: User9
ipaAnchorUUID:: OklQQTp0b29scy5jb250aW51dW0uaW86NGFhOTQ4NzYtZDg4YS0xMWU4LWE2ZD
ctMTIyYTE3YWNlMzJh
cn: grp-anaconda-users

# grp-anaconda-users, groups, accounts, tools.anaconda.io
dn: cn=grp-anaconda-users,cn=groups,cn=accounts,dc=tools,dc=anaconda,dc=io
objectClass: top
objectClass: groupofnames
objectClass: nestedgroup
objectClass: ipausergroup
objectClass: ipaobject
objectClass: posixgroup
cn: grp-anaconda-users
ipaUniqueID: 4aa94876-d88a-11e8-a6d7-122a17ace32a
gidNumber: 1666600026
member: uid=User1,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User2,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User3,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User4,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User5,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User6,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User7,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User8,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io
member: uid=User9,cn=users,cn=accounts,dc=tools,dc=anaconda,dc=io

# search result
search: 2
result: 0 Success

# numResponses: 3
# numEntries: 2

Remember, the information you need to create a group mapper can be found in these results. Save this information and keep it somewhere accessible for reference. Return to Keycloak and complete the following steps:

  1. Select User Federation from the left-hand navigation.

  2. Select your LDAP server.

  3. Select the Mappers tab.

  4. Select Add mapper.

  5. Enter a name for your group mapper, such as ldap_group_mapper.

  6. Select group-ldap-mapper from the Mapper Type dropdown menu. More options will appear based on your dropdown selection.

Let’s take a look at the Create new mapper page. Keep in mind that entries shown here are in alignment with the example provided above.

  1. Name

    Enter a name for Keycloak to display for the LDAP group mapper.

  2. Mapper Type

    Select group-ldap-mapper from the dropdown menu.

  3. LDAP Groups DN

    Get this information from your ldapsearch return. Provide the distinguished name of the group you would like to map.

  4. Group Name LDAP Attribute

    Get this information from your ldapsearch return. Enter the attribute that is associated with groups. In this example, the attribute is cn.

  5. Group Object Classes

    Get this information from your ldapsearch return. This field will often have multiple entries, separated by a comma.

  6. LDAP Filter

    If you have established groups of users in your LDAP server that you plan to provide with access to Anaconda, you can import those specific groups by providing search filter criteria here. However, depending on how your LDAP server is structured, filtering to the correct groups can be complicated.

    The search filter utilizes regular expressions (i.e. supports the use of wildcard characters). This example shows the search filter as cn=grp-anaconda-*, which will reach out to the LDAP server and import all groups that begin with grp-anaconda-.

  7. Mode

    Open the Mode dropdown menu and select READ_ONLY.

    Caution

    If implemented incorrectly, a custom filter could cause all users to have their access suspended or be functionally disabled.

Click Save to create your LDAP server mapper. An ID field appears at the top of the Mapper details page once it’s established.

Mapping group roles#

If you have groups of users that you plan to provide with access to Anaconda, you can import them using the LDAP filter as described above. Once complete, you need to provide your groups of users with permissions to work within Anaconda.

Roles determine a user’s permissions within Anaconda, and a set of “realm” roles are established for you by default when you install. As a final step in establishing a connection to your LDAP server, you can map these provided roles to groups within your LDAP server to provide your team members with everything they need to work within Anaconda. Once the group roles are established, if a new team member arrives, adding them to the correct LDAP group provides them with all the correct permissions they need to use Anaconda right away. For more information about roles, see Roles and groups.

In the example below, groups have been established within the LDAP Server specifically for Anaconda users. Roles have been assigned to these groups to provide their members with the “proper” level of functionality within Anaconda for them to perform their jobs. A brief explanation on what their set role permissions will allow them to do has been provided in the right-hand column.

LDAP Group

ae-admin

ae-creator

ae-deployer

ae-uploader

offline_access

uma_authorization

Description

grp-anaconda-biz-analyst

X

Business Analysts can access the system. They cannot create projects or grant others access to the system.

grp-anaconda-data-scientist

X

X

X

X

X

Data Scientists can create and share projects, but cannot deploy them.

grp-anaconda-data-engineer

X

X

X

X

Data Engineers can additionally deploy projects, as well as grant access to others.

grp-anaconda-devops

X

X

X

DevOps can deploy projects and upload packages, but cannot create projects.

grp-anaconda-sec-admin

This group should be used to administer user access within the system. Therefore, no roles should be defined in the AnacondaPlatform realm. If required, roles can be defined and access granted in the Auth Center Master realm.

grp-anaconda-sysadmin

X

By default, the ae-admin role is a superuser for all other roles.

grp-anaconda-sysacct

The roles for system accounts are yet to be defined. These could be used for automated CI/CD tasks.

grp-anaconda-users

This is used as a coarse-grained control for access to Workbench, so no roles are defined.

Use the Role Mappings tab to assign the appropriate role(s) to each group imported from your LDAP server.

Configuring LDAPS#

LDAP over SSL, or LDAPS, allows you to encrypt your LDAP server data while it travels during communications, in order to protect it from attacks like certificate theft. For more information, see the official Keycloak documentation on LDAPS.

Prerequisites#

You must have the Java jre package installed to complete this procedure. You must have SSL/TLS certificates for your LDAP server.

Establishing LDAPS#

If you are using a custom certificate authority (CA), you must create a truststore on your host. If you are using a public CA, you can skip the truststore installation and go straight to exporting your existing SSL certificates to the Data Science & AI Workbench auth service.

  1. If the CA certificates are directly available to you, generate your truststore by running the following command:

    # Replace <CA_CERT> with your with your CA .pem file
    keytool -import -file <CA_CERT> -alias auth -keystore LDAPS.jks
    

    Note

    If you need to add an intermediate certificate, run this command again with a unique alias, to include it in the LDAPS.jks file.

  2. Export the existing SSL certificates for your system by running the following commands:

    # Replace <PATH_TO> with the filepath for your secrets-exported.yml file
    kubectl get secrets anaconda-enterprise-certs --export -o yaml > /<PATH_TO>/secrets-exported.yml
    
  3. As a precautionary measure, create a backup of the secrets-exported.yml file prior to encoding it by running the following command:

    cp secrets-exported.yml secrets-exported-orig.yml
    
  4. Run the following command to encode the newly created truststore as base64:

    base64 -i --wrap=0 LDAPS.jks >> OUTPUT.jks
    
  5. Copy the output of this command, and paste it into the data section of the secrets-exported.yml file.

  6. Run the following command to update Workbench with the secrets certificate:

    # Replace <PATH_TO> with the filepath for your secrets-exported.yml file
    kubectl replace -f /<PATH_TO>/secrets-exported.yml
    
  7. Verify that the LDAPS.jks entry has been added to the secret:

    kubectl describe secret anaconda-enterprise-certs
    
  8. Run the following command to restart the auth service:

    kubectl get pods | grep ap-auth | cut -d' ' -f1 | xargs kubectl delete pods
    

    Caution

    If you are using Workbench version 5.6.0 or later, you must also complete the following steps to finish your LDAPS configuration. If you do not, your LDAPS configuration will not connect successfully.

  9. Create a backup of your current auth configuration:

    kubectl get deployment anaconda-enterprise-ap-auth -o yaml > auth.yaml
    
  10. Edit the auth pod deployment:

    kubectl edit deployment anaconda-enterprise-ap-auth
    
  11. Add the below to the JAVA_OPTS key/value:

    -Djavax.net.ssl.trustStore=/etc/secrets/certs/ldaps.jks
    -Djavax.net.ssl.trustStorePassword=anaconda
    

    Here is an example of what your auth.yaml file’s JAVA_OPTS might look like when complete.

  12. Save your changes.

  1. If the CA certificates are directly available to you, generate your truststore by running the following command:

    # Replace <CA_CERT> with your with your CA .pem file
    keytool -import -file <CA_CERT> -alias auth -keystore LDAPS.jks
    

    Note

    If you need to add an intermediate certificate, run this command again with a unique alias, to include it in the LDAPS.jks file.

  2. Encode the newly created truststore as base64 by running the following command:

    base64 -i --wrap=0 LDAPS.jks >> <OUTPUT.jks>
    
  3. Create the secret template for <OUTPUT.jks>

    # Replace <OUTPUT.jks> with the encoded truststore file you just created
    # Replace <TRUSTSTORE_SECRET> with the name of your LDAPS truststore secret
    # Replace <NAMESPACE> with your Anaconda Enerprise namespace
    kind: Secret
    apiVersion: v1
    metadata:
        name: <TRUSTSTORE_SECRET>
        namespace: <NAMESPACE>
    data:
        <OUTPUT.jks>
    type: kubernetes.io/tls
    
  4. Create the new kubernetes secret by running the following command:

    # Replace <SECRET_NAME> with a name for your truststore secret file
    kubectl create -f <SECRET_NAME>
    
  5. Save your configuration changes using the extract_config.sh script by running the following command:

    # Replace <NAMESPACE> with your Workbench namespace
    NAMESPACE=<NAMESPACE> ./extract_config.sh
    

    The extract_config.sh script creates a file called helm_values.yaml and saves it in the directory the script was run from.

  6. Verify the information captured in the helm_values.yaml file is correct and contains all of your current cluster configuration settings.

  7. Open the helm_values.yaml file and include the following lines:

    # Replace <OUTPUT.jks> with your encoded truststore file
    # Replace <TRUSTSTORE_PASS> with the password of thevtruststore
    # Replace <TRUSTSTORE_SECRET> with the name of your truststore secret
    keycloak:
        truststore: /etc/secrets/certs/<OUTPUT.jks>
        truststore_password: "<TRUSTSTORE_PASS>"
        truststore_secret: <TRUSTSTORE_SECRET>
    
    Example config variables
    keycloak:
        truststore: /etc/secrets/certs/ldaps.jks
        truststore_password: anaconda
        truststore_secret: ldaps_secret
    
  8. Update your Helm Chart by running the following command:

    helm upgrade --values ./helm_values.yaml anaconda-enterprise ./Anaconda-Enterprise/