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 | Backend DN + password. Backend DNs must be under auth.ldap.base; a matching configured local DN is handled locally |
u:<username> | u:alice | Username + password, resolved through LDAP or a configured local alias |
| Plain value | alice or uid=alice,... | DN-like strings bind as a configured local account or backend DN (backend DNs are subject to base enforcement); other values resolve as usernames |
# 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=*)"Local technical accounts
Section titled “Local technical accounts”Use local for a configured technical account that must authenticate repeatedly without a backend bind or username lookup:
auth { enabled = true methods = [local] local.accounts = [{ bind-dn = "cn=scribe,dc=example,dc=com" bind-password = ${SCRIBE_LOCAL_PASSWORD} username = "scribe" # optional; enables u:scribe and plain scribe delegation.enabled = false }]}The local DN is the canonical authenticated identity and does not need to be a directory entry. A successful local bind does not contact backend LDAP. LDAP Who-Am-I reports dn:<canonical local DN>. Reads of that DN remain ordinary directory operations: Scribe does not create a synthetic entry or merge profile attributes. A matching local account with a wrong password is rejected without falling through to another authentication method. Unknown names may use another explicitly configured method.
delegation.enabled defaults to true. When forwarding is enabled, an account may provide a complete delegation.bind-dn and delegation.bind-password pair; when omitted, Scribe uses the root ldap.bind-dn and ldap.bind-password pair. The local login password is never used implicitly. With delegation disabled, operations that require backend access fail with UNWILLING_TO_PERFORM before a backend connection is opened. Scribe access rules still apply to local callers, and local authentication does not grant roles or administrative access.
Authentication methods are tried in configured order for unknown credentials (for example, local, bearer, ropc, or ldap). A matching local account owns the Basic credential decision before fallback methods; local must be first in every mixed list. 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
Scribe applies its access rules before forwarding. For a local account, the backend connection uses the account’s selected configured backend identity and does not add ProxyAuth for the local caller. Client-supplied ProxyAuth is rejected for local accounts rather than silently changing identity.
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 | Before bind, StartTLS is forwarded (RFC 4513 pre-bind transport). After a local bind, local forwarding policy and ProxyAuth validation apply |
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 forwarded to the backend before authentication (RFC 4513 requires pre-bind transport negotiation). Once a local account is bound, local forwarding policy and ProxyAuth validation also apply; disallowed requests are rejected before backend access.
Bind failure delay: failed bind attempts use auth.failure-delay (default 2s) before responding, including local wrong, empty, unknown, and malformed credentials. It does not apply when authentication itself 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. A local account with delegation.enabled = false applies the same rejection to that account even when channel-wide delegation is allowed. 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.