LDAP 树与搜索过滤器,以及组织架构的陷阱
一句话总结
LDAP 是一棵树,而组指向人员,还是人员指向组,决定了查询方向。方向选错,登录耗时就会随用户数量增长。
为什么需要它
“接入企业账户”最终通常会落到 LDAP 查询。但如果过滤器写得粗糙,禁用账户与服务账户都会混入用户列表;如果不指定返回属性,从十万人的目录里取回所有属性,还会同时拖累服务器与网络。
更隐蔽的问题是方向。这个目录由组通过 member 指向人员,因此无论怎样查看用户条目,都看不到所属组。不理解这一点,就可能写出每次登录都扫描全部组的代码。100 个用户时无人察觉,增长到 1 万人后,登录就需要 3 秒。
DIT——目录是一棵树
LDAP 数据存放在称为 **DIT(Directory Information Tree)**的树中。每个节点(条目)都由 **DN(Distinguished Name)**唯一标识。
dc=labhub,dc=co,dc=kr ← 루트 (Base DN)
├── ou=People
│ ├── uid=hong,ou=People,dc=labhub,dc=co,dc=kr
│ └── uid=kim,ou=People,dc=labhub,dc=co,dc=kr
├── ou=Groups
│ ├── cn=dev-team,ou=Groups,...
│ └── cn=role-admin,ou=Groups,...
└── ou=Disabled ← 퇴사자 격리 구역
DN 要从下往上读。uid=hong,ou=People,dc=labhub,dc=co,dc=kr 表示“labhub.co.kr 组织中 People 部门的 hong”。它与文件路径方向相反,初学时容易混淆。
掌握以下主要属性缩写,就足以开始。
| 缩写 | 含义 | 示例 |
|---|---|---|
dc |
domainComponent | dc=labhub,dc=co,dc=kr |
ou |
organizationalUnit | ou=People |
cn |
commonName | cn=홍길동, cn=dev-team |
uid |
userid | uid=hong |
sn / givenName |
姓/名 | sn=홍, givenName=길동 |
mail |
邮件 | hong@labhub.co.kr |
title |
职级/职务 | title=과장 |
member |
组成员(DN) | member=uid=hong,ou=People,... |
搜索过滤器——RFC 4515 的前缀表示法
LDAP 过滤器采用操作符在前的括号表达式。起初不熟悉,但规则很简单。
(uid=hong) 단순 일치
(cn=홍*) 앞부분 일치 (와일드카드)
(&(A)(B)) A 그리고 B
(|(A)(B)) A 또는 B
(!(A)) A 가 아님
(objectClass=*) 해당 속성이 존재
组合后如下。
(&(objectClass=inetOrgPerson)(ou=개발팀)(title=과장))
(&(objectClass=inetOrgPerson)(|(title=부장)(title=차장))(!(ou=총무팀)))
使用 ldapsearch 时可以这样写。
ldapsearch -x -H ldap://127.0.0.1:1389 \
-D "cn=admin,dc=labhub,dc=co,dc=kr" -w labhub123 \
-b "dc=labhub,dc=co,dc=kr" \
"(&(objectClass=inetOrgPerson)(title=과장))" uid cn ou
-x表示简单认证(不用 SASL),-H是服务器 URI,-b是搜索起点(Base)-D/-w是 bind DN 与密码。-w会在进程列表中暴露密码。 生产脚本应使用-y 파일,这是安全检查中的常见问题- 最后的参数是要返回的属性列表;不写就会返回全部属性。面对十万人目录,拉取所有属性会同时压垮服务器与网络
还应了解搜索范围(-s):base(只查自身)、one(只查直接子节点)、sub(查全部后代,默认)。大型目录中过度使用 sub 会很慢。
组——存在两个方向
成员关系有两种表达方式,这一差异会直接影响性能与同步。
- 组指向人员(
groupOfNames的member属性)- 组条目保存成员 DN 列表
- 查询“这个组有哪些成员”很快
- 查询“这个人属于哪些组”则必须扫描全部组
- 人员指向组(
memberOf属性)- 用户条目保存所属组 DN 列表
- AD 会自动维护,OpenLDAP 需要启用 overlay
- 查询“这个人属于哪些组”很快,适合登录时判断权限
实际陷阱是:目录里有 member,却没有 memberOf,于是代码在每次登录时扫描全部组。100 个用户时看不出来,1 万人时登录就需要 3 秒。
AD 与 OpenLDAP 的实际差异
| 项目 | Active Directory | OpenLDAP |
|---|---|---|
| 登录 ID 属性 | sAMAccountName(或 userPrincipalName) |
通常是 uid |
| 不变标识符 | objectGUID |
entryUUID |
| 组反向引用 | 默认提供 memberOf |
需要 memberof overlay |
| 密码属性 | unicodePwd(不能通过明文 LDAP 修改) |
userPassword |
| 账户状态 | userAccountControl 位 |
自定义约定 |
为什么不变标识符很重要。 人可能因结婚改名,调动后 DN 也会改变。如果用 uid 或 DN 作为同步键,同一个人会被创建成两个新用户。 必须使用 objectGUID/entryUUID 这样的不变值作为关联键。设计错误会造成重复账户,而且可能静默积累数月。
userAccountControl 是位标志。 使用 AD 时,下面三项值得记住。
0x0002 ACCOUNTDISABLE 계정 비활성
0x0010 LOCKOUT 잠김
0x10000 DONT_EXPIRE_PASSWD 비밀번호 만료 없음
512 = 정상 계정
514 = 정상 + 비활성 (512 + 2)
66048 = 정상 + 비밀번호 만료 없음 (512 + 65536)
因此,只查询“活跃用户”的 AD 过滤器如下。
(&(objectCategory=person)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
中间的 OID 是“位 AND”匹配规则,含义是排除 userAccountControl 中第 2 位被置位的账户。不使用这个过滤器,禁用账户与计算机账户也会进入用户列表。
同步的两个陷阱
从目录定期同步用户到我们的系统(或 IdP)时,必然会遇到以下问题。
- 增量同步无法发现删除。 拉取“最近变更条目”的方式无法看见已经消失的条目,因此离职账户会继续留在本系统。这是幽灵账户的常见来源。 → 增量同步要频繁执行(每小时),同时还要定期执行全量同步(每周一次)。
- 变更时间取自目录服务器的时钟。 只要一台服务器的 NTP 有偏差,那个区间的变更就会静默遗漏。症状是“偶尔有一个人没有同步”,定位非常困难。
明文 389 会被审计指出
通过 ldap:// 明文连接执行 bind,密码会以明文经过网络。 生产环境必须使用 ldaps://(636)或 StartTLS;明文配置会在安全审计中立即被指出。
(本实验受 capability 限制,无法监听 1024 以下端口,因此使用明文 1389。这是教学配置,不是生产配置。)