ldap
The ldap backend allows Radiator to authenticate and authorize users against an LDAP directory.
Example configuration of an LDAP backend:
ldap "ldap.forumsys.com" {
# LDAP server
server "ldap.forumsys.com" {
# LDAP URL
url "ldap://ldap.forumsys.com:389/";
# Operation timeout (supports duration units like 3s, 5m, 1h)
timeout 3s;
# (Optional) Server authentication
authentication {
# Simple bind authentication
dn "cn=read-only-admin,dc=example,dc=com";
password "password";
# or from environment variables
# dn env.LDAP_BIND_DN;
# password env.LDAP_BIND_PW;
}
# How many sockets/connections at maximum to open for shared
# server-authentication. Default 10.
shared-connections 10;
# How many sockets/connections at maximum to open for exclusive
# per-operation authentication. Default 10.
exclusive-connections 10;
# Automatically close the connections after this idle time. If not
# defined the connections are kept open indefinitely.
# idle-timeout 60s;
# (Optional) TLS client configuration
#tls {...}
}
# A single backend can have multiple LDAP servers configured
#server "ldap2" {
# ...
#}
# LDAP operations
# LDAP search operation named "user_groups"
search "USER_GROUPS" {
base "dc=example,dc=com";
scope sub;
filter "(&(objectClass=groupOfUniqueNames)(uniqueMember=uid=%{aaa.identity},dc=example,dc=com))";
# (Optional) per-operation authentication. If not defined the
# server-level authentication is used
authentication {
dn "cn=mikem,dc=example,dc=com";
password "password";
}
mapping {
# = is fine here: entry::dn is always single-valued and unique per entry
vars.dn = entry::dn;
# ?= sets the target only if it has no value yet, making it a safe fallback chain:
# uid is the OpenLDAP convention; sAMAccountName is the Active Directory equivalent
user.username ?= uid;
user.username ?= sAMAccountName;
# += accumulates all values across every result entry; use this for list attributes
# such as group membership where you want every value, not just one
user.group += memberOf;
}
}
# Dynamic user bind test. This will reject if the bind fails.
bind "USER_BIND" {
dn vars.user_dn;
password vars.user_password;
}
}
Configuration Options
server
This clause defines an LDAP server to connect to. Multiple server blocks can be defined for high availability. Each server is identified by a unique name.
See server for all available server options.
server-selection
Controls how Radiator selects a server when multiple server blocks are configured.
See server-selection for details.
search
Defines a named LDAP search operation for querying the directory. Each search specifies a base DN, scope, filter, and result mapping. A search operation uses the shared connection pool.
If a search returns no entries, the backend rejects and sets aaa.reason to a
descriptive message that includes the backend name and query name.
search "FIND_USER" {
base "dc=example,dc=com";
scope sub;
filter "(&(uid=%{aaa.identity})(objectClass=inetOrgPerson))";
mapping {
user.username = uid;
vars.dn = entry::dn;
}
}
Search parameters
| Parameter | Required | Description |
|---|---|---|
base | No | Base DN for the search. Default is an empty string (directory root). |
scope | No | Search scope. Default is base. |
filter | Yes | LDAP search filter expression. |
mapping { ... } | No | Maps LDAP attributes from search results to Radiator context variables. |
authentication | No | Override server-level credentials for this search. Uses dn and password only (no SASL modes). |
base
The base distinguished name defines the starting point in the directory tree for the search. Accepts expressions with %{...} variable substitution -- special characters are automatically escaped for LDAP DN safety.
Default: Empty string (directory root).
scope
Controls how deep the LDAP search extends from the base DN.
| Value | Description |
|---|---|
base | Search only the base DN entry itself. |
one | Search one level below the base DN (immediate children). |
sub | Search the entire subtree below the base DN. |
Default: base
filter
The LDAP search filter expression selects which entries match. Follows standard LDAP filter syntax (RFC 4515). Accepts expressions with %{...} variable substitution -- special characters are automatically escaped for LDAP filter safety.
This parameter is required. If omitted, the configuration is rejected.
mapping
Maps LDAP attributes from search result entries to Radiator context variables. Each line specifies a target variable, an operator, and a source LDAP attribute.
Operators:
| Operator | Description |
|---|---|
= | Set the target to the source value. Overwrites any existing value. |
?= | Set the target only if it has no value yet. Useful for fallback chains across different attribute names. |
+= | Append the source value to the target. Accumulates all values across all result entries. |
Special source attributes:
| Source | Description |
|---|---|
entry::dn | The distinguished name of the matched LDAP entry. |
Example:
mapping {
# entry::dn is always single-valued and unique per entry
vars.dn = entry::dn;
# uid is the OpenLDAP convention; sAMAccountName is the Active Directory equivalent
user.username ?= uid;
user.username ?= sAMAccountName;
# += accumulates values across all result entries
user.group += memberOf;
}
authentication (per-operation)
Override the server-level authentication for a specific search operation. Only simple bind authentication (dn and password) is supported. The dn and password parameters accept expressions with %{...} variable substitution.
search "USER_GROUPS" {
base "dc=example,dc=com";
scope sub;
filter "(&(objectClass=groupOfUniqueNames)(uniqueMember=uid=%{aaa.identity},dc=example,dc=com))";
authentication {
dn "cn=group-reader,dc=example,dc=com";
password "secret";
}
mapping {
user.group += cn;
}
}
bind
Defines a named LDAP bind operation that tests user credentials by performing an LDAP bind with the provided DN and password. If the bind fails, the operation rejects the request. A bind operation uses the exclusive connection pool.
| Parameter | Required | Description |
|---|---|---|
dn | Yes | Expression that resolves to the distinguished name to bind as. |
password | Yes | Expression that resolves to the password for the bind. Must not be empty (see warning). |
Both dn and password accept expressions, so they can reference variables set by earlier operations (for example, vars.user_dn from a preceding search).
bind "USER_BIND" {
dn vars.dn;
password radius.request.password;
}
statistics
Enables in-process metrics collection for the LDAP backend. A statistics block can also be placed inside a server block for per-server metrics. See statistics for configuring counters, histograms, sample windows, and collection intervals.
Connection Pools
Each LDAP server maintains two separate connection pools.
Shared connections (shared-connections) stay permanently bound with the
server credentials configured in the authentication block. Use them for
search operations that run under the server identity. Radiator verifies
shared connections are still healthy on each pool checkout.
Exclusive connections (exclusive-connections) are used for bind
operations that authenticate an individual user. Radiator checks out one
connection per active bind, binds it with the user's credentials, then returns
it to the pool. Because exclusive connections are always rebound with new
credentials before use, Radiator only checks that the underlying TCP connection
is still open when recycling them -- it does not issue a WhoAmI round-trip.
Size each pool to match typical concurrency:
shared-connections: set to the number of concurrentsearchoperations you expect. A value equal to the number of worker threads is a good starting point.exclusive-connections: set to the maximum number of simultaneous user authentications (bind operations) the server must handle.
Server Selection and Priority
When multiple LDAP servers are configured, they can be prioritized using the optional priority parameter (integer 0-255, where lower numbers indicate higher priority, default is 0). Servers are tried according to the configured server-selection algorithm (default is fallback).
When servers have the same priority value, they are tried in alphabetical order by their server names.
backends {
ldap "LDAP_HA" {
server-selection fallback;
server "LDAP_PRIMARY" {
priority 0; # Highest priority, tried first
url "ldap://ldap1.example.com:389/";
# ...
}
server "LDAP_SECONDARY" {
priority 1; # Lower priority, tried if primary fails
url "ldap://ldap2.example.com:389/";
# ...
}
}
}
For detailed information about server selection algorithms and load balancing patterns, see Backend Load Balancing.
Authentication with dynamic LDAP bind
Sometimes it is not possible to retrieve the user password from the LDAP directory. In such cases, Radiator can perform a dynamic bind operation to test the user credentials.
Create the following LDAP backend:
backends {
ldap "LDAP" {
server "LDAP_SERVER" {
url "ldap://host:1389/";
authentication {
dn "cn=admin,dc=example,dc=org";
password "adminpassword";
}
}
search "FIND_USER" {
base "dc=example,dc=org";
scope sub;
filter "(&(cn=%{aaa.identity})(objectClass=inetOrgPerson))";
mapping {
user.username = uid;
# Store the user dn to be used in the later bind operation
vars.user_dn = entry::dn;
}
}
bind "BIND_USER" {
dn vars.user_dn;
password radius.request.password;
}
}
}
And authenticate the user with the following policy configuration:
aaa {
policy "DEFAULT" {
handler "AUTHENTICATION" {
conditions all {
radius.request.code == radius.ACCESS_REQUEST;
radius.request.attr.User-Name != none;
radius.request.attr.User-Name != "";
}
@execute {
if any {
radius.request.password == none;
radius.request.password == "";
} then {
reject "Empty or missing credentials";
}
backend {
name "LDAP";
query "FIND_USER";
}
backend {
name "LDAP";
query "BIND_USER";
}
}
}
}
}
This policy will reject the authentication request if the incoming password is missing or empty, if the user cannot be found, or if the bind operation fails with the provided password.
Anonymous bind protection
An LDAP anonymous bind occurs when a client sends empty credentials (no
password). Per RFC 4513, most LDAP servers accept an anonymous bind and return
success even though no real authentication took place. This means a bind
operation with an empty password succeeds at the LDAP level, and Radiator would
treat the authentication as passed.
Several RADIUS clients strip the User-Password attribute entirely when no
password is provided (for example, MAC authentication). In such cases,
radius.request.password evaluates to none and the bind operation fails
with an error because Radiator cannot extract a password from a missing value.
However, some clients do send an empty User-Password attribute, which Radiator
sees as a zero-length string -- not none. An LDAP server will accept the
resulting anonymous bind, silently letting the request pass.
To guard against this, add conditions that reject requests with missing or empty credentials before the LDAP operations:
aaa {
policy "DEFAULT" {
handler "AUTHENTICATION" {
conditions all {
radius.request.code == radius.ACCESS_REQUEST;
radius.request.attr.User-Name != none;
radius.request.attr.User-Name != "";
}
@execute {
# Reject early if the incoming RADIUS password is empty or missing
if any {
radius.request.password == none;
radius.request.password == "";
} then {
reject "Empty or missing credentials";
}
backend {
name "LDAP";
query "FIND_USER";
}
backend {
name "LDAP";
query "BIND_USER";
}
}
}
}
}
The if ... then { reject; } block stops the pipeline and returns an
authentication failure when the incoming RADIUS password is absent (none) or
zero-length (""). The handler conditions above already require a non-empty
incoming RADIUS User-Name. This avoids unnecessary LDAP round-trips.
Requests that carry both a User-Name and a real password continue to the
search and bind as normal.
Health Monitoring
Configure a service-level-objective block inside
each server to enable automatic circuit breaking. When a server reaches the failure rate,
Radiator marks it as degraded and routes traffic to healthier servers. See the
Service Level Objective documentation for details.
backends {
ldap "LDAP_HA" {
server-selection round-robin;
server "LDAP1" {
url "ldap://ldap1.example.com:389/";
timeout 3s;
authentication {
dn "cn=radiator,dc=example,dc=com";
password "ldap_password";
}
service-level-objective {
failure-rate 3/5;
recovery-probe-count 2;
initial-backoff-period 3s;
max-backoff-period 30s;
}
}
server "LDAP2" {
url "ldap://ldap2.example.com:389/";
timeout 3s;
authentication {
dn "cn=radiator,dc=example,dc=com";
password "ldap_password";
}
service-level-objective {
failure-rate 3/5;
recovery-probe-count 2;
initial-backoff-period 3s;
max-backoff-period 30s;
}
}
search "FIND_USER" {
base "dc=example,dc=com";
scope sub;
filter "(&(uid=%{aaa.identity})(objectClass=inetOrgPerson))";
mapping {
user.username = uid;
vars.dn = entry::dn;
}
}
}
}
These are the default values. When server-selection is configured, Radiator applies
them automatically to every server that does not have an explicit service-level-objective
block. The block is shown here to make the defaults visible; you only need to add it when
you want to override them.
Single-server backends (no server-selection) do not receive an automatic
service-level-objective. Add one explicitly if you want health monitoring on a
single-server LDAP backend.