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.
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
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.
I finally got this working. Hope this is useful for someone.
Solution for SSL/TLS connection to LDAP from:
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 root CA (self-signed) certificate for the LDAP server.
/usr/local/share/ca-certificates/
update-ca-certificates
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
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;
}
I found the following points useful for troubleshooting exceptions.
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);
).
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
Running ldapsearch with options -d 1
(debug level 1) is very useful for troubleshooting certificate problems.
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.