Skip to main content
Version: Next

ldap-auth-advanced

Description#

The ldap-auth-advanced Plugin adds LDAP authentication to a Route or a Service. Unlike ldap-auth, which binds with a DN assembled from the Consumer configuration, this Plugin searches the directory for the user first, then binds as the entry it found. When consumer_required is disabled, users do not need to be enumerated in APISIX; with the default true, each authenticated user must still map to a Consumer by its user_dn.

On each request the Plugin:

  1. Reads the credentials from the Proxy-Authorization header, falling back to Authorization.
  2. Searches base_dn for the entry whose attribute matches the supplied username, then binds as that entry with the supplied password.
  3. Attaches a matching Consumer, unless consumer_required is false.

The credential header uses the scheme word given by header_type, which defaults to ldap rather than basic, so the default expects Authorization: ldap <base64(username:password)>. Set header_type to basic to accept ordinary basic access authentication instead.

The Plugin distinguishes two failure modes, so an outage is never reported as a rejected credential:

StatusCause
401Missing, malformed, or rejected credentials; a username matching more than one entry; or consumer_required is true and no Consumer matches. Returned with a WWW-Authenticate header.
500LDAP transport, TLS, protocol, or server-side failures — for example an unreachable directory, a failed user search, or a user bind rejected with a result code other than invalidCredentials — as well as rejected bind_dn credentials.

This Plugin uses lua-resty-ldap to connect to the LDAP server.

Attributes#

For Consumer:

NameTypeRequiredDefaultValid valuesDescription
user_dnstringTrueDN of the LDAP user bound to this Consumer, for example cn=Jane Doe,ou=users,dc=example,dc=org. This field supports storing the value in Secret Manager using the APISIX Secret resource.

For Route:

NameTypeRequiredDefaultValid valuesDescription
ldap_uristringTrueAddress of the LDAP server as host or host:port. When the port is omitted, 636 is used if use_ldaps is enabled and 389 otherwise.
base_dnstringTrueDN of the subtree searched for the user, for example ou=users,dc=example,dc=org.
attributestringFalsecnUser attribute matched against the supplied username, for example uid or sAMAccountName.
bind_dnstringFalseDN used to bind before searching for the user. When unset, the search binds anonymously.
ldap_passwordstringFalsePassword for bind_dn. Required when bind_dn is set. The password is encrypted with AES before being stored in etcd.
use_ldapsbooleanFalsefalseIf true, connect over LDAPS. Mutually exclusive with use_starttls.
use_starttlsbooleanFalsefalseIf true, upgrade the connection with StartTLS. Mutually exclusive with use_ldaps.
ssl_verifybooleanFalsetrueIf true, verify the LDAP server's certificate. Requires ssl_trusted_certificate to be set in config.yaml, and the host in ldap_uri to match the host in the server certificate.
timeoutintegerFalse10000[1, 60000]Socket timeout in milliseconds.
keepalivebooleanFalsetrueIf true, return the connection to the pool for reuse instead of closing it.
keepalive_timeoutintegerFalse60000>= 1000Idle time in milliseconds after which a pooled connection is closed.
keepalive_pool_sizeintegerFalse5>= 1Maximum number of connections kept in the pool.
keepalive_pool_namestringFalseName of the connection pool. Set this to keep connections that use different credentials in separate pools.
size_limitintegerFalse2>= 2Maximum number of entries the user search may return. The login attribute is expected to be unique, so more than one match is treated as ambiguous and rejected.
time_limitintegerFalse5>= 0Time limit of the search in seconds. 0 uses the server default.
consumer_requiredbooleanFalsetrueIf true, reject the request with 401 when no Consumer matches the authenticated user.
header_typestringFalseldap["ldap", "basic"]Scheme word expected in the credential header.
realmstringFalseldapRealm in the WWW-Authenticate response header returned with a 401 Unauthorized response.

Examples#

The examples below assume an LDAP directory under dc=example,dc=org that contains a user Jane Doe with uid of jdoe and password janesecret.

note

You can fetch the admin_key from config.yaml and save it to an environment variable with the following command:

admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')

Authenticate Against an LDAP Directory#

The following example shows the minimum configuration: search base_dn for a matching uid, then bind as that user.

Create a Route with ldap-auth-advanced:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "ldap-auth-advanced-route",
"uri": "/anything",
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"consumer_required": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

Send a request with valid credentials:

curl -i "http://127.0.0.1:9080/anything" \
-H "Authorization: ldap $(echo -n 'jdoe:janesecret' | base64)"

You should receive an HTTP/1.1 200 OK response.

Send a request without credentials:

curl -i "http://127.0.0.1:9080/anything"

You should receive an HTTP/1.1 401 Unauthorized response with the following body:

{"message":"Authorization required"}

The response also carries the challenge built from realm:

WWW-Authenticate: ldap realm="ldap"

A request with a wrong password is rejected the same way.

If your directory does not allow anonymous searches, bind with a service account by adding bind_dn and ldap_password. The user is still authenticated with their own bind:

{
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "adminpassword",
"consumer_required": false
}
}

To accept standard basic authentication instead of the ldap scheme, set header_type to basic. Clients can then use curl -u jdoe:janesecret.

Map LDAP Identities to Consumers#

Associating an LDAP identity with a Consumer lets APISIX apply per-Consumer configuration, such as rate limits, and adds the X-Consumer-Username header to the Upstream request. A Consumer is bound to one LDAP user with user_dn.

Create a Consumer bound to a user:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"username": "jane",
"plugins": {
"ldap-auth-advanced": {
"user_dn": "cn=Jane Doe,ou=users,dc=example,dc=org"
}
}
}'

Update the Route to require a Consumer by removing consumer_required, which defaults to true:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "ldap-auth-advanced-route",
"uri": "/anything",
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "127.0.0.1:1389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

Send a request as jdoe:

curl "http://127.0.0.1:9080/anything" \
-H "Authorization: ldap $(echo -n 'jdoe:janesecret' | base64)"

You should see the Consumer identified in the Upstream request:

{
"headers": {
"X-Consumer-Username": "jane",
...
},
...
}

Users matching no Consumer are rejected with 401, unless consumer_required is set to false.

Connect over LDAPS#

Set use_ldaps to connect over LDAPS, or use_starttls to upgrade a plaintext connection. The two are mutually exclusive and the configuration is rejected if both are enabled.

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "ldap-auth-advanced-route",
"uri": "/anything",
"plugins": {
"ldap-auth-advanced": {
"ldap_uri": "ldap.example.org",
"use_ldaps": true,
"ssl_verify": true,
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"consumer_required": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'

When the port is omitted from ldap_uri, 636 is used with use_ldaps and 389 otherwise.

ssl_verify is enabled by default. Verification requires ssl_trusted_certificate in config.yaml to point at the CA that signed the LDAP server certificate, and the host in ldap_uri to match the certificate. A certificate that cannot be verified fails the request with 500.

Delete Plugin#

To remove the ldap-auth-advanced Plugin, you can delete the corresponding JSON configuration from the Plugin configuration. APISIX will automatically reload and you do not have to restart for this to take effect.

curl "http://127.0.0.1:9180/apisix/admin/routes/ldap-auth-advanced-route" -X PATCH \
-H "X-API-KEY: ${admin_key}" \
-d '{
"plugins": {}
}'