# Logging into OpenShift with Active Directory credentials Red Hat's OpenShift Container Platform is a powerful way to run legacy virtual machine workloads and modern container-based / Kubernetes-based workloads. The first step to unleashing the power of OpenShift is to allow developers and system administrators to login. OpenShift allows users to login through several mechanisms, inluding OAuth-based single-sign, integrations with GitHub and GitLab, locally-administered usernames & passwords, and via Active Directory. The list of identity providers (`IdP`) OpenShift supports can be found in the [official documentation](https://docs.openshift.com/container-platform/latest/authentication/understanding-identity-provider.html#supported-identity-providers). **Upfront acknowledgement:** Many of the examples I provide below were based on the excellent information I reviewed from others that came before me. My favorite examples are found at the [https://examples.openshift.pub](https://examples.openshift.pub/cluster-configuration/authentication/activedirectory-ldap/) website. I augemented those examples with additional information I borrowed from [RFC2255](https://www.rfc-editor.org/rfc/rfc2255.html). OpenShift uses RFC2255-formated URLS when authenticating users with Active Directory. I also benefited from, and document in the [Appendix](#Appendix), the use of Active Directory group enumeration for the specific use-case of [authenticating users based on nested-group membership](https://ldapwiki.com/wiki/Wiki.jsp?page=LDAP_MATCHING_RULE_IN_CHAIN). ## Key differences between LDAP and Active Directory :::warning It is very common for Active Directory servers to not have a `uid` attribute in their schema. An RFC2255 URL defaults to using the `uid` attribute to confirm if a user account exists. For Active Directory we need compare the name provided by the user loginng in with the `sAMAccountName` attribute instead. This is done by adding `...?sAMAAccountName` to the end of the LDAP URL. For example: ``` uri: ldaps://ad.example.com/CN=Users,DC=example,DC=com?sAMAccountName ``` ::: ## Looking closer at the LDAP URL :::info I recommend using OpenShift's Web Console to setup Active Directory authentication. I find it much easier than the [documented command-line + YAML procedure](https://docs.openshift.com/container-platform/4.14/authentication/identity_providers/configuring-ldap-identity-provider.html). ::: The RFC2255 specification explains that an LDAP URL should be created from the following example. ``` ldap://host/basedn?attribute?scope?filter ``` The URL variables are described as: 1. `ldap://` or `ldaps://` **is required**. This prefix forces OpenShift to use clear-text or encrypted communications when passing user names and passwords to Active Directory. SIDE NOTE: using `ldaps://` requires OpenShift to trust the AD servers certificate. More on that below. 2. `host` is **required**. It is the fully qualified domain name (DNS name) of the Active Directory server. In some cases a specific port number is appended, such as `ad.example.com:3269`. If no port number is provided, port 389 is used for `ldap://` connections and port 636 is used for `ldaps://` connections 3. `basedn` is **required**. It is the "distinguised name of the base". In other words, this is how OpenShift knows where to start looking for users. 4. `attribute` is **optional**. It tells OpenShift to compare the login name provided by the user with the contents of this attribute. The default is `uid`. This should most likely be changed to `sAMAccountName` for Active Directory. 5. `scope` is **optional**. It is used to prevent OpenShift from looking through too much of the Active Directory to find users. The default is `base` (don't look outside of the base). Changin this to `sub` tells Active Directory to look for users everywhere below the base. 6. `filter` is **optional**. Defining a filter in the LDAP URL would be used to allow or deny certain users to login, even if their passwords are correct, based on some other value -- like that user's group membership, or an attribute like `wasFired=false`. ## Configuring OpenShift to trust the AD server's certificate Many Active Directory administrators use self-signed certificates instead of creating or buying a certificate from a trusted Certificate Authority (CA) like [DigiCert](https://www.digicert.com/). **OpenShift will refuse to connect with an Active Directory that uses a self-signed certificate, unless it has been trusted first**. OpenShift automatically trusts all of the well-known public CA's. This is where I find using the Web Console to setup Active Directory authentication to be easier than using the command-line. The Web Console has a text box that accepts the Certificate Authority via copy-paste. :::info I often use this OpenSSL command-line tool to reveal the Certificate Authority from the Active Directory server itself. This can often be faster than reaching out to the AD administrator and waiting for a response. ``` # Simulate a client connection to the sever # Send no data (/dev/null) just hangup after showing the certificate details openssl s_client -showcerts -connect ad.example.com:636 < /dev/null ``` ::: ## Enable debug logging for the authentication Pods OpenShift does not emit a lot of detail regarding failed logins. This can be frustrating when you're setting up the connection and more information is needed to debug the root-cause. Was the certificate untrusted, did the user provide a bad password, does the OpenShift `bindDN` + `bindPassword` (aka Service Account credentials) have a typo? *I describe the bindDN / Service Account in the [Completed example](#Completed-example) below.* Its very easy to increase the verbosity of the authentication pods in order to identify common issues like: - OpenShift doesn't trust Active Directory's self-signed certificate - OpenShift's Service Account / `bindDN` credentials are wrong - The user can't be foudn in the `uid` or `sAMAccountName` attributes - The user was found, but their password is wrong / expired The commands below will: 1. Enable debug logging 2. Aggregate all of the logs from all of the authentication pods 3. Reset the logging level to default / normal when everything is confirmed to be working ``` # Enable debug logging (wait for new pods to rollout) oc patch authentications.operator.openshift.io/cluster --type=merge -p '{"spec":{"logLevel":"Debug"}}' # Follow the logs from all of the authentication pods oc logs -f -l app=oauth-openshift -n openshift-authentication | grep -v -e healthz -e metrics # Set the log level back to default / normal oc patch authentications.operator.openshift.io/cluster --type=merge -p '{"spec":{"logLevel":"Normal"}}' ``` ## Completed example OpenShift's Web Console creates the following YAML configuration behind the scenes. The Web Console takes care of creating a new `Secret` to hold the Service Account / `bindDN` password and uploading the Active Directory's certificate into a new `ConfigMap`. An example YAML looks like this: ``` --- apiVersion: config.openshift.io/v1 kind: OAuth metadata: name: cluster spec: identityProviders: - name: Active_Directory type: LDAP url: ldaps://ad.example.com/CN=Users,DC=example,DC=com?sAMAccountName insecure: false # The LDAP URL starts with ldaps so this must be false mappingMethod: claim ldap: attributes: email: - mail id: - dn name: - cn preferredUsername: - sAMAccountName bindDN: CN=OpenShift Service Account,CN=Users,DC=example,DC=com bindPassword: - name: some-secret-name ca: - name: some-secret-name ``` :::info The example above shows a `bindDN` and a `bindPassword`. These are generally refered to as Service Account credentials. Most Active Directory servers don't allow unauthenticated browsing. In other words, OpenShift can't figure out if the name provided by the person trying to login exists, unless it authenticates to the AD server first. OpenShift needs a set of credentials to login first and then its allowed to confirm if the user name provided actually exists. ::: ## Appendix ## Dumping all of Active Directory's attributes We can query the Active Directory schema/attributes from a Linux command-line like this. This is helpful to confirm if the `uid` attribute exists and which attribute stores the user's legal name, preferred name and email address. The example command ends with `sn=Smith` which limits/filters the results. Instead of dumping all attributes for all users, Active Directory only returns an attribute dump for users if their last name / family name / surname (sn) is "Smith" ``` ldapsearch -x -W \ -H ldaps://ad.example.com \ -D CN=OpenShift Service Account,CN=Users,DC=example,DC=com \ -b CN=Users,DC=example,DC=com \ sn=Smith ``` ## Handling nested groups Some organizations setup nested groups - or groups that only have other groups as their members. You can tell Active Directory to squash group membership before returning results by adding a specific `filter` to the URL The URL below only allows users to login if they belong to the "OpenShift-Users" group. Even if their membership is abstracted by being a member of a different group - and that group is a member of the "OpenShift-Users" group. ``` url: ldaps://ad.example.com/CN=Users,DC=example,DC=com?sAMAccountName?sub?(memberOf:1.2.840.113556.1.4.1941:=CN=OpenShift-Users,CN=Groups,DC=example,DC=com)' ``` More info here - https://ldapwiki.com/wiki/Wiki.jsp?page=LDAP_MATCHING_RULE_IN_CHAIN And even more information here - https://learn.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax