Saturday, September 27. 2014
Secure Login in LDAP
The main security constraint about the LDAP protocol is that passwords (in the common scenario) travel through the network in clear. If we keep in mind that this protocol is used mainly to authenticate users, the risk is even greater. This entry is going to show briefly the typical solutions for this security problem and exemplify how applications should interact with those solutions (Java examples).
Some time ago a previous entry about LDAP was presented, in that occasion the post talked about password policies. Nevertheless a simple Bind class was presented that checked the password for a user. This class used a tricky reconnect to retrieve the controls returned by the Bind, but now the solution is going to be improved in order to perform all the steps without re-login (you know, learn something new everyday). The class will be slightly modified for each different solution presented throughout the entry. In the server part two different LDAP servers will be used: OpenLDAP (default debian package working in standard ports 389 and 636) and the old OpenDS (installed apart in ports 1389 and 1636).
SASL / DIGEST-MD5
The Simple Authentication Security Layer (SASL) is a kind of wrapper that permit applications to authenticate using different security mechanisms in a generic way. SASL defines several mechanisms like Kerberos (an old friend inside the blog) or DIGEST-MD5. This time, the later is the first technique presented in the entry to avoid clear text passwords.
DIGEST-MD5 is a mechanism to negotiate the credentials invented long ago for web servers (although it can be used in any other server type). The idea is quite simple: the client sends that it wants to login using SASL/DIGEST-MD5 in the BIND; the server responds with a nonce (a random number generated by the server) and the realm (a name for the place we want to login); the client with that information, username and password performs a MD5 hash (using the nonce produces a unique hash), and sends it to the server; the server recomputes the same hash and checks if both are the same. The advantage of this algorithm is clear, the password is never sent in a clear way, but there is also a disadvantage, the server needs to know the real password of the user (it should be stored with a reversible cryptographic method).
OpenLDAP supports DIGEST-MD5 but it has two restrictions: the user password should be in clear text (not reversible but plain) and a mapping is needed between username and DNs. I followed this entry from sbahloul blog in order to configure everything properly. Nevertheless Debian now does not use slapd.conf file and you need to configure the mapping performing ldap commands against the configuration branch. The following command creates a mapping to match the username sent in DIGEST-MD5 against the entry with that username set as uid attribute:
# ldapmodify -Y EXTERNAL -H ldapi:/// dn: cn=config changetype: modify add: olcAuthzRegexp olcAuthzRegexp: uid=([^,]*),cn=digest-md5,cn=auth ldap:///dc=example,dc=com??sub?(uid=$1)
Then the password of a user was assigned in clear text:
$ ldapmodify -h localhost -p 389 -D "cn=admin,dc=example,dc=com" -W dn: uid=ricky,ou=people,dc=example,dc=com changetype: modify replace: userpassword userpassword: XXXXXXXX
Finally the following ldapsearch command performs a SASL/DIGEST-MD5 bind against OpenLDAP:
$ ldapsearch -LLL -Y DIGEST-MD5 -h localhost -p 389 -b "dc=example,dc=com" -Q -U "ricky" -w XXXXXXXX uid=ricky dn dn: uid=ricky,ou=People,dc=example,dc=com
In this command the -Y option forces DIGEST-MD5 and now the username is specified with the -U option (instead of the common -D with the full DN of the user only the username is sent, the previous mapping locates the entry with the password for that uid).
In OpenDS the configuration is very similar and with similar steps. But OpenDS just needs a reversible encryption for the password (it can be AES, 3DES, Blowfish, Clear, Base64,...), for example I setup AES as the default storage mechanism for the default policy.
./dsconfig set-password-policy-prop -h localhost -p 4444 -D "cn=Directory Manager" \ -w XXXXXXXX -X -n --policy-name "Default Password Policy" \ --set default-password-storage-scheme:AES
It is clearly a more secure solution, but the key should be accessible by the server and, in the case of OpenDS, it is stored under a special branch cn=admin data of the server (under this branch the OpenDS software stores all the different keys it uses). If that key is obtained the passwords are equally compromised.
OpenDS also uses a mapping to locate the user password from the username, the default mapping is exactly the one that was implemented in OpenLDAP (the DIGEST-MD5 mechanism uses the mapping defined in the entry cn=Exact Match,cn=Identity Mappers,cn=config which maps the username with the uid). So the same ldapsearch presented before works with OpenDS (attacking to the 1389 port instead default 389).
Finally the file DigestMD5Bind.java is the class to login using DIGEST-MD5 in Java. The main characteristic is that a new property Context.SECURITY_AUTHENTICATION is set to DIGEST-MD5 in the environment to specify that this mechanism will be used. As I commented before, the new class is different to the one presented in the previous entry because now no re-bind is done. The context is created without username and password (the connection is established but no BIND is sent) and then that information is added to the context and the reconnect executed (the real BIND is sent to the server, letting us recover controls). Using DIGEST-MD5 the Context.SECURITY_PRINCIPAL should be the username (uid mapping) and not the complete DN of the user as in a common login. The representative code is presented below.
Hashtableenv = new Hashtable (); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, args[0]); LdapContext ctx = null; try { System.out.print("Using LDAP url " + args[0] + "... "); ctx = new InitialLdapContext(env, null); System.out.println("OK"); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, args[1]); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, args[2]); ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5"); System.out.print("Connecting as " + args[1] + "... "); ctx.reconnect(null); System.out.println("OK"); }
And it can be executed with the following command (first argument is the LDAP URL, second the username and finally the password).
$ java -cp . DigestMD5Bind ldap://penance:389 ricky XXXXXXXX Using LDAP url ldap://penance:389... OK Connecting as ricky... OK
TLS
The previous login mechanism (DIGEST-MD5) avoids showing the clear password over the wire but compromises the solution storing user passwords in clear or with a reversible encryption algorithm. Those mechanisms were thought as a secondary option but never to replace the real and proper solution for this problem: TLS or SSL. The Transport Layer Security is a protocol to cryptographically secure any other TCP/IP protocol based on certificates and asymmetric keys.
In this first example the TLS mechanism will secure a previous non-secure connection. In this scenario the client opens a non-secure ldap connection and then it requests explicitly to start a TLS communication, in that moment the typical steps for securing the protocol are executed (certificate exchange, cipher selection,...). This way the application uses the same non-secure port (389 in OpenLDAP and 1389 in OpenDS) but, when the client explicitly requests it, the same socket (same port) starts a TLS session. This method avoids using a new port but the client should request explicitly the TLS connection.
In order to use TLS/SSL in OpenLDAP and OpenDS a new self-signed certificate was created (inside /etc/ldap directory).
# cd /etc/ldap # openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -subj "/C=ES/O=localhost/CN=penance" -keyout localhost.key -out localhost.crt
And the cn=config branch is modified in OpenLDAP to specify the new key and certificate file (debian instructions were followed) :
# ldapmodify -Y EXTERNAL -H ldapi:/// dn: cn=config changetype: modify add: olcTLSCertificateKeyFile olcTLSCertificateKeyFile: /etc/ldap/localhost.key - add: olcTLSCertificateFile olcTLSCertificateFile: /etc/ldap/localhost.crt
Besides the startup command should be changed, in Debian the file /etc/default/slapd is modified to include the secure port 636 in the variable SLAPD_SERVICES (that variable is used as the -h option in the slapd daemon startup, this way the process start listening in the secure port too, the new port will be used in the next scenario).
SLAPD_SERVICES="ldap:/// ldaps:/// ldapi:///"
Once the server is restarted the TLS can be tested using the following ldapsearch command in linux:
$ LDAPTLS_CACERT=/etc/ldap/localhost.crt \ ldapsearch -LLL -ZZ -H ldap://penance:389 -s base -b "dc=example,dc=com" \ -D "uid=ricky,ou=people,dc=example,dc=com" -w XXXXXXXX objectclass=* dn
The self-signed certificate is specified in the env variable LDAPTLS_CACERT as a trusted CA (PEM format) and then the option -ZZ is specified to force TLS in the communication.
Configuring OpenDS to use a certificate is even easier, it can be done at startup and this way the TLS/SSL is available at startup. The same ldapsearch presented before should work now for OpenDS in the port 1389.
Using Java the TLS should be started sending the StartTlsRequest as a extended operation (see this wonderful forum entry fro more information). The sample TlsBind.java is used to perform a TLS login, the context is again created without login (just establishing the connection), then the TLS request is executed and, finally, the login/BIND is executed. This way the BIND is performed once the secure connection is in place, so the password is sent in a secure way.
Hashtableenv = new Hashtable (); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, args[0]); LdapContext ctx = null; StartTlsResponse tls = null; try{ // create a context empty and then negotiate System.out.print("Using LDAP url " + args[0] + "... "); ctx = new InitialLdapContext(env, null); System.out.println("OK"); ExtendedRequest tlsRequest = new StartTlsRequest(); ExtendedResponse tlsResponse = ctx.extendedOperation(tlsRequest); tls = (StartTlsResponse)tlsResponse; System.out.print("Negotiating TLS... "); tls.negotiate(); System.out.println("OK"); // perform the login now that TLS is ready ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, args[1]); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, args[2]); System.out.print("Connecting as " + args[1] + "... "); ctx.reconnect(null); System.out.println("OK"); // do not send more operations the connection is closed by the ldap server }
And the previous class can be executed in a similar way to the previous digest command:
$ java -Djavax.net.ssl.trustStore=penance.jks TlsBind ldap://penance:389 uid=ricky,ou=people,dc=example,dc=com XXXXXXXX Using LDAP url ldap://penance:1389... OK Negotiating TLS... OK Connecting as uid=ricky,ou=people,dc=example,dc=com... OK
Only two tips are remarkable. First, the javax.net.ssl.trustStore should be set to add the self-signed certificate as trusted for the Java execution. Second, in all the documentation I have read (for example the previous forum entry or the official Java documentation) it is commented that the previous connection can be re-used once the TLS session is closed. Nevertheless I have checked that the connection is always closed by the server (both, OpenLDAP and OpenDS) as soon as the client closes the TLS request. I suppose that TLS standard admits this behavior, so please take this in mind, once the TLS connection is established you should continue using it until the end, you cannot come back to the non-secure connection.
SSL
The final solution is just using the SSL port (default ldaps port 636 in OpenLDAP and 1636 in OpenDS). With this configuration all the communication is directly encrypted and therefore nothing is sent in plain. SSL was already configured in the previous point for both servers. In order to test the secure port a new ldapsearch command can be executed.
$ LDAPTLS_CACERT=/etc/ldap/localhost.crt \ ldapsearch -LLL -H ldaps://penance:636 -s base -b "dc=example,dc=com" \ -D "uid=ricky,ou=people,dc=example,dc=com" -w XXXXXXXX objectclass=* dn
The new Bind.java is very similar to the presented in the previous entry (this solution represents almost zero code change). Now only the Context.SECURITY_PROTOCOL is modified (it should be ssl for ldaps and plain to non-secure ldap), I just guessed the value depending the URL of the connection (it will be ssl if the URL starts with ldaps://, plain if not) and the same code works for secure and non-secure situations.
Hashtableenv = new Hashtable (); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, args[0]); env.put(Context.SECURITY_PROTOCOL, args[0].startsWith("ldaps://")? "ssl" : "plain"); LdapContext ctx = null; try { System.out.print("Using LDAP url " + args[0] + "... "); ctx = new InitialLdapContext(env, null); System.out.println("OK"); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, args[1]); ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, args[2]); System.out.print("Connecting as user " + args[1] + "... "); ctx.reconnect(null); System.out.println("OK"); }
The main class file is executed this way:
$ java -Djavax.net.ssl.trustStore=penance.jks -cp . Bind ldaps://localhost uid=ricky,ou=people,dc=example,dc=com XXXXXXXX Using LDAP url ldaps://localhost... OK Connecting as user uid=ricky,ou=people,dc=example,dc=com... OK
Again the javax.net.ssl.trustStore system property should be specified in order to trust in the self-signed certificate configured in both servers.
Today's entry is simple and direct, LDAP servers usually authenticate users receiving passwords in clear over the network, the post has commented three common techniques to encrypt them. The first method used DIGEST-MD5 which avoids clear passwords without using TLS/SSL. This algorithm needs the server to know the password of the user (that means passwords should be stored in clear or, at least, in a reversible cryptographic algorithm inside the server). Nevertheless the definitive solution for this problem is using ldaps (ldap over SSL/TLS). The entry has exemplified two situations: starting TLS over non-secure connection and using directly a secure SSL port. Obviously the second option is the recommended cos it is easier to develop inside pre-existing applications but both are equally secure.
Cheerio!
Comments