TLS linux cache ldap client and windows AD

Primary tabs

LDAP, Caché

Some key points are emphasized in this article in order to save your time to get linux ldap client in cache working with windows AD (active directory) LDAP server.
The first thing to do is to get successful TLS connection to windows AD.
Raw tcp case is beyond of this article, there is no problem with it, it is trivial.
Windows ldap server uses port 636 for tls and this port can be used to get ldap certificate.
As we will see later there is reason for this.
linux ldap client uses STARTTLS special ldap extension to switch plain tcp to TLS only.
Hence, connection should use port 389.
After TLS connection, openldap client lib checks host domain name used in ldap Init() call against some fields of certificate of ldap server.
That is why it is worth to get certificate using openssl tool in order to be sure that this check will succed.
Unfortunately openldap client lib has pure diagnostic.
You can not distinguish case of failure TLS handshake and failure of hostname check described generally above.
Moreover openldap client lib doesn't report the details of TLS handshake failure by calling SSL_get_error() openssl api after SSL_connect().
For all cases one is reported one failure whether it is TLS handshake error or hostname check error.
Openldap client lib has an option to switch off hostname check but cache callout wrapper ldap.so doesn't support it.
Certificate supplied to openldap client might be ldap certificate (in this case certificate is verified on identity by itself) or AD CA certificate.
By the way, ldap certificate is signed by this CA certificate.
How to get CA certificate from domain controller.
Go to Administrative Tools, find License Authority. One runs it, it has certsrv text in title and one is possible to export CA certificate in pem format. Following picture gives clue how to do it.

certsrv (if picture is not shown due to html resize it is in attach)

Next is required to get certificate from ldap to clarify what host name in certificate.
Certificate Subject is important. openldap makes check of host connected to against domain name build from ldap certificate Subject.
(For details see openldap lib source ./libraries/libldap/tls.c function ldap_pvt_tls_check_hostname() )
Run command for ip and port 636 of AD ldap.
openssl s_client -connect 192.168.10.61:636

...
Subject: CN = w2k16dc.test16.org
Issuer: DC=org, DC=test16, CN=test16-W2K16DC-CA
...

You can copy and paste certificate pem text into file from output of this command.
openssl x509 -in ldap.test16.org -noout -text

    Signature Algorithm: sha256WithRSAEncryption
        Issuer: DC = org, DC = test16, CN = test16-W2K16DC-CA
        Validity
            Not Before: Apr  4 09:54:15 2017 GMT
            Not After : Apr  4 09:54:15 2018 GMT
        Subject: CN = w2k16dc.test16.org
        Subject Public Key Info:

Certificate pem part is copied in w2k16dc.pem. CA certificate is w2k16dc.ca.pem file.
In given case it is simple - host name w2k16dc.test.org is used.
Subject might be in form DC=org,DC=test16,CN=w2k16dc which means the same w2k16dc.test16.org.
It makes sense set ip in /etc/hosts to make dns resolving local.

192.168.10.61 w2k16dc.test16.org

Check dns is resolved to right ip

ping w2k16dc.test16.org 

Now it is possible to check initial TLS connection and hostname check to AD ldap from linux
ex.ldap.lxtoad.MAC

 #include %syLDAP

check(host,certfile,user,password)
 set LD=##Class(%SYS.LDAP).Init(host,389)
 if LD=0 {
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w err_" "_msg,! quit 0
 }

 set retCert=##Class(%SYS.LDAP).SetOption(LD,$$$LDAPOPTXTLSCACERTFILE,certfile)
 if retCert'=$$$LDAPSUCCESS {
  W "SetOption() CACERTFILE",!   
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w err_" "_msg,! quit 0        
 }    
 set retTLS=##Class(%SYS.LDAP).StartTLSs(LD)
 if retTLS'=$$$LDAPSUCCESS {
   W "StartTLS()",!  
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w err_" "_msg,! quit 0    
 }   

 set retBind=##Class(%SYS.LDAP).SimpleBinds(LD,username,password)
 if retBind'=$$$LDAPSUCCESS {
   W "SimpleBinds()",!   
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w err_" "_msg,! quit 0    

 }

 set n=$L(host,".")
 set base="" for i=2:1:n set base=base_",DC="_$P(host,".",i)
 set base="CN=Users"_base
 W "base="_base,!
 do work(base) 

 set retUnBind=##Class(%SYS.LDAP).UnBinds(LD)
 if retUnBind'=$$$LDAPSUCCESS {
   W "SimpleBinds()",!   
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w err_" "_msg,! quit 0    

 }  

 quit

work(base)
 set filter="(objectClass=*)"
 set ret4=##Class(%SYS.LDAP).SearchExts(LD,base, $$$LDAPSCOPESUBTREE,
     filter, "",, 
     ,,,, .Res
  ) 
 if ret4'=$$$LDAPSUCCESS {
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w "SearchExts failure "_err_" "_msg,!     
 } 

 set ret6=##Class(%SYS.LDAP).CountEntries(LD,Res)
 if ret6'=-1 {
    W "entries ",ret6,!  
    set e=##Class(%SYS.LDAP).FirstEntry(LD,Res)
    if e>0 {
      set dn=##class(%SYS.LDAP).GetDN(LD,e)
      W dn,!  
      D attrs(LD,dn,e)
      for {
        set e=##Class(%SYS.LDAP).NextEntry(LD,e)
        if e<1 quit  
        set dn=##class(%SYS.LDAP).GetDN(LD,e)
        W dn,!          
        D attrs(LD,dn,e)
      }             
    }    
 }   
 else {
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w "CountEntries failure "_err_" "_msg,!    
 }   

 set ret5=##Class(%SYS.LDAP).MsgFree(Res) 
 if ret5'=$$$LDAPSUCCESS {
   s err=##Class(%SYS.LDAP).GetLastError()
   s msg=##Class(%SYS.LDAP).Err2String(err)
   w "MsgFree "_err_" "_msg,!        
 }   
 quit

attrs(LD,dn,e)
 s AttrName=##Class(%SYS.LDAP).FirstAttribute(LD,e,.Ptr)

 while AttrName'="" {
    s AttrValue=##Class(%SYS.LDAP).GetValues(LD,e,AttrName) 
    W AttrName_"="_AttrValue,!
    set Users(dn,AttrName)=AttrValue
    s AttrName=##Class(%SYS.LDAP).NextAttribute(LD,e,.Ptr)   
 }
 quit

And now run it

 set host="w2k16dc.test16.org"
 set certfile="/isc/ex/cos/ldap/w2k16dc.ca.pem" ; don't forget to check linux file permission cache is able to read
 set username="YourUserName"
 set password="YourPassword"
 d check^ex.ldap.lxtoad(host,certfile,username,password)

If something goes wrong ldap api step is reported - fix issue of step in case of problem.

USER>d check^ex.ldap.lxtoad(host,certfile,username,password)
StartTLS()
20481 Unsupported LDAP operation for platform.

Error will be the same for different cases. In my case problem is that host name is not in /etc/hosts
so it is likely dns name resolving fails.
If all goes right output is something like

base=CN=Users,DC=test16,DC=org
entries 30
CN=Users,DC=test16,DC=org

LDAP certificate w2k16dc.pem also should work. What to use is up to you. I note that CA certificate 5-10 years, LDAP certificate 1 year.
I recommend this test before any other settings, because as more high level code is used then less clear what fails.
As practice shows people lose much time to get it working tens hours or many days and might finally fail.
These notes and this test might minimize time to localize and fix problem and get base of LDAP to work right.
Next is configuring LDAP authentication. The main problem here is to set config fields to right value.
I cite my example and this show idea how to set right values for your case.
Idea of settings

LDAP server is a Windows Active Directory server on

LDAP host names w2k16dc.test16.org

LDAP username to use for searches   
CN=domadm,CN=Users,DC=test16,DC=org

LDAP Base DN to use for searches
CN=Users,DC=test16,DC=org

LDAP Unique search attribute CN
Use TLS/SSL encryption for LDAP sessions    

The same in term of ^SECURITY utility

%SYS>D ^SECURITY


1) User setup
2) Role setup
3) Service setup
4) Resource setup
5) Application setup
6) Auditing setup
7) Domain setup
8) SSL configuration setup
9) Mobile phone service provider setup
10) OpenAM Identity Services setup
11) Encryption key setup
12) System parameter setup
13) X509 User setup
14) Exit

Option? 12

1) Edit system options
2) Edit authentication options
3) Edit LDAP options
4) Display system options
5) Export All Security settings
6) Import All Security settings
7) Exit

Option? 3

1) Edit LDAP options
2) Test LDAP authentication
3) Exit

Option? 1
Is the LDAP server a Windows Active Directory server? Yes => Yes
LDAP host names? => w2k16dc.test16.org 
LDAP DN user to use for searches?  => CN=domadm,CN=Users,DC=test16,DC=org
LDAP username password? ***** => 
Please enter the password again? 
LDAP Base DN to use for searches? => CN=Users,DC=test16,DC=org
LDAP Unique search attribute? CN => 
Use TLS/SSL encryption for LDAP sessions? Yes => Yes
TLS/SSL certificate file? => /isc/ex/cos/ldap/w2k16dc.ca.pem
User attribute to retrieve comment attribute? description => 
User attribute to retrieve full name from? displayName => 
Use LDAP Groups for Roles/Routine/Namespace? No => No
User attribute to retrieve default namespace? intersystems-Namespace => 
User attribute to retrieve default routine? intersystems-Routine => 
User attribute to retrieve roles? intersystems-Roles => 
LDAP attribute to retrieve for each user? 
LDAP Client Timeout? 10 => 
LDAP Server Timeout? 5 => 
Confirm changes to LDAP parameters? Yes => Yes
System LDAP parameters updated

LDAP setting might be configured in SMP (system manamegment portal) and I don't expect problem translate setting for that case.
Now time to test authentication. Here I used domadm acccount which is domain administrator.
It is too strong. Later you can change user which has only ldap search right but for launch ldap authentication
it is ok. Create some one more user for example user1 in Active Directory Users and Computers tool.

1) Edit LDAP options
2) Test LDAP authentication
3) Exit

Option? 2
Username to authenticate? user1
Password? 
Starting background process to perform LDAP authentication
Waiting for LDAP authentication to start
Starting LDAP authentication test
Initializing LDAP connection to w2k16dc.test16.org
Starting TLS
Certificate: /mnt/d/isc/ex/cos/ldap/w2k16dc.ca.pem
TLS Started
Searching Root DSE
Server supports LDAP versions: 3;2;
Attempting to retrieve Base DN from defaultNamingContext
Retrieved LDAP Base DN: DC=test16,DC=org
Configured Base DN:     CN=Users,DC=test16,DC=org
Configured Base DN might be incorrect
Now authenticating search user: CN=domadm,CN=Users,DC=test16,DC=org
Search user CN=domadm,CN=Users,DC=test16,DC=org authenticated
Searching for user user1
Username nz found
DN: CN=user1,CN=Users,DC=test16,DC=org
Retrieving nested groups for user user1
Found 2 nested groups for user user in .062082 seconds
Authenticating user nz
User user1 authenticated
Retrieving user nz attributes
Full name: user1
Comment:   Decoding error description, please update config attribute
Namespace: Decoding error intersystems-Namespace, please update config attribute
Routine:   Decoding error intersystems-Routine, please update config attribute
Using roles from Attribute intersystems-Roles
Roles:     Decoding error intersystems-Roles, please update config attribute
Roles on this instance:
Number of groups:        3
Number of nested groups: 2
Groups:
   CN=Schema Admins,CN=Users,DC=test16,DC=org
   CN=Remote Desktop Users,CN=Builtin,DC=test16,DC=org
   CN=Users,CN=Builtin,DC=test16,DC=org
Nested Groups:
   CN=Schema Admins,CN=Users,DC=test16,DC=org
   CN=Denied RODC Password Replication Group,CN=Users,DC=test16,DC=org
WARNING: Parameter "Role required to connect to this system" is not set
WARNING: Any LDAP user may be able to connect to this system
User user1  will be able to connect to this system
Testing authentication settings
Not using proxy server
Authentication settings test ok
Testing authentication performance
Authenticated user user1 40 times in 12.43578 seconds - 3.217/sec
Test completed

The same in programmatic style do TEST^%SYS.LDAP("user1",password).
Already there is ldap extention schema for cache (this is next subject soon ) so your output might be shorter.
Final step is adding cache ldap schema extension to AD. Be accurate here because being imported schema can not be deleted.
Here is cite of ldief file create_isc_schema.ldf

# intersystems schema for attribute Namespace Routine Roles
# Nick.Zhokhov@intersystems.com

#https://technet.microsoft.com/en-us/library/cc773309 AD Schema
# https://msdn.microsoft.com/en-us/library/cc223177.aspx syntax

# schemaIDGUID is not required will be generated automatically at import
# attributeSecurityGUID the same.

# 1.3.6.1.4.1.1466.115.121.1.5 syntax is not accepted by AD
# one is used 2.5.5.12 String(Unicode) oMSyntax 64
# https://msdn.microsoft.com/en-us/library/cc223177.aspx

# namespace
dn: CN=intersystems-Namespace,CN=Schema,CN=Configuration,DC=X
changetype: ntdsSchemaAdd
objectClass: top
objectClass: attributeSchema
cn: iscNamespace
attributeID: 1.2.840.113556.1.8000.2448.2.1
#attributeSyntax: 1.3.6.1.4.1.1466.115.121.1.5
attributeSyntax: 2.5.5.12
isSingleValued: TRUE
adminDisplayName: intersystems-Namespace
adminDescription: intersystems-Namespace
oMSyntax: 64
searchFlags: 1
lDAPDisplayName: intersystems-Namespace
systemOnly: FALSE
#schemaIDGUID:: IdIDCQyMSUK960Rg3LgiXQ==

# routine
dn: CN=intersystems-Routine,CN=Schema,CN=Configuration,DC=X
changetype: ntdsSchemaAdd
objectClass: top
objectClass: attributeSchema
cn: iscRoutine
attributeID: 1.2.840.113556.1.8000.2448.2.2
#attributeSyntax: 1.3.6.1.4.1.1466.115.121.1.5
attributeSyntax: 2.5.5.12
isSingleValued: TRUE
adminDisplayName: intersystems-Routine
adminDescription: intersystems-Routine
oMSyntax: 64
searchFlags: 1
lDAPDisplayName: intersystems-Routine
systemOnly: FALSE
#schemaIDGUID:: P2yKMWNXTgOsEuNMjet90g==

# roles
# routine
dn: CN=intersystems-Roles,CN=Schema,CN=Configuration,DC=X
changetype: ntdsschemaadd
objectClass: top
objectClass: attributeSchema
cn: iscRoles
attributeID: 1.2.840.113556.1.8000.2448.2.3
#attributeSyntax: 1.3.6.1.4.1.1466.115.121.1.5
attributeSyntax: 2.5.5.12
isSingleValued: FALSE
adminDisplayName: intersystems-Roles
adminDescription: intersystems-Roles
oMSyntax: 64
searchFlags: 1
lDAPDisplayName: intersystems-Roles
systemOnly: FALSE
#schemaIDGUID:: ksTBxxhAQoCOiBHByViOvg==

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

# Classes

dn: CN=IntersystemsAccount,CN=Schema,CN=Configuration,DC=X
changetype: ntdsSchemaAdd
objectClass: top
objectClass: classSchema
cn: IntersystemsAccount
governsID: 1.2.840.113556.1.8000.2448.1.1
mayContain: intersystems-Namespace
mayContain: intersystems-Routine
mayContain: intersystems-Roles
rDNAttID: cn
adminDisplayName: IntersystemsAccount
adminDescription: IntersystemsAccount
objectClassCategory: 3
lDAPDisplayName: IntersystemsAccount
name: IntersystemsAccount 
systemOnly: FALSE
#schemaIDGUID:: bEqSlFeMT7C5vPFyUXnqpQ==

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

dn: CN=User,CN=Schema,CN=Configuration,DC=X
changetype: ntdsschemamodify
add: auxiliaryClass
auxiliaryClass: IntersystemsAccount
-

dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-

Schema might to be loaded in AD by command

ldifde -i -f create_isc_schema.ldf -c "CN=Schema,CN=Configuration,DC=X" #schemaNamingContext

ADSI Edit which is standard tool in windows might be used to populate value of attributes intersystems-*.
From cache it might be done using %SYS.LDAP class.

Final step is to probe cache authentication and authorization.
Set system level ldap authentication and authorization to enable and set it for some service for example
terminal. Check login is ok and namespace is that pointed in namespace AD LDAP attribute.

  • + 8
  • 0
  • 1165
  • 0

Attached documents