一个站长迟早会遇到的麻烦:账号太多
刚开始做站的时候,你可能只有一两个后台:博客后台、服务器 SSH。随着时间推移,账号会像滚雪球一样多起来——Typecho 后台一个、phpMyAdmin 一个、监控面板一个、Git 服务一个、网盘一个、内网的各种小工具各一个。每个都要设密码,每个都要记,改一次密码要改一圈,删一个人要把所有系统挨个清一遍。
这就是「身份散落」的问题。解决办法在运维领域早有标准答案:把认证这件事从各个应用里抽出来,交给一个统一的目录服务,应用只负责问「这个人是谁、能不能进」,而不再自己存密码。这套机制在开源世界最主流的实现就是 LDAP(Lightweight Directory Access Protocol),具体到自建场景,通常是 OpenLDAP。
对个人站长来说,这不是「企业才需要」的复杂东西。一台 1 核 1G 的小机器就能跑起 OpenLDAP,装一次,之后每接一个新服务都只是填几个配置字段的事。更关键的是,它把「谁能进」从散落各处变成了一处管理,安全边界一下就清晰了。
LDAP 不是数据库,是目录
理解 LDAP 最大的障碍是把它当成关系型数据库。它不是。LDAP 是「目录服务」,专门为「大量读取、少量写入」的树状数据设计。它的数据结构叫 DIT(Directory Information Tree),很像一棵倒过来的文件系统:
dc=example,dc=com (根,你的域名)
├── ou=people (组织单元:人)
│ ├── uid=alice
│ └── uid=bob
├── ou=groups (组织单元:组)
│ ├── cn=admins
│ └── cn=devs
└── ou=services (组织单元:服务账号)每个节点叫一个「条目(entry)」,每个条目有若干「属性(attribute)」,以及一个唯一的「专有名称(DN)」,比如 uid=alice,ou=people,dc=example,dc=com。你以后每次配置应用,要填的都是 DN 和属性名,理解了这一层,后面的配置就全是填空题。
把域名拆成 dc= 部分是 LDAP 的惯例。dc 是 domain component 的缩写,ou 是 organizational unit,cn 是 common name,uid 是 user id。这些缩写看着陌生,用两次就熟了。
安装 OpenLDAP 服务端
Debian/Ubuntu 下安装 slapd(OpenLDAP 的服务端进程)和配套工具:
DEBIAN_FRONTEND=noninteractive apt install -y slapd ldap-utils
# 查看是否自动生成了管理员密码
cat /etc/ldap/slapd.d/* # 配置以目录形式存储在 slapd.d安装过程会引导你设置管理员密码。如果没设或忘了,可以用下面这条命令重设(它会重新配置并允许你输入新密码):
dpkg-reconfigure slapd重新配置时会问几个问题,这里逐条说清楚:
- Omit OpenLDAP server configuration? 选
No,我们要配置。 - DNS domain name:填你的域名,比如
example.com。它会自动转成dc=example,dc=com。 - Organization name:随意,填站点名即可。
- Administrator password:设置一个强密码,这是
cn=admin,dc=example,dc=com的密码。 - Database backend:选
MDB。这是现代 OpenLDAP 的默认后端,比老的 BDB 快且稳定。 - Remove database when slapd is purged? 选
No,避免误卸载时丢数据。
装好后确认服务在跑:
systemctl status slapd
ldapsearch -x -H ldap://localhost -b dc=example,dc=com -D cn=admin,dc=example,dc=com -W最后这条命令会提示输入管理员密码,然后返回目录树内容。能返回结果,说明服务端一切正常。
建组织结构:把人分进 OU
装完的库是空的,先建组织单元。写一个 LDIF 文件(LDAP 专用的数据交换格式),然后导入:
# base.ldif
dn: ou=people,dc=example,dc=com
objectClass: organizationalUnit
ou: people
dn: ou=groups,dc=example,dc=com
objectClass: organizationalUnit
ou: groups导入命令:
ldapadd -x -H ldap://localhost -D cn=admin,dc=example,dc=com -W -f base.ldif然后建第一个用户。这里用的是 inetOrgPerson 对象类,它是 LDAP 里表示「人」的标准类,邮箱、姓名、电话这些属性都来自它:
# alice.ldif
dn: uid=alice,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: alice
cn: Alice Zhang
sn: Zhang
givenName: Alice
mail: alice@example.com
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/alice
loginShell: /bin/bash
userPassword: 一个强密码注意 uidNumber/gidNumber/homeDirectory 来自 posixAccount 类。如果你只打算用它做 Web 应用的认证,可以不要这几个属性、去掉那三个 objectClass;但只要你想让 Linux 系统用户也走 LDAP 登录(后面会讲),这几个字段就是必需的。
导入用户后立刻验证一下能不能用:
ldapwhoami -x -H ldap://localhost -D uid=alice,ou=people,dc=example,dc=com -W返回 dn:uid=alice,... 就说明这个账号的密码设置正确、能绑定成功。
密码不能明文存:改成加密存储
上面 LDIF 里的 userPassword 是明文的,这在 LDAP 里属于大忌——因为只要有人读得到这个属性,密码就泄露了,而默认 ACL 下很多账号是可读的。正确的做法是存密码的哈希。
先生成哈希:
slappasswd -h '{SSHA}' -s '你的密码'
# 输出 {SSHA}xxxxxxxx...,这就是可入库的哈希然后把 LDIF 里的 userPassword 换成这个哈希值。这样即使目录被读,拿到的也只是不可逆的摘要。SSHA 是加盐的 SHA-1,兼容性最好;如果你的环境支持,也可以用 {ARGON2} 或 {SHA512},安全性更高。
更重要的是收紧 ACL。默认配置往往允许匿名读取部分属性,尤其是 userPassword。在 slapd 的配置里,必须保证 userPassword 只对自己和管理员可读。这条规则在 Debian 的 slapd.conf 时代写在文本配置里,现在配置迁移到了 /etc/ldap/slapd.d/ 目录下,要改得用 ldapmodify 操作 olcAccess 属性。一个典型的安全 ACL 大致是:
olcAccess: {0}to attrs=userPassword
by self write
by anonymous auth
by * none
olcAccess: {1}to *
by self read
by users read
by * none第一段的意思是:密码属性只有本人可改、匿名只能在认证(bind)时使用、其他所有人一律读不到。第二段允许已登录用户读其他属性。这两条是 LDAP 安全的地基,配置新库时务必先立起来。
让 Web 应用走 LDAP 认证
统一认证真正的价值在于「接应用」。以常见的自建服务为例,配置里通常只有这四个字段:
- LDAP 服务器地址:
ldap://127.0.0.1:389(本机)或ldaps://ldap.example.com:636(加密)。 - 基础 DN(Base DN):
ou=people,dc=example,dc=com,告诉应用从哪棵子树开始找用户。 - 绑定 DN(Bind DN):一个能读目录的服务账号,比如
cn=admin,dc=example,dc=com,或专门建的只读账号。 - 用户名字段(Filter):告诉应用拿登录框里输入的用户名去匹配哪个属性,常见是
(&(objectClass=inetOrgPerson)(uid=%s))或(mail=%s)。
认证流程分两步:应用先用 Bind DN 连上目录,按用户名字段搜到对应用户的 DN;再拿这个 DN 和用户输入的密码做一次 bind(绑定)。bind 成功,即密码正确;失败,即认证不通过。应用从头到尾拿不到密码原文,它只把密码转发给 LDAP 做校验——这正是统一的妙处。
这里有个容易踩的坑:Bind DN 这个服务账号通常需要有搜索全局的权限,如果只是给应用用,最好单独建一个只读账号,而不是用 cn=admin。用管理员账号会让每个应用的配置文件里都躺着一个超级凭据,一旦某个应用被攻破,整个目录就失守了。
加密:别再让密码裸奔
标准的 LDAP 协议(389 端口)是明文的,包括 bind 时传的密码。在公网上这么用,等于把密码写在明信片上寄出去。两个解决方案:
方案一:LDAPS(636 端口)。用 TLS 把整个连接包起来。需要给 slapd 准备好证书,并在配置里指定 olcTLSCertificateFile 和 olcTLSCertificateKeyFile,然后客户端连 ldaps:// 地址。这是最直接的做法,推荐用在跨机器访问的场景。
方案二:StartTLS。仍然连 389 端口,但在建立连接后通过 StartTLS 指令升级为加密。它比 LDAPS 更灵活,端口不需要额外开放。
另外,别以为「都在内网」就不必加密。内网里任何一个被攻破的低权限节点,都可能去嗅探明文 LDAP 流量。给 389 端口加上 TLS,是几乎零成本的加固。配置好后用这条命令验证加密是否生效:
ldapsearch -x -ZZ -H ldap://localhost -b dc=example,dc=com \
-D cn=admin,dc=example,dc=com -W-ZZ 的含义是「必须成功升级为 TLS,否则报错退出」。如果这条命令报证书错误,说明 TLS 还没配好,那就继续排查,别用 -Z(允许降级)蒙混过去。
可选进阶:让 Linux 系统登录也走 LDAP
如果你想让「一套账号登录 SSH 和后台」,可以装 libnss-ldapd 和 libpam-ldapd,把系统的 NSS 和 PAM 接到 LDAP 上。这样 uid=alice 就能直接 SSH 登录服务器,改密码也只需要改一处。但这一步的门槛和风险都明显更高:
- 必须预先给每个用户配好
uidNumber/gidNumber/homeDirectory,否则登录会失败。 - 身份源一旦断了(网络故障、slapd 挂掉),如果本地没缓存,所有人都登不进机器,包括你自己。所以一定要保留一个本地应急账号,绝不把系统的 root 登录完全押在 LDAP 上。
- PAM 配置写错会导致 SSH 直接无法登录,改动前最好在带外控制台或另开一个 SSH 会话里操作。
对多数个人站长来说,先在 Web 应用层面跑通 LDAP 就够了。等对目录服务足够熟悉、也接受了上面的风险,再考虑把系统登录也接进来。
备份与排错
LDAP 里存的是「谁能进你的所有系统」的关键数据,备份优先级很高。别用 stop 服务再 tar 目录这种粗暴方式,标准做法是在线导出:
slapcat -n 0 > config-backup.ldif # 备份配置库
slapcat -n 1 > data-backup.ldif # 备份主数据(MDB 通常是 db 1)
slapadd -n 1 -l data-backup.ldif # 恢复:先把库清空再导入排错时最常用的几条命令:
# 匿名查看根 DSE,确认服务与命名上下文
ldapsearch -x -H ldap://localhost -s base -b "" namingContexts
# 用特定用户 bind,验证密码
ldapwhoami -x -H ldap://localhost -D uid=alice,ou=people,dc=example,dc=com -W
# 查看日志(Debian 下 slapd 默认写到 syslog)
journalctl -u slapd -n 100 --no-pager常见报错对应关系:Invalid credentials (49) 说明密码错或 bind DN 写错;Insufficient access (50) 基本是 ACL 挡住了,多半是 bind 用的账号权限不够;No such object (32) 是 Base DN 写错,目录里没有这棵子树。把这三条记住,八成问题能自己定位。
小结
LDAP 的价值不在技术多新,而在于它把「身份」这块最难管的东西收敛到了一处。装一台 OpenLDAP,用户和组只维护一份;新应用接进来只需要填地址、Base DN、Bind DN、过滤条件四个字段;密码存哈希、连接走 TLS、ACL 锁死密码属性,安全边界就立起来了。对一个人管一堆自建服务的站长来说,这是一次性投入、长期省心的基础设施。
落地顺序建议是:先装 slapd 建好 people/groups 两个 OU,再建一个只读服务账号接第一个 Web 应用验证流程,跑通之后再逐个迁移剩余应用,最后才考虑系统登录这种高风险场景。稳扎稳打,LDAP 会成为你运维体系里最稳的那块地基。