ldap-auth-advanced
ldap-auth-advanced 插件对接 OpenLDAP、Active Directory 等 LDAP 目录完成客户端认证,并可把认证到的目录用户映射为消费者(Consumer)。
插件采用「先检索、后绑定」的方式解析用户:先在 base_dn 下检索 attribute 与客户端提供的用户名匹配的条目,再用客户端提供的密码以该条目执行绑定。由于用户的可分辨名称(DN)是检索出来的而非拼接出来的,因此无需把目录结构编码到网关配置中,用户也可以位于目录树的不同层级。
当 consumer_required 启用时(默认启用),插件会查找凭据中记录了已解析 DN 的消费者。匹配成功后,网关会在把请求转发到上游之前加上 X-Consumer-Username、X-Credential-Identifier 等请求头,从而让消费者级插件、限流和分析能力应用于 LDAP 认证流量。把 consumer_required 设为 false 则只对接目录完成认证,不要求存在对应的消费者。
自 API7 企业版 3.10.5 和 APISIX 3.18.0 起可用。
插件从 Authorization 请求头读取凭据;当 Proxy-Authorization 携带可用凭据时优先使用后者。方案名由 header_type 决定:默认值 ldap 对应 Authorization: ldap <base64(username:password)>;取值 basic 时使用标准的 Authorization: Basic <base64(username:password)>,因此已有的 HTTP Basic 客户端无需改动即可认证。
对于缺失、格式错误或被拒绝的用户凭据、匹配到多个用户,以及启用 consumer_required 时找不到消费者的情况,插件会返回 401 Unauthorized。LDAP 传输、TLS、协议、服务器、检索绑定及其他目录错误会返回 500 Internal Server Error,避免把服务中断误报为用户凭据无效。
示例
以下示例使用一个包含两个测试用户的隔离 OpenLDAP 目录。网关以 cn=admin,dc=example,dc=org 身份检索目录,并在把已认证的请求转发到上游之前移除凭据请求头。
设置 OpenLDAP
在网关所在的环境中启动目录服务。
- Docker
- Kubernetes
创建 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
为 TLS 示例生成短期有效的 CA 和服务器证书:
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"
创建证书扩展文件:
subjectAltName=DNS:openldap
extendedKeyUsage=serverAuth
签发服务器证书,并让容器化 LDAP 进程能够读取本地测试密钥:
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.key
将 GATEWAY_CONTAINER 设为 正在运行的 APISIX 或 API7 网关容器。创建专用网络并将网关连接到该网络:
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network create gateway-ldap-net
docker network connect gateway-ldap-net "$GATEWAY_CONTAINER"
启动 OpenLDAP 2.6 LTS,并启用 LDAP、StartTLS 和 LDAPS:
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.7
等待容器进入健康状态,然后验证 John 能够完成绑定并被检索到:
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 uid
创建 OpenLDAP 引导数据、Deployment 和 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: ldap
应用清单并等待目录服务就绪:
kubectl apply -f openldap.yaml
kubectl rollout status deployment/openldap -n aic
该测试环境使用教程凭据,并把目录数据存储在容器文件系统中。生产环境应使用持久化存储、Kubernetes Secret 或其他凭据管理器、受限网络访问,以及由可信证书保护的 TLS。
对接 LDAP 目录完成认证
以下示例只通过目录对客户端进行身份认证,不把客户端映射为消费者。
- Admin API
- ADC
- Ingress Controller
创建一个配置了 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
}
}
}'
创建一个配置了 ldap-auth-advanced 插件的路由:
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: 1
预览此示例所管理的变更,并确认差异中没有非预期的更新或删除:
adc diff -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=ldap-auth
同步已检查的服务配置:
adc sync -f adc.yaml \
--include-resource-type service \
--label-selector docs-example=ldap-auth
创建一个配置了 ldap-auth-advanced 插件的路由:
- Gateway API
- APISIX CRD
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: 80
把配置应用到集群:
kubectl apply -f ldap-auth-ic.yaml
apiVersion: 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: false
把配置应用到集群:
kubectl apply -f ldap-auth-ic.yaml
使用正确的凭据验证
用目录用户的凭据向该路由发送请求,凭据经 base64 编码后以 ldap 方案携带:
curl -i "http://127.0.0.1:9080/ldap-auth" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"
请求应返回 200 OK。上游响应中不应包含 Authorization 或 Proxy-Authorization。
使用错误的凭据验证
用错误的密码向该路由发送请求:
curl -i "http://127.0.0.1:9080/ldap-auth" \
-H "Authorization: ldap $(printf '%s' 'johndoe:wrong-password' | base64)"
请求应返回 401 Unauthorized,并携带 LDAP 质询:
WWW-Authenticate: ldap realm="ldap"
响应体应为:
{"message":"Authorization required"}
不携带凭据验证
不携带任何凭据向该路由发送请求:
curl -i "http://127.0.0.1:9080/ldap-auth"
请求应返回 401 Unauthorized。
把 LDAP 用户映射为消费者
以下示例把目录用户映射为消费者,从而让消费者级插件、限流和分析能力应用于该用户的流量。消费者凭据记录目录检索解析出的完整 DN。
ldap-auth-advanced 类型的消费者凭据通过 Admin API 或 Dashboard 创建。ADC 与 Ingress Controller 目前只支持 key-auth、basic-auth、jwt-auth 和 hmac-auth 类型的凭据。
创建消费者 johndoe:
curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "johndoe"
}'
为该消费者创建 ldap-auth-advanced 凭据,记录目录条目的 DN:
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"
}
}
}'
创建一个配置了 ldap-auth-advanced 的路由,consumer_required 保持默认值 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
}
}
}'
用目录用户的凭据向该路由发送请求:
curl -i "http://127.0.0.1:9080/ldap-consumer" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"
请求应返回 200 OK。上游应收到消费者身份,但不应收到任何凭据请求头:
{
"headers": {
"X-Consumer-Username": "johndoe",
"X-Credential-Identifier": "cred-john-ldap-auth"
}
}
Jane 存在于目录中,但没有消费者凭据。发送她的有效目录凭据:
curl -i "http://127.0.0.1:9080/ldap-consumer" \
-H "Authorization: ldap $(printf '%s' 'janedoe:jane-secret' | base64)"
请求应返回 401 Unauthorized,这说明目录认证成功也不会绕过消费者要求。
接受 HTTP Basic 认证方案
以下示例接受标准 HTTP Basic 方案,使现有 Basic 客户端无需更改凭据格式即可完成认证。
在路由上把 header_type 设为 basic:
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
}
}
}'
用普通的 Basic 凭据发送请求 :
curl -i "http://127.0.0.1:9080/ldap-basic" -u johndoe:john-secret
请求应返回 200 OK,且上游不应收到 Authorization 请求头。
发送不携带凭据的请求:
curl -i "http://127.0.0.1:9080/ldap-basic"
请求应返回 401 Unauthorized,并携带 Basic 质询:
WWW-Authenticate: Basic realm="ldap"
通过 TLS 连接目录
插件支持 LDAPS 和 StartTLS。上述 Docker 测试环境同时启用了这两种传输方式,而 Kubernetes 测试环境只公开明文 LDAP。下文使用 Docker 测试环境;若使用 Kubernetes,请先通过证书 Secret 配置目录服务并公开其 TLS 监听端口,再调整本示例。这两个插件选项互斥:
| 传输方式 | 目录地址 | 配置 |
|---|---|---|
| LDAPS | openldap:636 | use_ldaps: true |
| StartTLS | openldap:389 | use_starttls: true |
复制网关容器的系统 CA 包,追加目录证书的签发 CA,然后把合并后的证书包复制回容器:
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.crt
在 config.yaml 中配置网关使用合并后的证书包:
apisix:
ssl:
ssl_trusted_certificate: /usr/local/apisix/conf/gateway-ca-bundle.crt
保存配置后重新加载网关:
docker exec "$GATEWAY_CONTAINER" apisix reload
该信任证书包是全局配置。请保留部署现有的 CA 证书;在多实例部署中,应把此配置和证书包应用到每个网关实例。
创建一个使用 LDAPS 保护的路由,并保持证书校验启用:
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
}
}
}'
通过 LDAPS 发送已认证请求:
curl -i "http://127.0.0.1:9080/ldap-ldaps" \
-H "Authorization: ldap $(printf '%s' 'johndoe:john-secret' | base64)"
请求应返回 200 OK,且上游不应收到凭据请求头。该测试环境也支持 StartTLS,可使用表格中列出的配置值。