1. 为什么还要在 CentOS 7 上折腾 LDAP
先把话说在前头:CentOS 7 已经进入维护尾声,很多人觉得这时候再谈它的 LDAP 部署有点“考古”。但现实情况是,大量企业内部的老业务系统、测试环境、离线机房里的机器,依然跑在 CentOS 7 上。你不可能因为官方停止更新就把整套认证体系推倒重来,成本扛不住。所以,把 LDAP 在 CentOS 7 上部署稳、调通、能落地,依然是一项很实在的技能。
LDAP,全称轻量级目录访问协议,你可以把它理解成一个“专门用来存账号和组织结构的电话簿”。它不负责计算,也不负责存储业务数据,它只干一件事:把用户、用户组、部门层级这些信息,用一种树状结构管起来,然后让其他系统来查。邮件系统查它、代码仓库查它、内部管理后台查它、甚至操作系统的登录认证也可以查它。一旦你把 LDAP 跑起来,后面新增一个内部系统,就不用再单独建一套账号体系了,直接对接 LDAP 就行。
这篇文章面向的是谁?主要是三类人:第一类是有一定 Linux 基础,但没怎么接触过目录服务的运维人员;第二类是需要给内部系统做统一认证,但不想上太重方案的后端开发;第三类是在离线或半离线环境里做交付,必须自己动手搭一套认证服务的实施人员。我会从零开始,把 CentOS 7 下 LDAP 的部署过程拆开讲清楚,包括为什么选这个组件、每一步在干什么、参数怎么定、踩过哪些坑、怎么排查问题。你照着做,大概率能一次跑通。
需要提前说明的是,LDAP 的部署方案有很多种,我这里选择的是一套在 CentOS 7 上最稳妥、依赖最少、最容易复现的组合。这不是唯一解,但它是经过大量实际环境验证过的解。下面所有操作都基于常见实践补充了细节,你可以在自己的测试环境里先走一遍,确认没问题再上生产。
2. 部署前的整体设计与组件选型
2.1 为什么选 389-ds 而不是 OpenLDAP
在 CentOS 7 上部署 LDAP,你面前基本有两条路:一条是用 OpenLDAP,另一条是用 389-ds。OpenLDAP 名气大、资料多,但它的配置方式偏底层,很多操作要手写 LDIF 文件,对新手不太友好。389-ds 是 Red Hat 体系里长期维护的目录服务组件,在 CentOS 7 的官方源里就能直接装,自带图形化管理界面和命令行工具,配置起来更“开箱即用”。
我选 389-ds 的核心理由有三个。第一,它和 CentOS 7 的兼容性最好,安装包直接来自系统源,不需要额外编译,省去了很多依赖问题。第二,它提供了dsconf、dsctl这类现代命令行工具,也保留了ldapmodify这种通用方式,你既可以用命令快速操作,也可以写 LDIF 做批量处理。第三,它的日志和状态查询比较直观,出问题的时候更容易定位。
当然,OpenLDAP 也不是不能用。如果你的团队已经有 OpenLDAP 的运维经验,或者你需要对接的某些系统明确要求 OpenLDAP 的特定 schema,那继续用 OpenLDAP 也合理。但对于大多数“从零搭一套统一认证”的场景,389-ds 在 CentOS 7 上的综合体验更好。
2.2 目录树结构怎么规划才不给自己挖坑
LDAP 的核心是目录树,树的结构一旦定下来,后面改起来非常麻烦。所以部署之前,必须先想清楚你的组织结构和命名规则。我见过太多人上来就随手写一个dc=test,dc=com,结果后面要接真实业务的时候发现根本对不上,只能重建。
一个比较稳妥的做法是,用你公司的域名倒过来作为根节点。比如内部域名是example.internal,那根节点就是dc=example,dc=internal。如果公司有多个子品牌或者多个业务线,可以在根节点下面再分ou=brandA、ou=brandB。用户统一放在ou=people下面,用户组放在ou=groups下面,服务账号放在ou=services下面。这样结构清晰,后面做权限控制的时候也好写过滤规则。
这里有个细节要注意:LDAP 的条目命名尽量用英文和数字,不要用中文。虽然协议本身支持 UTF-8,但很多对接系统在处理中文 DN 的时候会出各种编码问题,排查起来非常痛苦。用户名可以用拼音或者工号,组名用英文,部门名如果一定要用中文,也尽量放在属性里,而不是放在 DN 的ou或cn上。
2.3 网络与端口规划
LDAP 默认监听 389 端口,加密的 LDAPS 监听 636 端口。在 CentOS 7 上,如果你只是内部测试,可以先只开 389,等调通了再配 TLS。但如果是生产环境,我强烈建议直接上 636,因为 LDAP 的简单绑定是明文传输密码的,哪怕在内网也不安全。
端口规划上,389 和 636 不要和系统上其他服务冲突。CentOS 7 默认不会占用这两个端口,但如果你之前装过其他目录服务,要先确认端口没有被占用。可以用ss -tlnp | grep -E '389|636'来检查。另外,如果服务器开了 firewalld,记得把这两个端口放行,否则本机测试没问题,远程连不上,你会以为是配置错了,其实是防火墙拦了。
3. 核心细节解析与实操要点
3.1 安装 389-ds 与基础环境准备
在 CentOS 7 上安装 389-ds 非常简单,因为它就在基础源里。执行下面这条命令:
yum install -y 389-ds-base 389-ds-base-libs装完之后,你会得到几个关键工具:dscreate用来创建实例,dsctl用来控制实例启停,dsconf用来修改配置,ldapsearch和ldapmodify用来查询和修改数据。这里要注意,CentOS 7 的 389-ds 版本可能不是最新的,但足够稳定,不要盲目去追新版本,因为新版本可能依赖更新的系统库,在 CentOS 7 上反而容易出问题。
安装完成后,先别急着创建实例。先检查一下主机名和 hosts 文件。389-ds 在创建实例的时候会用到主机名,如果主机名解析有问题,后面启动会报错。用hostname -f确认能返回完整域名,如果返回的是localhost或者报错,就去/etc/hosts里加一条记录,把主机名和 IP 对应上。这个坑我踩过,当时实例创建成功但启动失败,日志里只写“无法解析主机名”,查了半天才发现是 hosts 没配。
3.2 创建目录实例的关键参数
创建实例用dscreate命令,它支持交互式和配置文件两种方式。交互式适合第一次操作,配置文件适合批量部署。我先说交互式,你执行:
dscreate interactive它会依次问你实例名、端口、root DN 密码、后缀等信息。实例名随便起,比如ldap1。端口默认 389,如果你要同时跑多个实例,就改成 3890 之类的。root DN 是管理员账号,默认是cn=Directory Manager,这个不要改,改了后面很多工具会不认。密码要设复杂一点,至少 8 位,包含大小写和数字。
后缀(suffix)就是你的根节点,比如dc=example,dc=internal。这里填什么,后面目录树就从什么开始。填完之后,它还会问你要不要创建默认的ou=people和ou=groups,建议选是,省得后面手动建。
如果你要用配置文件方式,可以先生成一个模板:
dscreate create-template /tmp/ldap-instance.inf然后编辑这个文件,把里面的instance_name、root_password、suffix改成你的值,再执行:
dscreate from-file /tmp/ldap-instance.inf配置文件方式的好处是可重复、可版本控制,适合多台机器部署。但要注意,配置文件里的密码是明文的,部署完记得把文件删掉或者加密保存。
3.3 目录树初始化与基础条目创建
实例创建好之后,目录树里只有根节点和默认的ou=people、ou=groups。你需要往里加用户和组。加用户有两种方式:一种是用ldapadd命令加 LDIF 文件,另一种是用dsconf的user子命令。我习惯用 LDIF,因为可读性好,也方便批量导入。
先创建一个基础结构的 LDIF 文件,比如base.ldif:
dn: ou=services,dc=example,dc=internal objectClass: top objectClass: organizationalUnit ou: services dn: ou=departments,dc=example,dc=internal objectClass: top objectClass: organizationalUnit ou: departments然后用ldapadd导入:
ldapadd -x -D "cn=Directory Manager" -W -f base.ldif这里-x表示用简单认证,-D指定绑定账号,-W表示交互式输入密码,-f指定文件。导入成功后,用ldapsearch验证一下:
ldapsearch -x -b "dc=example,dc=internal" -D "cn=Directory Manager" -W "(objectClass=organizationalUnit)"如果能看到你刚加的ou,说明基础结构没问题。
加用户的时候,要注意objectClass的选择。一个标准的用户条目通常包含top、person、organizationalPerson、inetOrgPerson这几个类。inetOrgPerson提供了uid、mail、userPassword这些属性,是最常用的。下面是一个用户条目的例子:
dn: uid=zhangsan,ou=people,dc=example,dc=internal objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson uid: zhangsan cn: Zhang San sn: Zhang givenName: San mail: zhangsan@example.internal userPassword: {SSHA}xxxxxxxx密码不要直接写明文,要用slappasswd生成哈希:
slappasswd -s "你的密码"它会输出类似{SSHA}...的字符串,把这个字符串填到userPassword里。这样即使 LDIF 文件泄露,密码也不会直接暴露。
3.4 用户组管理与成员引用
用户组在 LDAP 里通常用groupOfNames或groupOfUniqueNames来表示。groupOfNames用member属性引用用户 DN,groupOfUniqueNames用uniqueMember。两者区别不大,我一般用groupOfNames,因为兼容性更好。
创建一个组的 LDIF:
dn: cn=devops,ou=groups,dc=example,dc=internal objectClass: top objectClass: groupOfNames cn: devops member: uid=zhangsan,ou=people,dc=example,dc=internal member: uid=lisi,ou=people,dc=example,dc=internal这里有个坑要注意:groupOfNames的member属性是必须的,也就是说你不能创建一个空组。如果你先建组再往里加人,第一次创建的时候必须至少写一个成员,否则会报错。解决办法是先用一个占位成员,后面再替换,或者直接用groupOfUniqueNames,它对空组更宽容一些。
另外,成员引用的是完整 DN,不是uid。很多人写member: zhangsan,这是错的,LDAP 不认。必须是uid=zhangsan,ou=people,dc=example,dc=internal这种完整路径。这个细节在批量导入的时候特别容易出错,建议用脚本生成 LDIF,不要手写。
4. 实操过程与核心环节实现
4.1 从零到可用的完整部署流程
我把整个部署过程压缩成一条清晰的路径,你按顺序执行就行。第一步,装包:
yum install -y 389-ds-base 389-ds-base-libs第二步,配 hosts,确保hostname -f能返回完整域名。第三步,创建实例:
dscreate interactive按提示填实例名、端口、root 密码、后缀。第四步,启动实例:
dsctl ldap1 start第五步,验证实例状态:
dsctl ldap1 status如果显示running,说明服务起来了。第六步,导入基础结构 LDIF。第七步,创建用户和组。第八步,用ldapsearch验证数据。第九步,配置防火墙放行 389 和 636。第十步,如果要用 TLS,生成证书并配置。
这套流程走下来,顺利的话半小时内能搞定。但实际环境中,最容易卡住的地方是实例创建后的启动失败,以及 LDIF 导入时的 schema 报错。下面我会专门讲这两个问题的排查。
4.2 TLS 加密配置的实操细节
生产环境一定要配 TLS。389-ds 支持用自签证书或者 CA 签发的证书。测试环境可以用自签,生产环境建议用内部 CA 签发。配置 TLS 的步骤稍微多一点,但每一步都有明确目的。
首先,生成证书和私钥。如果你有内部 CA,直接用它签。如果没有,用openssl自签:
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout /etc/dirsrv/slapd-ldap1/ldap.key \ -out /etc/dirsrv/slapd-ldap1/ldap.crt \ -subj "/CN=ldap.example.internal"然后把证书和私钥的权限设好,确保只有 dirsrv 用户能读:
chown dirsrv:dirsrv /etc/dirsrv/slapd-ldap1/ldap.key /etc/dirsrv/slapd-ldap1/ldap.crt chmod 600 /etc/dirsrv/slapd-ldap1/ldap.key接着,把证书导入到 389-ds 的 NSS 数据库:
certutil -d /etc/dirsrv/slapd-ldap1 -A -n "Server-Cert" -t "u,u,u" -i /etc/dirsrv/slapd-ldap1/ldap.crt最后,用dsconf启用 TLS:
dsconf ldap1 security enable --cert-name Server-Cert重启实例:
dsctl ldap1 restart然后用ldapsearch走 636 端口测试:
ldapsearch -x -H ldaps://ldap.example.internal:636 -b "dc=example,dc=internal" -D "cn=Directory Manager" -W如果连接成功,说明 TLS 配好了。这里有个常见问题:证书的 CN 必须和客户端连接时用的主机名一致,否则会报证书不匹配。如果你用 IP 连接,证书里就要包含 IP 的 SAN,否则也会报错。这个坑在测试环境经常遇到,因为大家习惯用 IP 连。
4.3 对接系统时的绑定与查询示例
LDAP 部署好之后,最终是要给其他系统用的。对接方式通常有两种:一种是应用直接用管理员账号绑定,然后查用户;另一种是应用先用一个只读账号绑定,再根据用户输入的账号密码做一次绑定验证。第一种简单但不安全,因为管理员账号权限太大。第二种更规范,但需要你创建一个只读账号。
创建只读账号的 LDIF:
dn: uid=readonly,ou=services,dc=example,dc=internal objectClass: top objectClass: person objectClass: organizationalPerson objectClass: inetOrgPerson uid: readonly cn: Read Only sn: ReadOnly userPassword: {SSHA}xxxxxxxx然后给它配置 ACI,限制它只能读不能写。389-ds 的 ACI 配置在dsconf里:
dsconf ldap1 aci create --name "readonly-read" \ --target "ldap:///dc=example,dc=internal" \ --scope sub \ --permission read,search,compare \ --bind-rule "userdn=ldap:///uid=readonly,ou=services,dc=example,dc=internal"配置好之后,用只读账号测试查询:
ldapsearch -x -H ldap://ldap.example.internal:389 \ -D "uid=readonly,ou=services,dc=example,dc=internal" -W \ -b "ou=people,dc=example,dc=internal" "(uid=zhangsan)"如果能查到用户信息,说明权限配置正确。这里要注意,ACI 的规则是从上往下匹配的,如果你之前有更宽泛的规则,可能会覆盖你的限制。所以配置完一定要用只读账号实际测一下,确认它不能做写操作。
5. 常见问题与排查技巧实录
5.1 实例启动失败怎么查
实例启动失败是最常见的问题,表现是dsctl ldap1 start执行后没有报错,但dsctl ldap1 status显示stopped。这时候不要瞎猜,直接看日志。389-ds 的日志在/var/log/dirsrv/slapd-ldap1/下面,主要看errors文件。
我遇到过几种典型的启动失败原因。第一种是主机名解析问题,日志里会写Unable to resolve hostname。解决办法是配好/etc/hosts。第二种是端口被占用,日志里会写Address already in use。用ss -tlnp找到占用端口的进程,要么停掉它,要么改 LDAP 的端口。第三种是权限问题,比如证书文件权限不对,日志里会写Permission denied。把文件权限改成dirsrv用户可读就行。
还有一种比较隐蔽的情况:实例创建时指定的后缀和已有的数据冲突。比如你之前创建过一个实例,删掉后没清理干净,再创建同名实例时就会失败。这时候需要手动清理/etc/dirsrv/slapd-ldap1和/var/lib/dirsrv/slapd-ldap1目录,再重新创建。
5.2 LDIF 导入报错速查
LDIF 导入报错通常是因为 schema 不匹配或者 DN 重复。下面这张表整理了几种常见错误和解决办法:
| 错误信息 | 原因 | 解决办法 |
|---|---|---|
Object class violation | 缺少必需的 objectClass 或属性 | 检查条目是否包含top和对应的结构类,补全必需属性 |
Already exists | DN 重复 | 用ldapdelete删掉旧条目,或者改用ldapmodify更新 |
No such attribute | 属性名拼写错误或 schema 不支持 | 确认属性名大小写正确,检查 schema 是否加载 |
Invalid syntax | 属性值格式不对 | 比如userPassword没加密,或者mail格式不对 |
Insufficient access | 绑定账号权限不够 | 换管理员账号操作,或者检查 ACI 配置 |
这里重点说Object class violation,因为它最常见。很多人写用户条目的时候只写inetOrgPerson,忘了写top和person。LDAP 的 objectClass 是有继承关系的,inetOrgPerson继承自organizationalPerson,organizationalPerson继承自person,person继承自top。虽然有些服务器会自动补全,但 389-ds 比较严格,最好显式写全。
5.3 客户端连接超时的排查思路
客户端连不上 LDAP,表现是连接超时或者拒绝连接。排查顺序应该是:先本机、再网络、最后配置。本机测试用ldapsearch -H ldap://localhost:389,如果本机都连不上,说明服务没起来或者端口没监听。用ss -tlnp | grep 389确认端口状态。
本机通了但远程连不上,先检查防火墙。CentOS 7 默认用 firewalld,执行firewall-cmd --list-ports看 389 和 636 有没有放行。如果没有,执行:
firewall-cmd --permanent --add-port=389/tcp firewall-cmd --permanent --add-port=636/tcp firewall-cmd --reload防火墙没问题的话,检查 LDAP 的监听地址。默认 389-ds 可能只监听localhost,不监听外部 IP。用dsconf ldap1 config get nsslapd-listenhost查看,如果是localhost,改成0.0.0.0或者具体的外部 IP:
dsconf ldap1 config set nsslapd-listenhost 0.0.0.0改完重启实例。这个配置项在实例创建时也可以指定,但很多人会忽略,导致远程连不上。
5.4 性能与日常维护的实操心得
LDAP 本身很轻量,日常维护主要是备份和监控。备份用dsctl ldap1 db2ldif导出数据:
dsctl ldap1 db2ldif --ldif-file /backup/ldap-backup.ldif恢复用dsctl ldap1 ldif2db:
dsctl ldap1 ldif2db --ldif-file /backup/ldap-backup.ldif注意,ldif2db会覆盖现有数据,执行前一定要确认。我一般会在恢复前先把当前数据导出一份,以防万一。
监控方面,主要看连接数和查询响应时间。用dsconf ldap1 monitor可以查看实时状态。如果连接数持续很高,可能是客户端没有正确关闭连接,需要检查客户端的连接池配置。另外,LDAP 的索引对查询性能影响很大,默认只对cn、sn、uid建了索引,如果你经常按mail查询,就要手动加索引:
dsconf ldap1 backend index add --index mail --attr mail --type eq加完索引后要重建索引才生效:
dsctl ldap1 db2index这个操作在数据量大的时候会比较慢,建议在业务低峰期做。
6. 几个容易被忽略的细节
6.1 密码策略与账号锁定
默认情况下,LDAP 不会锁定多次登录失败的账号,这在安全上是个隐患。389-ds 支持密码策略,可以配置失败次数和锁定时间。用dsconf配置:
dsconf ldap1 pwpolicy set --pwdsafestate on --pwdmaxfailure 5 --pwdlockoutduration 300这条命令的意思是:开启密码安全状态,连续失败 5 次锁定账号,锁定时间 300 秒。配置完之后,用户连续输错密码就会被临时锁定,能有效防止暴力破解。但要注意,锁定是针对账号的,如果攻击者用不同账号尝试,这个策略就防不住,还需要配合网络层的限流。
6.2 日志轮转与磁盘空间
389-ds 的日志默认会一直增长,如果不做轮转,时间长了会把磁盘占满。日志轮转可以用系统的logrotate,也可以配置 389-ds 自己的轮转策略。我一般用logrotate,在/etc/logrotate.d/下建一个文件:
/var/log/dirsrv/slapd-ldap1/*.log { daily rotate 7 compress missingok notifempty create 0600 dirsrv dirsrv }这样每天轮转一次,保留 7 天,自动压缩。配置完用logrotate -d /etc/logrotate.d/dirsrv测试一下,确认没有语法错误。这个细节很多人会忽略,直到磁盘报警才想起来,那时候可能已经影响服务了。
6.3 数据导出与迁移的注意事项
如果你要把 LDAP 数据迁移到另一台机器,不能直接拷贝数据库文件,因为 389-ds 的数据库格式和实例配置绑定。正确做法是用db2ldif导出,然后在目标机器上用ldif2db导入。导出的时候要注意,默认导出的是用户数据,不包含配置和 schema。如果目标机器的 schema 和源机器不一致,导入会失败。所以迁移前要先确认两边的 schema 一致,不一致的话先把 schema 文件拷过去。
另外,导出的时候可以用-r参数排除某些后缀,或者用-s只导出指定后缀。如果数据量很大,导出文件会很大,建议先压缩再传输。导入的时候如果数据量超过百万条,可能会比较慢,可以适当调整nsslapd-cachememsize和nsslapd-dbcachesize参数,给数据库更多内存缓存。
7. 我个人在实际操作中的体会
这套东西我在不同环境里部署过很多次,最大的感受是:LDAP 本身不复杂,复杂的是“想清楚你要用它来干什么”。如果你只是想让几个内部系统共用一套账号,那结构越简单越好,不要一上来就搞多级 OU、复杂 ACI,后面维护起来会非常痛苦。我见过一个环境,目录树设计了七八层,结果每次加用户都要确认半天该放哪个 OU,最后大家干脆都往ou=people里塞,前面的设计全白费了。
另一个体会是,文档和实际环境永远有差距。网上很多教程写的是 OpenLDAP,你照着做在 389-ds 上就是跑不通。所以遇到问题不要死磕教程,直接看日志,日志里写什么就查什么,比到处搜答案快得多。还有就是,任何配置改动之前先备份,LDAP 的数据虽然不大,但重建起来很麻烦,尤其是用户密码,丢了就得全部重置。
最后分享一个小技巧:如果你不确定某个dsconf命令的参数怎么写,直接加--help,比如dsconf ldap1 backend index add --help,它会列出所有可用参数和示例。这个比查文档快,而且不会因为版本差异导致命令不兼容。CentOS 7 上的 389-ds 版本虽然老,但常用功能都有,--help基本能解决大部分操作问题。