compose.yaml内容如下:
services:
openldap:
container_name: openldap
image: bitnami/openldap:latest
restart: unless-stopped
hostname: openldap
volumes:
- openldap_vol:/bitnami/openldap
- $PWD/certs:/opt/openldap/certs
networks:
- default
ports:
- 389:389
- 636:636
environment:
- TZ=Asia/Shanghai
- LDAP_ROOT=dc=abitacc,dc=com
- LDAP_ADMIN_DN=cn=admin,dc=abitacc,dc=com
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=${OPENLDAP_ADMIN_PASSWD}
- LDAP_ENABLE_TLS=yes
- LDAP_TLS_CERT_FILE=/opt/openldap/certs/openldap.crt
- LDAP_TLS_KEY_FILE=/opt/openldap/certs/openldap.key
- LDAP_TLS_CA_FILE=/opt/openldap/certs/openldapCA.crt
- LDAP_PORT_NUMBER=389
- LDAP_LDAPS_PORT_NUMBER=636
ldap-ui:
container_name: ldap_ui
image: dnknth/ldap-ui:latest
restart: unless-stopped
hostname: ldap_ui
ports:
- 5000:5000
networks:
- default
environment:
- TZ=Asia/Shanghai
- LDAP_URL=ldaps://ldap.abitacc.com:636
- BASE_DN=dc=abitacc,dc=com
- LOGIN_ATTR=cn
- BIND_PATTERN=cn=%s,dc=abitacc,dc=com
depends_on:
- openldap
ldap-user-ui:
container_name: ldap-user-ui
image: eryajf/go-ldap-admin:latest
restart: unless-stopped
hostname: ldap-user-manager
networks:
- default
depends_on:
- openldap
environment:
TZ: Asia/Shanghai
WAIT_HOSTS: openldap:636
DB_DRIVER: sqlite3
ports:
- 9020:8888
volumes:
- $PWD/data/go-ldap-admin:/app/data
- $PWD/data/config.yml:/app/config.yml
links:
- openldap:go-ldap-admin-openldap
networks:
default:
name: ldap_net
volumes:
openldap_vol:
name: openldap_data
使用 get_ldap_cert.sh
脚本完成TLS证书的获取和下载,内容如下:
#!/bin/bash
set -e
# --- Configuration ---
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="$SCRIPT_DIR/.env"
ACME_DIR="$SCRIPT_DIR/acme.sh"
ACME_SH="$ACME_DIR/acme.sh"
DOMAIN="ldap.abitacc.com"
RENEW_DAYS=30
REGISTER_EMAIL="wangkart@aliyun.com"
OUTPUT_DIR=""
# --- Helper Functions ---
print_usage() {
echo "Usage: $0 -o <output_directory>"
echo " -o, --output-dir Directory to save certificate files."
exit 1
}
# --- Argument Parsing ---
while [[ "$#" -gt 0 ]]; do
case $1 in
-o|--output-dir) OUTPUT_DIR="$2"; shift ;;
*) echo "Unknown parameter passed: $1"; print_usage ;;
esac
shift
done
if [ -z "$OUTPUT_DIR" ]; then
echo "Error: Output directory must be specified."
print_usage
fi
mkdir -p "$OUTPUT_DIR"
OUTPUT_DIR="$(cd "$OUTPUT_DIR" && pwd)" # Get absolute path
CRT_PATH="$OUTPUT_DIR/openldap.crt"
# Check if certificate exists and is valid for more than RENEW_DAYS
if [ -f "$CRT_PATH" ]; then
END_DATE=$(openssl x509 -enddate -noout -in "$CRT_PATH" | cut -d= -f2)
END_DATE_SEC=$(date -d "$END_DATE" +%s)
NOW_SEC=$(date +%s)
DAYS_LEFT=$(( (END_DATE_SEC - NOW_SEC) / 86400 ))
echo "[INFO] $CRT_PATH days left: $DAYS_LEFT"
if [ "$DAYS_LEFT" -gt "$RENEW_DAYS" ]; then
echo "[INFO] Certificate is valid for more than $RENEW_DAYS days, no renewal needed."
exit 0
else
echo "[INFO] Certificate is about to expire or already expired, proceeding to renew."
fi
else
echo "[INFO] Certificate file $CRT_PATH not found, proceeding to issue."
fi
# --- Main Logic ---
echo "Loading Aliyun credentials from $ENV_FILE"
if [ ! -f "$ENV_FILE" ]; then
echo ".env file not found: $ENV_FILE"
exit 1
fi
export $(grep -E 'ALIYUN_ACCESS_KEY_ID=|ALIYUN_ACCESS_KEY_SECRET=' "$ENV_FILE" | xargs)
if [ -z "$ALIYUN_ACCESS_KEY_ID" ] || [ -z "$ALIYUN_ACCESS_KEY_SECRET" ]; then
echo "Aliyun credentials not found in $ENV_FILE"
exit 1
fi
if [ ! -f "$ACME_SH" ]; then
echo "acme.sh not found, downloading..."
ACME_TAR_URL="https://github.com/acmesh-official/acme.sh/archive/refs/tags/3.1.1.tar.gz"
ACME_TAR="$SCRIPT_DIR/acme.sh.tar.gz"
wget -q "$ACME_TAR_URL" -O "$ACME_TAR"
tar -xzf "$ACME_TAR" -C "$SCRIPT_DIR"
ACME_EXTRACTED_DIR=$(find "$SCRIPT_DIR" -maxdepth 1 -type d -name "acme.sh-*")
mv "$ACME_EXTRACTED_DIR" "$ACME_DIR"
rm -f "$ACME_TAR"
chmod +x "$ACME_SH"
fi
echo "Setting Let's Encrypt as default CA"
"$ACME_SH" --home "$ACME_DIR" --set-default-ca --server letsencrypt
echo "Registering account email: $REGISTER_EMAIL"
"$ACME_SH" --home "$ACME_DIR" --register-account -m "$REGISTER_EMAIL"
echo "Issuing/Renewing certificate for $DOMAIN"
export Ali_Key="$ALIYUN_ACCESS_KEY_ID"
export Ali_Secret="$ALIYUN_ACCESS_KEY_SECRET"
"$ACME_SH" --home "$ACME_DIR" \
--issue --standalone --dns dns_ali \
-d "$DOMAIN" \
--days "$RENEW_DAYS" \
--force --keylength 2048
echo "Installing certificate files to $OUTPUT_DIR"
"$ACME_SH" --home "$ACME_DIR" \
--install-cert -d "$DOMAIN" \
--key-file "$OUTPUT_DIR/openldap.key" \
--fullchain-file "$OUTPUT_DIR/openldap.crt" \
--ca-file "$OUTPUT_DIR/openldapCA.crt"
echo "Adjusting file permissions to 644 for Docker volume mount"
chmod 644 "$OUTPUT_DIR/openldap.key"
chmod 644 "$OUTPUT_DIR/openldap.crt"
chmod 644 "$OUTPUT_DIR/openldapCA.crt"
echo "Certificate files for $DOMAIN have been successfully saved to $OUTPUT_DIR"
ls -l "$OUTPUT_DIR"
访问http://192.168.0.110:5000,弹出登录框,输入admin和password,无法进入
错误日志:
openldap | 20:37:49.39 INFO ==> ** LDAP setup finished! **
openldap |
openldap | 20:37:49.51 INFO ==> ** Starting slapd **
openldap | 6873a89d.1ff2a3d9 0x7f79d2340740 @(#) $OpenLDAP: slapd 2.6.10 (May 23 2025 04:19:14) $
openldap | @79176fda6d74:/bitnami/blacksmith-sandox/openldap-2.6.10/servers/slapd
openldap | 6873a89d.21b6ce18 0x7f79d2340740 slapd starting
ldap_ui | /usr/lib/python3.12/site-packages/starlette/config.py:60: UserWarning: Config file '.env' not found.
ldap_ui | warnings.warn(f"Config file '{env_file}' not found.")
ldap_ui | INFO: Started server process [1]
ldap_ui | INFO: Waiting for application startup.
ldap_ui | INFO: Application startup complete.
ldap_ui | INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
ldap_ui | INFO: 192.168.0.107:49959 - "GET / HTTP/1.1" 200 OK
ldap_ui | INFO: 192.168.0.107:49959 - "GET /assets/index-BxCLA1wZ.js HTTP/1.1" 200 OK
ldap_ui | INFO: 192.168.0.107:49960 - "GET /assets/index-CusJ2HRh.css HTTP/1.1" 200 OK
ldap_ui | INFO: 192.168.0.107:49959 - "GET /api/whoami HTTP/1.1" 401 Unauthorized
ldap_ui | INFO: 192.168.0.107:49960 - "GET /favicon.ico HTTP/1.1" 200 OK
ldap_ui | ERROR: Error in GET http://192.168.0.110:5000/api/whoami:
ldap_ui | Traceback (most recent call last):
ldap_ui | File "/usr/lib/python3.12/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
ldap_ui | await app(scope, receive, sender)
ldap_ui | File "/usr/lib/python3.12/site-packages/starlette/routing.py", line 73, in app
ldap_ui | response = await f(request)
ldap_ui | ^^^^^^^^^^^^^^^^
ldap_ui | File "/usr/lib/python3.12/site-packages/fastapi/routing.py", line 291, in app
ldap_ui | solved_result = await solve_dependencies(
ldap_ui | ^^^^^^^^^^^^^^^^^^^^^^^^^
ldap_ui | File "/usr/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 638, in solve_dependencies
ldap_ui | solved = await call(**solved_result.values)
ldap_ui | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ldap_ui | File "/usr/lib/python3.12/site-packages/ldap_ui/ldap_api.py", line 105, in authenticated
ldap_ui | await get_root_dse(connection)
ldap_ui | File "/usr/lib/python3.12/site-packages/ldap_ui/ldap_api.py", line 81, in get_root_dse
ldap_ui | connection.search(
ldap_ui | File "/usr/lib/python3.12/site-packages/ldap/ldapobject.py", line 628, in search
ldap_ui | return self.search_ext(base,scope,filterstr,attrlist,attrsonly,None,None)
ldap_ui | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ldap_ui | File "/usr/lib/python3.12/site-packages/ldap/ldapobject.py", line 614, in search_ext
ldap_ui | return self._ldap_call(
ldap_ui | ^^^^^^^^^^^^^^^^
ldap_ui | File "/usr/lib/python3.12/site-packages/ldap/ldapobject.py", line 128, in _ldap_call
ldap_ui | result = func(*args,**kwargs)
ldap_ui | ^^^^^^^^^^^^^^^^^^^^
ldap_ui | ldap.SERVER_DOWN: {'result': -1, 'desc': "Can't contact LDAP server", 'errno': 107, 'ctrls': [], 'info': 'Socket not connected'}
ldap_ui | INFO: 192.168.0.107:49963 - "GET /api/whoami HTTP/1.1" 500 Internal Server Error
从错误日志可以看出:
ldap.SERVER_DOWN: {'desc': "Can't contact LDAP server", 'info': 'Socket not connected'}
这一行是关键:问题很明确是ldap-ui
容器无法连接到 openldap
容器。
RootCause:
bitnami/openldap
镜像在容器内部默认监听的端口是 1389和1636,而不是标准的 LDAP 端口 389和636。在 ldap-ui
里配置 LDAP_URL=ldap://openldap
,默认会尝试连接 openldap
服务的 389
端口,因此连接失败。
在 docker-compose.yml
里设置如下两个环境变量,就会让openldap
监听389和636两个端口:
environment:
- LDAP_PORT_NUMBER=389
- LDAP_LDAPS_PORT_NUMBER=636
docker ps
的端口显示规则如下:
0.0.0.0:389->389/tcp
1389/tcp, 1636/tcp
因此如果没有在docker-compose.yml
里设置ports映射,即使设置LDAP_PORT_NUMBER和LDAP_LDAPS_PORT_NUMBER
两个变量,docker ps显示的端口一直是1389/tcp, 1636/tcp
进入openldap容器,执行
cat /opt/bitnami/openldap/var/run/slapd.args
/opt/bitnami/openldap/sbin/slapd -d 256 -h ldapi:/// ldap://:389/ ldaps://:636/ -F /opt/bitnami/openldap/etc/slapd.d
也能够看到当前openldap监听的端口
错误日志:
ldap_ui | INFO: Started server process [1]
ldap_ui | INFO: Waiting for application startup.
ldap_ui | INFO: Application startup complete.
ldap_ui | INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
ldap_ui | INFO: 192.168.0.107:50179 - "GET / HTTP/1.1" 304 Not Modified
openldap | 6873a9cb.1ffdb5b3 0x7f6de77b86c0 conn=1000 fd=12 ACCEPT from IP=192.168.64.3:55376 (IP=0.0.0.0:1389)
openldap | 6873a9cb.200d1a8d 0x7f6de77b86c0 conn=1000 op=0 SRCH base="" scope=0 deref=0 filter="(objectClass=*)"
openldap | 6873a9cb.201020e9 0x7f6de77b86c0 conn=1000 op=0 SRCH attr=* +
openldap | 6873a9cb.2018131e 0x7f6de77b86c0 conn=1000 op=0 SEARCH RESULT tag=101 err=0 qtime=0.000052 etime=0.000861 nentries=1 text=
openldap | 6873a9cb.2077ab79 0x7f6de77b86c0 conn=1000 op=1 SRCH base="dc=abitacc,dc=com" scope=2 deref=0 filter="(uid=admin)"
openldap | 6873a9cb.208283fc 0x7f6de77b86c0 conn=1000 op=1 SEARCH RESULT tag=101 err=0 qtime=0.000073 etime=0.000823 nentries=0 text=
ldap_ui | INFO: 192.168.0.107:50179 - "GET /api/whoami HTTP/1.1" 401 Unauthorized
openldap | 6873a9cb.2182341a 0x7f6de77b86c0 conn=1000 op=2 UNBIND
openldap | 6873a9cb.2189cd25 0x7f6de77b86c0 conn=1000 fd=12 closed
openldap | 6873a9dc.20b8973c 0x7f6de77b86c0 conn=1001 fd=12 ACCEPT from IP=192.168.64.3:33248 (IP=0.0.0.0:1389)
openldap | 6873a9dc.20c7ded2 0x7f6de77b86c0 conn=1001 op=0 SRCH base="dc=abitacc,dc=com" scope=2 deref=0 filter="(uid=admin)"
openldap | 6873a9dc.20d23874 0x7f6de77b86c0 conn=1001 op=0 SEARCH RESULT tag=101 err=0 qtime=0.000059 etime=0.000787 nentries=0 text=
ldap_ui | INFO: 192.168.0.107:50180 - "GET /api/whoami HTTP/1.1" 401 Unauthorized
openldap | 6873a9dc.21be05e3 0x7f6de77b86c0 conn=1001 op=1 UNBIND
openldap | 6873a9dc.21c6a288 0x7f6de77b86c0 conn=1001 fd=12 closed
openldap | 6873a9dd.243a6bc3 0x7f6de77b86c0 conn=1002 fd=12 ACCEPT from IP=192.168.64.3:50140 (IP=0.0.0.0:1389)
openldap | 6873a9dd.2446e074 0x7f6de5ab56c0 conn=1002 op=0 SRCH base="dc=abitacc,dc=com" scope=2 deref=0 filter="(?uid=)"
openldap | 6873a9dd.2451165a 0x7f6de5ab56c0 conn=1002 op=0 SEARCH RESULT tag=101 err=0 qtime=0.000070 etime=0.000780 nentries=0 text=
ldap_ui | INFO: 192.168.0.107:50180 - "GET /api/whoami HTTP/1.1" 401 Unauthorized
openldap | 6873a9dd.2533e208 0x7f6de77b86c0 conn=1002 op=1 UNBIND
openldap | 6873a9dd.25477364 0x7f6de77b86c0 conn=1002 fd=12 closed
问题已经从“无法连接”变成了“认证失败”。从日志中可以看到:
openldap
日志显示 ACCEPT from IP=192.168.64.3
,这说明 ldap-ui
已经成功连接到 OpenLDAP 容器的 1389
端口。openldap
日志显示 SRCH ... filter="(uid=admin)"
和 nentries=0
。ldap-ui
日志显示 GET /api/whoami HTTP/1.1" 401 Unauthorized
。RootCause:
ldap-ui
默认使用 uid
(User ID) 作为登录属性来查找用户。但是,bitnami/openldap
默认创建的管理员用户的标识是 cn
(Common Name),而不是 uid
。所以,当你在 ldap-ui
中输入 admin
时,它向 LDAP 服务器查询 “请找到 uid
是 admin
的用户”,而 LDAP 服务器中只有 “cn
是 admin
的用户”,所以服务器回答 “找不到 (nentries=0
)”。
解决办法:
需要告诉 ldap-ui
使用 cn
作为登录属性。这可以通过添加一个环境变量来完成。在你的 docker-compose.yml
文件中为 ldap-ui
服务添加 LOGIN_ATTR=cn
环境变量。
再次尝试登录还是无法进入。
错误日志:
ldap_ui | warnings.warn(f"Config file '{env_file}' not found.")
ldap_ui | INFO: Started server process [1]
ldap_ui | INFO: Waiting for application startup.
ldap_ui | INFO: Application startup complete.
ldap_ui | INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
openldap | 6873aaa3.1fd578e2 0x7fd97cf406c0 conn=1000 fd=12 ACCEPT from IP=192.168.80.3:42208 (IP=0.0.0.0:1389)
openldap | 6873aaa3.1fde1932 0x7fd97cf406c0 conn=1000 op=0 SRCH base="" scope=0 deref=0 filter="(objectClass=*)"
openldap | 6873aaa3.1fe2d54d 0x7fd97cf406c0 conn=1000 op=0 SRCH attr=* +
openldap | 6873aaa3.1feae5ce 0x7fd97cf406c0 conn=1000 op=0 SEARCH RESULT tag=101 err=0 qtime=0.000050 etime=0.000963 nentries=1 text=
openldap | 6873aaa3.2085e3e1 0x7fd97cf406c0 conn=1000 op=1 SRCH base="dc=abitacc,dc=com" scope=2 deref=0 filter="(cn=admin)"
openldap | 6873aaa3.20905f85 0x7fd97cf406c0 conn=1000 op=1 SEARCH RESULT tag=101 err=0 qtime=0.000090 etime=0.000816 nentries=0 text=
ldap_ui | INFO: 192.168.0.107:50301 - "GET /api/whoami HTTP/1.1" 401 Unauthorized
openldap | 6873aaa3.2196ca13 0x7fd97cf406c0 conn=1000 op=2 UNBIND
openldap | 6873aaa3.219d9edd 0x7fd97cf406c0 conn=1000 fd=12 closed
分析openldap
日志:
filter="(cn=admin)"
: 这非常好!说明我们上次的修改 (LOGIN_ATTR=cn
) 已经生效,ldap-ui
现在使用正确的属性 cn
来查找用户。SEARCH RESULT ... nentries=0
: 这是新的问题所在。即使使用了正确的 cn=admin
进行搜索,LDAP 服务器仍然返回了 0 个条目,意思是“在 dc=abitacc,dc=com
这个根目录下,还是找不到 cn
为 admin
的用户”。从ldap-ui官方文档可以看到如下内容:
The UI always uses a simple bind
operation to authenticate with the LDAP directory. How the bind
DN is obtained from a given user name depends on a combination of OS environment variables, possibly from a .env
file:
uid
, which can be overridden by the environment variable LOGIN_ATTR
, e.g. LOGIN_ATTR=cn
.BIND_PATTERN
is set, then no search is performed. Login with a full DN can be configured with BIND_PATTERN=%s
, which for example allows to login as user cn=admin,dc=example,dc=org
. If a partial DN like BIND_PATTERN=%s,dc=example,dc=org
is configured, the corresponding login would be cn=admin
. If a specific pattern like BIND_PATTERN=cn=%s,dc=example,dc=org
is configured, the login name is just admin
.BIND_DN
and BIND_PASSWORD
can be set in the environment. This is for demo purposes only, and probably a very bad idea if access to the UI is not restricted by any other means.方法一:LOGIN_ATTR
(我们最初的尝试)
ldap-ui
以匿名身份连接到 LDAP,然后根据 LOGIN_ATTR
(我们设为 cn
) 搜索您在登录框输入的用户名(如 admin
)。找到用户的完整 DN 后,再用这个 DN 和您输入的密码尝试绑定(登录)。bitnami/openldap
默认配置是安全的,它不允许匿名用户搜索目录中的用户条目。因此,ldap-ui
的第一步“搜索 cn=admin
”就因为权限不足而失败了,直接导致 nentries=0
,登录无法继续。方法二 BIND_PATTERN
根据文档这种方式最适合的场景、既安全又方便的方法是使用 BIND_PATTERN
。
ldap-ui
服务添加 BIND_PATTERN=cn=%s,dc=abitacc,dc=com
环境变量。在登录框的用户名处输入 admin
,然后输入您的密码,ldap-ui
会将 admin
填入 BIND_PATTERN
的 %s
中,构成 cn=admin,dc=abitacc,dc=com
,然后用这个 DN 和您的密码去登录。这样就能成功了。方法三:BIND_DN
和 BIND_PASSWORD
ldap-ui
应用本身在启动时就使用您提供的固定管理员凭据登录到 LDAP。尝试在compose.yaml里面给ldap-ui
服务添加 BIND_DN
和 BIND_PASSWORD
两个环境变量。再次访问登录,这次可以不需要输入admin和password,这次成功进入了。
端口与协议的对应关系
ldap://
协议默认用于明文连接,默认端口是 389。ldaps://
协议用于加密(TLS)连接,默认端口是 636(或自定义的 1636)。
如果开启了 TLS,证书的 Common Name(CN)或 Subject Alternative Name(SAN)必须与客户端访问时用的域名或主机名一致,否则会出现证书校验失败。
openssl x509 -in /opt/openldap/certs/openldap.crt -noout -text | grep -A 2 "Subject:"
openssl x509 -in /opt/openldap/certs/openldap.crt -noout -text | grep -A 10 "Subject Alternative Name"
使用说明:
Openldap Server使能TLS后,使用如下命令验证TLS是否生效
ldapsearch -H ldaps://ldap.abitacc.com:636 -x -b "dc=abitacc,dc=com" -D "cn=admin,dc=abitacc,dc=com" -W -d 1
为了能够访问ldap.abitacc.com:636
,首先需要在阿里云和frp里面配置好链路,不然命令会报连接失败,
这样才能让ldaps://ldap.abitacc.com:636
访问抵达内网,与运行的openldap服务建立连接。
错误日志1:
ldap_url_parse_ext(ldaps://ldap.abitacc.com:636)
ldap_create
ldap_url_parse_ext(ldaps://ldap.abitacc.com:636/??base)
Enter LDAP Password:
ldap_sasl_bind
ldap_send_initial_request
ldap_new_connection 1 1 0
ldap_int_open_connection
ldap_connect_to_host: TCP ldap.abitacc.com:636
ldap_new_socket: 3
ldap_prepare_socket: 3
ldap_connect_to_host: Trying 47.97.122.181:636
ldap_pvt_connect: fd: 3 tm: -1 async: 0
attempting to connect:
connect errno: 111
ldap_close_socket: 3
ldap_err2string
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
这个就是上面的域名解析和frpc的配置未完成或者不对导致的。
错误日志2:
ldapsearch -H ldaps://abitacc.com:636 -x -b "dc=abitacc,dc=com" -D "cn=admin,dc=abitacc,dc=com" -W -d 1
ldap_url_parse_ext(ldaps://abitacc.com:636)
ldap_create
ldap_url_parse_ext(ldaps://abitacc.com:636/??base)
Enter LDAP Password:
ldap_sasl_bind
ldap_send_initial_request
ldap_new_connection 1 1 0
ldap_int_open_connection
ldap_connect_to_host: TCP abitacc.com:636
ldap_new_socket: 3
ldap_prepare_socket: 3
ldap_connect_to_host: Trying 47.97.122.181:636
ldap_pvt_connect: fd: 3 tm: -1 async: 0
attempting to connect:
connect success
TLS: can't connect: The TLS connection was non-properly terminated..
ldap_err2string
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)
说明:TCP连接已建立,但TLS握手失败。
docker logs openldap
21:41:02.29 INFO ==> ** Starting LDAP setup **
21:41:02.39 INFO ==> Validating settings in LDAP_* env vars
21:41:02.43 INFO ==> Initializing OpenLDAP...
21:41:02.47 INFO ==> Using persisted data
21:41:02.49 INFO ==> ** LDAP setup finished! **
21:41:02.58 INFO ==> ** Starting slapd **
687a4eee.2399f911 0x7f5e44ed0740 @(#) $OpenLDAP: slapd 2.6.10 (May 23 2025 04:19:14) $
@79176fda6d74:/bitnami/blacksmith-sandox/openldap-2.6.10/servers/slapd
687a4eee.24ff8a78 0x7f5e44ed0740 slapd starting
687a4eff.2c9d358b 0x7f5dfffff6c0 conn=1000 fd=14 ACCEPT from IP=192.168.240.1:39440 (IP=0.0.0.0:636)
687a4eff.2defa65e 0x7f5dfffff6c0 TLS: init_def_ctx: .
687a4eff.2df426e8 0x7f5dfffff6c0 conn=1000 fd=14 closed (TLS negotiation failure)
排查方向:
1. 检查证书和私钥是否配对一致
openssl x509 -noout -modulus -in openldap.crt | openssl md5
openssl rsa -noout -modulus -in openldap.key | openssl md5
acme.sh 默认生成的证书类型可能是 EC(椭圆曲线)证书,而部分 OpenLDAP 版本或配置只支持 RSA 证书和密钥。如果 slapd 不支持 EC 密钥,也会导致 TLS 握手失败。--keylength 2048
会生成 RSA 证书。
2. 用 openssl 测试 TLS 握手
openssl s_client -connect abitacc.com:636 -CAfile /opt/openldap/certs/openldapCA.crt
3. 检查证书的权限
核对compose.yaml配置都是正确的,那么 TLS negotiation failure
的问题很可能出在文件权限上。Bitnami/openldap容器通常以非 root 用户(用户 ID 1001
)运行。虽然在 docker-compose.yml
中指定了 user: root
,容器内的启动脚本切换到root 用户,但是挂载的证书文件在host主机上权限设置不当,容器内的非 root 用户和root用户也可能无法读取这些证书。容器的root用户没有映射到host root,也起不到多大的作用。
-rw-r--r-- 1 root root 1802 7月 18 21:32 openldapCA.crt
-rw-r--r-- 1 root root 3587 7月 18 21:32 openldap.crt
-rw------- 1 root root 1679 7月 18 21:32 openldap.key
没有chmod的情况下,openldap.key
的权限会让容器读取不到,从而导致TLS握手失败。
LDAP_ADMIN_USERNAME
(数据管理员)dc=yourdomain,dc=com
这个根节点下的所有条目(用户、组、组织单元等)拥有完全的增、删、改、查权限。cn=admin,dc=yourdomain,dc=com
。LDAP_CONFIG_ADMIN_USERNAME
(配置管理员)cn=config
。cn=config
目录拥有完全的读写权限。cn=admin,cn=config
。posixAccount
或其他应用添加对象类)。特性 | LDAP_ADMIN_USERNAME |
LDAP_CONFIG_ADMIN_USERNAME |
---|---|---|
角色 | 数据管理员 (馆长) | 配置管理员 (工程师) |
管理对象 | 用户、组、密码等数据 | 服务器 Schema、索引、ACL 等配置 |
根节点 | dc=yourdomain,dc=com |
cn=config |
典型DN | cn=admin,dc=yourdomain,dc=com |
cn=admin,cn=config |
常用性 | 非常常用 | 较少使用,仅在修改服务器核心配置时 |
直接在ldap-ui里面添加USER需要非常熟悉LDAP组织结构和属性才行,因此对新手极其不友好,用这个去查看和修改ldap server里面的数据还可以,使用它新增user不合适。
为了保持目录整洁,最佳实践是先创建组织单元(OU)来存放用户和组。如果你已经创建过了,可以跳过此步。
dc=abitacc,dc=com
)。users
。groups
的 OU。ou=users
。cn
, sn
, uid
等常用属性。cn
(Common Name): 通常是用户的全名或用户名。例如:Test User
。sn
(Surname): 姓氏。例如:User
。displayName
: 显示名称。例如:Test
。uid
(User ID): 这个非常重要! 很多应用(包括 Authelia)会使用 uid
作为登录名。输入一个唯一的用户名,例如:testuser
。mail
: 用户的电子邮件地址。例如:test@abitacc.com
。在 LDAP 中,设置密码是一个独立的操作。
为了方便权限管理,通常会将用户添加到一个或多个组中。
ou=groups
下创建一个组。点击 Add -> Generic: Group,在 cn
字段中输入组名,例如 developers
。member
属性,点击右侧的 + 号。cn=Test User,ou=users,dc=abitacc,dc=com
。现在,你的 LDAP 中已经有了一个新用户 testuser
。其他应用(如 Authelia)的配置需要与此对应:
ou=users,dc=abitacc,dc=com
(uid={input})
或 (&(objectClass=inetOrgPerson)(uid={input}))
。这会告诉应用使用 uid
字段作为登录名。testuser
和你刚刚设置的密码。应用会使用自己的绑定凭据连接到 LDAP,根据用户输入的 testuser
在 ou=users
下搜索 uid=testuser
的条目,然后用找到的条目和用户输入的密码尝试进行一次绑定操作。如果绑定成功,则认证通过。