STARTTLS is the other way to get an encrypted LDAP connection, alongside LDAPS (see LDAP vs LDAPS). Instead of encrypting from the first byte, it upgrades an existing plain connection in place.
- The client opens a normal, unencrypted connection on port 389.
- Before sending any bind request, the client sends the
StartTLSextended operation. - The server responds, and both sides perform a TLS handshake over the same TCP socket.
- From this point on, the connection is encrypted — the client can now safely send a bind with real credentials.
1. TCP connect :389
2. StartTLS extended operation ──▶
3. TLS handshake (both directions)
4. Bind (now encrypted)
5. Search / other operations
Security
Sending a bind with a real password before the STARTTLS upgrade completes defeats the entire point — it goes out in plaintext exactly like plain LDAP. Always wait for the TLS handshake to finish first.
ldapjs exposes this as client.starttls(), called before any bind:
import ldap from "ldapjs";
import fs from "node:fs";
const client = ldap.createClient({ url: "ldap://dc1.example.com:389" });
client.starttls(
{ ca: [fs.readFileSync("/etc/ssl/certs/corp-ca.pem")] },
null,
(err) => {
if (err) {
console.error("STARTTLS failed:", err);
return;
}
// The connection is now encrypted — safe to bind.
client.bind(bindDn, password, (err) => {
// ...
});
},
);
If you don't need to support plain-port connectivity checks, using ldaps:// directly (see Connecting) is simpler and avoids this extra step entirely.
LDAPS was never formally standardized as part of the core LDAP RFCs — it became a de facto convention. STARTTLS, defined in RFC 4513, was introduced as the standards-track way to add TLS to LDAP without needing a second dedicated port. In practice, both are widely supported and equally secure when configured correctly; the choice mostly comes down to what your server and existing tooling expect.
Continue to LDAP injection — encryption protects data in transit, but it doesn't protect against unescaped input in filters or DNs.