Links#
https://www.openldap.org/doc/admin26/tls.html
https://www.port389.org/docs/389ds/howto/howto-ssl.html
https://www.freeipa.org/page/V4/Automatic_Certificate_Request_Generation
https://learn.microsoft.com/en-us/entra/identity/domain-services/tutorial-configure-ldaps1. Important Points#
Production LDAP should not send credentials over cleartext network.
preferred:
LDAPS on 636
or StartTLS on 389 with strict certificate verification
avoid:
ldap:// over untrusted network
anonymous bind in production
insecure certificate verification
expired self-signed certs nobody monitorsLDAPS vs StartTLS:
| Mode | Port | Meaning | Notes |
|---|---|---|---|
| LDAPS | 636 | TLS from connection start | operationally simple |
| StartTLS | 389 | plaintext connection upgraded to TLS | standards-friendly, must enforce upgrade |
| Plain LDAP | 389 | no TLS | dev/local only |
2. Server Configuration#
certificate layout#
/etc/ldap/tls/
ldap.example.com.crt
ldap.example.com.key
ca.crt
permissions:
private key readable only by LDAP service user
cert and CA readable by LDAP service
cert SAN contains ldap.example.comOpenLDAP example#
Use cn=config to point slapd to certificate files:
dn: cn=config
changetype: modify
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/tls/ca.crt
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/tls/ldap.example.com.crt
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/tls/ldap.example.com.keyApply:
ldapmodify -H ldapi:/// \
-Y EXTERNAL \
-f tls.ldif
sudo systemctl restart slapdEnable LDAPS listener in service config according to distro packaging. Common listener set:
ldap:/// ldapi:/// ldaps:///389 Directory Server example#
389 DS manages TLS through NSS database and dsconf tooling. Exact commands vary by version and deployment method.
Checklist:
import CA
import server certificate and private key
enable TLS listener
require secure binds where appropriate
restart instance
verify with ldapsearch over ldapsActive Directory / managed directory#
AD DS LDAPS usually requires a certificate on domain controllers with proper Server Authentication usage and SAN matching the DC name. Managed directory products often require uploading a certificate chain and enabling secure LDAP from the service console/API.
Checklist:
certificate:
trusted by clients
has correct EKU for server auth
SAN matches LDAP hostname
private key installed on domain controller/service
network:
allow 636 only from trusted clients
do not expose LDAPS publicly3. Client Configuration / Verify#
Verify LDAPS:
openssl s_client -connect ldap.example.com:636 -servername ldap.example.com </dev/nullVerify RootDSE:
LDAPTLS_CACERT=/etc/ssl/company-ca.pem \
ldapsearch -H ldaps://ldap.example.com:636 \
-x \
-b "" \
-s base \
namingContexts supportedLDAPVersionVerify bind:
LDAPTLS_CACERT=/etc/ssl/company-ca.pem \
ldapwhoami -H ldaps://ldap.example.com:636 \
-x \
-D "uid=svc-keycloak,ou=service-accounts,dc=example,dc=com" \
-WExpected:
certificate verify ok
hostname matches certificate SAN
ldapsearch returns namingContexts
ldapwhoami returns service account DNKeycloak connection:
Connection URL:
ldaps://ldap.example.com:636
Bind DN:
uid=svc-keycloak,ou=service-accounts,dc=example,dc=com
Users DN:
ou=people,dc=example,dc=com
Trust:
import LDAP CA into Keycloak runtime trust store or container trust store4. Java#
JNDI LDAPS example:
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
public class LdapCheck {
public static void main(String[] args) throws Exception {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://ldap.example.com:636");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "uid=svc-keycloak,ou=service-accounts,dc=example,dc=com");
env.put(Context.SECURITY_CREDENTIALS, System.getenv("LDAP_BIND_PASSWORD"));
DirContext ctx = new InitialDirContext(env);
System.out.println(ctx.getNameInNamespace());
ctx.close();
}
}Custom truststore:
keytool -importcert \
-alias company-root-ca \
-file company-root-ca.crt \
-keystore truststore.p12 \
-storetype PKCS12
java \
-Djavax.net.ssl.trustStore=truststore.p12 \
-Djavax.net.ssl.trustStorePassword=changeit \
LdapCheckDo not disable endpoint identification / certificate validation in production.
5. Python#
Python with ldap3:
import os
from ldap3 import Server, Connection, Tls
import ssl
tls = Tls(
ca_certs_file="/etc/ssl/company-ca.pem",
validate=ssl.CERT_REQUIRED,
version=ssl.PROTOCOL_TLS_CLIENT,
)
server = Server("ldap.example.com", port=636, use_ssl=True, tls=tls)
conn = Connection(
server,
user="uid=svc-keycloak,ou=service-accounts,dc=example,dc=com",
password=os.environ["LDAP_BIND_PASSWORD"],
auto_bind=True,
)
conn.search(
search_base="ou=people,dc=example,dc=com",
search_filter="(uid=alice)",
attributes=["uid", "cn", "mail"],
)
print(conn.entries)
conn.unbind()Do not use no-verify TLS settings in production.
6. Go#
Go with go-ldap:
package main
import (
"crypto/tls"
"crypto/x509"
"os"
ldap "github.com/go-ldap/ldap/v3"
)
func main() {
ca, err := os.ReadFile("/etc/ssl/company-ca.pem")
if err != nil {
panic(err)
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(ca)
conn, err := ldap.DialURL(
"ldaps://ldap.example.com:636",
ldap.DialWithTLSConfig(&tls.Config{
ServerName: "ldap.example.com",
MinVersion: tls.VersionTLS12,
RootCAs: pool,
}),
)
if err != nil {
panic(err)
}
defer conn.Close()
err = conn.Bind(
"uid=svc-keycloak,ou=service-accounts,dc=example,dc=com",
os.Getenv("LDAP_BIND_PASSWORD"),
)
if err != nil {
panic(err)
}
}Do not set InsecureSkipVerify: true in production.
7. Node.js#
Node.js with ldapts:
import { readFileSync } from "node:fs";
import { Client } from "ldapts";
const client = new Client({
url: "ldaps://ldap.example.com:636",
tlsOptions: {
ca: [readFileSync("/etc/ssl/company-ca.pem")],
servername: "ldap.example.com",
minVersion: "TLSv1.2",
},
});
await client.bind(
"uid=svc-keycloak,ou=service-accounts,dc=example,dc=com",
process.env.LDAP_BIND_PASSWORD,
);
const result = await client.search("ou=people,dc=example,dc=com", {
scope: "sub",
filter: "(uid=alice)",
attributes: ["uid", "cn", "mail"],
});
console.log(result.searchEntries);
await client.unbind();Do not use NODE_TLS_REJECT_UNAUTHORIZED=0 or rejectUnauthorized: false in production.
8. Rotation Checklist#
before rotation:
issue certificate with correct SAN
distribute new CA/intermediate to clients
update Keycloak trust
update app trust stores
test ldapsearch against staging
during rotation:
replace server certificate
restart/reload LDAP service if required
verify ldaps with openssl and ldapsearch
verify Keycloak User Federation sync
monitor failed bind/search rate
after rotation:
remove old cert after overlap window
update certificate inventory
confirm expiry alert sees new date