Skip to content

LDAP Channel

Point your existing LDAP apps at Scribe. They’ll keep working — same protocol, same tools, same queries — but now they’re reading from PostgreSQL instead of hitting your directory server directly.

Configure one or more listeners:

channels.ldap {
listen = [
{ port = 10389 } # Plain LDAP
{ port = 10636, ssl { # LDAPS
ca = "certs/ca.pem"
cert = "certs/server.pem"
key = "certs/server-key.pem"
}}
]
}

Test with any LDAP client:

Terminal window
ldapsearch -H ldap://localhost:10389 -x -D "u:alice" -W \
-b "ou=users,dc=example,dc=com" "(cn=john*)" cn mail

For all listener settings (host binding, client certificates, socket options, connection limits), see Configuration Reference.

Scribe authenticates LDAP bind requests locally using the unified auth infrastructure. Several bind DN formats are supported:

Bind DN formatExampleHow it works
bearer:<token>bearer:eyJhbGci...JWT or opaque token — password is ignored
dn:<dn>dn:uid=alice,ou=users,dc=example,dc=comExplicit DN + password. DN must be under auth.ldap.base
u:<username>u:aliceUsername + password, DN resolved via search under auth.ldap.base
Plain valuealice or uid=alice,...Disambiguated via DN.isValidDN — DN-like strings bind by DN (with base enforcement), others resolve as username
Terminal window
# Bind with bearer token
ldapsearch -H ldap://localhost:10389 -x \
-D "bearer:eyJhbGciOiJSUzI1NiJ9..." -w "ignored" \
-b "ou=users,dc=example,dc=com" "(cn=*)"
# Bind with username
ldapsearch -H ldap://localhost:10389 -x \
-D "u:alice" -W \
-b "ou=users,dc=example,dc=com" "(cn=*)"

Authentication methods are tried in order (e.g., bearer, then ropc, then ldap). Both channels.ldap.auth and channels.identity-hub.auth inherit from global auth {} and can override enabled, methods, and rules per channel. See Authentication for method configuration.

The LDAP channel supports Proxy Authorization V2 (RFC 4370) for delegation — a service account binds, then acts on behalf of a user:

Terminal window
ldapsearch -H ldap://localhost:10389 -x \
-D "cn=service,dc=example,dc=com" -W \
-e "authzid=dn:uid=alice,ou=users,dc=example,dc=com" \
-b "ou=users,dc=example,dc=com" "(cn=*)"

ProxyAuth accepts the same formats as bind: bearer:<token>, dn:<dn>, u:<username>, or plain values. DN and username formats (including plain values) require auth.ldap configuration so Scribe can verify DNs and resolve usernames; without it, ProxyAuth with those formats returns AUTHORIZATION_DENIED. Use bearer:<token> when LDAP auth is not configured. Bearer tokens with a dn claim are subject to auth.ldap.base scope validation — the DN must be a strict descendant of the base.

Delegation Pattern

Proxy authorization to backend LDAP

Client
Scribe
Backend LDAP
1
BIND dn=service
2
BIND success
3
SEARCH (proxyAuth: dn=user)
4
BIND dn=service
5
BIND success
6
SEARCH (proxyAuth: dn=user)
7
Results (ACL enforced)
8
Results
  1. Bind establishes the service identity (who’s calling)
  2. ProxyAuth establishes the subject identity (on whose behalf)
  3. The backend receives operations with a ProxyAuth control containing the resolved effective DN

All authentication happens locally within Scribe. The upstream LDAP server only sees the resolved DN.

Standard LDAP search with filters, scopes, and attribute selection:

Terminal window
# Subtree search with filter and specific attributes
ldapsearch -H ldap://localhost:10389 -x -D "..." -W \
-b "ou=users,dc=example,dc=com" \
-s sub "(|(cn=john*)(mail=*@example.com))" cn mail uid

Use Simple Paged Results for large result sets:

Terminal window
ldapsearch -H ldap://localhost:10389 -x -D "..." -W \
-b "ou=users,dc=example,dc=com" \
-E pr=100/noprompt "(objectClass=inetOrgPerson)"

VLV (Virtual List View) is also supported.

Server-Side Sorting works on indexed attributes:

Terminal window
ldapsearch -H ldap://localhost:10389 -x -D "..." -W \
-b "ou=users,dc=example,dc=com" \
-E sss=-sn "(objectClass=inetOrgPerson)"

Access rules are evaluated for all operation types. The exact checks differ:

Operation typeAuth gating
Search(1) Auth required flag (if configured), (2) access rules, (3) ProxyAuth control validation
Add, Compare, Delete, Modify, ModifyDNAccess rules only
ExtendedStartTLS always forwarded (RFC 4513 pre-bind transport). Others: access rules + ProxyAuth

Search has the strictest gating: when channels.ldap.auth.enabled is true, unauthenticated Search requests are rejected before access rules are evaluated. Non-Search operations (add, modify, etc.) rely solely on access rules. Extended operations: StartTLS is always forwarded to the backend (RFC 4513 requires pre-bind transport negotiation); other extended operations are subject to access rules and ProxyAuth validation.

Bind failure delay: failed bind attempts use auth.failure-delay (default 2s) before responding, mitigating brute-force attacks. Does not apply when auth is delegated to the backend.

Some requests can’t be answered from the local cache — schema queries, unsupported filters, directory browser operations. When that happens, Scribe forwards the request to the upstream LDAP server.

Set channels.ldap.prevent-delegation = true to fail these requests instead of forwarding them. See Configuration Reference for delegation reasons and telemetry.

SymptomCheck
Can't contact LDAP serverListener configured? Port not blocked?
ResultCode 49 (INVALID_CREDENTIALS)Bind DN and password correct? For bearer tokens: token valid and not expired?
ResultCode 53 (AUTHORIZATION_DENIED)Empty bearer token, or ProxyAuth with DN/username when auth.ldap is not configured
ResultCode 4 (SIZE_LIMIT_EXCEEDED)Use pagination (-E pr=100/noprompt) or refine the filter

See Signals for metrics, Logging and Traces for trace debugging, Error Handling for the complete ResultCode mapping.