Skip to content

LDAP Channel

Query IdentityScribe via LDAP v3 protocol. This guide covers configuration for operators and connection details for clients.

The LDAP channel exposes directory operations via the LDAP v3 protocol. It supports:

  • Standard LDAP search, compare, and bind operations
  • Simple Paged Results and VLV controls for pagination
  • Server-Side Sorting (SSS) control
  • LDAPS (TLS) for secure connections
  • Client certificate authentication

Related:

LDAP channel configuration lives in the channels.ldap namespace. Settings inherit from the shared ldap configuration block.

Configure one or more LDAP listeners in channels.ldap.listen:

channels.ldap {
listen = [
# Plain LDAP
{
port = 10389
# host = null # Listen on all interfaces (default)
}
# LDAPS (TLS)
{
port = 10636
ssl {
ca = "certs/ca.pem"
cert = "certs/server.pem"
key = "certs/server-key.pem"
# requestClientCertificate = false
# requireClientCertificate = false
}
}
]
}
SettingTypeDefaultDescription
portintListen port (1–65535, or 0 for system-assigned)
hoststringnullBind address (null = all interfaces)
sslobjectTLS configuration (presence enables LDAPS)
ssl.castringCA certificate file or directory
ssl.certstringServer certificate chain (PEM)
ssl.keystringPrivate key (PEM, optionally encrypted)
ssl.passwordstringPrivate key password (if encrypted)
ssl.requestClientCertificatebooleanfalseRequest client certificate during TLS
ssl.requireClientCertificatebooleanfalseRequire client certificate

Fine-tune connection behavior:

channels.ldap {
# TCP keepalive (default: true)
useKeepAlive = true
# SO_LINGER option (default: true)
useLinger = true
lingerTimeoutSeconds = 5
# SO_REUSEADDR (default: true)
use-reuse-address = true
# TCP_NODELAY - disable Nagle's algorithm (default: true)
tcp-no-delay = true
# Connection limits
max-connections = 0 # 0 = unlimited
maxMessageSizeBytes = 0 # 0 = use LDAP SDK default
# Buffer sizes (0 = system default)
receiveBufferSize = 0
sendBufferSize = 0
}

Control search behavior:

channels.ldap {
# Require authentication for all operations (default: true)
authenticationRequired = true
# Maximum entries per search (default: 5000)
maxSizeLimit = 5000
}

Database session hints for LDAP queries. These override database.connection-hints defaults:

channels.ldap {
connection-hints {
# Statement execution timeout (PostgreSQL aborts queries exceeding this)
# statement-timeout = 60s
statement-timeout = ${?SCRIBE_LDAP_STATEMENT_TIMEOUT}
# Lock acquisition timeout
# lock-timeout = 5s
lock-timeout = ${?SCRIBE_LDAP_LOCK_TIMEOUT}
# PostgreSQL work_mem per-query memory (e.g., "256MB", "1GB")
# Valid units: B, kB, MB, GB, TB
# work-mem = "512MB"
work-mem = ${?SCRIBE_LDAP_WORK_MEM}
# Session flags (comma-separated): force-custom-plan, jit-off
# session-flags = "force-custom-plan"
session-flags = ${?SCRIBE_LDAP_SESSION_FLAGS}
}
}

Priority: Channel values override database-level defaults. For example, if database.connection-hints.statement-timeout = 60s and channels.ldap.connection-hints.statement-timeout = 120s, LDAP queries use 120s.

The LDAP channel connects to an upstream LDAP server configured in the shared ldap block:

ldap {
# Backend LDAP server(s) - comma-separated for failover
url = "ldap://ldap.example.com:389"
url = ${?SCRIBE_LDAP_URL}
# Bind credentials for backend connections
bind-dn = "cn=admin,dc=example,dc=com"
bind-dn = ${?SCRIBE_LDAP_BIND_DN}
bind-password = "secret"
bind-password = ${?SCRIBE_LDAP_BIND_PASSWORD}
# TLS for backend connections
ssl {
ca = "certs/ldap-ca.pem"
}
}
VariableConfig PathDescription
SCRIBE_LDAP_URLldap.urlBackend LDAP server URL(s)
SCRIBE_LDAP_BIND_DNldap.bind-dnBackend bind DN
SCRIBE_LDAP_BIND_PASSWORDldap.bind-passwordBackend bind password
SCRIBE_LDAP_MAX_SIZE_LIMITldap.max-size-limitMaximum search result entries
SCRIBE_LDAP_STATEMENT_TIMEOUTchannels.ldap.connection-hints.statement-timeoutQuery statement timeout
SCRIBE_LDAP_LOCK_TIMEOUTchannels.ldap.connection-hints.lock-timeoutLock acquisition timeout
SCRIBE_LDAP_WORK_MEMchannels.ldap.connection-hints.work-memPer-query memory
SCRIBE_LDAP_SESSION_FLAGSchannels.ldap.connection-hints.session-flagsSession flags

Connect using standard LDAP tools or libraries:

Terminal window
# Plain LDAP
ldapsearch -H ldap://localhost:10389 -x -D "cn=admin,dc=example,dc=com" -W \
-b "ou=users,dc=example,dc=com" "(cn=john*)"
# LDAPS
ldapsearch -H ldaps://localhost:10636 -x -D "cn=admin,dc=example,dc=com" -W \
-b "ou=users,dc=example,dc=com" "(cn=john*)"

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

Terminal window
# 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
# Search with size limit
ldapsearch -H ldap://localhost:10389 -x -D "..." -W \
-b "ou=users,dc=example,dc=com" \
-z 100 \
"(objectClass=inetOrgPerson)"

Use Simple Paged Results for large result sets:

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

Server-Side Sorting is supported for indexed attributes:

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

LDAP errors are returned as standard LDAP ResultCodes. See Failures for:

  • Complete ResultCode mapping table
  • Diagnostic message format
  • Code-specific overrides (e.g., DIRECTORY_BUSYBUSY (51))
ResultCodeMeaningAction
0 (SUCCESS)Operation completed
2 (PROTOCOL_ERROR)Invalid requestFix request syntax
3 (TIME_LIMIT_EXCEEDED)Query timeoutSimplify query or increase limit
4 (SIZE_LIMIT_EXCEEDED)Too many resultsUse pagination or refine filter
32 (NO_SUCH_OBJECT)Entry not foundCheck DN
49 (INVALID_CREDENTIALS)Auth failedCheck credentials
51 (BUSY)Server overloadedRetry after delay
52 (UNAVAILABLE)Service unavailableRetry with backoff

The LDAP channel emits metrics with channel="LDAP" label:

MetricDescription
scribe_channel_requests_total{channel="LDAP",op,result}Request count
scribe_channel_request_duration_seconds{channel="LDAP",op,result}Latency histogram
scribe_channel_inflight{channel="LDAP",op}Active requests

PromQL examples:

# LDAP request rate
rate(scribe_channel_requests_total{channel="LDAP"}[5m])
# LDAP p99 latency
histogram_quantile(0.99,
rate(scribe_channel_request_duration_seconds_bucket{channel="LDAP"}[5m])
)
# LDAP error rate
sum(rate(scribe_channel_requests_total{channel="LDAP",result!="ok"}[5m]))
/ sum(rate(scribe_channel_requests_total{channel="LDAP"}[5m]))

LDAP operations create spans named LDAP.{Operation}:

Span NameDescription
LDAP.BindAuthentication
LDAP.SearchSearch operation
LDAP.CompareCompare operation

Span attributes:

AttributeDescriptionExample
scribe.operationOperation nameLDAP.Search, LDAP.Bind
scribe.resultOutcomeok, not_found, deadline_exceeded
scribe.search.kindPagination modesimple, paged, vlv
scribe.entry_typeEntry type(s)inetOrgPerson
scribe.ldap.message_idLDAP message ID1, 42
failure.codeError code (on failure)scope.outside_all_bases
failure.kindFailure kind (on failure)invalid_argument

Slow or failed LDAP operations are logged with full context:

{
"trace_id": "abc123",
"route": "LDAP.Search",
"duration_ms": 5200,
"result": "deadline_exceeded",
"scribe.entry_type": "user",
"scribe.search.kind": "paged"
}

Default threshold: 5 seconds (configurable via monitoring.log.filters).

Symptom: ldapsearch: Can't contact LDAP server

Checks:

  1. Verify the channel is enabled: channels.ldap.listen is configured
  2. Check the port is correct and not blocked by firewall
  3. Review startup logs for listener errors

Symptom: ResultCode 49 (INVALID_CREDENTIALS)

Checks:

  1. Verify bind DN and password
  2. Check if authenticationRequired = true (default)
  3. For anonymous access, set authenticationRequired = false

Symptom: ResultCode 4 (SIZE_LIMIT_EXCEEDED)

Fixes:

  1. Use pagination (-E pr=100/noprompt)
  2. Refine search filter to match fewer entries
  3. Increase maxSizeLimit (if appropriate for your use case)

Symptom: High latency, potential timeouts

Checks:

  1. Check scribe_query_stage_duration_seconds to identify slow stage
  2. Review /observe/hints for missing index suggestions
  3. Check scribe_query_permit_pressure for resource saturation

See Observability for detailed operational playbooks.