One of the more glaring issues with the environment I’m currently supporting is that there’s no single sign on. Workstations and web tools (mostly Atlassian in nature – Stash, Jira, Confluence, et al) all authenticate against our Active Directory environment. However, all of our Linux and Solaris hosts authenticate against a separate OpenLDAP environment, so users have to maintain two different sets of credentials and passwords. This has all the extra baggage that comes from maintaining password policies, such as two different auth sources that you can get locked out from. I elected to take up the task to eliminate one of the environments, and given that our OpenLDAP database was significantly smaller than Active Directory, we decided to eliminate OpenLDAP from the environment.
I considered three approaches for achieving this. The quick and dirty method is to use SASL passthrough authentication, which is supported by OpenLDAP as detailed in this page. In short, you can take an existing user account in OpenLDAP and modify their userPassword attribute, replacing the existing hash with “{SASL}user@domain” which will point to a matching user in Active Directory. While this would satisfy the requirement of single sign-on, it still requires that you maintain an OpenLDAP backend database for all other data, such as homeDirectory, loginShell, uid, gid, and so on.
A second option is to not use an OpenLDAP server at all, and configure all your clients to natively talk to Active Directory. This isn’t terribly difficult as long as you’re using SSSD – see this excellent article on the subject. However, I’d have to gut a large amount of the environment to move to sssd, and it requires some software bloat – namely, Samba – in order to work. While this wasn’t necessarily a dealbreaker, I didn’t think it was the cleanest option.
The third method – and ultimately is the one we chose to pursue – is to continue leveraging an OpenLDAP backend, but proxy the requests to Active Directory. This has the benefit of utilizing a single source of authentication (AD, in our case) and allows us to continue using the native OpenLDAP client that all our hosts are currently utilizing without any additional configuration. Additionally, it would allow any applications that have weaker LDAP client support to continue to work. The OpenLDAP proxy can also remap fields on the fly, taking an OpenLDAP attribute and remap it to its AD equivalent – translating “uid” to “sAMAccountName”, for example. This allows for maximum flexibility without requiring any special configuration on the client side. The only real downside is that you add an additional layer of complexity to the flow of authentication, which means one extra spot you may have to troubleshoot in the event of auth issues. Still, it seemed this approach would be the best one for us.
This article in the Samba wiki has a good starting point for your configurations, but it will definitely require some additional modifications. To get started, the only package you’ll need to pull in from your repo is openldap-servers. It will grab all the necessary dependencies on its own.
First and foremost, you’ll want to secure this via TLS. I won’t go into the details of generating or acquiring an SSL certificate, since this thing called Google exists. Once you have them, you’ll need to place them somewhere that slapd can read from as the ldap user, and where SELinux won’t get in the way – generally, /etc/pki/tls/certs is as good a place as any on RHEL/CentOS hosts. Here is the relevant bits from slapd.conf that you need to worry about:
TLSCipherSuite HIGH:!NULL
TLSCACertificateFile /etc/pki/tls/certs/cacert.pem
TLSCertificateFile /etc/pki/tls/certs/slapd.pem
TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem
TLSVerifyClient never
# Disallow non-encrypted binds - this will refuse any connection that isn't
# secured with at least 128-bit encryption
security ssf=128
A couple of notes here:
- – Adding a !NULL in TLSCipherSuite disallows null ciphers – that is, it will not allow unencrypted binds and lookups. When you’re talking about a host that will be directly handling passwords, this seems like a no-brainer to me. You don’t want any schmuck running a tcpdump on a host that’s talking to OpenLDAP to be able to sniff passwords at their leisure because of a misconfiguration on the LDAP client.
– The security directive dictates the minimum acceptable security strength factors, or SSF, that are allowed to connect. In this case, slapd will refuse to honor any bind that isn’t secured with at least AES 128-bit encryption. This (probably) negates the need for !NULL in the TLSCipherSuite directive, but it doesn’t hurt, either.
– In case you were wondering – yes, this listens on port 389, which is the standard LDAP port, not the LDAPS port. However, with the minimum ssf set and null ciphers disallowed, the ldap result will be empty unless the bind begins with STARTTLS, thereby encryption the rest of the transaction. Otherwise, you’ll get a result like this if you try to insecurely bind:
# search result
search: 2
result: 13 Confidentiality required
text: confidentiality required
– You can also allow LDAPS if you’d like, although its usage is deprecated in favor of using STARTTLS. This is done by editing /etc/sysconfig/ldap and changing “SLAPD_LDAPS” to “yes”, and then restarting the slapd service.
Next, we have the meaty bits of our slapd.conf to worry about. Here is where we configure the AD backend.
### Database definition (Proxy to AD) #########################################
database ldap
readonly yes
protocol-version 3
rebind-as-user yes
uri "ldap://adserver:389"
suffix "dc=base,dc=dn"
idassert-bind bindmethod=simple
mode=none
binddn="CN=SVC-LDAP-READ,OU=SERVICE ACCOUNTS,OU=USERS,DC=base,DC=dn"
credentials=<%= @ldapproxypwd %>
starttls=yes
tls_cacertdir=/etc/pki/tls/certs
tls_reqcert=never
idassert-authzFrom "*"
overlay rwm
rwm-map attribute uid sAMAccountName
rwm-map attribute homeDirectory unixHomeDirectory
rwm-map objectClass posixAccount person
- - To the best of my knowledge, Active Directory does not allow anonymous binds, at least by default. In order for slapd to proxy authentication transactions to AD, it requires a read-only account - preferably a service account with a non-expiring password - in order to connect. You also want this connection to be secure from prying eyes, hence enabling starttls in the idassert-bind. The "binddn" directive is just that, the distinguished name of the service account in AD that will be performing the bind operation, and "credentials" is just the plaintext password for this.
- I searched high and low through the OpenLDAP documentation and user groups to find out if it was possible to encrypt or hash the password for this AD service account used in idassert-bind, and it seems as if this is not possible. This sort of sucks, but I was able to work around it by leveraging chef. My slapd.conf is a chef template, with the password passed to the template via a variable (ldapproxypwd) kept in an encrypted databag inside of chef. Note this only protects the password in the template file itself in chef! Once chef runs, the password appears in plaintext in the file. Make sure that you keep slapd.conf readable only be root and the ldap user on the host that you deploy this on.
- The rwm overlay allows for the remapping of attributes and object classes that I mentioned earlier. In this case, we remap uid to sAMAccountName, homeDirectory to unixHomeDirectory (because AD already has a homeDirectory attribute). It also remaps the posixAccount object class to "person" in AD, since posixAccount is not a valid object class in Active Directory, and many OpenLDAP clients try and filter on (&(objectClass=posixAccount)(uid=foo)) when doing authentication. This is a tidy little workaround that negates the need for extra client configuration and needing to use nslcd or sssd.
- It's probably best practice to actually get a copy of your Active Directory CA cert and install it on your proxy hosts; I haven't done so here, because Reasons. This will simply connect via TLS to your Active Directory domain controller and disable cert checking to see if it's a valid CA. For my purposes, it was not needed. This will encrypt all traffic to your domain controllers regardless, which is Good Enough for the moment.
That's more or less all you need. There's a few small gotchas related to certificates and slapd, if you're using a relatively recent version of it.
- Certificates for OpenLDAP need to be hashed. This drove me absolutely mad for a while until I stumbled across this bug report which detailed that cacertdir_rehash needs to be run against whichever folder you keep your certificates. Why? I'm not sure, exactly. I'd love to read a good explanation as to why this needs to happen.
- As of OpenLDAP 2.4.23, slapd.conf is not read natively by the slapd service. Instead, it reads its configurations from /etc/openldap/slapd.d, and all the configurations are kept in LDIF format. This has the benefit of allowing you to make changes to the configuration on the fly without having to restart slapd, but its formatting is completely awful. Instead, you can put slapd.conf in its normal location, and then dynamically generate the contents of the slapd.d folder by running:
slaptest -f /etc/openldap/slapd.conf -F /etc/openldap/slapd.d
Either run this command as the ldap user to ensure permissions are correct, or recursively chown the target folder as ldap:ldap to make sure that slapd can read everything correctly.
- Your clients will also need a copy of your CA certificate to be able to find unless you disable cert checking. The default location is /etc/openldap/cacerts, and you'll also need to run cacertdir_rehash once the certificate is placed there.
I'm in the middle of rolling this out through our various test environments, but if I have any final words of wisdom, I'll write a follow-up post with lessons learned.
For reference, here are the full, lightly-redacted contents of my slapd.conf. For maximum availability, I recommend having more than one host running this, and stand up a load balancer in front of it. While you're at it, create a load balancer VIP for your AD Domain Controllers to use as the target LDAP uri in your slapd.conf, too!
### Schema includes ###########################################################
include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/misc.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/ad.schema
## Module paths ##############################################################
modulepath /usr/lib64/openldap/
moduleload back_ldap
moduleload rwm
### Logging ###################################################################
logfile /var/log/slapd/slapd.log
loglevel 256
# Main settings ###############################################################
pidfile /var/run/openldap/slapd.pid
argsfile /var/run/openldap/slapd.args
TLSCipherSuite HIGH:!NULL
TLSCACertificateFile /etc/pki/tls/certs/cacert.pem
TLSCertificateFile /etc/pki/tls/certs/slapd.pem
TLSCertificateKeyFile /etc/pki/tls/certs/slapd.pem
TLSVerifyClient never
# Disallow non-encrypted binds - this will refuse any connection that isn't
# secured with at least 128-bit encryption
security ssf=128
# Allow v2 binding for legacy clients #########################################
allow bind_v2
### Database definition (Proxy to AD) #########################################
database ldap
readonly yes
protocol-version 3
rebind-as-user yes
uri "ldap://adserver:389"
suffix "dc=base,dc=dn"
idassert-bind bindmethod=simple
mode=none
binddn="CN=SVC-LDAP-READ,OU=SERVICE ACCOUNTS,OU=USERS,DC=base,DC=dn"
credentials=<%= @ldapproxypwd %>
starttls=yes
tls_cacertdir=/etc/pki/tls/certs
tls_reqcert=never
idassert-authzFrom "*"
overlay rwm
rwm-map attribute uid sAMAccountName
rwm-map attribute homeDirectory unixHomeDirectory
rwm-map objectClass posixAccount person
I welcome feedback, if you've done something similar or have any suggestions to improve upon this implementation. Drop me a line if you have.