LDAP (Lightweight Directory Access Protocol) is an open and cross platform protocol used for directory services authentication.
LDAP provides the communication language that applications use to communicate with other directory services servers. Directory services store the users, passwords, and computer accounts, and share that information with other entities on the network.
OpenLDAP is a free, open-source implementation of the Lightweight Directory Access Protocol (LDAP) developed by the OpenLDAP Project. It is released under its own BSD-style license called the OpenLDAP Public License.
There are 2 commonly used mechanisms to secure LDAP traffic - LDAPS and StartTLS. LDAPS is deprecated in favor of Start TLS [RFC2830].
During some recent infrastructure changes I found out the hard way that LDAP plugin for Jenkins does not support LDAP over TLS (StartTLS). Given that LDAPS is officially deprecated, I began work on a PR to add StartTLS support myself.
Before I could start coding, I needed to create a local development environment with an LDAP server speaking StartTLS. Unfortunately, this was harder than I anticipated, as StartTLS (while officially supported since LDAPv3) is not well documented.
In the following post ,I’ll show you how to get OpenLDAP up and running with StartTLS, using valid certificates from LetsEncrypt.
As always, the code is Open Source and lives on Github:
Self-Signed vs Trusted CA Certificates
There are two types of SSL Certificates when you’re talking about signing. There are Self-Signed SSL Certificates and certificates that are signed by a Trusted Certificate Authority (and are usually already trusted by your system).
Most OpenLDAP documentation I was able to find used Self-Signed certifates. While that works fine for most development, I am trying to replicate a production-like environment, which means real, trusted certificates. Thankfully, we can utilize short-lived trusted certificates provided by LetsEncrypt to secure our test OpenLDAP server.
Generate LetsEncrypt Certificate
mkdir data # We cannot use a wildcard domain with OpenLDAP, so let's pick a simple obvious subdomain. echo "ldap.example.com" > data/domains.txt docker run --rm \ -v `pwd`/data:/data \ -e DEHYDRATED_GENERATE_CONFIG=yes \ -e DEHYDRATED_CA="https://acme-v02.api.letsencrypt.org/directory" \ -e DEHYDRATED_CHALLENGE="dns-01" \ -e DEHYDRATED_KEYSIZE="4096" \ -e DEHYDRATED_HOOK="/usr/local/bin/lexicon-hook" \ -e DEHYDRATED_RENEW_DAYS="30" \ -e DEHYDRATED_KEY_RENEW="yes" \ -e DEHYDRATED_ACCEPT_TERMS=yes \ -e DEHYDRATED_EMAIL="email@example.com" \ -e PROVIDER=cloudflare \ -e LEXICON_CLOUDFLARE_USERNAME="mycloudflareusername" \ -e LEXICON_CLOUDFLARE_TOKEN="mycloudflaretoken" \ docker.io/matrixdotorg/dehydrated
NOTE: pay attention to those last 3 environmental variables. They are passed to lexicon and should be changed to match your DNS provider.
dehydrated prints its success messge , you should see a handful of new subfolders in
data ├── accounts │ └── xxxxxxxxxxxxxx │ ├── account_id.json │ ├── account_key.pem │ └── registration_info.json ├── certs │ └── ldap.example.com │ ├── cert-xxxxxx.csr │ ├── cert-xxxxxx.pem │ ├── cert.csr -> cert-xxxxxx.csr │ ├── cert.pem -> cert-xxxxxx.pem │ ├── chain-xxxxxx.pem │ ├── chain.pem -> chain-xxxxxx.pem │ ├── combined.pem │ ├── fullchain-xxxxxx.pem │ ├── fullchain.pem -> fullchain-xxxxxx.pem │ ├── privkey-xxxxxx.pem │ └── privkey.pem -> privkey-xxxxxx.pem ├── chains ├── config └── domains.txt
Let’s leave these files alone for now, and continue to standing up and configuring our OpenLDAP server.
Deploying OpenLDAP via Docker
Since we’re not actually deploying a production instance (with HA/monitoring/security hardening/etc) we can take some short-cuts and use an off-the-shelf Docker image.
The analogj/docker-openldap-starttls image we’re using in the example below is based on the rroemhild/test-openldap Docker image, which provies a vanilla install of OpenLDAP, and adds Futurama characters as test users.
I’ve customized it to add support for custom Domains, dynamic configuration & the ability to enforce StartTLS on the serverside (which is great for testing).
Before we start the OpenLDAP container, lets rename and re-organize our LetsEncrypt certificates in a folder structure that the container expects:
mkdir -p ldap cp data/fullchain.pem ldap/fullchain.crt cp data/cert.pem ldap/ldap.crt cp data/privkey.pem ldap/ldap.key
Next, lets start the OpenLDAP Docker container:
docker run --rm \ -v `pwd`/ldap:/etc/ldap/ssl/ \ -p 10389:10389 \ -p 10636:10636 \ -e LDAP_DOMAIN="example.com" \ -e LDAP_BASEDN="dc=example,dc=com" \ -e LDAP_ORGANISATION="Custom Organization Name, Example Inc." \ -e LDAP_BINDDN="cn=admin,dc=example,dc=com" \ -e LDAP_FORCE_STARTTLS="true" \ ghcr.io/analogj/docker-openldap-starttls:master
LDAP_DOMAINshould be your root domain (
ldap.example.comfrom your certificate). It’s used for test user email addresses.
Pay attention to the
LDAP_BINDDNvariables, they should match your Domain root as well.
LDAP_FORCE_STARTTLS=trueis optional, you can use it to conditionally start your LDAP server with StartTLS enforced.
If everything is correct, you should see
slapd starting as your last log message.
Lets test that the container is responding correctly, though the certificate will not match since we’re going to query it
# LDAPTLS_REQCERT=never tells ldapsearch to skip certificate validation # -Z is required if we used LDAP_FORCE_STARTTLS="true" to start the container. LDAPTLS_REQCERT=never ldapsearch -H ldap://localhost:10389 -Z -x -b "ou=people,dc=example,dc=com" -D "cn=admin,dc=example,dc=com" -w GoodNewsEveryone "(objectClass=inetOrgPerson)" # ... # search result # search: 3 # result: 0 Success # # numResponses: 8 # numEntries: 7
Wiring up DNS to correctly resolve to the new container running on you host is left as a exercise for the user.
For testing, I just setup a simple A record pointing
ldap.example.com to my laptop’s private IP address
It obviously won’t resolve correctly outside my home network, but it works fine for testing.
$ ping ldap.example.com PING ldap.example.com (192.168.0.123): 56 data bytes 64 bytes from 192.168.0.123: icmp_seq=0 ttl=64 time=0.045 ms
NOTE: Remember, DNS updates can take a while to propagate. You’ll want to set a low TTL for the new record if your IP will be changing constantly (DHCP). You may also need to flush your DNS cache if the changes do not propagate correctly.
You can test that the container is up and running (and accessible via our custom domain) with some handy
# List all Users (only works with LDAP_FORCE_STARTTLS=false) ldapsearch -H ldap://ldap.example.com:10389 -x -b "ou=people,dc=example,dc=com" -D "cn=admin,dc=example,dc=com" -w GoodNewsEveryone "(objectClass=inetOrgPerson)" # Response: # ldap_bind: Confidentiality required (13) # additional info: TLS confidentiality required # Request StartTLS (works with LDAP_FORCE_STARTTLS=true/false) ldapsearch -H ldap://ldap.example.com:10389 -Z -x -b "ou=people,dc=example,dc=com" -D "cn=admin,dc=example,dc=com" -w GoodNewsEveryone "(objectClass=inetOrgPerson)" # Enforce StartTLS (only works with LDAP_FORCE_STARTTLS=true) ldapsearch -H ldap://example:10389 -ZZ -x -b "ou=people,dc=example,dc=com" -D "cn=admin,dc=example,dc=com" -w GoodNewsEveryone "(objectClass=inetOrgPerson)" # Query Open LDAP using Localhost url, also works with self-signed certs (-ZZ forces StartTLS) LDAPTLS_REQCERT=never ldapsearch -H ldap://localhost:10389 -ZZ -x -b "ou=people,dc=example,dc=com" -D "cn=admin,dc=example,dc=com" -w GoodNewsEveryone "(objectClass=inetOrgPerson)"
How does it work?
Other than my changes that allow you to customize the domain, there are only 2 main changes from rroemhild’s amazing work.
A slightly modified
tls.ldiffile, which uses the fullchain, private key and certificate provided by LetsEncrypt
dn: cn=config changetype: modify replace: olcTLSCACertificateFile olcTLSCACertificateFile: /etc/ldap/ssl/fullchain.crt - replace: olcTLSCertificateFile olcTLSCertificateFile: /etc/ldap/ssl/ldap.crt - replace: olcTLSCertificateKeyFile olcTLSCertificateKeyFile: /etc/ldap/ssl/ldap.key - replace: olcTLSVerifyClient olcTLSVerifyClient: never
A new (conditionally loaded)
force-starttls.ldiffile, which tells OpenLDAP to force TLS
dn: cn=config changetype: modify add: olcSecurity olcSecurity: tls=1
Getting all the details right took some time, but it was worth it. With this containerized setup, its easy to start up a fresh “trusted” OpenLDAP image for testing, and conditionally enforce StartTLS.
Thankfully, I was able to use this local containerized OpenLDAP server to finish my work in the Jenkins LDAP-Plugin, which I’ll be writing about in a future blog post.