ldap-auth-advanced
The ldap-auth-advanced plugin authenticates clients against an LDAP directory, such as OpenLDAP or Active Directory, and optionally maps the authenticated directory user onto a consumer.
The plugin resolves users with a search-then-bind flow. It searches base_dn for an entry whose attribute matches the client-supplied user name. It then binds as that entry with the client-supplied password. Because the user's distinguished name (DN) is discovered rather than constructed, users can live at different depths without encoding the directory layout in gateway configuration.
When consumer_required is enabled, which is the default, the plugin looks for a consumer whose credential records the resolved DN. On a match, the gateway adds headers such as X-Consumer-Username and X-Credential-Identifier before proxying the request upstream. Per-consumer plugins, rate limits, and analytics can then apply to LDAP-authenticated traffic. Setting consumer_required to false authenticates against the directory without requiring a consumer.
Available in API7 Enterprise from version 3.10.5 and in APISIX from version 3.18.0.
info
The plugin first tries Proxy-Authorization when that header contains credentials for the configured scheme, then falls back to Authorization. The scheme word is determined by header_type: with the default ldap, clients send Authorization: ldap <base64(username:password)>; with basic, they send an ordinary Authorization: Basic <base64(username:password)> header, which lets existing HTTP Basic clients authenticate unchanged.
The plugin returns 401 Unauthorized for missing, malformed, or rejected user credentials, ambiguous user matches, and a missing consumer when consumer_required is enabled. LDAP transport, TLS, protocol, server, search-bind, and other directory failures return 500 Internal Server Error so an outage is not presented as an invalid user credential.
Examples
The examples use an isolated OpenLDAP directory with two test users. The gateway searches the directory as cn=admin,dc=example,dc=org and removes the credential header before proxying authenticated requests upstream.
Set Up OpenLDAP
Start the directory in the same environment as the gateway.
Create users.ldif to add the test users:
dn: ou=users,dc=example,dc=org
objectClass: organizationalUnit
ou: users
dn: uid=johndoe,ou=users,dc=example,dc=org
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
uid: johndoe
mail: johndoe@example.org
userPassword: john-secret
dn: uid=janedoe,ou=users,dc=example,dc=org
objectClass: inetOrgPerson
cn: Jane Doe
sn: Doe
uid: janedoe
mail: janedoe@example.org
userPassword: jane-secretGenerate a short-lived CA and server certificate for the TLS examples:
mkdir -p ldap-certs
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout ldap-certs/ca.key \
-out ldap-certs/ca.crt \
-sha256 -days 30 \
-subj "/CN=LDAP Docs CA"
openssl req -newkey rsa:2048 -nodes \
-keyout ldap-certs/server.key \
-out ldap-certs/server.csr \
-subj "/CN=openldap"Create the certificate extensions file:
subjectAltName=DNS:openldap
extendedKeyUsage=serverAuthSign the server certificate and make the local evaluation key readable by the containerized LDAP process:
openssl x509 -req \
-in ldap-certs/server.csr \
-CA ldap-certs/ca.crt \
-CAkey ldap-certs/ca.key \
-CAcreateserial \
-out ldap-certs/server.crt \
-days 30 -sha256 \
-extfile ldap-certs/server.ext
chmod 644 ldap-certs/server.keySet GATEWAY_CONTAINER to the running APISIX or API7 Gateway container. Create a dedicated network and connect the gateway to it:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-ldap-net
docker network connect gateway-ldap-net "$GATEWAY_CONTAINER"Start OpenLDAP 2.6 LTS with LDAP, StartTLS, and LDAPS enabled:
docker run -d \
--name openldap \
--hostname openldap \
--network gateway-ldap-net \
-e LDAP_DOMAIN=example.org \
-e LDAP_BASE_DN=dc=example,dc=org \
-e 'LDAP_ORGANISATION=Example Organization' \
-e LDAP_ADMIN_PASSWORD=admin-secret \
-e LDAP_CREATE_PEOPLE_OU=false \
-e LDAP_CREATE_GROUPS_OU=false \
-e LDAP_ENABLE_TLS=true \
-e LDAP_ENABLE_LDAPS=true \
-e LDAP_TLS_CERT_FILE=/etc/openldap/certs/server.crt \
-e LDAP_TLS_KEY_FILE=/etc/openldap/certs/server.key \
-e LDAP_TLS_CA_FILE=/etc/openldap/certs/ca.crt \
-v "$PWD/users.ldif:/docker-entrypoint-initdb.d/20-users.ldif:ro" \
-v "$PWD/ldap-certs:/etc/openldap/certs:ro" \
chrroessner/openldap:2.6.15-debian13.7Wait until the container is healthy, then verify that John can bind and be found:
docker inspect --format '{{.State.Health.Status}}' openldap
docker exec openldap ldapsearch -x \
-H ldap://127.0.0.1:389 \
-D 'cn=admin,dc=example,dc=org' \
-w admin-secret \
-b 'ou=users,dc=example,dc=org' \
'(uid=johndoe)' dn uidCreate the OpenLDAP bootstrap data, Deployment, and Service:
apiVersion: v1
kind: ConfigMap
metadata:
namespace: aic
name: openldap-bootstrap
data:
20-users.ldif: |
dn: ou=users,dc=example,dc=org
objectClass: organizationalUnit
ou: users
dn: uid=johndoe,ou=users,dc=example,dc=org
objectClass: inetOrgPerson
cn: John Doe
sn: Doe
uid: johndoe
mail: johndoe@example.org
userPassword: john-secret
dn: uid=janedoe,ou=users,dc=example,dc=org
objectClass: inetOrgPerson
cn: Jane Doe
sn: Doe
uid: janedoe
mail: janedoe@example.org
userPassword: jane-secret
---
apiVersion: v1
kind: Secret
metadata:
namespace: aic
name: openldap-credentials
type: Opaque
stringData:
admin-password: admin-secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: openldap
spec:
replicas: 1
selector:
matchLabels:
app: openldap
template:
metadata:
labels:
app: openldap
spec:
containers:
- name: openldap
image: chrroessner/openldap:2.6.15-debian13.7
env:
- name: LDAP_DOMAIN
value: example.org
- name: LDAP_BASE_DN
value: dc=example,dc=org
- name: LDAP_ORGANISATION
value: Example Organization
- name: LDAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: openldap-credentials
key: admin-password
- name: LDAP_CREATE_PEOPLE_OU
value: "false"
- name: LDAP_CREATE_GROUPS_OU
value: "false"
ports:
- name: ldap
containerPort: 389
readinessProbe:
tcpSocket:
port: ldap
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: bootstrap
mountPath: /docker-entrypoint-initdb.d/20-users.ldif
subPath: 20-users.ldif
readOnly: true
volumes:
- name: bootstrap
configMap:
name: openldap-bootstrap
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: openldap
spec:
selector:
app: openldap
ports:
- name: ldap
port: 389
targetPort: ldapApply the manifest and wait for the directory to be ready:
kubectl apply -f openldap.yaml
kubectl rollout status deployment/openldap -n aicThe fixture uses tutorial credentials and stores its directory in the container filesystem. For production, use persistent storage, Kubernetes Secrets or another credential manager, restricted network access, and TLS with a trusted certificate.
Authenticate Against an LDAP Directory
This example authenticates clients against the directory without mapping them onto consumers.
Create a route with ldap-auth-advanced:
curl "http://127.0.0.1:9180/apisix/admin/routes/ldap-auth-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/ldap-auth",
"plugins": {
"proxy-rewrite": {
"uri": "/anything",
"headers": {
"remove": ["Authorization", "Proxy-Authorization"]
}
},
"ldap-auth-advanced": {
"ldap_uri": "openldap:389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "admin-secret",
"consumer_required": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'Create a route with the ldap-auth-advanced plugin configured:
services:
- name: ldap-auth-service
labels:
docs-example: ldap-auth
routes:
- name: ldap-auth-route
uris:
- /ldap-auth
plugins:
proxy-rewrite:
uri: /anything
headers:
remove:
- Authorization
- Proxy-Authorization
ldap-auth-advanced:
ldap_uri: openldap:389
base_dn: ou=users,dc=example,dc=org
attribute: uid
bind_dn: cn=admin,dc=example,dc=org
ldap_password: admin-secret
consumer_required: false
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=ldap-authSynchronize the reviewed service configuration:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=ldap-authCreate a route with the ldap-auth-advanced plugin configured:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
ports:
- name: http
port: 80
targetPort: 80
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: ldap-auth-plugin-config
spec:
plugins:
- name: proxy-rewrite
config:
uri: /anything
headers:
remove:
- Authorization
- Proxy-Authorization
- name: ldap-auth-advanced
config:
ldap_uri: openldap.aic.svc:389
base_dn: ou=users,dc=example,dc=org
attribute: uid
bind_dn: cn=admin,dc=example,dc=org
ldap_password: admin-secret
consumer_required: false
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: ldap-auth-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /ldap-auth
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: ldap-auth-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80Apply the configuration to your cluster:
kubectl apply -f ldap-auth-ic.yamlapiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
port: 80
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: ldap-auth-route
spec:
ingressClassName: apisix
http:
- name: ldap-auth-route
match:
paths:
- /ldap-auth
upstreams:
- name: httpbin-external-domain
plugins:
- name: proxy-rewrite
enable: true
config:
uri: /anything
headers:
remove:
- Authorization
- Proxy-Authorization
- name: ldap-auth-advanced
enable: true
config:
ldap_uri: openldap.aic.svc:389
base_dn: ou=users,dc=example,dc=org
attribute: uid
bind_dn: cn=admin,dc=example,dc=org
ldap_password: admin-secret
consumer_required: falseApply the configuration to your cluster:
kubectl apply -f ldap-auth-ic.yamlVerify with Valid Credentials
Send a request to the route with the directory user's credentials, base64 encoded and presented in the ldap scheme:
curl -i "http://127.0.0.1:9080/ldap-auth" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"The request should return 200 OK. The upstream response should not contain Authorization or Proxy-Authorization.
Verify with Invalid Credentials
Send a request to the route with an incorrect password:
curl -i "http://127.0.0.1:9080/ldap-auth" \
-H "Authorization: ldap $(printf '%s' 'johndoe:wrong-password' | base64)"The request should return 401 Unauthorized with the LDAP challenge:
WWW-Authenticate: ldap realm="ldap"The response body should be:
{"message":"Authorization required"}Verify without Credentials
Send a request to the route without any credentials:
curl -i "http://127.0.0.1:9080/ldap-auth"The request should return 401 Unauthorized.
Map LDAP Users to Consumers
This example maps a directory user onto a consumer so consumer-scoped plugins, rate limits, and analytics apply to their traffic. The consumer credential records the full DN resolved by the directory search.
info
Consumer credentials of type ldap-auth-advanced are created through the Admin API or the Dashboard. ADC and the Ingress Controller currently support only key-auth, basic-auth, jwt-auth, and hmac-auth credentials.
Create a consumer johndoe:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "johndoe"
}'Create an ldap-auth-advanced credential for the consumer, recording the DN of the directory entry:
curl "http://127.0.0.1:9180/apisix/admin/consumers/johndoe/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-john-ldap-auth",
"plugins": {
"ldap-auth-advanced": {
"user_dn": "uid=johndoe,ou=users,dc=example,dc=org"
}
}
}'Create a route with ldap-auth-advanced, leaving consumer_required at its default of true:
curl "http://127.0.0.1:9180/apisix/admin/routes/ldap-auth-consumer-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/ldap-consumer",
"plugins": {
"proxy-rewrite": {
"uri": "/anything",
"headers": {
"remove": ["Authorization", "Proxy-Authorization"]
}
},
"ldap-auth-advanced": {
"ldap_uri": "openldap:389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "admin-secret"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'Send a request to the route with the directory user's credentials:
curl -i "http://127.0.0.1:9080/ldap-consumer" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"The request should return 200 OK. The upstream should receive the consumer identity without either credential header:
{
"headers": {
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-ldap-auth"
}
}Jane exists in the directory but has no consumer credential. Send her valid directory credentials:
curl -i "http://127.0.0.1:9080/ldap-consumer" \
-H "Authorization: ldap $(printf '%s' 'janedoe:jane-secret' | base64)"The request should return 401 Unauthorized, confirming that successful directory authentication does not bypass the consumer requirement.
Accept the HTTP Basic Authentication Scheme
This example accepts the standard HTTP Basic scheme so existing Basic clients can authenticate without changing their credential format.
Set header_type to basic on the route:
curl "http://127.0.0.1:9180/apisix/admin/routes/ldap-auth-basic-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/ldap-basic",
"plugins": {
"proxy-rewrite": {
"uri": "/anything",
"headers": {
"remove": ["Authorization", "Proxy-Authorization"]
}
},
"ldap-auth-advanced": {
"ldap_uri": "openldap:389",
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "admin-secret",
"header_type": "basic",
"consumer_required": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'Send a request using an ordinary Basic credential:
curl -i "http://127.0.0.1:9080/ldap-basic" -u johndoe:john-secretThe request should return 200 OK, and the upstream should not receive the Authorization header.
Send a request without credentials:
curl -i "http://127.0.0.1:9080/ldap-basic"The request should return 401 Unauthorized with the Basic challenge:
WWW-Authenticate: Basic realm="ldap"Connect to the Directory Over TLS
The plugin supports LDAPS and StartTLS. The Docker fixture above enables both transports, while the Kubernetes fixture exposes plaintext LDAP only. The remainder of this section uses the Docker fixture. For Kubernetes, configure the directory with a certificate Secret and expose its TLS listener before adapting this example. The two plugin options are mutually exclusive:
| Transport | Directory address | Configuration |
|---|---|---|
| LDAPS | openldap:636 | use_ldaps: true |
| StartTLS | openldap:389 | use_starttls: true |
Copy the gateway container's system CA bundle, append the directory's issuing CA, and copy the combined bundle back into the container:
docker cp \
"$GATEWAY_CONTAINER":/etc/ssl/certs/ca-certificates.crt \
ldap-certs/system-ca-bundle.crt
cat ldap-certs/system-ca-bundle.crt ldap-certs/ca.crt \
> ldap-certs/gateway-ca-bundle.crt
docker cp ldap-certs/gateway-ca-bundle.crt \
"$GATEWAY_CONTAINER":/usr/local/apisix/conf/gateway-ca-bundle.crtConfigure the gateway to use the combined bundle in config.yaml:
apisix:
ssl:
ssl_trusted_certificate: /usr/local/apisix/conf/gateway-ca-bundle.crtReload the gateway after saving the configuration:
docker exec "$GATEWAY_CONTAINER" apisix reloadThe trust bundle is global. Preserve the deployment's existing CA certificates and apply the configuration and bundle to every gateway instance in a multi-instance deployment.
Create an LDAPS-protected route. Certificate verification remains enabled:
curl "http://127.0.0.1:9180/apisix/admin/routes/ldap-auth-ldaps-route" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"uri": "/ldap-ldaps",
"plugins": {
"proxy-rewrite": {
"uri": "/anything",
"headers": {
"remove": ["Authorization", "Proxy-Authorization"]
}
},
"ldap-auth-advanced": {
"ldap_uri": "openldap:636",
"use_ldaps": true,
"ssl_verify": true,
"base_dn": "ou=users,dc=example,dc=org",
"attribute": "uid",
"bind_dn": "cn=admin,dc=example,dc=org",
"ldap_password": "admin-secret",
"consumer_required": false
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'Send an authenticated request through LDAPS:
curl -i "http://127.0.0.1:9080/ldap-ldaps" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"The request should return 200 OK, and the upstream should not receive the credential header. The same fixture also supports StartTLS with the values shown in the table.