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.
Quick start
Section titled “Quick start”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:
ldapsearch -H ldap://localhost:10389 -x -D "u:alice" -W \ -b "ou=users,dc=example,dc=com" "(cn=john*)" cn mailFor all listener settings (host binding, client certificates, socket options, connection limits), see Configuration Reference.
Authentication
Section titled “Authentication”Scribe authenticates LDAP bind requests locally using the unified auth infrastructure. Several bind DN formats are supported:
| Bind DN format | Example | How it works |
|---|---|---|
bearer:<token> | bearer:eyJhbGci... | JWT or opaque token — password is ignored |
dn:<dn> | dn:uid=alice,ou=users,dc=example,dc=com | Explicit DN + password. DN must be under auth.ldap.base |
u:<username> | u:alice | Username + password, DN resolved via search under auth.ldap.base |
| Plain value | alice or uid=alice,... | Disambiguated via DN.isValidDN — DN-like strings bind by DN (with base enforcement), others resolve as username |
# Bind with bearer tokenldapsearch -H ldap://localhost:10389 -x \ -D "bearer:eyJhbGciOiJSUzI1NiJ9..." -w "ignored" \ -b "ou=users,dc=example,dc=com" "(cn=*)"
# Bind with usernameldapsearch -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.
If a service account binds with the ldap method, that account must pass auth.ldap.filter before channel access rules run. Use auth.ldap.filter to decide who may authenticate, then use channels.ldap.auth.rules, monitoring.observe.auth.rules, and auth.ldap.roles for LDAP-channel and operator UI authorization.
Proxy Authorization
Section titled “Proxy Authorization”The LDAP channel supports Proxy Authorization V2 (RFC 4370) for delegation — a service account binds, then acts on behalf of a user:
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
Section titled “Delegation pattern”Proxy authorization to backend LDAP
- Bind establishes the service identity (who's calling)
- ProxyAuth establishes the subject identity (on whose behalf)
- The backend receives operations with a
ProxyAuthcontrol containing the resolved effective DN
All authentication happens locally within Scribe. The upstream LDAP server only sees the resolved DN.
Searching
Section titled “Searching”Standard LDAP search with filters, scopes, and attribute selection:
# Subtree search with filter and specific attributesldapsearch -H ldap://localhost:10389 -x -D "..." -W \ -b "ou=users,dc=example,dc=com" \ -s sub "(|(cn=john*)(mail=*@example.com))" cn mail uidPagination
Section titled “Pagination”Use Simple Paged Results for large result sets:
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.
LDAP paging response controls use channels.ldap.default-count, which inherits query.default-count unless overridden.
The default is exact so clients can use the response size as a membership count. eDirectory clients that do not need a
count can send the disable-count control to skip count work for that request.
Sorting
Section titled “Sorting”Server-Side Sorting works on indexed attributes:
ldapsearch -H ldap://localhost:10389 -x -D "..." -W \ -b "ou=users,dc=example,dc=com" \ -E sss=-sn "(objectClass=inetOrgPerson)"Auth gating by operation type
Section titled “Auth gating by operation type”Access rules are evaluated for all operation types. The exact checks differ:
| Operation type | Auth gating |
|---|---|
| Search | (1) Auth required flag (if configured), (2) access rules, (3) ProxyAuth control validation |
| Add, Compare, Delete, Modify, ModifyDN | Access rules only |
| Extended | StartTLS 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.
Modify forwarding
Section titled “Modify forwarding”Modify operations (including multi-attribute batch modifications from clients such as Apache Directory Studio) are forwarded to the upstream directory as a single atomic operation exactly as received — the modification list is never inspected, split, reordered, or deduplicated by Scribe. Atomicity and schema enforcement are governed by the upstream directory. If a Modify would leave the entry schema-invalid, the upstream returns an error result code (see Common issues below) and Scribe relays it unchanged.
Search delegation
Section titled “Search delegation”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.
Common issues
Section titled “Common issues”| Symptom | Check |
|---|---|
Can't contact LDAP server | Listener 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 |
| ResultCode 65 (OBJECT_CLASS_VIOLATION) | Returned by the upstream when a Modify would leave the entry schema-invalid — for example, removing a structural objectClass, or removing an auxiliary class whose MUST attributes are still present. Sequence removals so each intermediate state is schema-valid, or remove dependent attributes first. Scribe relays this result code unchanged. |
See Signals for metrics, Logging and Traces for trace debugging, Error Handling for the complete ResultCode mapping.