朕喜技术(ZhenxiJS)
2026.09.11
1 分钟阅读

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.confgetent 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 objectnslcd 默认匿名 bind;本目录禁止匿名读,必须配 binddn/bindpw
4PAM 会话认证通过但 /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

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-11-0002-ldap-nsswitch-nslcd-pam

分享这篇文章