LDAP[轻型目录访问协议](Lightweight Directory Access Protocol)(属于X.500) 是一种目录服务(Directory service)标准。
目录是一个特殊的数据库,它的数据经常被查询,但是不经常更新。不像普通的数据库,目录不包括对事件(transaction)的支持也不包括回滚特性。目录是很容易被复制的,以便增加它的可用性和可靠性。当目录被复制时,临时的数据不一致情况是允许出现的,只要最终这些数据得到同步即可。
LDAP由互联网工程任务组(IETF)的文档RFC定义,使用了描述语言ASN.1定义。最新的版本是版本3,由RFC 4511所定义。
轻型目录访问协议(Lightweight Directory Access Protocol) 议是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。 目录服务在开发内部网和与互联网程序共享用户、系统、网络、服务和应用的过程中占据了重要地位。
OpenLDAP 是一个基于标准实现的使用最多的 LDAP Server。
BTW,微软的 Active Directory Domain Services(AD DS) 也提供目录服务,支持 LDAP 协议,不过具体的属性跟 OpenLDAP 有很大的区别。
phpLDAPadmin
phpLDAPadmin 是 LDAP 的一个 Web GUI。
需要注意大部分的文档都是 v1 的,v2 进行了重构,有很大的区别。并且不能用 rootdn 登录
就个人来说,并不太喜欢 v2,虽说直接使用 rootdn 有安全性问题,但是直接不让用就很尬。具体见Issue
名词解析
OpenLDAP HDB 与 MDB 后端对比
老的 openldap 2.4.44,后端是HDB
OpenLDAP 中两种主要数据库后端:HDB(Hierarchical Berkeley DB)与MDB(Memory-Mapped Database),并对它们的架构、性能和配置差异进行对比。
- HDB 使用 Oracle Berkeley DB (BDB) 存储数据,支持完全的层次结构和子树重命名,但需要合理配置
DB_CONFIG
、idlcachesize
等缓存参数以获得良好性能。 - MDB 基于 LMDB(Lightning Memory-Mapped Database)库,无需外部缓存,直接通过内存映射实现高效存取,功能等同于 HDB 且支持子树重命名。
- 自 OpenLDAP 2.6 起,HDB 已移除,MDB 为推荐默认后端;官方文档建议新部署使用 MDB,并逐步将旧 HDB 迁移至 MDB。
Distinguished Name (DN)
DN:每个 LDAP 条目在目录树中的唯一标识,类似文件系统中的完整路径;由若干层级的 RDN(Relative Distinguished Name,相对可分辨名称)组成,每个 RDN 为属性–值对。
RDN 示例:uid=john.doe 或 cn=Users,多属性 RDN 如 givenName=John+sn=Doe
rootdn
rootdn(Root Distinguished Name)是 LDAP 数据库中的超级管理员 DN,具备对该数据库条目的完全控制权限,不受常规访问控制列表(ACL)限制。
超级权限
rootdn 对应的用户拥有忽略所有 ACL 的完全访问权限,可执行添加、删除、修改、查询等任意操作。
配置示例
1 | database mdb |
SASL 绑定
也可以指定 SASL 身份作为 rootdn,例如:
1 | rootdn "uid=root,cn=example.com,cn=digest-md5,cn=auth" |
OU(Organizational Unit)
是 LDAP 中用于构建目录层次结构、组织条目、应用策略和委派管理的基本构建块。它像文件夹一样,帮助你将目录中的信息整理得井井有条,使其更易于管理和理解。
OU 的名字是可以自定义的,不过因为一些教程和预定义设置的原因的原因,很多人可能会使用比较常见的例如ou=People
和ou=Group
或ou=users
和ou=groups
,如 bitnami 的 OpenLDAP 的 docker 预制就是后者,而 dokuwiki 的过滤器中默认则用前者过滤。
AD 中也有相同的概念,不过有些管理可能会把 OU 当部门使用,然后把不同的用户塞到不同的 OU。总之,这些跟后面的过滤器是强相关的,因为使用起来五花八门,所以过滤器这块基本都是可以自定义的。
LDIF 文件格式介绍
LDIF(LDAP Data Interchange Format,LDAP 数据交换格式)是一种基于文本的格式,用于表示 LDAP(轻量目录访问协议)目录中的数据。LDIF 文件通常用于:
- 导入(import)或导出(export)LDAP 条目;
- 批量添加、删除或修改 LDAP 数据;
- 与
ldapadd
,ldapmodify
,ldapdelete
等命令行工具配合使用。
LDIF 文件由一系列的“条目(entry)”组成。每个条目表示 LDAP 目录中的一个对象,并由若干“属性(attribute)”组成。条目之间用空行分隔。
示例:
1 | dn: cn=John Doe,dc=example,dc=com |
动态配置
OpenLDAP 的动态配置,也称为cn=config
或 DIT (Directory Information Tree) 配置,是一种现代的 OpenLDAP 服务器配置管理方式。它取代了传统的基于单个文本文件 (slapd.conf) 的静态配置方法。
简单来说,动态配置就是将 OpenLDAP 服务器自身的所有配置信息存储在它自己的一个特殊 LDAP 数据库(DIT)中。这个配置数据库的根条目通常是cn=config
。
- 配置即数据: 所有的配置参数,如数据库后端、索引、覆盖层 (overlays)、访问控制列表 (ACLs)、TLS 设置、模块加载等,都以 LDAP 条目 (entries) 和属性 (attributes) 的形式存在。
- LDAP 操作管理: 你可以使用标准的 LDAP 客户端工具(如 ldapadd, ldapmodify, ldapdelete)通过 LDAP 协议来查看和修改这些配置条目。
- 实时生效: 大部分配置更改在通过 LDAP 操作提交后会立即生效,无需重启 slapd 服务。
- 结构化存储: 配置信息在磁盘上以 LDIF (LDAP Data Interchange Format) 文件的形式存储在一个特定目录中(通常是 /etc/ldap/slapd.d/ 或 /usr/local/etc/openldap/slapd.d/)。
注意不能直接编辑这些 LDIF 文件!所有更改都应通过 LDAP 操作进行。
优势
无需重启服务 (No Server Restart)
核心优势: 这是最主要和最吸引人的优点。对大多数配置参数的更改可以立即生效,无需停止和重新启动 slapd 服务。这大大提高了服务的可用性,尤其是在生产环境中,最大限度地减少了服务中断时间。
对于 slapd.conf,任何微小的更改都需要重启服务才能应用。
远程管理 (Remote Administration)
由于配置本身就是 LDAP 数据,你可以从任何具有 LDAP 客户端工具和适当权限的机器上远程管理 OpenLDAP 服务器的配置。
使用 slapd.conf 时,你通常需要登录到服务器本地文件系统才能编辑配置文件。
标准化访问控制 (Standardized Access Control)
对cn=config
树的访问和修改可以像对普通数据 DIT 一样,使用 LDAP ACLs 进行精细控制。你可以定义哪些用户或组有权限修改哪些配置项。
slapd.conf 的访问控制依赖于操作系统的文件权限。
配置复制 (Configuration Replication)
cn=config
DIT 可以像其他数据 DIT 一样被复制到其他 OpenLDAP 副本服务器。这对于维护多主复制 (multi-master replication) 或高可用性集群中的配置一致性至关重要。
使用 slapd.conf 时,你需要手动同步配置文件到所有副本服务器,容易出错。
原子性和事务性 (Atomicity and Transactionality)
LDAP 操作本质上具有一定的原子性。复杂的配置更改(例如,添加一个新数据库及其相关覆盖层和 ACL)可以作为一个或多个 LDAP 修改操作来执行。
虽然不是完全的事务性,但比编辑文本文件更容易管理复杂更改的完整性。
模式感知 (Schema Aware)
cn=config
本身也遵循特定的 LDAP 模式。这意味着服务器可以验证你尝试应用的配置更改是否符合预定义的结构和数据类型,从而减少配置错误。
slapd.conf 只是一个文本文件,其语法错误通常在服务器启动时才会被发现。
更易于脚本化和自动化 (Easier Scripting and Automation)
可以使用标准的 LDAP 命令行工具或各种编程语言的 LDAP 库来编写脚本,以编程方式进行配置更改和管理,这比解析和修改文本文件更可靠、更强大。
版本控制和审计
虽然 LDAP 本身不直接提供版本控制,但结合 LDAP 的日志记录和备份策略,可以更容易地跟踪配置更改历史。一些第三方工具也可以辅助实现。
OpenLDAP 的 ACL(访问控制列表)
OpenLDAP 中的访问控制列表 (ACL) 是一套强大的规则,用于定义谁 (who) 可以对目录中的哪些数据 (what) 执行何种操作 (access level)。正确配置 ACL 是保护 LDAP 目录安全、确保数据完整性和实现细粒度权限管理的关键。
OpenLDAP 的 ACL 规则遵循一个基本结构:
1 | access to <what> |
或者在动态配置 cn=config
中(推荐方式),使用 olcAccess
属性:
1 | olcAccess: {<index>}to <what> |
其中:
{<index>}
: 是一个非负整数,用于对多条olcAccess
规则进行排序。规则按索引号从小到大评估。to <what>
: 定义规则适用的目标资源(条目或属性)。by <who> <access_level> [<control>]
: 定义授权子句。一个access to
块可以包含多个by
子句。
ACL 加载顺序
- OpenLDAP 服务器会按照 olcAccess 规则的索引顺序(或 slapd.conf 中的顺序)逐个评估 access to 块。当请求的目标与某个 access to 块的
部分匹配时,服务器会继续在该块内部按顺序评估 by 子句。通常,该块内第一个匹配的 by 子句所授予的权限会生效,除非使用了 continue 或 break 等控制流指令。 - 设计 ACL 规则时,强烈建议遵循“从最具体到最通用”的排序原则。例如,先为特定条目或属性定义规则,然后是子树,最后是全局规则。在 by
子句内部也应遵循此原则。 - 如果请求的操作没有匹配任何 access to 块,或者在匹配的块内没有 by
子句授予权限,并且没有配置非限制性的 olcDefaultAccess,则访问将被拒绝。为确保明确的默认拒绝策略,最佳实践是在 ACL 规则集的末尾添加一条 access to * by * none 规则。
ACL 语法详解
to <what>
(目标资源)
这部分指定 ACL 规则应用于目录中的哪些对象或属性。
*
: 应用于所有条目和属性。dn[.<style>]=<pattern>
: 应用于 DN (Distinguished Name) 匹配的条目。- Pattern: DN 字符串或正则表达式。
例如:dn.subtree="ou=users,dc=example,dc=com"
- Style:
base
(或exact
): 精确匹配 DN。to dn.exact="ou=users,dc=example,dc=com"
one
(或onelevel
): 匹配指定 DN 的下一级子条目。to dn.one="ou=groups,dc=example,dc=com"
subtree
: 匹配指定 DN 及其所有子树条目。to dn.subtree="dc=example,dc=com"
children
: 匹配指定 DN 的所有子树条目,但不包括该 DN 本身。regex
: 使用正则表达式匹配 DN。to dn.regex="uid=[^,]+,ou=users,dc=example,dc=com"
- Pattern: DN 字符串或正则表达式。
attrs=<attrlist>
: 应用于条目中的特定属性。attrlist
: 逗号分隔的属性名列表 (e.g.,userPassword,mail
) 或特殊值:entry
: 条目本身(控制添加、删除、重命名等操作),不包括其属性。children
: 条目的子条目(控制添加、删除子条目)。to attrs=userPassword,shadowLastChange
to attrs=entry
filter=<ldap_filter>
: 应用于匹配 LDAP 搜索过滤器的条目。to filter="(objectClass=inetOrgPerson)"
val.<attribute>=<value>
: (较少用) 应用于其<attribute>
属性具有特定<value>
的条目。
by <who>
(访问者)
指定谁可以执行操作。
*
: 任何人,包括匿名用户。by * read
anonymous
: 未认证的(匿名)用户。by anonymous auth
users
: 任何已成功认证的用户。by users read
self
: 正在访问的条目与当前绑定用户的 DN 相同。
这是实现用户修改自己信息(如密码)的关键。by self write
dn[.<style>]=<pattern>
: 特定 DN 或匹配模式的 DN。
Style:base
(或exact
),regex
,subtree
(表示绑定用户是该子树下的用户)。by dn.exact="cn=admin,dc=example,dc=com" manage
group[/<objectclass>[/<groupattr>]][.<style>]=<pattern>
: 属于特定组的成员。objectclass
: 组对象的 objectClass (e.g.,groupOfNames
,groupOfUniqueNames
)。
默认为groupOfNames
。groupattr
: 包含组成员 DN 的属性 (e.g.,member
,uniqueMember
)。
默认为member
。- Style and Pattern: 用于匹配组的 DN。
by group.exact="cn=administrators,ou=groups,dc=example,dc=com" write
peername.<style>=<pattern>
: 基于客户端的 IP 地址或主机名。by peername.ip="192.168.1.0/24" read
sockname.<style>=<pattern>
: 基于服务器监听的 IP 地址。dnattr=<attrname>
: 绑定用户的 DN 存在于目标条目的某个指定属性中
(例如,条目的owner
属性值是当前用户的 DN)。by dnattr=owner write
set=<set_expression>
: 使用布尔逻辑 (AND
,OR
,NOT
,&
,|
,!
) 组合多个<who>
条件。by set="user and group/groupOfNames/member.exact=cn=editors,ou=groups,dc=example,dc=com" write
或者更简洁的语法 (OpenLDAP 2.5+):by set="[cn=editors,ou=groups,dc=example,dc=com]/member & user" write
<access_level>
(权限级别)
指定授予的权限。权限是分级的,较高的权限通常隐含较低的权限。
none
: 无任何访问权限。disclose
: 允许在访问被拒绝时报告更详细的原因 (不常用)。auth
: 认证权限 (允许用户使用该条目进行 BIND 操作,例如检查密码)。compare
: 比较权限 (允许执行 LDAP 比较操作)。search
: 搜索权限 (允许在搜索操作中返回此条目/属性,隐含compare
权限)。read
: 读取权限 (允许读取条目/属性的值,隐含search
和compare
权限)。write
: 写入权限 (允许修改、添加、删除属性;也允许add
,delete
,modrdn
操作,如果应用到entry
或children
。隐含read
,search
,compare
权限)。manage
: 管理权限 (OpenLDAP 2.5+)。这是一个更高级别的权限,通常授予对子条目、模式等的管理能力。隐含write
。
<control>
(控制流)
可选,控制 ACL 处理流程。
stop
: 如果此by
子句匹配,则停止处理当前access to
块中的后续by
子句。这是默认行为。continue
: 如果此by
子句匹配,则授予指定的权限,并继续处理当前access to
块中的后续by
子句。这允许权限累积或被后续规则覆盖。break
: 如果此by
子句匹配,则停止处理所有后续的access to
块。implicit
: (OpenLDAP 2.5+) 使其行为如同由olcDefaultAccess
定义的默认访问权限。self <access_level>
: 类似于by self <access_level>
,但通常用于特定场景,例如结合set
。
例子
一个用于hdb的支持用户自行修改密码的 ACL,注意目前正常普遍是mdb
1 | dn: olcDatabase={2}hdb,cn=config |
其它简单示例 (olcAccess 格式):
1 | # {0} Allow rootdn full access (this is usually implicit but can be explicit) |
注意: 上面的 write_protect="userPassword" 不是标准的OpenLDAP ACL语法,它更像是一个概念性描述。正确的做法是,针对 userPassword 单独设置规则,例如:
1 | # Users can change their own password |
OpenLDAP 权限验证
-Y EXTERNAL -H ldapi:///
本地 root 权限绑定,仅限ldapserver本地使用,也就是要在docker内执行命令(权限级别与使用rootdn一样)
EXTERNAL 是一种 SASL(Simple Authentication and Security Layer)机制,用于通过已有的外部身份验证方式(例如 Unix socket 权限)来认证你的 LDAP 操作。
SASL 是一种认证框架,OpenLDAP 支持多种 SASL 机制,例如:
- PLAIN
- DIGEST-MD5
- GSSAPI
- EXTERNAL
EXTERNAL 是其中一种最常见的机制,通常用于本地系统用户以 root 权限访问 OpenLDAP 的配置数据库(cn=config),不需要输入用户名和密码。
当你通过 EXTERNAL 使用 Unix socket 登录时,OpenLDAP 会通过 socket 的所有者判断是谁发起的连接,比如:
- 如果你是 root 用户,就会映射为 gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth。
- 通常系统会在 olcAccess 中授予该身份读写权限。
LDAP 集成
主要有两种配置,一个是对 AD 的集成,还有一个则是对 OpenLDAP 的集成。
需要注意的是,AD 虽然底层也符合标准,但跟 OpenLDAP 的各项属性有很大区别,很多服务会特别的专门适配 AD,但有些会以 OpenLDAP 为标准,连接 AD 会需要自行修改配置项。
OS 集成
SSSD 或 NSCD
sssd 会更现代一些,不过强制 ldaps,安全以及复杂。如果各种 ssl 苦手且目标环境做了物理隔离,建议 nscd。(不过 sssd 战未来)
注意事项
debian 是有一个 ldapd,https://wiki.debian.org/LDAP/PAM
ubuntu 也有 ldapd,但是没有存在感, ubuntu 所有的教程还是基于非 ldapd 的,甚至 AI 都不知道有这个包。
集成配置
deb 系是 sudo pam-auth-update,rpm 系是有 ''authconfig --enableldap --enableldapauth''。后者看 redhat 文档就好
bindn
通常 AD 下使用具有域控管理员权限的账号,不过出于安全性考虑可以降低权限(普通 domain user 没有读取其他用户邮箱的权限),目前都使用的管理员权限的账号做bind
OpenLDAP 中则一般使用 rootdn (也可以不使用,不过需要创建具有特殊权限的账号)
Base DN
定义:LDAP 搜索操作(例如 ldapsearch)使用的“起始 DN”,决定了查询范围的顶端节点。
格式:通常为域组件(DC,Domain Component),如 dc=example,dc=com;也可为组织单元(OU,如 ou=Users,dc=example,dc=com)或通用名称(CN,如 cn=Administrator,cn=Users,dc=example,dc=com)。
在客户端或应用配置中填入 Base DN 后,后续的搜索操作会从该节点及其子树进行。例如,若 Base DN 为 ou=Users,dc=example,dc=com,那么只有位于该 OU 下的用户才会被索引。
上面加粗了,因为实际上大多会使用 Users 或 People ,主要是查找用户
不过你需要注意下有些服务是不需要同步 LDAP 组的,例如 dokuwiki 里就没有设basedn,他会让你设用户过滤和组的过滤条件。
LDAP 过滤器
LDAP 过滤器 (LDAP Filter) 在与 LDAP/AD 集成的应用程序(如 DokuWiki 的 authad 插件 或 Snipe-IT)中扮演着至关重要的角色。它定义了如何查找和验证用户。
简单来说,LDAP 过滤器就像是你在 Active Directory 这个大数据库里进行搜索时使用的搜索条件。你需要告诉应用程序:当用户尝试登录时,应该使用哪些标准来找到 AD 中对应的用户对象,并确认这个对象是有效的、允许登录的。
一个可供参考的问题见 Issue
LDAP 过滤器的基本语法
- 使用括号
()
包裹整个过滤器。 - 基本条件通常是
(属性名=值)
的形式。 - 可以使用逻辑运算符将多个条件组合起来:
* &:与 (AND) - 所有条件都必须满足。格式:(&(条件1)(条件2)...)
* |:或 (OR) - 至少一个条件满足。格式:(|(条件1)(条件2)...)
* !:非 (NOT) - 条件不满足。格式:(!(条件))
- 值可以是具体的值,也可以是通配符
*
,或者是一个占位符,应用程序会在运行时将其替换为用户输入的内容(通常是用户名)。
常用的 AD 用户属性及过滤器构建块
objectCategory=person
:查找对象类别为 "person" 的对象(通常包含用户)。objectClass=user
:查找对象类别为 "user" 的对象。通常与objectCategory=person
一起使用,更精确地定位用户。sAMAccountName
:用户的登录名(通常是 Windows 2000 之前的短名称,例如johndoe
)。这是最常用的登录名字段。userPrincipalName
(UPN):用户的邮箱风格登录名(例如johndoe@yourdomain.local
)。mail
:用户的电子邮件地址。有时也用作登录名。userAccountControl
:一个包含用户账户状态(如是否禁用)的位掩码。查找未禁用的用户通常使用位操作符:(!(userAccountControl:1.2.840.113556.1.4.803:=2))
。这里的2
代表ADS_UF_ACCOUNTDISABLE
标志位,:1.2.840.113556.1.4.803:=
是进行位与 (Bitwise AND) 比较的 LDAP 匹配规则。整个表达式的意思是“userAccountControl 属性中没有设置值为 2 的那个位”。memberOf
:用于检查用户是否属于某个特定的组。值需要是组的完整 Distinguished Name (DN),例如CN=AppUsers,OU=Groups,DC=yourdomain,DC=local
。
常见的 LDAP 用户过滤器示例
注意: 以下示例使用占位符 {username}
代表用户输入的用户名。请务必查阅你所使用的应用程序(如 DokuWiki authad 插件)的文档,确认它实际使用的占位符是什么,可能是 @USER@
, %user%
, $user
等。
最基本:使用 sAMAccountName 查找未禁用的用户 (推荐作为起点)
下面最外层的括号或许需要取消
1 | (&(objectCategory=person)(objectClass=user)(sAMAccountName={username})(!(userAccountControl:1.2.840.113556.1.4.803:=2))) |
&
: 所有条件都需满足。objectCategory=person
和objectClass=user
: 确保找到的是用户对象。sAMAccountName={username}
: 用户的sAMAccountName
属性必须等于登录时输入的用户名。(!(userAccountControl:1.2.840.113556.1.4.803:=2))
: 确保账户未被禁用。(部分服务会同步已禁用账号,然后在本地显示账号禁用,如果不需要可以直接通过过滤器排除掉)
OpenLDAP
因为大部分都会以 OpenLDAP 为主,所以基本没什么好写的,你只要注意不要因为自己设的不同名字的 OU 被坑了就行
1 | (&(objectClass=posixGroup)(|(memberUid=%{uid})(gidNumber=%{gid}))) |
objectCategory=posixGroup
: 确保找到的是组对象。|(memberUid=%{uid})(gidNumber=%{gid})
: 用户主组是gidNumber,memberUid是次组
etc
最新的 OpanLDAP 通常使用动态配置
bitnami 的 openldap 中获取信息
1 | slapcat -F /bitnami/openldap/slapd.d/ |