Radiator Server Documentation — v10.33.1

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.

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.

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
ParameterRequiredDescription
baseNoBase DN for the search. Default is an empty string (directory root).
scopeNoSearch scope. Default is base.
filterYesLDAP search filter expression.
mapping { ... }NoMaps LDAP attributes from search results to Radiator context variables.
authenticationNoOverride 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.

ValueDescription
baseSearch only the base DN entry itself.
oneSearch one level below the base DN (immediate children).
subSearch 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:

OperatorDescription
=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:

SourceDescription
entry::dnThe 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.

ParameterRequiredDescription
dnYesExpression that resolves to the distinguished name to bind as.
passwordYesExpression 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 concurrent search operations 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.