Debian/Ubuntu 接入 LDAP 统一认证后 getent 查不到用户、ssh 认证失败、家目录不创建:nss-pam-ldapd(nslcd) + NSS + PAM 全链路排查
日期:2026-09-11 环境:Debian 12 (bookworm) 容器 / OpenLDAP 2.4 (osixia/openldap:1.5.0) / Docker Compose 技术栈关键词:LDAP、OpenLDAP、nss-pam-ldapd、nslcd、libnss-ldapd、libpam-ldapd、NSS、nsswitch.conf、PAM、pam_ldap.so、pam_mkhomedir、minimum_uid、sshd、getent、pamtester、docker compose 报错关键词:
getent passwd无输出、Permission denied, please try again、Could not chdir to home directory、ldap_simple_bind_s(NULL,NULL)、err=32 No such object、Authentication failure
一、问题背景
公司把一批机器接入 LDAP 统一认证(OpenLDAP,dc=example,dc=com)。
在测试机上「按文档装好了包」:
apt-get install -y libnss-ldapd libpam-ldapd nslcd ldap-utils
验证时三连击全挂:
$ getent passwd alice
(没有任何输出,LDAP 用户"不存在")
$ ssh alice@127.0.0.1
alice@127.0.0.1's password:
Permission denied, please try again. # 目录密码明明是对的
$ ldapsearch -x -H ldap://ldap:389 -D "cn=admin,dc=example,dc=com" -w adminpass \
-b "dc=example,dc=com" "(uid=alice)"
dn: uid=alice,ou=people,dc=example,dc=com # 服务端数据/密码/权限全都没问题
最迷惑的点:
ldapsearch拿 admin 直连,能读到用户、userPassword也在 → 服务端是好的;- 但本机
getent/id/ssh全都不认识alice→ 客户端链路是断的; - 就算侥幸认证进去,
/home/alice不存在,卡在Could not chdir to home directory; systemctl status nslcd看起来"装了包",没有任何一条报错指向真正原因。
这就是最经典的一类问题:「装包 ≠ 配好」。NSS 和 PAM 是两套独立机制, 再加上 nslcd 自己的配置和目录访问控制,任何一环漏掉都表现为"登不上"。
二、复现步骤
本仓库提供了 Docker 一键环境(OpenLDAP 服务端 + 一台"缺配置的" Debian 客户端):
git fetch origin && git checkout 2026-09-11-0002
cd scenario/2026-09-11-0002
docker compose up -d --build # 拉起 r46-ldap + r46-client
sleep 15 # 等 LDAP 首次导入数据
docker compose exec client bash
进容器后一键复现,脚本会把每一步证据打出来:
bash /workspace/scenario/2026-09-11-0002/scripts/reproduce.sh
关键输出(4 处坑 + 1 个隐藏陷阱全部命中):
== 1. LDAP 服务端是好的吗? ==
dn: uid=alice,ou=people,dc=example,dc=com
result: 0 Success ← 服务端没问题
== 2. getent passwd alice ==
→ 身份解析失败(getent 查不到 LDAP 用户)
== 4. nsswitch.conf ==
passwd: files ← 坑 1:缺 ldap
group: files
shadow: files
== 5. nslcd.conf ==
uri ldap://127.0.0.1/ ← 坑 2:包默认值,指向自己
base dc=example,dc=net ← 坑 2:域名也不对
nslcd 没有在运行! ← 坑 2:没 systemd,装包不拉起
== 6. pamtester login alice ==
pamtester: Authentication failure ← 认证链路断
== 7. /home/alice 不存在(PAM 没配 mkhomedir) ← 坑 4
== 8. carol(uid=800)==
auth [success=1 default=ignore] pam_ldap.so minimum_uid=1000 use_first_pass
→ carol 认证失败! ← 坑 5
想一次看全量排查信息(真实运维最省时间的做法):
bash /workspace/scenario/2026-09-11-0002/scripts/inspect.sh
三、根本原因
五处问题,各自独立、缺一不可:
| # | 位置 | 现象 | 缺了什么 |
|---|---|---|---|
| 1 | /etc/nsswitch.conf | getent passwd alice 查不到 | passwd/group/shadow 行缺 ldap,NSS 根本不会去问 LDAP |
| 2 | /etc/nslcd.conf | 即使 NSS 配好也查不到 | 包默认值是 uri ldap://127.0.0.1/ + base dc=example,dc=net,与本环境不符;且 nslcd 进程没跑 |
| 3 | 目录访问控制 | nslcd 连上仍读不到,服务端 err=32 No such object | nslcd 默认匿名 bind;本目录禁止匿名读,必须配 binddn/bindpw |
| 4 | PAM 会话 | 认证通过但 /home/alice 不存在,用户被踢 | pam_mkhomedir 未启用(/usr/share/pam-configs/mkhomedir 默认 Default: no) |
| 5 | 隐藏陷阱 | carol 能 getent 到,但认证必失败 | pam_ldap.so minimum_uid=1000,carol 的 uidNumber=800 < 1000,被静默忽略 |
坑 3 的定位方法(最隐蔽)
把 nslcd 临时指向正确地址后开调试模式,直接看到它 bind 的身份是 NULL:
sed -i "s#^uri .*#uri ldap://ldap:389#; s#^base .*#base dc=example,dc=com#" /etc/nslcd.conf
timeout 8 /usr/sbin/nslcd -d > /tmp/nslcd-debug.log 2>&1 &
sleep 3; getent -s ldap passwd alice
grep -iE "bind|failed" /tmp/nslcd-debug.log
nslcd: DEBUG: ldap_simple_bind_s(NULL,NULL) (uri="ldap://ldap:389")
nslcd: ldap_result() failed: No such object
ldap_simple_bind_s(NULL,NULL) 就是匿名 bind。服务端侧日志完全对应:
docker compose logs ldap | grep -E 'BIND dn=""|err=32'
# conn=1003 op=0 BIND dn="" method=128
# conn=1003 op=1 SEARCH RESULT tag=101 err=32 nentries=0 text=
err=32 No such object 极具误导性:看着像"对象不存在",实际是匿名 bind
无权限看到任何条目时 OpenLDAP 的返回(而不是 50 Insufficient access)。
用下面两次查询对比即可一眼区分:
# 匿名 → 被拒
ldapsearch -x -H ldap://ldap:389 -b dc=example,dc=com "(uid=alice)" | tail -1
# result: 32 No such object
# 带 DN → 成功
ldapsearch -x -H ldap://ldap:389 -D cn=admin,dc=example,dc=com -w adminpass \
-b dc=example,dc=com "(uid=alice)" uid uidNumber | grep -E "uid|result|numEntries"
# uid: alice
# uidNumber: 10001
# result: 0 Success
# numEntries: 1
四、解决办法
1. NSS 接线:/etc/nsswitch.conf 加 ldap
for db in passwd group shadow; do
sed -i -E "s/^(${db}:[[:space:]]*)files/\1files ldap/" /etc/nsswitch.conf
done
grep -E '^(passwd|group|shadow):' /etc/nsswitch.conf
# passwd: files ldap
# group: files ldap
# shadow: files ldap
顺序有讲究:
files在前面。把ldap放前面会让每次ls -l、ps都去问目录,拖慢系统。
2. nslcd 配置 + 起进程:/etc/nslcd.conf
sed -i -E "s#^uri[[:space:]]+.*#uri ldap://ldap:389#" /etc/nslcd.conf
sed -i -E "s#^base[[:space:]]+.*#base dc=example,dc=com#" /etc/nslcd.conf
cat >> /etc/nslcd.conf <<'LDIF'
binddn cn=admin,dc=example,dc=com
bindpw adminpass
LDIF
mkdir -p /var/run/nslcd
/usr/sbin/nslcd # 容器无 systemd,必须手动拉起
sleep 2; pgrep -a nslcd
生产环境不要把密码明文写这里:应收紧权限(chmod 600)、或改用 SASL/GSSAPI、
或建一个专门的只读代理账号(生产千万别用 admin)。
3. PAM 会话:启用 pam_mkhomedir
echo "session optional pam_mkhomedir.so skel=/etc/skel umask=0022" >> /etc/pam.d/common-session
grep pam_mkhomedir /etc/pam.d/common-session
更规范:把
/usr/share/pam-configs/mkhomedir的Default: no改为yes后跑pam-auth-update。 直接改common-session立即生效,但下次pam-auth-update会覆盖。
4. carol(uid=800)的处理:方案 A 推荐
minimum_uid=1000 不是 bug 而是有意的安全策略(防止目录里混入 uid=0 之类账号提权)。
我在目录侧把 carol 改成 >=1000,视为纠正一条无效的运维配置:
ldapmodify -x -H ldap://ldap:389 -D cn=admin,dc=example,dc=com -w adminpass <<'LDIF'
dn: uid=carol,ou=people,dc=example,dc=com
changetype: modify
replace: uidNumber
uidNumber: 10800
-
replace: gidNumber
gidNumber: 10800
LDIF
方案 B(生产慎用):改 /usr/share/pam-configs/ldap 去掉 minimum_uid=1000,
再 pam-auth-update --force 重新生成 /etc/pam.d/*。
这会放开 UID 下限,等于给低 UID 账号开登录后门,务必明确接受风险再动。
五、验证
1. 身份解析
$ getent passwd alice
alice:x:10001:10001:Alice Wong:/home/alice:/bin/bash
$ getent group devs
devs:*:10001:bob,alice
2. PAM 认证(务必含反向用例)
$ pamtester login alice authenticate <<< alicepw123
pamtester: successfully authenticated
$ pamtester login alice authenticate <<< wrongpw
pamtester: Authentication failure # 错误密码必须被拒,不能"放开校验换通过"
3. ssh 登录 + 家目录自动创建
$ rm -rf /home/alice
$ mkdir -p /run/sshd && /usr/sbin/sshd -e
$ sshpass -p alicepw123 ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null alice@127.0.0.1 'id; echo HOME=$HOME; pwd'
uid=10001(alice) gid=10001(devs) groups=10001(devs)
HOME=/home/alice
/home/alice
$ ls -ld /home/alice
drwxr-xr-x 2 alice devs 73 ... /home/alice # mkhomedir 生效
id 里的 gid=10001(devs) 顺便证明组解析也走通了 LDAP,不只是 passwd。
4. 一键验收全绿
docker compose exec client bash /workspace/scenario/2026-09-11-0002/scripts/verify.sh
# 结果:PASS=12 FAIL=0
5. 可复现性回归
不是「改完当前容器能过」就算完。重建容器回到故障态,再跑一遍修复,验证从零可复现:
docker compose up -d --force-recreate client # 回到 BROKEN 状态
docker compose exec client bash .../scripts/fix_reference.sh
docker compose exec client bash .../scripts/verify.sh
# 结果:PASS=12 FAIL=0
六、经验与避坑
1. 记住这个排查顺序(每条都独立验证,别跳步)
1) 服务端好着吗? ldapsearch 带 admin DN 直连 → result: 0 就说明服务端 OK
2) NSS 接线了吗? grep ^passwd /etc/nsswitch.conf 有没有 ldap
3) nslcd 在跑吗? pgrep -a nslcd
4) nslcd 连得上吗? getent -s ldap passwd alice(只问 ldap 这一源,绕过 nsswitch 顺序)
5) bind 是谁? nslcd -d,看 ldap_simple_bind_s(NULL,NULL) 就是匿名 bind
6) PAM 通吗? pamtester login <user> authenticate
7) 家目录建了吗? grep mkhomedir /etc/pam.d/common-session
2. getent -s ldap 是分诊神器
它绕过 nsswitch.conf 的顺序,只问 LDAP。用它区分「nsswitch 没接线」还是「nslcd 自己有问题」:
getent passwd alice # 走完整 NSS 链路
getent -s ldap passwd alice # 只问 ldap,直连 nslcd
3. err=32 No such object ≠ 数据不存在
匿名 bind 无权限看到条目时,OpenLDAP 常回 32 而不是 50。
先怀疑权限,再怀疑数据——用「匿名 vs 带 DN」两次 ldapsearch 对比是最快的判据。
4. 容器 / 精简系统里「装包不会自动起服务」
没有 systemd,apt-get install nslcd 不会拉起进程。手动 /usr/sbin/nslcd,
并先 mkdir -p /var/run/nslcd(否则 socket 目录不存在)。
5. nslcd -d 的调试输出走 stderr
看着"没反应"时要 2>&1 重定向,否则日志全丢了。
6. minimum_uid 这类"安全的静默拦截"最容易漏
它静默忽略低 UID 用户,日志里什么都不说。遇到"能 getent 到却认证必失败",
先查 grep minimum_uid /usr/share/pam-configs/ldap /etc/pam.d/common-auth。
7. 验收必须包含反向用例
只测"正确密码能过"是不够的——把 pam_ldap.so 写错成 pam_permit.so 也能"通过"。
必须同时验证错误密码被拒绝,否则等于开了免密登录。
8. 「装了包」只解决「有程序」 「能用」还需要 NSS 接线 + nslcd 配置与进程 + 目录访问权限 + PAM 会话 四件事同时到位。
9. DinD 环境下的 bind mount 视图差异
在容器内跑 docker compose 时,宿主机改的文件容器里可能看不到,
用 docker cp 拷进去即可(普通宿主机无此现象)。
附:可复现命令速查
# 一键拉起
git checkout 2026-09-11-0002
cd scenario/2026-09-11-0002
docker compose up -d --build && sleep 15
# 复现 / 全量排查 / 修 / 验收
docker compose exec client bash /workspace/scenario/2026-09-11-0002/scripts/reproduce.sh
docker compose exec client bash /workspace/scenario/2026-09-11-0002/scripts/inspect.sh
docker compose exec client bash /workspace/scenario/2026-09-11-0002/scripts/fix_reference.sh
docker compose exec client bash /workspace/scenario/2026-09-11-0002/scripts/verify.sh
# 清理
docker compose down -v
