Search code examples
c#linuxsslldap

C# How to connect to LDAP (AD DC) server using SSL or TLS from Linux Ubuntu 22.04


I am running a C# .NET 6 App in a Linux Ubuntu 22.04 container. I need the app to connect to an Active Directory Domain Controller in order to authenticate users of the app. I can get non-secure LDAP connections to the DC to work, but I get exceptions for any attempts at SSL or TLS LDAP connections. The exception messages are fairly vague so I am stuck on how to investigate further. I need to use secure connections to LDAP so that the app does not send users' credentials in the clear.

My Linux container is not connected to the Active Directory Domain.

I am using nuget packages System.DirectoryServices Version 7.0.1 and System.DirectoryServices.Protocols Version 7.0.1. According to Pull Request #52904, support for TLS has been added but I'm not clear on what the prerequisites are or on which scenarios are supported.

Should I be using a different AuthType or am I missing a prerequiste package? Any help would be much appreciated.

Testing

I have been testing using the code below. (Where username is a sAMAccountName and userDN is a Distinguished Name). My thinking had been that I was using the wrong connection options so this code cycles through various options.

internal class LdapConnectionTest
    {

        enum EncryptionOption
        {
            None = 0,
            SSL = 1,
            TLS = 2
        }

        public static void StartTest(
            string ldapServer,
            string userName,
            string userDN,
            string password)
        {

            foreach (AuthType authType in new AuthType[] { 
                AuthType.Anonymous, 
                AuthType.Basic, 
                AuthType.Negotiate, 
                AuthType.Ntlm })
            {
                
                foreach (EncryptionOption encryptionOption in new EncryptionOption[] { 
                    EncryptionOption.None, 
                    EncryptionOption.SSL, 
                    EncryptionOption.TLS})
                {

                    foreach (int protocolVersion in new int[] {2,3})
                    {

                        foreach (ReferralChasingOptions referralChasingOption in new ReferralChasingOptions[] {
                            ReferralChasingOptions.None,
                            ReferralChasingOptions.All})
                        {

                            foreach (bool skipVerifyCertificate in new bool[] { false, true })
                            {

                                int port = 389; //TLS is also on port 389

                                if (encryptionOption == EncryptionOption.SSL)
                                {
                                    port = 636;
                                }

                                if (encryptionOption == EncryptionOption.TLS && protocolVersion == 2) continue; //TLS not supported in LDAP v2

                                Console.WriteLine();
                                Console.WriteLine($"##### Attempt with  AuthType: {authType}  Encryption: {encryptionOption}  Port: {port}  Protocol Version: {protocolVersion}  ReferalChasing: {referralChasingOption}  Skip Verify Cert: {skipVerifyCertificate}");

                                try
                                {

                                    LdapDirectoryIdentifier id = new(ldapServer, port);


                                    LdapConnection connection = new(id)
                                    {
                                        AuthType = authType
                                    };


                                    connection.SessionOptions.ReferralChasing = referralChasingOption;

                                    if (encryptionOption == EncryptionOption.SSL)
                                    {
                                        connection.SessionOptions.SecureSocketLayer = true;

                                        if (skipVerifyCertificate)
                                        {
                                            connection.SessionOptions.VerifyServerCertificate = (con, cer) => true; //this is insecure
                                        }
                                    }


                                    connection.SessionOptions.ProtocolVersion = protocolVersion;


                                    NetworkCredential credential = new(authType == AuthType.Basic ? userDN : userName, password);

                                    if (encryptionOption == EncryptionOption.TLS)
                                    {
                                        if (skipVerifyCertificate)
                                        {
                                            connection.SessionOptions.VerifyServerCertificate = (con, cer) => true; //this is insecure
                                        }
                                        connection.SessionOptions.StartTransportLayerSecurity(null);
                                    }

                                    if (authType == AuthType.Anonymous)
                                    {
                                        connection.Bind();
                                    }
                                    else
                                    {
                                        connection.Bind(credential);
                                    }

                                    Console.WriteLine($"Successfull connection  Protocol Version: {connection.SessionOptions.ProtocolVersion}  Secure: {connection.SessionOptions.SecureSocketLayer}");

                                    connection.Dispose();

                                }
                                catch (Exception? ex)
                                {
                                    while (ex != null)
                                    {
                                        Console.WriteLine($"Exception {ex.GetType().ToString()}  {ex.Message}");
                                        Console.WriteLine(ex.StackTrace);

                                        //exit on wrong credential in order to not lock out account
                                        if (ex.Message.Contains("credential", StringComparison.InvariantCultureIgnoreCase)) return;

                                        ex = ex.InnerException;

                                        if (ex != null)
                                        {
                                            Console.WriteLine("Inner Exception:");
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }


    }

All the test scenarios work when I run the test code from a Windows workstation. When I try it on Ubuntu 22.04 the insecure connections work but I get the following Exceptions for secure connections (summary of output from the code above). I was expecting at least one of the secure connection options to work.

##### Attempt with  AuthType: Basic  Encryption: None  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Successfull connection  Protocol Version: 3  Secure: False

##### Attempt with  AuthType: Basic  Encryption: SSL  Port: 636  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The LDAP server is unavailable.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind(NetworkCredential newCredential)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 110

##### Attempt with  AuthType: Basic  Encryption: TLS  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The connection cannot be established.
   at System.DirectoryServices.Protocols.LdapSessionOptions.StartTransportLayerSecurity(DirectoryControlCollection controls)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 101

##### Attempt with  AuthType: Negotiate  Encryption: None  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The feature is not supported.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind(NetworkCredential newCredential)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 110

##### Attempt with  AuthType: Negotiate  Encryption: SSL  Port: 636  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The feature is not supported.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind(NetworkCredential newCredential)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 110

##### Attempt with  AuthType: Negotiate  Encryption: TLS  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The connection cannot be established.
   at System.DirectoryServices.Protocols.LdapSessionOptions.StartTransportLayerSecurity(DirectoryControlCollection controls)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 101

##### Attempt with  AuthType: Ntlm  Encryption: None  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  An unknown authentication error occurred.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind(NetworkCredential newCredential)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 110

##### Attempt with  AuthType: Ntlm  Encryption: SSL  Port: 636  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  An unknown authentication error occurred.
   at System.DirectoryServices.Protocols.LdapConnection.BindHelper(NetworkCredential newCredential, Boolean needSetCredential)
   at System.DirectoryServices.Protocols.LdapConnection.Bind(NetworkCredential newCredential)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 110

##### Attempt with  AuthType: Ntlm  Encryption: TLS  Port: 389  Protocol Version: 3  ReferalChasing: None  Skip Verify Cert: False
Exception System.DirectoryServices.Protocols.LdapException  The connection cannot be established.
   at System.DirectoryServices.Protocols.LdapSessionOptions.StartTransportLayerSecurity(DirectoryControlCollection controls)
   at MyTestApp.Ldap.LdapConnectionTest.StartTest(String ldapServer, String userName, String userDN, String password) in /src/MyTestApp/MyTestApp/Ldap/LdapConnectionTest.cs:line 101

Environment

I have installed the AD root certificate using update-ca-certificates. My ldap.conf file points to the crt file where all my CAs are installed.

# cat ldap.conf
#
# LDAP Defaults
#

# See ldap.conf(5) for details
# This file should be world readable but not world writable.

#BASE   dc=example,dc=com
#URI    ldap://ldap.example.com ldap://ldap-provider.example.com:666

#SIZELIMIT      12
#TIMELIMIT      15
#DEREF          never

# TLS certificates (needed for GnuTLS)
TLS_CACERT      /etc/ssl/certs/ca-certificates.crt

Other details of my environment:

# dotnet --info
.NET SDK (reflecting any global.json):
 Version:   6.0.122
 Commit:    dc5a76ad5c

Runtime Environment:
 OS Name:     ubuntu
 OS Version:  22.04
 OS Platform: Linux
 RID:         ubuntu.22.04-x64
 Base Path:   /usr/lib/dotnet/sdk/6.0.122/

global.json file:
  Not found

Host:
  Version:      6.0.22
  Architecture: x64
  Commit:       4bb6dc195c

.NET SDKs installed:
  6.0.122 [/usr/lib/dotnet/sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 6.0.22 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 6.0.22 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]

Download .NET:
  https://aka.ms/dotnet-download

Learn about .NET Runtimes and SDKs:
  https://aka.ms/dotnet/runtimes-sdk-info
#

I have installed the following packages in my container:

FROM ubuntu:22.04

#update software package lists
RUN apt-get update && \
    apt update && \
#install .NET ASP Core runtime
    apt-get install -y aspnetcore-runtime-6.0 && \
#install LDAP library + symlink from old version that .NET wants https://github.com/dotnet/runtime/issues/69456
    apt-get install -y libldap-2.5-0 && \
    ln -s /usr/lib/x86_64-linux-gnu/libldap-2.5.so.0 /usr/lib/x86_64-linux-gnu/libldap-2.4.so.2 && \
#install ca certificates (required for ssl)
    apt-get install -y ca-certificates && \
#tidyup
    apt clean && \
    apt-get clean

I am using the symlink work-around described in #69456 for the hard-coded reference to libldap-2.4.so.2 which is not available in Ubuntu 22.04.

I have successfully run ldapsearch to make an ldaps:// connection to the domain controller from my Linux container.


Solution

  • I finally got this working. Hope this is useful for someone.

    Solution for SSL/TLS connection to LDAP from:

    • C# .NET 6
    • System.DirectoryServices Version 7.0.1
    • System.DirectoryServices.Protocols Version 7.0.1
    • Ubuntu 22.04 Container

    Prerequisites

    Packages

    Excerpt from my docker file:

    FROM ubuntu:22.04
    
    #update software package lists
    RUN apt-get update && \
        apt update && \
    #install .NET ASP Core runtime
        apt-get install -y aspnetcore-runtime-6.0 && \
    #install LDAP library and symlink from old version - workaround for Ubuntu 22.04 from https://github.com/dotnet/runtime/issues/69456
        apt-get install -y libldap-2.5-0 && \
        ln -s /usr/lib/x86_64-linux-gnu/libldap-2.5.so.0 /usr/lib/x86_64-linux-gnu/libldap-2.4.so.2 && \
    #install ca certificates (required for ssl)
        apt-get install -y ca-certificates && \
    #tidyup
        apt clean && \
        apt-get clean
    

    The version 7.0.1 Directory Services libraries are hard coded to use libldap-2.4.so.2 so if you are using Ubuntu 22.04 make sure you have the workaround applied where you install libldap-2.5-0 and create a symbolic link since libldap-2.4 is not available on Ubuntu 22.04. This is not required on Ubuntu 20.04.

    Install Certificate Authority

    Install root CA (self-signed) certificate for the LDAP server.

    • Copy .crt CA certificate to /usr/local/share/ca-certificates/
    • Run update-ca-certificates

    Check Default ldap.conf

    Check the settings in /etc/ldap/ldap.conf, to make sure there is an entry for TLS_CACERT which points to the file where update-ca-certificates installs your CA certificates. For me the default was correct.

    # TLS certificates (needed for GnuTLS)
    TLS_CACERT      /etc/ssl/certs/ca-certificates.crt
    

    C# Code

    I have found the following code to work, where:

    • ldapServerFQDN is the Fully Qualified Domain Name (FQDN) of the LDAP server you want to connect to, for example myldapserver.mydomain.com. In Windows you can connect using SSL/TLS specifying the name of the domain only and it finds an LDAP/AD server for you but that does not work in Linux. In Linux you need to specify the FQDN because it is used to check against the SAM names in the SSL/TLS certificate.
    • userName is the user name. Usually this has to be the full Distinguished Name, such as CN=myusername,OU=Users,DC=mydomain,DC=com. If the LDAP server is actually an Active Directory DC then other name formats are also accepted such as NETBIOSDOMAIN\myusername.
    • password is the password for the userName.
            private const int LDAP_PORT = 389;
            private const int LDAPS_PORT = 636;
    
            private static LdapConnection ConnectBasicNoEncryption(
                string ldapServerFQDN,
                string userName,
                string password)
            {
                //Not secure. Credentials will be sent in the clear.
    
                LdapDirectoryIdentifier id = new(ldapServerFQDN, LDAP_PORT);
    
                LdapConnection connection = new(id)
                {
                    AuthType = AuthType.Basic
                };
    
                //required for searching on root of ldap directory https://github.com/dotnet/runtime/issues/64900
                connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
    
                //also works with version 2
                connection.SessionOptions.ProtocolVersion = 3; 
    
                NetworkCredential credential = new(userName, password);
    
                connection.Bind(credential);
    
                return connection;
    
            }
    
    
    
            private static LdapConnection ConnectBasicSSL(
                string ldapServerFQDN,
                string userName,
                string password)
            {
                //Credentials will be sent encrypted.
    
                //use LDAPS port for SSL
                LdapDirectoryIdentifier id = new(ldapServerFQDN, LDAPS_PORT); 
    
                LdapConnection connection = new(id)
                {
                    AuthType = AuthType.Basic
                };
    
                //required for searching on root of ldap directory https://github.com/dotnet/runtime/issues/64900
                connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
    
                //also works with version 2
                connection.SessionOptions.ProtocolVersion = 3;
    
                //use SSL
                connection.SessionOptions.SecureSocketLayer = true;
    
                NetworkCredential credential = new(userName, password);
    
                connection.Bind(credential);
    
                return connection;
    
            }
    
            private static LdapConnection ConnectBasicTLS(
                string ldapServerFQDN,
                string userName,
                string password)
            {
                //Credentials will be sent encrypted.
    
                //use LDAP port for TLS (not LDAPS port)
                LdapDirectoryIdentifier id = new(ldapServerFQDN, LDAP_PORT); 
    
                LdapConnection connection = new(id)
                {
                    AuthType = AuthType.Basic
                };
    
                //required for searching on root of ldap directory https://github.com/dotnet/runtime/issues/64900
                connection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
    
                //must be version 3 for TLS. TLS is not supported in version 2.
                connection.SessionOptions.ProtocolVersion = 3; 
    
                //use TLS
                connection.SessionOptions.StartTransportLayerSecurity(null);
    
                NetworkCredential credential = new(userName, password);
    
                connection.Bind(credential);
    
                return connection;
    
            }
    
    

    Troubleshooting

    I found the following points useful for troubleshooting exceptions.

    Use Only AuthTypes: Anonymous, Basic, Negotiate

    According to comments on issue 66945:

    In Linux, we currently only support two types of authentication:

    • AuthType.Basic = You use credentials to authenticate. This is NOT the default value, so you do need to change your code and specifically set your AuthType to basic when using credentials. You can decide to use this over SSL or TLS so that credentials are encrypted when the connection happens.
    • AuthType.Negotiate = You don't provide credentials to authenticate, and you are running on a machine/user that is domain-joined (AD-joined) to the LDAP server. In this case, we use the underlying GSSAPI libs to authenticate using the machine/user existing kerberos token.

    All of the rest of the Authentication types are currently not supported in Linux. We do plan to add support for them, but haven't planned that work for a milestone yet.

    My Linux container is not domain-joined so I only looked at the AuthType.Basic scenario. I have also been able to connect successfully using AuthType.Anonymous (in which case you call connection.Bind(); rather than connection.Bind(credential);).

    Unsupported options may give misleading Exception messages

    There are some unsupported options which give Exception messages such as "Ldap Service is unavailable." when they really should be saying "The feature is not supported." This can make it very difficult to troubleshoot. Keep this in mind when investigating a problem based on the Exception message. There are at least two issues on the Github on this topic Issue 60972 and Issue 84825.

    The two options below are not supported in Linux and throw exceptions if you try to set them:

    • connection.SessionOptions.VerifyServerCertificate
    • connection.SessionOptions.AutoReconnect

    ldapsearch

    Running ldapsearch with options -d 1 (debug level 1) is very useful for troubleshooting certificate problems.

    tcpdump

    Running tcpdump -vvv -A -i any -l port 389 or port 636 in another session while testing LDAP connection code is useful for checking that the credentials are being encrypted - just search the output for the password you used, if you find it then your SSL/TLS session was not working.