精通-OpenLDAP-全-
精通 OpenLDAP(全)
原文:
annas-archive.org/md5/4503670b84da1882021e5545f8a87bf1
译者:飞龙
前言
OpenLDAP 目录服务器是一个成熟的产品,自 1995 年以来(以某种形式)一直存在。所有主要的 Linux 发行版都包含 OpenLDAP 服务器,许多主要的应用程序(无论是开源还是专有)都支持目录功能,并能够使用 OpenLDAP 提供的服务。然而,OpenLDAP 服务器似乎一直笼罩在神秘之中,只有专家和黑客才了解和掌握它。本书不仅旨在揭开 OpenLDAP 的神秘面纱,还希望为系统管理员和软件开发人员提供扎实的理解,帮助他们在实际应用中有效利用 OpenLDAP 的目录服务。
OpenLDAP 是一个开源服务器,提供网络客户端目录服务。目录服务器可用于将组织信息存储在集中位置,并将这些信息提供给授权的应用程序。客户端应用程序可以通过轻量级目录访问协议(LDAP)连接到 OpenLDAP。然后它们可以搜索目录,并在具有适当权限的情况下,修改和操作目录中的记录。LDAP 服务器最常用于为用户提供基于网络的认证服务。但 LDAP 还有许多其他用途,包括将目录用作地址簿、DNS 数据库、组织工具,甚至作为应用程序的网络对象存储。本书将介绍其中的一些用途。
本书的目标是为系统管理员或软件开发人员准备好使用 OpenLDAP 构建目录,并在网络环境中应用该目录。为此,本书将采取实际操作的方法,重点强调如何完成任务。偶尔,我们将深入探讨 LDAP 的理论方面,但这些讨论只会在理解理论有助于回答实际问题时进行。
本书内容
在第一章中,我们将讨论目录服务器和 LDAP 的一般概念,回顾 LDAP 的历史及 OpenLDAP 服务器的演变,最后提供 OpenLDAP 的技术概述。
接下来的章节重点讲解如何使用 OpenLDAP 构建目录服务,我们将在这些章节中深入探讨 OpenLDAP 服务器。
第二章从在 GNU/Linux 服务器上安装 OpenLDAP 开始。安装完服务器后,我们将进行基本的后安装配置,以确保服务器能够运行。
第三章介绍了 OpenLDAP 服务器的基本使用。我们将使用 OpenLDAP 命令行工具向新目录中添加记录、搜索目录并修改记录。本章介绍了与 LDAP 数据操作相关的许多关键概念。
第四章 涵盖了安全性,包括如何处理目录的身份验证、配置访问控制列表(ACL)以及使用安全套接字层(SSL)和传输层安全性(TLS)保护基于网络的目录连接。
第五章 讨论了 OpenLDAP 服务器的高级配置。在这里,我们详细介绍了各种后端数据库选项,还讨论了性能调优设置,以及最近引入的目录覆盖技术。
第六章 重点介绍通过创建和实施 LDAP 模式来扩展目录结构。模式提供了一种定义新属性和结构的程序,以扩展目录并提供根据需求定制的记录。
第七章 重点介绍目录复制以及如何通过不同方式让目录服务器在网络上互操作。OpenLDAP 可以将其目录内容从主服务器复制到任意数量的从属服务器。在这一章中,我们将设置两个服务器之间的复制过程。
第八章 讨论了如何配置其他工具与 OpenLDAP 互操作。我们从 Apache Web 服务器开始,使用 LDAP 作为认证和授权的来源。接着,我们安装 phpLDAPadmin,这是一个基于 Web 的目录服务器管理程序。然后,我们查看其主要功能,并进行一些定制调优。
附录包括逐步指导如何从源代码构建 OpenLDAP (附录 A)、如何使用 LDAP URL 的指南 (附录 B),以及有用的 LDAP 客户端命令汇编 (附录 C)。
本书所需内容
为了最大限度地从本书中受益,你需要安装 OpenLDAP 服务器软件,以及客户端命令行工具。这些都可以免费获取(作为开源软件)并以源代码形式从 openldap.org
下载。不过,你也可以选择使用特定 Linux 或 UNIX 发行版提供的 OpenLDAP 版本。
虽然 OpenLDAP 可以在 Linux、各种版本的 UNIX、MacOS X 和 Windows 2000 等系统上运行,但本书中的示例使用的是 Linux 操作系统。
由于基本的 LDAP 工具是命令行应用程序,你需要具备一定的 Linux/UNIX shell 环境操作知识。本书不详细涵盖网络协议,假设读者已经具备基本的客户端-服务器网络模型的理解。同时,也假设读者具备对 Web 和电子邮件服务结构的基本了解。
约定
在本书中,您将找到多种文本样式,用以区分不同种类的信息。以下是这些样式的一些示例,并解释它们的含义。
代码有三种样式。文本中的代码词汇显示如下:“telephoneNumber
属性有两个值,每个值代表一个不同的电话号码。”
一段代码将按如下方式设置:
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
by * none
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
directory /var/lib/ldap
# directory /usr/local/var/openldap-data
index objectClass sub,eq
index cn sub,eq
任何命令行输入和输出将按如下方式编写:
$ sudo slaptest -v -f /etc/ldap/slapd.conf
新术语 和 重要单词 以粗体字样介绍。您在屏幕上看到的,或者在菜单或对话框中看到的词汇,例如,出现在我们的文本中像这样:“点击简单搜索屏幕顶部的高级搜索表单链接将加载一个具有更多选项的搜索屏幕”。
注意
重要的说明将以类似这样的框展示。
提示
提示和技巧以这种形式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对本书的看法,您喜欢什么或可能不喜欢什么。读者反馈对我们开发真正能让您受益的书籍非常重要。
要发送一般反馈,只需给我们发送一封电子邮件至 <feedback@packtpub.com>
,并确保在邮件的主题中提及书名。
如果您需要某本书并希望我们出版,请通过 www.packtpub.com 上的 SUGGEST A TITLE 表单或发送电子邮件至 <suggest@packtpub.com>
给我们留言。
如果您在某个主题上有专业知识,并且有兴趣撰写或贡献书籍,请参见我们的作者指南:www.packtpub.com/authors。
客户支持
现在您已经成为 Packt 图书的骄傲拥有者,我们有很多方法可以帮助您从购买中获得最大收益。
下载本书的示例代码
访问 www.packtpub.com/support
,然后从书籍列表中选择本书,以下载本书的示例代码或额外资源。可供下载的文件将随后显示。
勘误
尽管我们已尽力确保内容的准确性,但错误还是会发生。如果您在我们的书籍中发现错误——可能是文本或代码错误——我们将非常感激您能报告给我们。这样,您不仅能帮助其他读者避免困扰,还能促进后续版本的改进。如果您发现任何勘误,可以访问www.packtpub.com/support
报告,选择您的书籍,点击提交勘误链接,并输入您的勘误详情。您的勘误经过验证后,将被接受并添加到现有的勘误列表中。您可以通过访问www.packtpub.com/support
并选择您的书名来查看现有的勘误。
问题
如果您在书籍的某个方面遇到问题,可以通过<questions@packtpub.com>
与我们联系,我们将尽最大努力解决问题。
第一章:目录服务器与 LDAP
在本章的第一部分,我们将介绍 LDAP 的基础知识。尽管本书的大多数章节采用实践操作的方式,但第一章的内容较为高层,具有引导性。我们将介绍目录服务器和 LDAP,包括常用的目录术语。我们还将看到 OpenLDAP 服务器在目录结构中的位置,它的起源以及它的工作原理。本章涵盖的主要内容如下:
-
LDAP 目录的基础知识
-
LDAP 和 OpenLDAP 服务器的历史
-
OpenLDAP 服务器的技术概述
LDAP 基础
LDAP 是 轻量级目录访问协议(Lightweight Directory Access Protocol)的缩写。顾名思义,LDAP 最初是为了提供一种访问现有目录服务器的网络协议,但随着 LDAP 及其相关技术的发展,LDAP 这一术语逐渐与一种特定的目录架构划上了等号。我们在谈论符合该架构的目录服务时,会使用 LDAP 这一术语,正如 LDAP 规范中所定义的。
注释
LDAP 是标准化的。LDAP 标准的内容,包括网络协议、目录结构以及 LDAP 服务器提供的服务,都可以通过 RFC(请求评论)文档形式获得。在本书中,我将引用具体的 LDAP RFC 作为 LDAP 的权威信息来源。
当前版本的 LDAP 是 LDAP v.3(版本 3),这是 1997 年以 RFC 2251 标准发布的,并在业界广泛实施。原始规范在 2006 年 6 月进行了更新,RFC 4510-4519 提供了更加清晰和一致的 LDAP 规范。
尽管一般目录和特别是 LDAP 目录在信息技术领域并不新颖或罕见,但推动这些技术的发展,尤其是 LDAP,远不如关系型数据库等近亲技术那样被广泛理解。本章(以及本书)的一个目标是介绍并澄清 LDAP 目录的功能和用途。
在本节中,我们将介绍一些理解 LDAP 所必需的重要概念。最好的起点是理解“目录”这一概念。
什么是目录?
当我们想到“目录”时,脑海中会浮现电话簿或地址簿的画面。我们使用这些目录查找个人或组织的信息。例如,我可能翻阅我的地址簿查找朋友 Jack 的电话号码,或者快速浏览电话簿寻找 Acme Services 的地址。
目录服务器 也是如此使用的。它维护着关于某些实体(如个人或组织)的信息,并提供访问这些信息的服务。
当然,目录服务器还必须具备添加、修改和删除信息的功能。但是,尽管电话簿被假定为主要用于阅读,目录服务器中的信息也假定主要是读取而非写入。关于目录服务器使用的这一假设在“高读写、低写”这一短语中有所总结或概括。因此,许多 LDAP 技术的应用都侧重于读取和搜索信息。
注意
虽然许多目录服务器已针对快速读取进行了优化,而牺牲了快速修改的能力,但 OpenLDAP 的情况不一定是这样。OpenLDAP 在这两方面都非常高效,且可以用于需要频繁写入数据的应用。
一些类型的目录服务器(比如一个简单的基于服务器的地址簿实现)只提供狭窄且特定的服务。一个单一目的的目录服务器,如在线地址簿,可能只存储一种非常特定类型的数据,如一组人的电话号码、地址和电子邮件信息。这类目录是不可扩展的。相反,它们是单一目的的。
但 LDAP(及其 X.500 前身)被设计为一个通用的目录服务器。它并非专门为捕获特定类型的数据(如电话号码或电子邮件地址)而设计。相反,它的设计目的是让实施者能够清晰且谨慎地定义目录应存储的数据。
这样的通用目录服务器应能够存储多种不同类型的信息。实际上,它应该能够存储关于不同类型实体的不同信息。例如,一个通用目录应该能够存储关于如人类和火成岩样本等多种实体的信息。但我们不想存储与人类相关的所有信息也适用于岩石。
一个人可能有姓氏、电话号码和电子邮件地址,如下图所示:
一块岩石样本可能有一个编号、关于其地理来源的信息以及一个硬度分类。
LDAP 使得定义一个人条目的样子和一块岩石条目的样子成为可能。它的通用架构提供了管理大量不同目录条目所需的能力。
在本节的其余部分,我们将探讨 LDAP 目录中的信息是如何结构化的。我们将从了解目录条目的概念开始,接着是区分名称和属性。然后,我们将研究条目如何在目录信息树中进行组织。在本节结束时,你应该理解 LDAP 目录中信息的基本结构。
目录条目的结构
让我们继续比较目录服务器和电话簿。电话簿包含一种非常特定类型的信息,以非常特定的方式组织,并且设计的目的是为了实现非常特定的功能。以下是一个电话簿条目的示例:
Acme Services
123 W. First St.
Chicago, IL 60616-1234
(773) 555-8943 or (800) 555 9834
如前所述,这种目录具有特定的信息,以特定的方式组织,旨在实现特定的目的:它是关于如何联系特定组织(Acme Services)的信息,按照一种熟悉的模式(地址和电话号码)组织。它的设计目的是使人们在心中有一个特定名称时,能够快速扫描目录(按组织名称字母顺序排列),并找到所需的联系信息。
但关于电话簿条目,有几个值得注意的地方:
-
数据的排列方式仅供按一个值进行搜索:组织的名称。如果你手头有某个组织的电话号码,但没有该名称,在电话簿中查找匹配的电话号码以确定名称将是一项费力的、很可能无效的任务。
-
该条目的格式较为简洁,要求读者能够识别格式,并提供解释数据所需的辅助信息。习惯于阅读电话簿条目的人能够根据前一个条目推断,并以此方式识别信息:
Organization Name: Acme Services Street Address: 123 West First Street City: Chicago State: Illinois Postal Code: 60616-1234 Country: USA Phone Number: +1 773 555 8943 Phone Number: +1 800 555 9834
在这个示例中,信息的含义变得更加明确。每个值前都有一个标识给定信息类型的名称。Acme Services 现在被识别为一个组织的名称。信息也被分解成更小的部分(城市和州分成不同的行),并且在之前隐含的部分(例如国家)现在变得显式。而且,之前将两个信息项(两个电话号码)压缩到一行中的部分,现在已经被分开,使信息更加明确。
这种条目形式更接近于 LDAP 目录中记录的方式。但仍然存在另一个问题需要解决。我们如何区分两个非常相似的记录?
例如,假设我们有一个涵盖整个伊利诺伊州的电话目录。在伊利诺伊州,有一家名为 Acme Services 的公司位于芝加哥市,另有一家名为 Acme Services 的公司位于斯普林菲尔德市。
仅知道公司名称是不足以从电话簿中筛选出唯一条目的信息。为了做到这一点,我们需要某种唯一名称——一个在整个目录中仅出现一次的名称,可以用来指代一个特定的条目。
唯一名称:DN
区分两个非常相似的记录的一种方式是为每个目录中的记录创建一个唯一名称。这是 LDAP 采用的策略;目录中的每个记录都有一个区分名称。区分名称是一个重要的 LDAP 术语,通常缩写为DN。
在 LDAP 目录中,目录设计者决定组成 DN 的组件,但通常,DN 反映了记录在目录中的位置(这是我们将在下一部分中探讨的概念),以及一些区分该记录与其他相近记录的信息。
因此,DN 由目录信息的组合组成,类似于以下内容:
dn: o=Acme Services, l=Chicago, st=Illinois, c=US
这个单一标识符足以将其与同名的 Springfield 公司区分开来。根据之前的方案,名为 Acme Services 的 Springfield 公司 DN 可能类似于以下内容:
dn: o=Acme Services, l=Springfield, st=Illinois, c=US
从这个例子中可以明显看出,在定义组成 DN 的字段时,必须确保这些字段足够精细,以区分两个不同的条目。换句话说,要打破 DN 语法,只需要在芝加哥出现另一个 Acme Services。
小贴士
DN 不是区分大小写的
LDAP 记录的某些部分是区分大小写的,而其他部分则不是。例如,DN 是区分大小写的。
DN 是 LDAP 条目中的一个重要元素。接下来,我们将更详细地探讨 LDAP 条目的概念,以及构成条目的组件。
一个示例 LDAP 条目
让我们具体看看一个LDAP 条目是什么样子的。
一个 LDAP条目,或记录,是存储有关目录中个体项信息的目录单元。再次借用在其他目录中找到的思想:电话簿中的条目描述该目录中特定的一个信息单元。同样,LDAP 目录中的记录包含有关特定单元的信息,尽管(由于 LDAP 是通用的)该单元的确切目标并未明确规定。它可能是一个人,或者一家公司,或者一块石头,或者像 Java 对象这样的虚拟实体。
注意
最初,LDAP 规范规定条目必须与现实世界中的某个事物相关联。虽然这可能是早期目录服务器开发者的初衷,但实际上并没有理由要求目录服务器条目必须与目录外部的任何事物(无论是现实的还是虚拟的)相关联。
一个条目由 DN 和一个或多个属性组成。DN 作为 LDAP 目录信息树中的唯一标识符。属性提供关于该条目的信息。让我们将之前的电话目录条目转换为 LDAP 记录:
dn: o=Acme Services, l=Chicago, st=Illinois, c=US
o: Acme Services
postalAddress: 123 West First Street
l: Chicago
st: Illinois
postalCode: 60616-1234
c: US
telephoneNumber: +1 773 555 8943
telephoneNumber: +1 800 555 9834
objectclass: organization
第一行是 DN。此记录中的其他所有行表示属性。
请注意,这个例子与我们之前检查的电话簿示例的主要区别在于条目中每个字段的名称;这些名称现在已经压缩成一种目录可以轻松解析的形式。
提示
这些属性名称,如o
和postalAddress
,指的是在 LDAP 模式中定义良好的属性定义。它们不能在运行时“发明”或“随意编造”。创建新属性需要编写模式。模式在本书第六章中进行讲解。
一个属性描述了一种特定类型的信息。在我们的示例中,有八个属性,分别代表以下内容:
-
组织名称(
o
) -
邮寄地址(
postalAddress
) -
本地性(
l
),可以是城市、镇、村庄等名称 -
州或省(
st
) -
邮政编码或 ZIP 代码(
postalCode
) -
国家(
c
) -
电话号码(
telephoneNumber
) -
对象类(
objectclass
),指定此条目是何种类型(或多种类型)的记录
一个属性可以有一个或多个属性名称,这些名称是同义词。例如,c
和countryName
都是用来标识国家的属性类型的名称。两者标识相同的信息,LDAP 会将这两个名称视为描述相同类型的信息。
在任何给定记录中,一个属性可以有一个或多个值(前提是该属性的定义允许多个值)。上面的记录只有一个属性包含多个值。telephoneNumber
属性有两个值,每个值代表一个不同的电话号码。
属性在属性定义中定义,后者将在第六章详细讨论。这些定义提供了关于存储在值中的信息的语法和长度的相关信息,所有适用的属性名称,属性是否可以有多个值等等。存储在 LDAP 目录中的记录必须遵循这些属性定义。
例如,国家名称的属性定义给出以下信息:
-
名称
c
和countryName
可以引用此对象。默认名称为c
。 -
国家名称作为字符串存储。
-
在进行属性值匹配时,可以忽略大小写。
-
匹配可以在整个字符串上进行(例如
Canada
),或者使用子字符串(Ca*
)。 -
一个国家名称不能超过 32768 个字符。
-
每个记录只允许有一个国家名称。
所有这些信息被打包成一个紧凑的模式定义,目录服务器在启动时读取该定义。
属性名称不区分大小写。属性名称o
与O
被视为同义词。同样,GivenName
、givenname
和givenName
都被评估为相同的属性名称。
至于属性的值,大小写敏感性取决于属性定义。例如,DN 和 objectclass
属性的值是不区分大小写的,但 URI(labeledURI
)属性值是区分大小写的。
对象类属性
给定记录中的最后一个属性是 objectclass
属性。这是一个特殊属性,提供关于该记录(或条目)类型的信息。
对象类决定了可以为记录赋予哪些属性。organization
对象类表示该记录描述的是一个组织。根据该对象类的定义,一个 organization
记录可以包含地点(l
)和邮政编码(postalCode
),以及记录中存在的所有其他属性。
其中一个字段,组织名称(o
),是任何具有 organization
对象类的条目所必需的。
对象类还允许多个其他属性,这些属性在我们的记录中不存在,如 description
和 facsimileTelephoneNumber
。
给定对象类属性(这是每个条目都必需的),目录可以确定该条目中必须、可以或不能存在的属性。
与其他属性一样,objectclass
属性可以有多个值,尽管哪些值可以给定受 对象类定义 和 架构定义 的约束——即有关哪些属性属于哪些对象类,以及如何组合这些对象类的规则。
注意
LDAP 架构 由定义目录中记录类型的规则组成,以及这些记录如何彼此关联。架构中存储的主要两项内容(尽管还有其他内容)是属性类型定义和对象类定义。本书第六章专门讨论架构。
虽然一个记录可能有多个对象类,但其中一个对象类必须是该记录的 结构对象类。结构对象类决定了该记录是何种类型的对象。我们将在本书后面讨论结构对象类。
LDAP 记录由一个单一的 DN 和一个或多个属性组成(记住,objectclass
是必需的)。这些属性包含关于由 DN 标识的实体的信息。
一个 LDAP 目录包含多个条目的集合,这些条目按树形结构在一个或多个层次中排列。
操作性属性
除了常规属性外,目录服务器还可能会将特殊的 操作性属性 附加到条目上。操作性属性由目录服务器本身使用,用于存储关于条目的信息。这些属性不是为最终用户设计的(尽管偶尔它们可能会有用),通常在 LDAP 查询中不会返回。
在本书的多个部分,我们将使用操作性属性。但大多数时候,当我们讨论属性时,我们指的是常规属性。
目录信息树
到目前为止,我们一直在将 LDAP 目录与地址簿或电话簿进行比较。但现在,我将介绍 LDAP 目录服务器中数据结构与许多其他类型目录之间的主要区别之一。
电话簿中的信息通常以一长串字母顺序排列。但在 LDAP 目录中,组织结构更加复杂。
LDAP 目录中的信息被组织成一个或多个层级结构,其中层级结构的顶部是基条目,其他条目则以树状结构组织在基条目下。层级中的每个节点都是一个条目,具有 DN 和多个属性。
这种层级组织的条目集合称为目录信息树,有时简称为目录树或DIT。
要理解这种组织信息的方法,可以参考公司的组织结构图。
层级结构的顶端是公司本身。在其下,有多个部门和组织单元,而在这些部门和组织单元下,则是员工、承包商以及其他与公司有正式关系的个人。我们可以将其绘制成一个层级结构:
LDAP 目录也以层级关系存储数据。目录信息树的顶部是根条目。其下是一个从属条目,它本身可能还有自己的从属条目。每个记录都有自己的 DN 和属性。
提示
文件系统类比
大多数现代文件系统也以层级方式表示数据。例如,目录 /home
可能有多个子目录:/home/mbutcher
、/home/ikant
、/home/dhume
。我们可以说 /home
有三个从属项,但每个从属项都有一个上级(即 /home
目录)。在思考 LDAP 目录树时,将其与文件系统的布局进行比较可能会有所帮助。
将其与前面的示例结合,我们可以轻松地创建一个表示组织结构图的 LDAP 目录信息树:
请注意,每个条目的 DN 包含有关其上级条目(其上面的记录)的信息。实际上,DN 由两部分组成:第一部分是相对 DN(RDN),它包含条目中的一个或多个属性。第二部分是上级条目的完整 DN。我们将在第三章进一步讨论这种关系。
在接下来的几章中,我们将创建目录时,会创建一个类似树形结构的记录集合。
现在你应该对如何在目录信息树中表示目录有了基本的了解。由 DN 和一些属性组成的记录被组织成一个层级结构。层级结构的顶部是基条目,其下的条目被组织成树枝。
使用 LDAP 服务器时需要做什么
我已经描述了 LDAP 目录是什么,但同样有帮助的是了解 LDAP 目录的用途。LDAP 服务器的功能是什么?它要解决什么问题?
第一个,也是最明显的答案是,LDAP 旨在提供一个数字目录——一个在线展示,相当于电话簿或地址簿。当然,这其中确实有一些真实性,LDAP 服务器确实可以这样使用。但关系型数据库甚至更基本的数据结构也可以做到这一点。
我们可以在这个答案上做进一步扩展,指出 LDAP 提供了强大的服务层——使用复杂的过滤器进行搜索、通过属性表示复杂实体、允许对数据进行精细的访问控制等等——这些都提供了复杂的目录服务。
一个更经典的解释,源于 LDAP 从 X.500 目录演变而来的历史背景,是,LDAP 被设计用于表示组织,包括其结构、物理资产和人员。从这个角度来看,LDAP 不仅仅是一个花哨的电话簿,它更像是一个企业管理工具。事实上,这也是使用 LDAP 目录的常见方式之一。
LDAP 最常见的用途,基于将 LDAP 视为一种狭义的企业管理工具的理解,是作为网络用户、组和账户的中央权威。一个 LDAP 目录存储着网络中每个用户账户的信息——如用户名、密码、全名和电子邮件地址等。网络上的其他服务,从工作站到电子邮件服务器再到 web 应用程序,都可以将 LDAP 作为用户信息的权威来源。应用程序可以通过目录对用户进行身份验证。一个用户账户可以在多个(甚至是所有)企业应用程序之间共享。
最后,还有一种更通用或抽象的看法,关于 LDAP 服务的功能。LDAP 其实就是一种特殊的数据库,它将数据组织成树状结构,类似于文件系统层级。这种看法通过将 LDAP 目录与关系型数据库(RDB)系统进行对比更容易理解。
关系型数据库将信息存储在表格中,而表格由记录组成。在关系型数据库(RDB)中,表与表之间的关系是通过不同表中的记录建立的,关系有多种形式:一对多、一对一、多对一,等等。关系型数据库支持对数据进行读写操作,通常通过某个版本的 SQL(标准查询语言)来实现,并且它们通常监听网络连接,使得网络上的其他应用程序能够访问数据。
与 RDB 相比,LDAP 也可以看作是一个存储系统。然而,与以表格结构展示数据的 RDB 不同,LDAP 以层级结构(像文件系统一样)存储条目。LDAP 中的基本关系包括优先到从属关系(一对多)和从属到优先关系(一对一),虽然也可以使用其他关系。
提示
LDAP 中的其他关系
虽然优先/从属关系是最常用的,但它们并不是唯一被支持的关系。数据库中任意条目之间的关系通常通过使用属性将 DNs 链接在一起来建模。当我们在第四章讨论组时,我们将详细研究这种用法。
通过 LDAP 操作,支持使用复杂的过滤器和数据结构(如 LDIF(LDAP 数据交换格式))对数据库进行读写。而且,像 RDB 服务器一样,LDAP 目录通常会监听网络套接字,以向其他应用程序提供服务。
我已经提出了 LDAP 目的的不同视角。这些中是否有一个是正确的答案?没有。每种 LDAP 的使用方式都是合法的,LDAP 目录可以用来解决各种各样的问题。
LDAP 和 OpenLDAP 的历史
初看之下,LDAP 这个术语似乎有些误导。例如,当我们谈论 Web 的主要协议 HTTP(超文本传输协议)时,我们是指 Web 应用程序如何通过网络传输信息。我们并不是在谈论跨网络传输的数据的格式,也不是在谈论这些数据如何存储在服务器上或从服务器中检索。
但当我们谈论 LDAP 时,通常不仅仅是指网络协议,还指一种特定类型的服务器,它将格式明确的数据存储在一个特殊的数据库中。这种看似误导的名称背后有一个历史原因。
最初,LDAP 只是一个网络协议,用于从 X.500 目录(一个在 1980 年代设计并于 1988 年标准化的目录服务器架构)中获取数据。这是 Yeong、Howes 和 Killie 在 1993 年最初起草 LDAP 规范 RFC 1487 时的初衷。
提示
关于 RFC
RFC(请求评论)是一系列技术文档,通常用于指定标准。每个 RFC 都有一个编号,按顺序排列——较早的 RFC 编号较低。有许多网站提供 RFC 数据库的完整或部分内容,其中一个示例来源是 RFC 编辑器(www.rfc-editor.org
),本书中也使用了该网站。
最早的 LDAP 服务器是 X.500 目录的网关,但这些服务器很快发展成了完整的目录服务器。Tim Howes 和他在密歇根大学的同事们创建了开源的密歇根大学 LDAP 实现,该实现成为其他 LDAP 服务器的参考实现。
注意
密歇根大学 LDAP 项目的历史信息仍然可以在线获取:www.umich.edu/~dirsvcs/ldap/ldap.html
随着密歇根大学的 LDAP 服务器的成熟,涌现了大量新的标准。LDAP 在业界逐渐获得势头。Tim Howes 被 Netscape 雇佣,LDAP 逐渐走向主流。
到 1990 年代末,Netscape、Novell、Oracle 和 Microsoft(等公司)都推出了 LDAP 产品。1997 年发布的 RFC 2251 标准化了 LDAPv3,并对早期的 LDAP 标准做出了重大改进。
LDAP 服务器市场逐渐成熟,但密歇根大学项目失去了动力。主要开发人员离开了大学,转向其他项目。
1998 年,OpenLDAP 项目由 Kurt Zeilenga 启动。不久后,Howard Chu(曾任密歇根大学职员,现为该项目的架构师)加入。他们挽救了密歇根大学的代码库,并重新开始开发。最终,OpenLDAP 2.0 取得了巨大成功,并进入了几乎所有主要的 Linux 发行版。
注意
OpenLDAP 贡献者的完整名单,从项目开始到现在,可以在 www.openldap.org/project/
找到。
自 90 年代末以来,OpenLDAP 在 OpenLDAP 基金会的监督下不断成熟,并得到行业赞助商的贡献支持。截止目前,版本 2.3 是稳定版,版本 2.4 正处于测试阶段。
正如密歇根大学 LDAP 服务器的初衷一样,OpenLDAP 仍然严格遵循 LDAP 标准。事实上,Kurt Zeilenga 对 2006 年 6 月 LDAP 标准的多次更新负有重要责任。
除了高度符合标准外,OpenLDAP 还是市场上最快的目录服务器之一,远远超越了其他开源目录服务器实现的产品。
OpenLDAP 技术概览
本书是一本面向实践的技术书籍,旨在帮助您快速搭建并运行 OpenLDAP,并帮助您将 LDAP 集成到自己的应用程序中。
我们现在将开始从前面介绍的高层次内容转向对 OpenLDAP 套件的更实际的检视。首先,让我们简要看一下 OpenLDAP 的技术结构。
OpenLDAP 套件可以分为四个组件:
-
服务器:提供 LDAP 服务
-
客户端:操作 LDAP 数据
-
工具:支持 LDAP 服务器
-
库:提供 LDAP 的编程接口
在本书中,我们将查看这四个类别。此处,我们只做概述:
该图表解释了这四个元素之间的关系。
服务器
LDAP 套件中的主要服务器是SLAPD(独立 LDAP 守护进程)。该服务器提供对一个或多个目录信息树的访问。客户端通过 LDAP 协议连接到服务器,通常使用基于网络的连接(尽管 SLAPD 也提供了一个 UNIX 套接字监听器)。
服务器可以将目录数据存储在本地,或仅仅访问(或代理访问)外部资源。通常,它提供认证和搜索服务,也可能支持添加、删除和修改目录数据。它提供对目录的精细访问控制。
SLAPD 是本书的主要焦点,我们将在接下来的章节中详细讨论它。
客户端
客户端通过 LDAP 网络协议访问 LDAP 服务器。它们的工作方式是请求服务器代其执行操作。通常,客户端会首先连接到目录服务器,然后进行绑定(认证),接着执行零个或多个其他操作(如搜索、修改、添加、删除等),最后解除绑定并断开连接。
实用工具
与客户端不同,实用工具不通过 LDAP 协议执行操作。相反,它们在更低的层次上操作数据,并且不通过服务器进行中介。它们主要用于帮助维护服务器。
库
有几个在 LDAP 应用之间共享的 OpenLDAP 库。这些库为这些应用提供 LDAP 功能。客户端、实用工具和服务器都共享对其中一些库的访问权限。
应用程序编程接口(APIs)用于允许软件开发人员编写自己的 LDAP 感知应用,而无需重写基础的 LDAP 代码。
虽然 OpenLDAP 提供的 API 是用 C 语言编写的,但 OpenLDAP 项目还提供了两个 Java API。这些 Java 库不包含在 OpenLDAP 套件中,也不在本书的讨论范围内。然而,两者都可以从 OpenLDAP 网站上获取:openldap.org
。
随着本书的深入,我们将详细探讨 LDAP 架构的各个组成部分。
小结
在本章中,我们介绍了 LDAP 目录的一般基础知识,特别是 OpenLDAP 服务器的基础知识。我们讨论了 LDAP 的历史、重要术语以及一些 OpenLDAP 的高层技术方面。现在我们准备开始应用这些知识。
在下一章中,我们将关注 OpenLDAP 的安装和配置过程。
第二章:安装与配置
在本章中,我们将逐步演示安装和配置 OpenLDAP 工具套件的过程。这里我们只涵盖 SLAPD 服务器的基本配置。这将为后续章节(特别是第 4 至第七章)打下基础,在那些章节中我们将探讨更高级的配置选项。我们将涉及的具体内容包括:
-
安装二进制的 OpenLDAP 包
-
使用
slapd.conf
文件配置 LDAP 服务器 -
使用
slaptest
验证slapd.conf
配置 -
启动和停止服务器
-
使用
ldap.conf
文件配置客户端工具 -
使用
ldapsearch
从目录中获取根 DSE 条目
开始之前
OpenLDAP 由 OpenLDAP 基金会维护。该基金会维护一套工具,我们称之为 OpenLDAP 工具套件。正如我们在第一章中所见,OpenLDAP 工具套件包括以下几类工具:
-
守护进程(
slapd
和slurpd
) -
库文件(尤其是
libldap
) -
客户端应用程序(
ldapsearch
、ldapadd
、ldapmodify
等) -
支持工具(
slapcat
、slapauth
等)
官方的 OpenLDAP 源代码发行版将这些工具打包成一个下载包。然而,某些二进制版本可能将其拆分为多个子包。通常,工具套件被拆分为三个包:库文件、客户端 和 服务器。
OpenLDAP 可以在各种操作系统上编译和运行。然而,OpenLDAP 项目本身并不提供其软件的二进制版本。因此,不同的厂商和操作系统维护者会编译并提供他们自己的二进制版本。目前,已经为大多数 UNIX 变种(包括 Mac OS X)编译了 OpenLDAP 版本,同时也有适用于 Windows 操作系统的版本。有些二进制发行版甚至提供商业支持。
操作系统的 OpenLDAP 二进制版本
在本书中,我们将使用 Ubuntu Linux 作为首选操作系统。Ubuntu 是一个基于著名的 Debian Project 的 GNU/Linux 发行版。像 Debian(以及其他许多基于 Debian 的发行版)一样,Ubuntu 使用 Debian 包格式。因此,如果你使用的是另一种基于 Debian 的发行版,安装过程应该会非常类似。
注意
Ubuntu 是一个用户友好的 Linux 发行版。你可以在 www.ubuntu.com/
上了解更多关于 Ubuntu 的信息。想了解更多关于 Ubuntu 所基于的 Debian 项目,可以访问 debian.org/
。
几乎每个主要的 Linux 和 BSD 发行版都提供 OpenLDAP 的官方支持。你可能需要查阅你所选择的发行版的文档,以了解如何获取和安装 OpenLDAP。在某些情况下,OpenLDAP 会随基础操作系统一起安装。
对于 Windows、Mac 和其他 UNIX 变种,查找可用二进制包的最佳方法是浏览由 OpenLDAP Faq-O-Matic 维护的发行版列表(www.openldap.org/faq/data/cache/108.html
)。
商业版 OpenLDAP 发行版
如果你需要一个有商业支持的 OpenLDAP 发行版,可以考虑 Symas 提供的版本。Symas(www.symas.com/
)由许多与 OpenLDAP 套件贡献者相同的人员拥有和运营。他们提供一个商业二进制版的 OpenLDAP 套件,名为 Connexitor Directory Services (CDS)。
有多个不同的 CDS 版本可供选择,每个版本都根据特定的组织需求进行了调优和优化。例如,它们的铂金版特别针对拥有超过 1.5 亿条记录的目录进行优化!Symas 还提供 LDAP 培训、维护和支持服务以及咨询服务。
源代码编译
如果你不想安装二进制文件,可以选择自行编译 OpenLDAP 的源代码。这个过程在本书的附录 A 中有简单的步骤说明。
从源代码构建的主要优势在于,你可以在这些修订版本发布到主流软件包之前,就享受到许多改进。OpenLDAP 稳定分支的开发重点是修复 bug。因此,从源代码构建通常能提高 OpenLDAP 的稳定性。
关于版本的简要说明
当前,OpenLDAP 的稳定分支是 2.3 分支(2.4 版本处于早期 beta 阶段)。然而,一些 Linux 发行版仍在使用 2003 年发布的老旧 2.2 版本。如果你所选择的操作系统的最新软件包仍在 2.2 分支,你可能想要考虑寻找适用于你平台的 非官方 2.3 版本,或者甚至编译一个自定义二进制文件(参见 附录 A)。
安装
本节将演示在运行 Ubuntu Linux 7.04 的系统上进行安装的过程。以后,Ubuntu 版本可能会遵循相同的安装模式。
依赖项
Ubuntu 中的基本 OpenLDAP 配置需要一些额外的库和软件包。具体如下:
-
Berkeley 数据库(
bdb4
)版本 4.2(但不包括 4.3 版本,因为它有稳定性问题):在 Ubuntu 的默认配置中,OpenLDAP 将目录存储在 BDB 数据库中。Berkeley 数据库通常简称为 BDB。 -
OpenSSL 库:这些库提供 SSL 和 TLS 安全性。SSL 和 TLS 为网络连接到目录提供加密。
-
Cyrus SASL 库:该库提供对安全 SASL 身份验证的支持。
-
Perl 编程语言:它可以提供自定义的后端脚本。
-
iODBC 数据库连接层:OpenLDAP 可以将目录存储在关系型数据库(RDBMS)中。iODBC 库用于连接到 RDBMS。
OpenLDAP 还依赖一些标准系统库包(例如 libc6
),这些库包在所有 UNIX/Linux 发行版中都会安装。在默认安装中,Ubuntu 包括了 BDB、OpenSSL 和 Perl。其他依赖项的安装会由包管理器自动处理,因此无需担心手动安装这些。
安装 OpenLDAP
像许多其他发行版一样,Ubuntu 将 OpenLDAP 拆分成多个小包。守护进程(slapd
和 slurpd
)包含在 slapd
包中。客户端包含在 ldap-utils
中,库文件包含在 libldap-2.3-0
中。当 Ubuntu 7.04 发布时,提供的是 OpenLDAP 版本 2.3.30。随着安全修复的发布,Ubuntu 可能会通过在线更新发布新版本。尽管旧版的 2.2.26 包仍然可用,但应该避免使用。
要安装 Ubuntu,我们可以使用 Synaptic 图形化安装程序或任何命令行包管理工具。为了简化,我们将使用 apt-get。这将从官方 Ubuntu 仓库下载所有必要的包(包括依赖项)并为我们安装。请注意,使用这种方式安装需要访问互联网(或者,使用其他形式的 Ubuntu 分发媒体,如 CD-ROM)。我们需要运行以下命令。
$ sudo apt-get install libldap-2.3-0 slapd ldap-utils
下载和安装包可能需要一些时间。
一旦 apt-get
完成,LDAP 服务器及其所有客户端应该就安装好了。接下来,我们将开始配置 SLAPD 服务器。
配置 SLAPD 服务器
OpenLDAP 包含两个守护进程:SLAPD 服务器和 SLURPD 服务器。SLAPD,有时被称为 OpenLDAP 服务器,处理客户端请求和目录管理,而 SLURPD 则管理将更改复制到其他目录。SLURPD 现在已经弃用,取而代之的是一种更新的、更强大的复制过程,并将在 OpenLDAP 的未来版本中移除。
在下一章中,我们将更详细地讨论这两个守护进程的作用。现在我们只关心如何启动 SLAPD 服务器,以便能够开始连接到(并使用)我们的目录。
SLAPD 有一个主要配置文件和若干辅助配置文件。在本节中,我们将编辑主配置文件。这个文件叫做 slapd.conf
,在 Ubuntu 的发行版中,它位于 /etc/ldap/
(如果你是从源码构建的,默认位置是 /usr/local/etc/openldap/
)。
提示
使用 find . –type f –name slapd.conf
或者如果启用了 locate
服务,可以使用 locate slapd.conf
。
虽然 Ubuntu 提供了一个不错的基础slapd.conf
文件供你使用,但如果你选择不使用它,我们将从头开始。为了我们的目的,我们将从一个空文件开始并创建一个slapd.conf
配置文件。你可能希望在我们开始之前先备份原始的slapd.conf
文件。可以通过在终端中运行以下命令来进行备份:
$ sudo mv /etc/ldap/slapd.conf /etc/ldap/slapd.conf.orig
这将把文件从slapd.conf
重命名为slapd.conf.orig
。
提示
默认情况下,Ubuntu 不会激活root
账户。每次需要以超级用户身份执行操作时,应该使用 sudo。然而,如果需要成为root
(例如,连续执行多个命令),可以输入sudo su
。
现在我们准备好创建新的slapd.conf
文件了。打开文本编辑器并创建一个基本的slapd.conf
文件:
# slapd.conf - Configuration file for LDAP SLAPD
##########
# Basics #
##########
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
loglevel none
modulepath /usr/lib/ldap
# modulepath /usr/local/libexec/openldap
moduleload back_hdb
##########################
# Database Configuration #
##########################
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
# directory /usr/local/var/openldap-data
index objectClass,cn eq
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
by * none
access to *
by self write
by * none
文件中有三个标题(基础设置、数据库配置和ACLs),接下来我们将详细查看每个标题。
提示
如果你是从源代码构建的,则需要调整上面文件中的路径(或者,您也可以重新定位文件系统上的文件)。请在文件系统的/usr/local
部分查找正确的位置(例如,modulepath
位于/usr/local/libexex/openldap/
)。
基础设置
配置文件的第一部分,标记为基础设置,包含了各种配置参数:
##########
# Basics #
##########
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
loglevel none
modulepath /usr/lib/ldap
# modulepath /usr/local/libexec/openldap
moduleload back_hdb
首先要注意的是,所有以井号(#
)开头的行都被视为注释,并且会被 SLAPD 忽略。
前三行功能性(非注释)行都以include
指令开头。include
指令后应始终跟随文件系统上文件的完整路径。当 SLAPD 遇到include
指令时,它将尝试加载指定的文件。这些文件将作为当前配置文件的一部分进行处理。因此,当 SLAPD 读取这三行时,它将尝试加载三个架构文件(core.schema
、cosine.schema
和inetorgperson.schema
)。
include
指令可用于加载任何配置参数(在下一章中,我们将使用它来包含包含 ACL 的文件)。传统上,架构信息是与其他配置指令分开存储的,并在服务器启动时加载(使用include
指令)。这可以提高代码的可读性,并有助于防止意外修改架构信息。
架构
架构提供了(除了其他内容)OpenLDAP 应该支持的不同对象类和属性类型的定义。利用这些,OpenLDAP 可以确定它允许存储哪些条目、任何给定的条目是否有效以及如何最佳存储条目。
这里加载的三个模式包含了最常用的选项。core.schema
包含了 LDAP v.3 规范中的所有属性和对象类定义。cosine.schema
和 inteorgperson.schema
文件包含了常用的标准化扩展的模式定义(参见 RFC 4524 和 2798)。OpenLDAP 还提供了许多其他模式,我们将在第六章中讨论其中的一些。
更多指令
在包含模式后,接下来的两个指令,pidfile
和 argsfile
,告诉 SLAPD 文件存储的位置(以及在哪里查找)包含以下信息的文件:
-
SLAPD 服务器进程的进程 ID
-
启动时传递给
slapd
命令的参数
注意
由于 SLAPD 需要写入这些文件,因此运行 slapd
的用户需要具有 读取
和 写入
这些文件的权限。由于文件在 SLAPD 服务器关闭时会被删除,因此运行 slapd
的用户还需要对存储这些文件的目录(在此案例中是 /var/run/slapd/
)具有写入权限。
接下来,loglevel
指令被设置为 none
。loglevel
指令指定了 SLAPD 应该向系统日志(通过 syslogd
)发送多少信息。loglevel 指令接受关键字(any
、none
、trace
等),整数(0
、128
、32768
)和十六进制数字(0x2
、0x80
、0x100
)。
将其设置为 none
会导致 SLAPD 仅记录关键事件。为了完全关闭日志记录,请使用 0
。要开启所有日志记录,这将为每个请求生成大量日志,请使用 any
。SLAPD 的手册页(man slapd
)提供了所有支持的日志级别的完整列表。
模块指令
基础 部分中的最后几个指令是 modulepath
和 moduleload
。这些是用于加载 OpenLDAP 模块的指令。
模块 是一种特殊类型的库,可以在 SLAPD 启动时加载。与将所有 SLAPD 代码编译成一个大二进制文件不同,模块使得可以为 LDAP 代码的不同功能单元创建较小的库文件。
通常,模块有两种不同的类型:
-
后端:OpenLDAP 服务器可以使用不同的存储后端,包括 BDB、SQL 数据库、平面文件(LDIF 格式)或甚至另一个 LDAP 目录服务器。每个后端都可以被编译成自己的模块。然后,在配置过程中,我们可以选择仅加载所需的模块(或模块)。
-
覆盖层:OpenLDAP 包括许多可选的扩展,称为覆盖层,这些覆盖层可以修改服务器的行为(我们将在本书中讨论几个覆盖层)。这些也存储在模块中。
让我们来看看在 slapd.conf
文件中使用的指令:
-
modulepath
指令提供模块(编译的库)存储目录的完整路径。默认情况下,Ubuntu 将 LDAP 库放在/usr/lib/ldap
中。如果出于某种原因,您的模块存储在多个目录中,您可以指定多个路径列表,用冒号分隔:modulepath /usr/lib/ldap:/usr/local/lib/custom-ldap
-
moduleload
指令指示 OpenLDAP 加载特定的模块。该指令接受要加载的模块的文件名(例如back_hdb
)或模块文件的完整路径(以/
开头)。如果仅指定名称,SLAPD 将在modulepath
中指定的目录中查找。如果指定了完整路径,它将尝试从该路径加载(完全不使用modulepath
)。 -
moduleload back_hdb
指示 SLAPD 加载提供存储目录的服务的模块,该目录使用 层次化 数据库(HDB)后端。这就是我们将在 数据库 配置 部分中配置的数据库。
到目前为止,这些是我们在 基础 部分中所需要的唯一指令。不过还有其他选项,我们将在第四章和第五章中详细讨论它们。
数据库配置
我们 slapd.conf
文件的下一个部分是数据库配置部分。该部分处理数据库存储机制的配置。OpenLDAP 并不限于使用单一数据库,每个服务器可以使用多个数据库,每个数据库存储其自己的目录树(或子树)。例如,单个 OpenLDAP 实例可以从一个数据库提供基本为 o=My Company,c=US
的目录树,并从另一个数据库提供根为 dc=example,dc=com
的目录树。
注意
正如我们在第一章中看到的,目录树的基础 DN 由属性名/属性值对组成。例如,DN o=My Company, c=US
表示组织名称 (o)
是 My Company,其原籍国家 (c)
是美国(ISO 两字母代码为 US)。同样,第二个 DN 由属性名/值对组成,这次代表域组件 (dc)
,来自组织注册的域名,在这里是虚构的 Example.Com
。
我们将在第五章中讨论这个选项。在我们的简单 slapd.conf
文件中,我们只定义了一个数据库:
##########################
# Database Configuration #
##########################
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
# directory /usr/local/var/openldap-data
index objectClass,cn eq
数据库配置部分的第一个指令是 database
指令。该指令指定将使用哪个数据库后端。在本例中,我们将使用 层次化数据库 (HDB),因此我们指定 hdb
。
注意
HDB 是 OpenLDAP 的新一代存储机制。与其前身 BDB 后端类似,HDB 也使用 Oracle Berkeley DB 数据库进行存储,但 HDB 采用层次结构存储条目,这非常适合 LDAP 的树状结构。旧的 BDB 后端仍然受到支持,您可以通过在 database
指令中指定 bdb
而不是 hdb
来使用它。
接下来的指令 suffix
表示此数据库将包含目录树的哪些部分。基本上,它表明该数据库的基础将是 suffix
指令中指定的 区分名称(DN)条目(dc=example
,dc=com
)。我们在第一章中讨论过 区分 名称。
当服务器接收到树中某些内容的请求时(例如,cn=Matt
,dc=example
,dc=com
),它会在该数据库中进行搜索。下图可以更好地说明这一点:
这里,客户端正在搜索特定的 DN,cn=Matt
,dc=example
,dc=com
。SLAPD 服务器包含一个目录信息树,其基础 DN 为 dc=example
,dc=com
。
DN cn=Matt
,dc=example
,dc=com
属于 dc=example
,dc=com
。它存在于 dc=example
,dc=com
树中。因此,SLAPD 会在 dc=example
,dc=com
数据库中搜索一个 DN 为 cn=Matt
,dc=example
,dc=com
的记录。一旦找到该记录,它将返回给客户端。
如果客户端请求记录 cn=Matt
,dc=test
,dc=net
,会发生什么情况?由于这个 DN 不包含服务器处理的基础 DN,服务器将不会搜索该记录。根据配置,服务器可能会返回一个错误给客户端,或者将客户端重定向到另一个可能能够处理该请求的服务器。
同样,如果客户端尝试添加一个具有与 suffix
指令中指定的基础 DN 不同的记录,LDAP 服务器将拒绝将该记录添加到目录信息树中。
slapd.conf
中的 suffix
指令指定了存储或引用在该数据库中的信息的基础 DN。这在很大程度上决定了该数据库将包含、搜索或允许添加哪些记录。
注意
一个数据库可以包含多个树(第五章有详细说明)。
接下来的两行指定了目录管理员的记录,并为管理员条目设置了密码。rootdn
指令指定了被视为该目录管理员的 DN。按照约定,root DN 是通过将 cn=Manager
添加到目录树的 base DN 前面来创建的。因此,我们的目录管理员是 cn=Manager
,dc=example
,dc=com
。接下来的字段 rootpw
用于为目录管理员指定密码。请注意,这个密码存储在目录外部,而不是内部。例如,目录中记录的 userPassword
属性。这是为了防止管理员被锁定,无法访问目录。
目录管理员是一个具有特殊权限的特殊用户。管理员的请求不会通过 ACL 进行过滤——管理员的访问不能受到限制。此外,管理员对指定后缀或后缀下的所有记录具有写权限。因此,管理员 DN 应仅用于管理任务,而不应用于其他用途。
此外,由于管理员的必要字段存储在 slapd.conf
文件中,因此目录中不应有包含管理员 DN 的记录(虽然 SLAPD 并未明确禁止,但这是最佳实践推荐的做法)。
由于管理员的 DN 和密码存储在 slapd.conf
文件中,并且管理员对目录中的所有内容都有访问权限,因此我们应该将 slapd.conf
文件的文件系统权限设置得尽可能严格。
提示
加密管理员的密码
您还可以通过使用 ldappasswd
工具为 rootpw
设置加密密码,具体内容将在下一章介绍。
directory
指令指示文件系统中的哪个目录应该存储数据库文件。在本例中,数据库存储在 /var/lib/ldap/
。
最后,index
指令由应被索引的属性列表组成,后面跟着该索引将用于的匹配类型。我们的示例如下所示:
index objectClass,cn eq
这意味着我们正在创建一个支持属性 objectClass
和 cn
上等值(eq
)匹配的索引。当服务器收到所有 cn Rob
或 commonName Rob
的条目请求时,服务器可以通过访问索引而不是搜索整个数据库,显著加速服务。然而,如果请求是 Rob*
(注意 *
通配符字符),则服务器不会寻找等于 "Rob*" 的 CN,而是寻找以 "Rob" 开头的任何 CN。在这种情况下,我们创建的索引将不会被使用。
可以使用多个索引指令,我们可以通过将索引指令拆分为两个不同的指令,来支持更快速的 CN 搜索,比如查询 Rob*
:
index objectClass eq
index cn eq,sub
在给定的示例中,objectClass
属性维护了等值(eq
)索引,而 cn
属性则为等值匹配(eq
)和子字符串匹配(sub
)建立了索引。
某些属性不支持所有类型的索引。例如,objectClass
属性不支持子字符串(sub
)索引匹配。在第五章中,我们将更加仔细地查看索引指令。
一旦创建了数据库,每次修改 slapd.conf
中的 index
指令时,都应使用 slapindex
命令行工具重建索引。然而,由于我们尚未在数据库中放入任何数据,因此现在不需要运行此命令。
现在我们准备继续配置文件的第三个也是最后一个部分。
ACLs
slapd.conf
文件的最后一部分是 ACL 部分。ACL(访问控制列表)决定了哪些客户端可以访问哪些数据,以及在什么条件下可以访问。我们将在第四章中更详细地讨论 ACL。然而,从一开始就配置一些基本的 ACL 是很重要的,因此我们将简要介绍两个简单的 ACL:
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
by * none
access to *
by self write
by * none
ACL 只是复杂语法的指令——它们以访问指令开始,后面跟着一系列条件。条件可以跨越多行,只要每行的续写都以一个或多个空白字符(如制表符或空格)开始。
提示
slapd.conf 文件中的行续写
任何指令,不仅仅是 ACL,都可以跨越多行,只要每一行的续行都以空白字符开始。例如,moduleload back_hdb
可以写成:
moduleload
back_hdb
让我们详细看看第一个访问控制:
access to attrs=userPassword
by anonymous auth
by self write
by * none
这个访问控制的目的是保护用户密码的安全。具体来说,它允许匿名用户请求服务器在登录过程中对密码进行身份验证比较。此外,它授予用户更改自己密码的权限。最后,它拒绝其他任何人访问密码。这就是该规则的作用。那么,我们该如何实现这一点呢?
每一行包含by
的代码都应该缩进:
access to
[资源]
by
[谁] [授予的访问类型]
by
[谁] [授予的访问类型]
by
[谁] [授予的访问类型]
每个access
指令可以有一个to
短语,并且可以有多个by
短语。我们的第一个规则有三个by
短语。让我们更详细地看看这些:
-
在
access to attrs=userPassword
中,attrs
表示接下来会有一个或多个属性的列表。在我们的例子中,只有一个属性:userPassword
。userPassword
属性用于存储目录中对象的密码值。注意
虽然并非目录中的所有对象都有
userPassword
,但有许多不是用户的对象也可以拥有密码。userPassword
属性最常见的用途是用于描述用户的记录。在这个访问控制中,没有明确提到该规则应用于目录的特定部分。鉴于此,ACL 将对所有
userPassword
实例进行强制执行。因此,该规则指定了对userPassword
属性的访问。接下来的三句话将表明谁可以访问userPassword
属性,以及他们可以获得什么样的访问权限。 -
接下来是
by anonymous auth
。这个短语授予匿名用户(尚未进行身份验证的用户)使用密码进行身份验证的权限。更准确地说,它表示当用户提交身份验证请求时,目录服务器被允许执行身份验证操作(即将提交的密码与相应用户条目中的userPassword
属性值进行比较)。 -
by
短语的最后部分指定了记录被授予的权限类型。权限级别可以通过几种方式授予,这将在第四章中详细讨论。
目前,我们将看看 ACL 中用于授予常见权限级别的四个关键字:
-
auth
:服务器可以使用此资源执行身份验证操作。 -
read
:客户端可以拥有auth
访问权限,并且还可以读取该资源,但不能进行任何更改。 -
write
:客户端可以拥有auth
和read
访问权限,并且还可以对资源上指定的内容执行添加、修改和删除操作。 -
none
:服务器不应允许客户端对该资源进行任何访问。
在第四章中,当我们深入研究 ACL 时,我们将了解其他关键字,并探索创建更精细的权限级别,例如允许写入访问而不授予读取访问。
因此,第二个 by
短语 by self write
表示一旦一个 DN(通常是用户)成功连接并通过身份验证到 LDAP 服务器,它就可以更改 userPassword
的值。
最后,最后一个 by
短语是 by * none
。*
是一个通配符,将应用于每个人。none
,正如我们所知,拒绝对 userPassword
属性的任何访问。这个规则表示每个人都应该被拒绝访问密码属性。
这个第三个 by
短语很好地说明了 ACL 的应用方式。ACL 是按顺序评估的。在上面的规则中,一旦服务器找到适用于当前 DN 的规则,它就会停止处理该 ACL。举个例子。当一个匿名用户尝试使用 DN 和密码进行身份验证(绑定)时,服务器会检查 ACL,看看该 DN 是否有权请求使用 userPassword
属性进行身份验证比较。
当 SLAPD 评估这个 ACL 时,它会看到第一个 by
短语适用;使用这个规则并跳过另外两个。然而,另一方面,如果一个经过身份验证的用户尝试读取另一个 DN 的 userPassword
,服务器将继续搜索 by
短语,直到找到一个匹配的规则。它会评估并跳过前两个规则,然后应用第三个规则,这会拒绝该用户访问另一个记录的 userPassword
属性。
提示
默认的 by 短语
在处理 ACL 时,SLAPD 默认拒绝访问。这意味着每个访问指令都会以隐式的 by
短语 by * none
结束。因此,为了节省空间,我们本可以省略我们两个 ACL 中的最后一个短语。
现在我们理解了第一个 ACL,第二个应该会非常简单。我们来看看第二个:
access to *
by self write
by * none
这个最后的 ACL 成为我们目录的默认规则。它可以这样转述:对于任何对象及其所有属性(to *
),如果当前连接的 DN 是该对象的 DN,则该对象可以被写入(by self write
)。否则,当前连接的 DN 没有任何访问权限(by * none
)。简而言之,它允许对象写入自己,但拒绝其他所有人对该对象的所有权限。
提示
限制管理器
应该注意,ACL 不能用于限制在 rootdn
指令中指定的特殊目录管理员帐户。
请记住,ACL 是按顺序处理的。因此,第二条规则只有在前一条规则未生效时才会应用。
这些访问控制非常严格,会阻止目录用户从目录中获取太多信息。在第五章中,我们将创建更多规则,使目录更易于访问,但目前这些简单的规则已足够。
验证配置文件
我们现在已经完成了配置文件的操作。开始服务器之前,最后一步是验证配置文件是否有效。
OpenLDAP 提供了一个工具,用于测试配置文件,确保其格式正确且指令使用得当。它还检查 OpenLDAP 环境的元素,确保所需的文件位于正确的位置。这个测试工具叫做slaptest
,其显示方式如下:
$ sudo slaptest -v -f /etc/ldap/slapd.conf
由于slapd.conf
的文件系统权限非常严格,我们使用sudo
以 root 用户身份执行测试。slaptest
命令需要知道slapd.conf
文件的位置。通过-f
参数指定配置文件的路径来实现。我们还使用了-v
标志以要求详细输出。由于slapd.conf
没有问题,所以只打印了一行:
config file testing succeeded
但如果有任何错误,slaptest
会提供诊断信息。我们来看一个配置错误的slapd.conf
文件:
# slapd.conf - Configuration file for LDAP SLAPD
##########
# Basics #
##########
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
loglevel none
modulepath /usr/lib/ldap
# modulepath /usr/local/libexec/openldap
moduleload back_hdb
##########################
# Database Configuration #
##########################
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
# directory /usr/local/var/openldap-data
index objectClass sub,eq
index cn sub,eq
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
by * none
access to *
by self write
by * none
这个配置文件是我们在本节中一直在检查的配置文件的小变体。问题是objectClass
属性不能处理子字符串匹配。这个原因(在第六章中会有更详细的解释)是因为架构不允许对objectClass
属性进行子字符串匹配。
做出上述更改后,我们运行slaptest
命令:
$ sudo slaptest -v -f slapd.conf
以下信息会出现:
slapd.conf: line 48: substr index of attribute
"objectClass" disallowed
slaptest: bad configuration file!
正如你所见,这些信息对于在尝试启动服务器之前快速发现并修复问题非常有用。
提示
Ubuntu 的疏忽
由于 Ubuntu 打包维护者的配置疏忽,slaptest 程序在发现未知指令时不会发出警告。因此,错误的指令名称可能在验证阶段被忽略。例如,将index
拼写为idnex
不会导致错误。
提示
使用 slapd 测试 slapd.conf
slaptest
命令实际上不过是一个指向 slapd 的符号链接,slapd 是用于启动服务器的命令。虽然这样做没有明显的优势,但你可以使用 slapd 程序来测试slapd.conf
:
$ slapd -T dest -f /etc/ldap/slapd.conf
一旦配置文件通过slaptest
程序的检查,我们就可以准备启动服务器了。
此时,我们已经走完了基本的slapd.conf
配置文件部分。这个配置文件将使我们的目录启动并运行,在本书的后续章节中,我们将介绍一些更高级的设置,这些设置可以包含在配置文件中。
注意
如果你有兴趣了解更多关于slapd.conf
的配置选项,可能想查看手册(man)页面。OpenLDAP 的 man 页面提供了详尽的(尽管有时表述简洁)参考资料,尤其是slapd.conf
页面非常有用。
$ man slapd.conf
在该页面的底部,有一份相关手册页面的列表,例如slapd-hdb
,该页面列出了特定于 HDB 数据库的指令。
启动和停止服务器
到这里,我们已经配置好了slapd.conf
文件。现在我们准备启动服务器。有两种不同的方式来运行 SLAPD 服务器:我们可以使用发行版提供的初始化脚本,或者直接运行slapd
命令。每种方式都有其优点,我们将在这里介绍两种方式。
使用初始化脚本
与 Ubuntu 一起安装的 OpenLDAP 包包括一个启动脚本,该脚本与其他服务启动脚本一起位于/etc/init.d/
目录中。/etc/init.d/
中的脚本,通常称为初始化脚本,用于在系统运行级别变化时(当系统启动、关闭或重启时)自动启动和停止服务,默认情况下,OpenLDAP 应配置为在服务器启动时启动,并在关闭和重启时停止。
ldap
初始化脚本提供了一种方便的方式来启动、停止和重启服务器。你可以使用 Ubuntu 的invoke-rc.d
命令来启动它(如果它尚未运行):
$ sudo invoke-rc.d slapd start
你也可以使用相同的脚本来停止服务器,只需将start
改为stop
:
$ sudo invoke-rc.d slapd stop
同样,要重启服务器,可以使用restart
命令,而不是start
或stop
。
初始化脚本设置了默认参数并传递了许多系统选项。其中一些存储在位于/etc/default/slapd
的单独配置文件中。例如,通过将SLAPD_USER
和SLAPD_GROUP
变量设置为特定的系统用户 ID 和组 ID,你可以以非默认用户身份运行 SLAPD。
OpenLDAP 服务器必须以 root 用户身份启动,以便绑定到正确的 TCP/IP 端口(默认是 389 或 636)。然后,它会切换并使用位于/etc/default/slapd
文件中指定的用户账户和组。
注意
Ubuntu 创建了一个名为openldap
的特殊用户和组来运行 SLAPD。其他发行版将 SLAPD 作为 root 用户运行,这从安全角度来看并不推荐。
其他设置,例如日志设置,也可以在此配置文件中进行。
直接运行 SLAPD
有时,从命令行直接启动 SLAPD 是有用的。这可能有助于在服务器启动失败时查看错误消息,或者在对初始化脚本或其配置文件进行更改之前测试配置。
要直接启动 SLAPD 服务器,只需运行slapd
命令:
$ sudo slapd
这将在后台启动 SLAPD 服务器。
注意
如果你是从源代码编译 OpenLDAP,slapd
命令会位于 /usr/local/libexec/
目录下,默认情况下,该目录不在 $PATH
中。你需要使用完整路径来运行该命令:/usr/local/libexec/slapd
。
服务器会将其进程 ID 写入 slapd.conf
中 pidfile
指令指定的位置。在我们的案例中,路径为 /var/run/slapd/slapd.pid
。我们可以通过使用标准的 kill
命令来停止服务器:
$ sudo kill `cat /var/run/slapd/slapd.pid`
该命令首先使用 cat
程序打印文件的内容(即 slapd
的进程 ID)。请注意,cat
命令被反引号(`
)包围,而不是单引号('
)。反引号告诉 shell 将语句作为要执行的命令。然后,进程 ID 会传递给 kill
命令,指示进程自我终止。
如果 slapd.pid
文件不可用,您可能会发现使用此命令来杀死服务器更加便捷:
$ sudo kill `pgrep slapd`
有时候,将命令启动在前台并设置调试信息以打印到终端窗口是更有用的做法。这也可以很容易做到:
$ sudo slapd -d config
在上面的命令中,我们使用 -d
标志将日志信息打印到 shell 的标准输出。这意味着 slapd
将信息打印到终端窗口。-d
标志需要一个参数——调试级别。我们指定了 config
,它指示服务器打印关于配置文件处理的详细日志信息。
输出大概是这样的:
@(#) $OpenLDAP: slapd 2.3.24 (Jun 16 2006 23:35:48) $
mbutcher@bezer:/home/mbutcher/temp/openldap-2.3.24/servers/slapd
reading config file /etc/ldap/slapd.conf
line 6 (include /etc/ldap/schema/core.schema)
reading config file /etc/ldap/schema/core.schema
line 44 (rootdn "cn=Manager,dc=example,dc=com")
line 45 (rootpw ***)
line 47 (directory /var/lib/ldap)
line 48 (index objectClass eq)
index objectClass 0x0004
line 49 (index cn eq,sub,pres,approx)
index cn 0x071e
slapd starting
这也是一种有用的方法来查找配置问题。-d
标志将接受 slapd.conf
手册页中指定的任何调试级别。我发现 acl
对调试访问问题很有用,而 filter
在解决搜索问题时常常非常有用。
当指定 -d
时,程序将在前台运行。要停止服务器,只需按 CTRL+C。这将停止服务器并返回到 shell 提示符。
与 slapd
一起使用的其他有用命令行参数包括 -u
和 -g
。每个参数都需要一个值:-u
接受一个用户名,-g
接受一个组名。这些参数控制 SLAPD 运行时的有效 UID 和 GID(用户 ID 和组 ID)。一旦 SLAPD 启动并连接到适当的端口(必须以 root 身份进行连接),它将切换到这些参数中指定的 UID 和 GID。
注意
要获取可以与 slapd
一起使用的其他命令行标志,请参阅 slapd
的手册页。
在接下来的部分中,我们将使用一些 OpenLDAP 客户端连接到我们的目录。这需要 SLAPD 服务器正在运行。你可以通过检查 /var/run/slapd/slapd.pid
是否存在来验证 slapd
是否在运行,或者通过运行 pgrep slapd
来查看 slapd
的进程 ID。如果没有返回进程 ID 号,说明 slapd
没有在运行。
配置 LDAP 客户端
在前几个章节中,我们专注于 SLAPD 服务器。现在服务器已启动,我们需要获取客户端配置,以便可以进行测试连接。
幸运的是,所有 OpenLDAP 客户端程序共享一个通用的配置文件 ldap.conf
,该文件在 Ubuntu 中位于 /etc/ldap/ldap.conf
(如果你是从源码构建的,参见附录 A,该文件的默认位置是 /usr/local/etc/openldap/ldap.conf
)。
其他程序,例如使用 OpenLDAP 客户端库的程序(如 PHP 和 Python 的 LDAP API),也可能使用 ldap.conf
文件作为检索基本配置的默认位置。
提示
太多的 ldap.conf 文件
偶尔,一些 Linux 发行版会创建两个不同的 ldap.conf
文件——一个用于 OpenLDAP,另一个用于 PAM 或 NSS LDAP 工具。这可能会导致混淆,不知道哪个 ldap.conf
文件用于哪个进程。然而,Ubuntu 为其他软件包提供了明确命名的配置文件,例如 /etc/pam_ldap.conf
。
一个基本的 ldap.conf 文件
ldap.conf
文件的目的是双重的:
-
它提供了一个定义客户端行为特定方面的地方,例如它们如何处理 SSL/TLS 证书或是否遵循别名条目。
-
它为 OpenLDAP 客户端提供了有用的默认设置。通过指定一些默认值,我们可以减少在命令行运行 OpenLDAP 客户端时必须传递的参数数量。
注意
别名是目录中的一个条目,它指向另一个条目。从概念上讲,它类似于 UNIX/Linux 文件系统中的符号链接,或 Microsoft Windows 中的快捷方式。
ldap.conf
文件有三种不同类型的指令:
-
一般设置,指定默认的服务器和 DN 等内容。
-
SASL 特定设置,用于确定在使用 SASL(简单身份验证和安全层)身份验证机制时,OpenLDAP 客户端将如何尝试进行身份验证。
-
TLS 特定设置,用于指定 OpenLDAP 如何处理使用 SSL(安全套接层)和 TLS 加密的连接。
此时我们只关心一般设置。稍后的章节中,我们将返回到这个文件,配置 SSL/TLS 和 SASL。
现在,我们需要查看一个基本的 ldap.conf
文件。ldap.conf
文件位于与 slapd.conf
相同的目录中——/etc/ldap/
(如果你是从源码构建的,则为 /usr/local/etc/openldap/
)。我们现在将把 LDAP 客户端设置插入到这个基本的 ldap.conf
文件中:
# LDAP Client Settings
URI ldap://localhost
BASE dc=example,dc=com
BINDDN cn=Manager,dc=example,dc=com
SIZELIMIT 0
TIMELIMIT 0
同样地,像 slapd.conf
一样,所有以井号(#
)开头的行都被视为注释,并且会被 OpenLDAP 客户端工具忽略。
接下来,我们有指令:
-
URI 指令指示如果客户端没有显式指定服务器时,要联系的服务器(或者多个服务器,因为此指令可以接受多个 URI,用空格分隔)。
因为服务器运行在我们将要执行客户端命令的同一台机器上,所以我们应该将 URI 设置为
ldap://localhost
。这个 URI 指定了默认的客户端连接应通过回环接口(127.0.0.1
或localhost
)使用(未加密的)LDAP 协议。由于没有指定端口,它将使用默认的 LDAP 端口,即 389。 -
第二个指令是
BASE
。它告诉客户端程序从目录中的哪里开始搜索。它接受一个完整的 DN 作为值。在这种情况下,我们将其设置为服务器的基础 DN——即我们目录树中根条目的 DN,以便所有搜索都从根开始。你可能还记得,当我们在
slapd.conf
中配置数据库时,我们将相同的基础 DN,dc=example,dc=com
,设置为存储在那里数据库的后缀。所以,我们在这里所做的就是告诉客户端从服务器管理的相同目录树根开始。这通常是配置BASE
在ldap.conf
文件中最方便的方式。 -
第三个指令是
BINDDN
,它指定了连接服务器时使用的默认 DN。在这个文件中,我将其设置为管理员的 DN,cn=Manager,dc=example,dc=com
。虽然这在下一章的示例中会非常有帮助,但通常来说,这并不是一个好主意,绝不应在生产环境中这样设置。通常,BINDDN
的默认值应该设置为一个具有有限权限的用户,或者应该省略(在这种情况下将不会使用默认 DN)。
大小和时间限制
接下来的两个指令,SIZELIMIT
和 TIMELIMIT
,分别表示返回记录的最大数量(SIZELIMIT
)和客户端等待服务器响应的最大时间(TIMELIMIT
)。在这里,我们将它们都设置为 0,这是这两个指令的一个特殊值,表示没有限制。
大小和时间限制的处理方式可能会有点让人困惑。在客户端一侧,有两种方式可以指定这些限制:通过 ldap.conf
配置文件(正如我们在这里所做的)和通过命令行参数(我们将在下一章中看到)。
然而,上面提到的 SIZELIMIT
和 TIMELIMIT
指令并不完全是通常意义上的默认值。它们是客户端可以请求的绝对上限。通过命令行参数,客户端可以指定更低的时间和大小限制,并且这些较低的数值将会被使用。但如果客户端试图指定更大的大小或时间限制,它们将被忽略,改为使用 SIZELIMIT
和 TIMELIMIT
的值。
但事情并不止于此。SLAPD 服务器还可以定义大小和时间限制(通过 slapd.conf
中的 limits
、sizelimit
和 timelimit
指令)。如果客户端指定的限制超过了服务器的限制,服务器将忽略客户端的限制,并使用自己的限制。我们将在第五章中进一步探讨如何设置服务器限制。
现在我们有了一个有效的ldap.conf
文件,它将减少在命令行中指定这些参数的需求。
本章的最后一件事是使用 OpenLDAP 客户端来测试 SLAPD 服务器。
测试服务器
到此为止,我们已经配置并启动了一个 SLAPD 服务器,并且拥有一个指定了许多默认设置的ldap.conf
文件。现在,我们将查询目录并获取一些信息。
事实上,我们还没有在数据库中放入任何条目。那么我们查询什么呢?SLAPD 确实提供了对某些信息的基于目录的访问,包括当前加载的架构和子架构、配置信息,以及一个名为根 DSE的特殊记录。根 DSE(DSA 特定条目,其中DSA代表目录服务代理—LDAP 服务器的技术术语)是一个特殊条目,提供有关服务器本身的信息。与 LDAP 中的所有其他条目一样,根 DSE 也有一个 DN。与所有其他条目不同,根 DSE 的 DN 是一个空字符串。
为什么使用空字符串作为 DN?答案很简单:任何客户端都可以连接到服务器,并了解服务器支持哪些操作,所有这些都可以在不需要客户端知道服务器上托管的目录结构的情况下完成。客户端只需执行一个空 DN 的搜索。
注
LDAPv3 目录信息模型规范(RFC 4512)规定,任何符合标准的 LDAP 服务器都应该提供一个带空 DN 的根 DSE。
根 DSE 包含有关服务器支持的 LDAP 协议版本、服务器支持的协议扩展以及其他有助于客户端与目录有效交互的有用信息。
我们将使用ldapsearch
命令行客户端搜索此条目。
由于我们设置的 ACL 方式限制性较强,我们将需要身份验证才能查看根 DSE。由于我们只有一个定义的用户,即目录管理员,因此我们将以该用户身份登录,并执行对根 DSE 的搜索:
$ ldapsearch -x -W -D 'cn=Manager,dc=example,dc=com' -b "" -s base
上述所有内容应当在 shell 提示符下放在一行中。为了进行搜索,我们必须指定多个不同的参数:
-
-x
:这告诉服务器使用简单身份验证(而不是更复杂但更安全的 SASL 身份验证)。 -
-W
:这告诉客户端提示我们输入交互式密码。客户端将显示以下提示:Enter LDAP Password:
-
-D 'cn=Manager','dc=example','dc=com'
:这指定了我们希望用于连接目录的 DN。在本例中,我们使用的是目录管理员账户。 -
-b ""
:这设置了搜索的基准 DN。在ldap.conf
文件中,我们将默认基准设置为dc=example,dc=com
。但是为了获取根 DSE(它不在dc=example,dc=com
下),我们需要指定一个空的搜索基准。 -
-s base
:这表示我们只想搜索一个(基础)条目——在-b
参数中指定的 DN 条目(根 DSE 的空 DN)。
当我们运行此搜索时,这是服务器返回的结果:
# extended LDIF
#
# LDAPv3
# base <> with scope baseObject
# filter: (objectclass=*)
# requesting: ALL
#
#
dn:
objectClass: top
objectClass: OpenLDAProotDSE
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
结果顶部是搜索处理方式的总结。高亮部分显示了根 DSE 条目。服务器返回了三个属性:dn
(为空)和两个对象类规范。
高亮部分下方的最后一节显示了一个总结,包括返回了多少条记录(两条:DSE 条目和总结)以及错误代码(0
表示成功)。
该记录很简洁,仅包含少数几个属性。它没有提供关于目录配置或能力的太多信息。但根 DSE 包含的信息远不止这些。我们该如何获取这些信息呢?
为了从根 DSE 获取更广泛的信息,我们需要查询该记录的所有操作属性。
注意
如第一章所述,操作属性是用于内部的属性。RFC 4512 规定,根 DSE 的许多属性应视为操作属性。
这是一个修改过的搜索版本,增加了对任意对象类'(objectclass=*)'
的过滤,并请求所有操作属性(+
)。由于我们在过滤器中使用了星号字符(*
),所以过滤器必须用单引号括起来,以避免 shell 扩展:
$ ldapsearch -x -W -D 'cn=Manager,dc=example,dc=com' -b "" -s base \
'(objectclass=*)' +
该命令的输出大致如下:
Enter LDAP Password:
# extended LDIF
#
# LDAPv3
# base <> with scope baseObject
# filter: (objectclass=*)
# requesting: +
#
#
dn:
structuralObjectClass: OpenLDAProotDSE
configContext: cn=config
namingContexts: dc=example,dc=com
supportedControl: 1.3.6.1.4.1.4203.1.9.1.1
supportedControl: 2.16.840.1.113730.3.4.18
supportedControl: 2.16.840.1.113730.3.4.2
supportedControl: 1.3.6.1.4.1.4203.1.10.1
supportedControl: 1.2.840.113556.1.4.319
supportedControl: 1.2.826.0.1.334810.2.3
supportedControl: 1.2.826.0.1.3344810.2.3
supportedControl: 1.3.6.1.1.13.2
supportedControl: 1.3.6.1.1.13.1
supportedControl: 1.3.6.1.1.12
supportedExtension: 1.3.6.1.4.1.4203.1.11.1
supportedExtension: 1.3.6.1.4.1.4203.1.11.3
supportedFeatures: 1.3.6.1.1.14
supportedFeatures: 1.3.6.1.4.1.4203.1.5.1
supportedFeatures: 1.3.6.1.4.1.4203.1.5.2
supportedFeatures: 1.3.6.1.4.1.4203.1.5.3
supportedFeatures: 1.3.6.1.4.1.4203.1.5.4
supportedFeatures: 1.3.6.1.4.1.4203.1.5.5
supportedLDAPVersion: 3
supportedSASLMechanisms: NTLM
supportedSASLMechanisms: DIGEST-MD5
supportedSASLMechanisms: CRAM-MD5
entryDN:
subschemaSubentry: cn=Subschema
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
上面的结果再次是相同的记录——根 DSE 记录。只是现在我们得到了一个更大的记录,包含了该记录的所有操作属性。
这次从服务器返回的信息包括支持的功能、扩展、控制和 SASL 机制的列表(其中大多数并不特别适合人类阅读)。
虽然记录中的许多项目当前对我们并不有用,但其中一些在实际应用中可能非常有用。例如,supportedLDAPVersion
属性指示该服务器使用的 LDAP 协议版本。namingContexts
属性给出了该服务器上托管的每个目录信息树的基础 DN。supportedSASLMechanisms
列表告诉我们在执行 SASL 绑定时可以进行哪些身份验证例程(我们将在第四章中详细讨论)。
一些 LDAP 客户端程序甚至会查询根 DSE,并利用这些信息来确定服务器支持哪些操作,调整客户端自身的功能以适应服务器提供的服务级别。
然而,这个练习中最重要的是,我们已经验证了成功配置了 SLAPD 服务器和 OpenLDAP 客户端。我们已经连接、认证(使用简单绑定),并从 LDAP 服务器中检索到了一条记录。
总结
本章的重点是安装和配置 OpenLDAP 工具套件。我们在 Ubuntu 系统上安装了 OpenLDAP,然后讲解了如何编写 slapd.conf
文件。创建并测试了 slapd.conf
后,我们转向了 ldap.conf
文件,它包含 OpenLDAP 客户端使用的设置和默认值。最后,我们使用 ldapsearch
从目录中请求根 DSE 记录,验证了客户端和服务器都已正确配置。
在下一章,我们将讲解 OpenLDAP 工具和客户端应用程序。在此过程中,我们会向目录中添加一些记录。
第三章:使用 OpenLDAP
现在我们已经安装、配置并运行了基本的 OpenLDAP 服务器,是时候将注意力转向使用 OpenLDAP。在本章中,我们将了解 OpenLDAP 套件 中各种应用的功能。在此过程中,我们将讨论 LDAP 操作,创建我们的初始目录树,并使用 OpenLDAP 客户端和实用程序与目录服务器进行交互。在此过程中,我们将覆盖以下内容:
-
OpenLDAP 工具的基本功能分为:守护进程、客户端和实用程序
-
基本的目录服务器操作
-
在 LDIF 文件中构建初始目录树
-
将数据加载到目录中
-
处理目录记录
-
搜索目录
-
设置密码并对目录进行身份验证
在此过程中,我们还将看到许多新的 LDAP 术语和概念。
LDAP 套件简要调查
在上一章中,我们看到 OpenLDAP 套件由守护进程、库、客户端和实用程序组成。
在 UNIX 术语中,守护进程是在长时间内运行而无需用户交互的进程。它是在后台运行的进程。服务器是一种守护进程,用于响应来自其他应用程序(客户端)的请求。在 OpenLDAP 套件中有两个守护进程:SLAPD 守护进程(服务器)和 SLURPD 守护进程。在接下来的章节中,我们将详细讨论这两者。
OpenLDAP 还包含许多实用程序。实用程序是帮助管理目录但不使用 LDAP 协议的程序。它们执行诸如维护索引、转储数据库内容和帮助从一个目录迁移记录到另一个目录等任务。
客户端与实用程序相反,是使用 LDAP 协议连接到目录服务器并执行目录操作的程序,例如搜索、添加、修改和删除目录中的记录。
我们将查看所有实用程序和客户端。但在深入讨论之前,我们将查看守护进程以及 LDAP 客户端和服务器之间通信所涉及的一些概念。这将为我们使用 LDAP 实用程序和客户端提供基础知识。
服务器端的 LDAP
OpenLDAP 包含两个守护进程:SLAPD 和 SLURPD。SLAPD 是主服务器,在本书中我们将详细讨论其操作。SLURPD 是用于复制目录的特殊用途守护进程。虽然它仍在使用中,但现在已不推荐使用,推荐使用更健壮的复制机制。在本书中我们将只简要介绍它。
SLAPD
第一个 SLAPD 是独立的 LDAP 守护进程。它是 LDAP 服务器。它监听客户端请求,收到请求后执行请求的操作并返回任何必要的数据。在最常见的情况下,客户端将向服务器发送查询消息。然后 SLAPD 服务器将查找信息并返回结果。让我们考虑一个例子(用口语化的英语):
-
客户端:使用密码 Password 登录用户 Bob
-
服务器:Bob 现在已登录
-
客户端:Bob 想要获取所有电子邮件地址以“m”开头的用户的用户名。
-
服务器:有四个电子邮件地址以“m”开头的用户。用户 ID 分别是:mattb、markd、melaniek、melindaq
-
客户端:注销 Bob
-
服务器:好的
这个示例非常简化(并省略了 LDAP 事务的很多细节),但它应该能给你 SLAPD 的主要工作流程。
SLAPD 程序名恰如其分,叫做slapd
。它位于/usr/sbin
(如果你是从源代码编译的,它位于/usr/local/libexec
)。在上一章中,我们使用/etc/ldap/slapd.conf
配置文件配置了 SLAPD。
SLAPD 服务器处理所有客户端交互,包括身份验证、处理 ACL、执行搜索以及处理数据的更改、添加和删除。它还管理存储 LDAP 内容的数据库。本章中我们讨论的所有客户端都直接与 SLAPD 进行交互。实用程序为 SLAPD 提供维护服务,但它们很少直接与 SLAPD 服务器交互(它们通常对目录使用的文件进行操作)。
让我们更技术性地看一下我们在这里概述的简单 LDAP 交换。我们可以将交换分为两个主要部分:身份验证过程(在 LDAP 术语中称为绑定)和搜索过程。
绑定操作
首先必须发生的事情是客户端必须对服务器进行身份验证。请记住,为了与 LDAP 服务器交互,客户端必须提供两项信息:DN 和密码。
通常,客户端可以通过两种不同的方式认证到服务器:通过简单绑定(Simple Bind)和通过 SASL 绑定(SASL Bind)。也可以编写自定义绑定方法,但这是一项复杂的工作。让我们来看一下客户端如何通过简单绑定方法连接到 LDAP。
通常,为了验证用户,SLAPD 会在目录中查找 DN(及 DN 的userPassword
属性),并验证以下内容:
-
提供的 DN 存在于目录中。
-
在当前条件下,允许连接 DN(例如从源 IP 地址或使用当前实施的安全特性)。
-
提供的密码与 DN 的
userPassword
属性的值匹配。
在我们的示例场景中,用户 Bob 希望绑定到目录。为了让 Bob 按照概述的步骤进行绑定,客户端必须提供 Bob 的完整 DN,类似于cn=Bob,dc=example,dc=net
。但是,并非所有客户端都知道用户的完整 DN。大多数应用程序只需要用户名和密码,而不需要完整的 DN。为了解决这个问题,LDAP 服务器支持匿名用户的概念。
当 LDAP 服务器收到一个空 DN 和空密码字段的绑定请求时,服务器会将用户视为匿名用户。匿名用户可以根据 SLAPD 指定的 ACLs 被授予或拒绝访问目录中的信息。通常,匿名用户的任务是从目录中获取 Bob 的 DN,并请求对 Bob 进行身份验证。
这怎么发生的呢?客户端首先以匿名身份连接到服务器,然后使用类似以下的过滤器在目录中搜索 Bob 的条目:条目 其 CN 为 "Bob" 并且 其 objectclass 为 "organizationalPerson"。
注意
此请求的实际 LDAP 过滤器将如下所示:(&(cn=Bob)(objectClass=organizationalPerson))
假设过滤器足够具体,并且目录中确实有 Bob 的条目,那么服务器将返回一个 DN 给客户端:cn=Bob,dc=example,dc=net
。客户端随后将重新绑定,这次作为cn=Bob,dc=example,dc=net
(并使用 Bob 的密码),而不是作为匿名用户。
为了使匿名身份验证工作,ACLs 需要允许匿名用户进行绑定并尝试执行身份验证。我们在上一章中添加到slapd.conf
的 ACLs 允许匿名用户请求使用userPassword
属性的身份验证服务。
在本章中,我们将使用简单绑定,尽管我们将指定一个完整的 DN,而不是像匿名身份一样先搜索再重新绑定。简单绑定将密码从客户端发送到服务器。没有额外的安全措施(如 SSL 或 TLS 加密),这使得身份验证过程容易受到攻击。SASL(简单认证和安全层)绑定提供了另一种依赖外部安全措施以增强安全性的认证方法。在第四章中,我们将更详细地探讨身份验证过程,特别关注安全性。
搜索操作
在我们的示例场景中,Bob 在成功身份验证后搜索所有以字母m开头的电子邮件地址。让我们更详细地检查这个过程。
为了搜索目录,我们需要知道以下内容:
-
基础 DN:从目录中的哪个位置开始
-
范围:在树形结构中查找的深度
-
属性:我们想要检索的信息
-
过滤器:查找的内容
让我们看看 Bob 想从目录中获取什么。Bob 想要获取他所在的 Example.Com 组织中,所有电子邮件地址以字母m开头的人的列表。从这些信息中,我们可以构建一个搜索条件。
首先,Bob 希望了解 Example.Com 组织中的所有人。在目录中,这就是 Example.Com 条目下的所有内容:dc=example,dc=com
。另外,由于我们知道 Bob 希望获取所有以m开头的电子邮件地址,而不仅仅是下一级;我们知道 Bob 想要搜索dc=example,dc=com
下的整个子树。因此,我们有:
-
基础 DN:
dc=example,dc=com
-
范围: 整个子树
接下来,我们想知道 Bob 想要服务器返回哪些属性。DN 会自动返回。除此之外,Bob 只关心存储邮箱地址的属性。邮箱地址存储在 mail
属性中。我们还可以获取任何数量的属性,例如用户的名字 (cn
) 和电话号码 (telephoneNumber
)。因此,我们有:
-
属性:
mail
,cn
,telephoneNumber
注意
属性描述
mail
属性还有一个别名:rfc822Mailbox
。这两个名称被称为属性描述,因为它们都描述了一个共同的属性。每个属性至少有一个属性描述,但可以有多个描述(例如cn
和commonName
,或dc
和domainComponent
)。当一个属性有多个描述时,使用哪个描述并不重要。所有描述应该返回相同的结果。
最后,我们需要根据 Bob 的标准创建一个过滤器。Bob 想要所有邮箱地址以字母 m 开头的条目。
这是搜索过滤器:
(mail=m*)
这个简单的过滤器由四个部分组成:
-
首先,过滤器被括号括起来。括号用于在过滤器中分组元素。对于任何过滤器,整个过滤器应始终被括号包围。
-
第二,过滤器以属性描述开始:
mail
。 -
第三个是匹配规则。匹配规则有四种:相等 (
=
),近似匹配 (~=
),大于或等于 (>=
),和小于或等于 (<=
)。这些规则的使用(以及是否可以使用)在很大程度上由目录模式决定,我们将在第六章详细讨论。在这种情况下,过滤器执行字符串匹配。 -
最后,我们有了断言值——我们希望结果匹配的字符串或模式。在这种情况下,它由字符
m
和通配符字符 (*
) 组成。这表示字符串必须以m
开头,后面可以跟零个或多个字符。
这种类型的搜索称为子字符串搜索,因为过滤器只提供部分字符串,并请求服务器响应所有匹配该子字符串(根据提供的模式)的条目。
如果 Bob 还需要所有以 n
开头的邮箱地址的用户怎么办?我们可以进行两次独立的搜索,或者我们可以创建一个更复杂的过滤器:
(|(mail=m*)(mail=n*))
这个过滤器由两个子过滤器组成:(mail=m*)
和 (mail=n*)
。第一个只匹配以 m 开头的邮箱地址,而第二个只匹配以 n 开头的地址。这两个子过滤器通过管道符号 (|
) 连接。意味着将执行一个 OR 操作,如果记录匹配 (mail=m*)
或 (mail=n*)
中的任意一个,过滤器就会匹配该记录。
语法可能一开始看起来有点不寻常,因为操作符(OR)位于两个过滤条件之前。
注意
在过滤器中可以使用三种逻辑运算符:AND (&
)、OR (|
) 和 NOT (!
)。
为了让事情更有趣,假设 Bob 想要将列表限制为只有办公室房间号为 300 或以上的人。我们可以简单地在我们的列表中添加一个子过滤条件,就能得到 Bob 想要的结果:
(&(|(mail=m*)(mail=n*))(roomNumber>=300))
为了更好地可视化这一点,我们可以添加一些换行和空格:
(&
(|
(mail = m*)
(mail = n*)
)
(roomNumber >= 300)
)
现在,应该更容易理解这个过滤器是如何被解释的。在最内层,电子邮件地址如果以 m 或 n 开头则视为匹配。现在,只有当它们的房间号大于或等于 300 时,才会返回这些匹配项。它们必须匹配 (mail=m*)
或 (mail=n*)
,并且还必须满足 (roomNumber >= 300)
。
一旦 Bob 执行搜索,指定了基础 DN、范围、属性和过滤条件后,他将收到来自服务器的响应,其中包含类似以下内容的记录列表:
dn:cn=Matt B,dc=example,dc=com
mail: mattb@example.com
cn: Matt B
cn: Matthew B
telephoneNumber: +1 555 555 55555
dn: cn=Melanie K,dc=example,dc=com
mail: melaniek@example.com
cn: Melanie K
elephoneNumber: +1 555 555 4444
搜索返回的是位于 dc=example,dc=com
DN 下子树中匹配我们过滤条件的所有内容。返回的记录只包含 DN 和我们指定的属性:mail
、cn
和 telephoneNumber
。
在我们最复杂的过滤器中,我们使用了 roomNumber
属性。为什么它在上面的记录中没有出现?即使在过滤器中使用了它,属性值也不会在响应中返回,除非我们明确请求它。
在继续之前,有一件事需要提及关于搜索的内容。在搜索过程中,整个请求会被检查与访问控制列表是否匹配。
如果一个 ACL 指定 Bob 无法访问 telephoneNumber
属性,那么搜索将返回相同的 DN,但不包括 telephoneNumber
属性。同样,如果 ACL 阻止 Bob 访问某些人的记录,那么服务器将仅返回 Bob 有权限查看的记录。
服务器不会向 Bob 提供任何信息,表明某些数据因为 ACL 的原因被隐藏。
更多操作:添加、修改和删除
在我们展示 Bob 查找电子邮件地址的过程中,我们只涵盖了绑定和搜索。当然,LDAP 也支持添加、修改和删除。这三项操作都需要用户首先进行绑定,且都受到 ACL 的限制。
添加操作
在添加操作中,一个新记录将被添加到服务器中。在这种情况下,客户端必须提供一个新的(且唯一的)DN 和一组属性/值对。这些属性/值对必须包括该条目所属的对象类列表。例如,如果该条目是一个新用户,并且包含用户 ID 和电子邮件账户,那么修改操作必须包括至少三个对象类的属性/值对。
一个完整的用户记录可能如下所示:
dn: uid=bjensen,dc=exaple,dc=com
cn: Barbara Jensen
mail: bjensen@example.com
uid: bjensen
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
修改操作
修改作用于特定记录,由 DN 指定。一次修改请求中可以对同一记录进行多个更改。
对于特定记录,修改操作可以添加、替换或删除属性。而且它可以在同一请求中组合操作。也就是说,它可以在一个请求中移除一个属性并替换另一个属性。让我们来看这些属性:
-
添加请求需要一个属性名称和一个或多个值。它将这些值添加到该属性现有值的集合中。例如,考虑如下记录:
dn: cn=Matt,dc=example,dc=com cn: Matt telephoneNumber: 1 555 555 1234 telephoneNumber: 1 555 555 4321 objectClass: person
如果我们想通过添加
cn: Matthew
来修改此记录,结果将如下所示:dn: cn=Matt,dc=example,dc=com cn: Matt cn: Matthew telephoneNumber: 1 555 555 1234 telephoneNumber: 1 555 555 4321 objectClass: person
修改操作是以“全有或全无”的方式处理的。当多个修改一起发送时,要么它们全部成功,要么它们全部失败。
-
替换请求也需要一个属性和一个或多个值。但值列表会替换现有的值。例如,如果 Matt 搬家并且电话号码发生变化,则用新的属性
telephoneNumber
:1 555 555 6543
替换后,记录会变成这样:dn: cn=Matt,dc=example,dc=com cn: Matt cn: Matthew telephoneNumber: 1 555 555 6543 objectClass: person
新的号码被添加,旧的号码被移除。
-
删除请求也需要一个属性和一个值列表。它只删除在值列表中指定的属性值。例如,删除
cn: Matthew
将得到以下记录:dn: cn=Matt,dc=example,dc=com cn: Matt telephoneNumber: 1 555 555 6543 objectClass: person
只有匹配的 CN 被删除。如果删除请求仅指定属性(没有值),那么该属性的所有实例将被移除。
删除操作
最后,整个 LDAP 记录可以被删除。像修改一样,删除操作作用于特定记录,即记录的 DN。在删除操作中,整个记录都会从目录中移除——DN 及所有属性。
只有没有子项的记录才能从目录中删除。如果条目有子项,则必须先从目录中删除子项(或将其移至树的另一部分),然后才能删除父条目。
不常用操作
客户端可以调用一些操作,但它们的使用频率通常低于绑定、搜索、添加、修改和删除。我们将简要地看一下三个操作:ModifyDN、Compare和Extended Operation。
ModifyDN 操作
ModifyDN 用于需要更改记录 DN 的情况。通常,DN 不应频繁更改,因为它们旨在作为目录树中的唯一且稳定的定位符。然而,可以想象到一些需要更改 DN 的情况。下图展示了一个(完整的)DN:
一个(完整的)DN 由两部分组成:
-
首先,有一个特定于即时记录的部分,称为相对 DN 或 RDN。例如,在 DN
cn=Matt,dc=example,dc=com
中,RDN 部分是cn=Matt
。 -
其次,有一个部分是指 DN 的父记录。它是特定于此记录的。示例中
dc=example,dc=com
部分指向该记录的父级。
给定 DN,我们知道该记录位于目录树中的哪一层。它位于树根以下的一层——基础 DN(dc=example,dc=com
)。
ModifyDN 操作提供了一种仅更改 RDN 或整个 DN 的方法。更改后者等同于将记录移动到目录树的另一部分。
Compare 操作
Compare 操作接受一个 DN 和一个属性值断言(属性 = 值),并检查该属性断言是否为真或假。例如,如果客户端提供 DN cn=Matt,dc=example,dc=com
和属性值断言 cn=Matthew
,那么如果记录中有一个属性 cn
其值为 Matthew
,服务器将返回true,否则返回false。与在客户端进行比较操作相比,此操作可能更快(同时也更安全)。
注意
在 OpenLDAP ACL 中,auth
权限设置(以及我们将在下一章中查看的 =x
权限设置)允许使用 Compare 操作,但不允许在搜索中返回属性值。read
权限(=xw
)允许 Compare 操作和在搜索结果中返回属性值。
扩展操作
最后,OpenLDAP 实现了 LDAP v.3 扩展操作,使得服务器能够实现自定义操作。
扩展操作的具体语法将取决于扩展的实现。支持的扩展操作在根 DSE 下的 supportedExtension
属性中列出。请查看第二章末的根 DSE。在该记录中有两个扩展操作:
-
1.3.6.1.4.1.4203.1.11.1:此修改密码扩展在 RFC 3062 中定义(
www.rfc-editor.org/rfc/rfc3062.txt
)。该扩展提供了一种在目录中更新密码的操作。 -
1.3.6.1.4.1.4203.1.11.3:此Who Am I? 扩展在 RFC 4532 中定义(
www.rfc-editor.org/rfc/rfc4532.txt
)。该扩展使得当前活动的 DN 可以从服务器了解自己。
在本章稍后,我们将查看使用修改密码和 Who Am I? 扩展的工具。
SLAPD 概述
在本节中,我们已经查看了 SLAPD 服务器提供给客户端的一些操作。我们查看了最常见的操作(绑定、搜索、修改、添加和删除)。我们还查看了一些较少为人知的操作,如 modifyDN、Compare 和扩展操作。
到目前为止,你应该已经对 SLAPD 服务器为客户端提供的服务有了较好的了解。客户端可以绑定(或认证)到 SLAPD 服务器并执行强大的目录搜索。通过 SLAPD,目录树中的信息可以得到维护。
这些概念将是本章及本书其余部分的核心内容。
接下来,我们将讨论 SLURPD 守护进程,但我们不会深入细节。
SLURPD
SLAPD 和 SLURPD 是 OpenLDAP 套件中包含的两个守护进程。上面,我们已经讨论了 SLAPD 服务器。现在我们将转向第二个守护进程。
SLURPD,独立的 LDAP 更新复制守护进程,比 SLAPD 使用的频率低,并且正逐渐走向过时。SLURPD 提供了一种保持多个 LDAP 目录副本同步的方式(参见第一章的讨论)。基本上,它通过跟踪对 主 SLAPD 目录服务器的更改(添加、删除、修改)来工作。当主目录发生更改时,SLURPD 会将更新发送到所有下属的 从属 服务器。
SLURPD
程序位于 /usr/sbin
(如果是从源代码编译的,则位于 /usr/local/libexec
)。在使用 SLURPD 的配置中,slurpd
通常会在 slapd
启动后立即启动。SLURPD 没有自己的配置文件,它会在 slapd.conf
文件中查找配置信息。
在第七章,我们将讨论可能替代 SLURPD 的技术:最近版本(OpenLDAP 2.2 及更高版本)的 SLAPD 内置的 LDAP 同步复制功能。
创建目录数据
在上一节中,我们讨论了两个 LDAP 守护进程,SLAPD 和 SLURPD。但是,尽管我们已经有一个正在运行的目录,但我们的目录中并没有任何条目(除了由 SLAPD 创建的条目,如架构记录和根 DSE)。
在本节中,我们将创建一个文件来存储我们的 LDAP 数据,并设计一些要放入该文件的目录条目。下一节我们将把数据加载到目录中。
LDIF 文件格式
在本书的整个过程中,我们展示了以纯文本形式呈现的 LDAP 记录示例,每行包含一个属性描述,后面跟着冒号和一个值。记录的第一行是 DN,通常记录的最后几行是对象类属性:
dn: uid=bjensen,dc=exaple,dc=com
cn: Barbara Jensen
mail: bjensen@example.com
uid: bjensen
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
这种格式是以文本文件表示 LDAP 目录条目的标准方式。它是写成 LDAP 数据交换格式 (LDIF) 版本 1 的记录示例。
注意
LDIF 文件格式是作为密歇根大学 LDAP 服务器项目的一部分开发的。2000 年,LDIF 版本 1 在 RFC 2849 中进行了标准化。该标准可以在线查看,链接为 www.rfc-editor.org/rfc/rfc2849.txt
。
LDIF 标准定义了一种文件格式,不仅用于表示目录的内容,还用于表示某些 LDAP 操作,如添加、修改和删除。在 ldapmodify
客户端的章节中,我们将使用 LDIF 来指定目录服务器中记录的更改,但现在我们关心的是创建一个表示目录内容的文件。
注意
LDIF 不是唯一的目录文件格式。还有一种基于 XML 的目录标记语言,叫做 DSML(目录服务标记语言)。虽然有一个标准化的 DSML 版本 1,但该项目似乎已经失去动力,以至于官方网站 dsml.org 已经消失。然而,某个开源 DSML 工具网站仍然托管着旧版 dsml.org 网站的镜像:www.dsmltools.org/dsml.org/
。
OpenLDAP 套件不直接支持 DSML。
LDIF 文件的结构
一个 LDIF 文件由一系列记录组成,每条记录代表目录中的一个条目。每个条目必须有一个 DN(因为任何 LDAP 条目都需要 DN),然后是一个或多个属性或更改记录(add
、modify
、delete
、modrdn
、moddn
)。现在我们将仅讨论属性,并将更改记录的讨论推迟到我们讨论 ldapmodify
时再进行。
记录通过空行分隔,每条记录必须以 DN 开头:
# First Document: "On Liberty" by J.S. Mill
dn: documentIdentifier=001,dc=example,dc=com
documentIdentifier: 001
documentTitle: On Liberty
documentAuthor: cn=John Stuart Mill,dc=example,dc=com
objectClass: document
objectClass: top
# Second Document: "Treatise on Human Nature" by David Hume
dn: documentIdentifier=002,dc=example,dc=com
documentIdentifier: 002
documentTitle: Treatise on Human Nature
documentAuthor: cn=David Hume,dc=example,dc=com
objectClass: document
objectClass: top
以井号或数字符号(#
)开头的行被视为注释,并会被忽略。请注意,井号必须是该行的第一个字符,前面不能有任何空白字符。
尽管记录通常以 objectClass
属性结束,这是因为这样更易于阅读,但并没有强制要求这样做。LDIF 记录中属性的顺序是无关紧要的。
对象类(在模式定义中定义)表示记录所代表的对象类型或类型。在前面的示例中,两个记录都是 documents
。对象类定义决定了哪些属性是必需的,哪些属性是允许的。当编写 LDIF 文件时,你需要知道哪些字段是必需的。任何条目的 DN 当然是必需的,objectclass
属性也是必需的。在表示模式层次结构根的 top
对象类中,除了 objectclass
外没有其他必需字段。document
对象类定义要求有 documentIdentifier
,并允许包含十一种其他字段,包括 documentTitle
(其值为字符串)和 documentAuthor
(其值为 DN,指向目录中的另一个记录)。
提示
文档对象类
LDAP 目录可以建模多种不同类型的对象。前面例子中使用的document
对象类代表目录中的文档(如书籍、论文和手册)。document
对象类及相关的documentSeries
对象类的模式包含在cosine.schema
中,并在 RFC 4524 的第 3.2 节中定义(ftp://ftp.rfc-editor.org/in-notes/rfc4524.txt)。模式将在第六章中详细讨论。
让我们来看一下document
和documentSeries
对象类的属性列表:
任何在 DN 中使用但不属于目录基础 DN 的属性必须出现在记录中。例如,假设基础 DN 为dc=example,dc=com
。如果条目的 DN 为cn=Matt,dc=example,dc=com
,则该条目必须包含一个值为Matt
的cn
属性。在前面的例子中,由于documentIdentifier
在 DN 中被使用,因此记录中必须有一个匹配的documentIdentifier
属性。
注
实际上,document
对象类需要documentIdentifier
属性,因此,即使该属性在 DN 中未被使用,任何文档记录仍然需要一个documentIdentifier
。
同样,一个 DN 为cn=Matt,ou=Users,dc=example,dc=com
的条目必须包含属性cn:Matt
和ou:Users
。
在 LDIF 中表示属性值
不是所有的属性值都是简单的短 ASCII 字符串。LDIF 提供了对更复杂数据类型进行编码的功能。
有时属性值无法在一行内显示。如果属性值过长,无法在一行中显示,可以将其续行,前提是续行的第一字符是空白字符:
dn: documentIdentifier=003,dc=example,dc=com
documentIdentifier: 003
documentTitle: An essay on the nature and conduct of the passions
and affections with illustrations on the moral sense.
documentAuthor: cn=Francis Hutchison,dc=example,dc=com
objectClass: document
objectClass: top
根据 RFC,LDIF 文件只能包含 ASCII 字符集中的字符。然而,非 ASCII 字符可以通过 Base-64 编码值在 LDIF 中表示。Base-64 编码的属性值略有不同,属性描述后面跟着两个冒号,而不是一个:
dn: documentIdentifier=004,dc=example,dc=com
documentIdentifier: 004
documentTitle:: bW9uYWRvbG9neQ==
documentAuthor: cn=G. W. Leibniz,dc=example,dc=com
objectClass: document
objectClass: top
在以下情况下,你应该考虑使用 Base-64 编码:
-
当属性值包含二进制数据(例如 JPEG 照片)时。
-
当字符集不是 ASCII 时。通常,目录数据应该以 UTF-8 格式存储,但这意味着为了保持与 LDIF 标准的兼容性,值应该进行 Base-64 编码。
-
当值中有换行符或其他不可打印字符时。(请注意,为了接受这样的值,模式必须允许这些字符,否则目录服务器即使这些字符已被编码,也不会允许它们被上传。)
-
当值以空白字符开始或结束(并且希望保留这些空白字符),或以冒号(
:
)或小于号(<
)开始时。
即使是 DN,也可以进行 Base-64 编码,只要 DN 是 Base-64 编码的,你就可以在 DN 中使用 UTF-8 字符。
有几种 UNIX/Linux 工具可以用来进行 base-64 编码。最流行的是 uuencode
程序,它包含在 sharutils
包中。然而,这个程序在 Ubuntu 中并不是默认安装的。你可以通过命令行使用 apt-get
快速安装它:
$ sudo apt-get install sharutils
一旦安装了 sharutils
,你可以使用 uuencode
编码一个值:
$ echo -n " test" | uuencode -m name
begin-base64 644 name
IHRlc3Q=
====
在这个例子中,我们将字符串 " test"
(注意前导空格)转换为 base-64 编码的字符串。这是通过命令行中的几个命令来完成的(此例中使用 Bash shell)。
uuencode
命令通常用于将文件编码为电子邮件附件,因此我们需要做一点工作,让它按我们想要的方式运行。首先,我们使用 echo
命令回显我们想要编码的字符串。echo
程序默认会在回显的字符串末尾添加一个换行符。我们使用 -n
标志来防止它添加换行符。
字符串 " test"
被回显到标准输出(/dev/stdout
),然后通过管道符(|
)传递到 uuencode
命令中。-m
标志指示 uuencode
使用 base-64 编码,name
字符串被 uuencode
用来为附件生成名称。当使用 uuencode
生成电子邮件附件时,这很有用,但对我们来说没有实际作用。因为我们不将这个文件附加到任何东西上,所以放什么内容都无所谓;foo
也能同样起作用。
uuencode
程序然后会打印出三行输出:
begin-base64 644 name
IHRlc3Q=
====
只有代码中的第二行(高亮的那一行),即实际的 base-64 编码值,对我们来说才重要。我们可以复制 IHRlc3Q=
并将其粘贴到我们的 LDIF 文件中。
注意
另一个流行的 base-64 编码工具是 mimencode
,它由 metamail
包提供。Perl 和 Python 脚本语言也有 base-64 编码工具。
在某些情况下,将一个较长的属性值(例如整个 base-64 编码的图像文件,甚至是大量文本)插入到 LDIF 文件中会使文件过大,以至于无法高效地使用文本编辑器进行编辑。即使是一个小的图像文件,经过 base-64 编码后,也会有数百个字符。与其直接将 base-64 编码的字符串插入到文件中,不如使用特殊的文件引用,文件的内容将在导入 LDIF 文件时被检索并加载到目录中。
dn: documentIdentifier=005,dc=example,dc=com
documentIdentifier: 005
documentTitle: Essays in Pragmatism
documentAuthor: cn=William James,dc=example,dc=com
description:< file:///home/mbutcher/long-description.txt
objectClass: document
objectClass: top
高亮的代码行展示了如何插入外部文件的引用。
这个例子中有两个重要的特点需要注意:
-
左尖括号(
<
)字符用于表示应该导入文件。这个字符在 UNIX/Linux shell 中也用于相同的目的。 -
文件的路径遵循标准的
file://
URL 方案来表示文件路径。
注意
请注意,在文件方案中,通常需要在开头使用三个斜杠(file:///path/to/file
)来指示没有主机字段。RFC 3986 (ftp://ftp.rfc-editor.org/in-notes/rfc3986.txt) 定义了 URI 和 URL 的一般结构。file://
是一种特定的 URL 方案,大致定义见 RFC 1738 的第 3.1 节 (ftp://ftp.rfc-editor.org/in-notes/rfc1738.txt)。
在拥有多种语言的属性值时,你可以将语言信息与属性描述一起存储:
dn: documentIdentifier=006,dc=example,dc=com
documentIdentifier: 006
documentTitle;lang-en: On Generation and Corruption
documentTitle;lang-la: De Generatione et Corruptione
documentAuthor: cn=Aristotle,dc=example,dc=com
objectClass: document
objectClass: top
语言信息存储在目录中,客户端将能够使用它来显示与地区相符的语言。
这涵盖了 LDIF 文件格式的基本内容,接下来我们将创建一个 LDIF 文件并加载到目录中。
Example.Com 的 LDIF
现在我们准备在 LDIF 文件中建模我们的目录树。首先需要做的是决定目录结构。我们将代表一个组织在我们的目录树中。当然,你可以建模的树种类几乎是无限的,但我们将坚持使用那些在 LDAP 目录中最常见的类型。
定义组织目录树根节点有两种流行的方法:
-
第一个方法是创建一个根条目,指示组织的官方名称和组织的地理位置(通常只是国家)。以下是几个示例:
o=Arius Ltd.,c=UK o=Acme GmBH,c=DE o=Example.Com,c=US
在这三个示例中,
o
代表组织名称,c
是两字符的国家代码。 -
第二种流行的模型是使用组织的域名。例如,如果 Airius 公司注册了
airus.co.uk
域名,那么根 DN 将由三个 域组件(dc
)属性组成:dc=airius,dc=co,dc=uk
同样,其他两个记录也可以使用各自的域组件重新编写:
dc=acme,dc=de dc=example,dc=com
使用组织/国家配置有其优点。拥有多个域的公司可能会发现这种形式更具吸引力。但第二种形式,依赖于域组件的方式,已变得更加普遍。在大多数情况下,我更喜欢域组件形式,因为它与互联网上信息的引用方式更为相关。
当然,关于 DN 结构的具体要求并没有硬性规定,你可能会发现其他基本 DN 结构更具吸引力。
定义基本 DN 记录
既然我们已经选择了基本 DN 风格,接下来让我们开始为 Example.Com 构建一个目录。LDIF 文件是按顺序逐条读取的。因此,基本 DN 必须排在最前面,因为所有其他记录都会在它们的 DN 中引用它。同样,在构建目录信息树时,我们需要确保父条目始终出现在子条目之前。
我们的基本 DN 看起来是这样的:
dn: dc=example,dc=com
description: Example.Com, your trusted non-existent corporation.
dc: example
o: Example.Com
objectClass: top
objectClass: dcObject
objectClass: organization
让我们从底部开始,按顺序反向分析这个示例。该记录有三个对象类:top
、dcObject
和organization
。正如我们已经看到的,top
对象类是对象类层次结构的根节点,目录中的所有记录都属于top
对象类。
下面是显示对象类的图示:
dcObject
对象类仅描述域组件——域名的组成部分。例如,域www.packtpub.com有三个域组件:www
、packtpub
和com
。由于我们在 DN 中使用了域组件,所以需要dcObject
类,它要求有一个属性:dc
。
您可能注意到,虽然在 DN 中有两个dc
属性(dc=example
和dc=com
),但在记录中只列出了一个(dc:example
)。虽然乍一看似乎不太直观,但其实原因非常简单。该记录并未描述整个域,而只是单个域组件(example
)。就像 DNS 记录一样,父组件(com
)指向层次结构中另一个地方的实体。
因此,使用dcObject
对象类的每条记录只能描述一个域组件,因此记录中只能有一个dc
属性(尽管 DN 可能有多个dc
属性,指明该记录在域层次结构中的位置)。
那么,dc=com
的记录是否应该在我们的目录中?由于该目录的根节点(如slapd.conf
文件中指定的)是dc=example,dc=com
,我们不应在数据库中找到dc=com
的记录,因为它不在dc=example,dc=com
树部分下(而是dc=com
位于该部分之上,或者说是该部分的上级)。
注意
处理目录树外的记录请求
如果我们收到针对dc=com
的搜索请求,该怎么办?或者,如果我们收到dc=otherExample,dc=com
的请求呢?这些是我们目录中不应有的记录。通过在slapd.conf
文件中使用转介指令,您可以将这类请求引导到另一个可能在该问题上更具权威性的服务器。该指令的语法是referral <ldap URL>
,例如:referral ldap://root.openldap.org
。
现在我们已经指定了记录描述的域组件。但我们仍然需要更多的信息。我们不能只依赖top
和dcObject
对象类的记录,因为这样有两个原因——一个是实际的,另一个是技术性的。
实际上,仅凭这些简略信息,这条记录并不会特别有用,因为它并没有真正告诉我们目录树的基础(除了它有一个域名之外)。
从技术上讲,top
和 dcObject
这两个对象类都不足以构成完整的记录。原因在于,这两个对象类都不是结构化对象类,(top
是抽象的,dcObject
是辅助的),而且目录中的每一条记录都必须有一个被视为该记录结构化对象类的对象类。有关详细的解释,以及一些有关记录结构的有用信息,请参阅第六章。
如何使我们的基本记录更加有用(并满足记录必须具有结构化对象类的要求)呢?organization
对象类描述了一个组织,顾名思义。它要求一个字段,o
(或其同义词 organizationName
),用于指定组织的(法定)名称。此外,organization
对象类还允许使用二十一个可选字段,提供有关组织的更详细信息,例如 postalAddress
、telephoneNumber
和 location
。在前面的示例中,我们使用了 description
字段,它也是 organization
对象类允许的二十一种属性之一。
这是我们目录的基本入口。它描述了目录信息树根部的记录。接下来,我们希望为目录添加一些结构。
使用组织单位(OU)结构化目录
LDAP 目录服务器模型的一个优势是它能够将数据组织成层级结构。在这一节中,我们将使用组织单位(OUs)在我们的 dc=example,dc=com
根目录下创建多个子树。
我们的 Example.Com 目录主要用于保存用户和账户信息。因此,我们将希望使用组织单位(OU)来创建子树。
注意
例如,如果我们正在创建一个 document
记录的目录(正如我们在 LDIF 文件格式 章节中所做的那样),我们可能不会使用 OUs,而是使用 documentSeries
记录。
OpenLDAP 并不提供默认的 OU 子树结构,因此你需要创建自己的结构。这可以通过多种方式实现,但在这里我们将看到两种主要理论,说明 OUs 应该如何结构化。
理论 1:目录作为组织结构图
第一个理论是,目录应当结构化为表示你正在建模的组织的组织结构图。例如,如果该组织有三个主要部门——会计、人力资源(HR)和信息技术(IT)——那么你应该有三个 OUs。以下是相应的图示:
在给定的截图中,每个 OU 代表组织结构图中的一个单位。在会计部门工作的员工将会在目录子树 ou=Accounting,dc=example,dc=com
中拥有他们的用户账户,而在 IT 部门工作的员工则会在 ou=IT,dc=example,dc=com
中拥有账户。
这种方法有一些明显的优点。了解组织如何运作将有助于你在目录中查找信息。相反,目录将作为一个工具,帮助你理解组织的结构。组织中人与人之间或记录与记录之间的关系将更容易被识别。例如,只需看一下uid=Marvin,ou=Accounting,dc=example,dc=com
的记录(或仅查看 DN),你就会知道 Marvin 与 Barbara 在同一个部门工作。
然而,在以这种方式构建目录之前,需要考虑以下几点:
-
首先,虽然组织结构会发生变化——有时变化过于频繁——但在目录中重新定位 DN 并不是一项简单的任务(在某些情况下需要删除树中某个部分的记录,并在树的另一个部分创建类似的记录)。
如果会计部经理 Barbara 调任人力资源部,她的 DN 必须更改(以反映新的 OU)。一些(较旧的)后台不允许更改 DN,因此 Barbara 的会计部记录需要被删除,然后在 HR OU 中为她创建新的记录。此外,存储用户 DN 的应用程序也必须重新配置。类似地,一些员工可能会在两个部门之间分配时间。如何处理这种情况?
-
第二个考虑因素是与 LDAP 目录的技术使用有关的,而这一点并不那么明显。如果用户记录分散在目录树的各个地方,那么应用程序需要足够智能,能够在整个树中搜索用户记录。
这个问题通常通过预认证搜索技术来解决,例如以匿名用户或特定认证用户身份绑定,搜索目录中用于认证的账户,然后以正确的账户身份绑定(如果找到的话)。但并非所有客户端(更不用说所有目录)都允许预认证搜索。并且预认证搜索可能会给服务器带来更大的负担,而其他技术可能对服务器更为友好。
-
第三个考虑因素是你希望在目录中存储哪些其他类型的信息。如果你主要将目录作为建模组织结构图的工具,那么这种特定的目录结构方法将非常适合你。你可以在目录中跟踪员工、资产(车队车辆、计算机等)和其他资源,并定位它们在组织中的位置。
但是,如果目录的主要目的是创建一个 IT 服务用户的目录,那么这种结构就不太理想了,需要应用程序做更多的工作来定位用户(在某些情况下,还要求用户了解更多关于他们的 LDAP 账户信息)。
理论 2:目录作为 IT 服务
第二种理论是,目录应当结构化为系统(网络、服务器、用户应用)访问记录的方式。在这种情况下,LDAP 目录的结构应当针对这些 IT 服务的使用进行优化。虽然组织结构图方法是按记录与组织的关系进行分组,但这种方法则是按功能单元分组记录,目录中的位置主要由应用程序和服务所需的任务来决定。
一种常见的目录结构方式是将其拆分为一个用于用户的单元、一个用于组的单元以及一个系统级记录的单元,后者是应用程序所需的,但用户无需访问。我们来看一个示例:
在这种情况下,所有用户账户都位于目录的特定子树下:ou=Users,dc=example,dc=com
。应用程序只需在目录的一部分中进行搜索即可找到用户账户,当组织发生变化时,目录的结构不必也发生变化。
注意
使用组织单位(OU)来划分目录信息树并没有什么神奇之处。你可以使用其他记录类型和其他属性(如cn
——常用名称)将目录划分为多个分支。尽管如此,使用 OU 是传统做法,但在目录信息树并未反映组织结构图的情况下,可能并不是最合适的选择。
这种方法也有一些缺点。首先,目录结构从设计上看并未提供组织结构的任何明确线索。当然,组织信息,比如部门 ID,可以存储在单独的记录中,便于通过这种方式进行检索。
更重要的是,如果目录支持大量用户,那么ou=Users
分支将会有大量记录。这不一定是性能问题,但它可能使浏览目录(与搜索目录相对)变得冗长。
在某些情况下,通过在用户的分支下添加额外的子树来缓解这个问题。有时,这通过创建一个混合配置来实现,其中ou=Users
下有表示组织部门的子树,例如ou=Accounting,ou=Users,dc=example,dc=com
。有时也会使用其他分类系统,例如按字母顺序的方案来处理这种情况:uid=matt,ou=m-p,ou=Users,dc=example,dc=com
。
但是对于小型和中型的目录,用户的分支通常没有任何额外的子树,这简化了与其他应用程序的集成过程。
LDAP 还具有描述目录中记录组的对象类。通常,将这些与用户账户存储在一起没有意义,因此它们可以被移动到一个单独的分支。
最后,系统分支用于存储诸如系统账户、邮件服务器、Web 服务器等记录,其他各种应用程序通常需要(或者在拥有自己 LDAP 账户时表现更好)。但如果可以避免的话,应该避免将它们与用户账户放在一起。
我概述了两种不同的目录信息树结构方式——一种反映组织结构,另一种便于 IT 服务。但这些只是两种目录结构方式,你可能会发现其他结构更适合你的需求。然而,对于我们的目的,接下来的 LDIF 文件构建将采用 IT 服务结构。
在 LDIF 中表示组织单位
现在我们准备将选定的组织单位写入 LDIF 文件。我们将创建三个组织单位——Users、Groups 和 System,如下所示:
# Subtree for users
dn: ou=Users,dc=example,dc=com
ou: Users
description: Example.Com Users
objectClass: organizationalUnit
# Subtree for groups
dn: ou=Groups,dc=example,dc=com
ou: Groups
description: Example.Com Groups
objectClass: organizationalUnit
# Subtree for system accounts
dn: ou=System,dc=example,dc=com
ou: System
description: Special accounts used by software applications.
objectClass: organizationalUnit
这三个组织单位的结构相同。
每个组织单位必须有organizationalUnit
对象类。该对象类有一个必填属性:ou
。下面是显示组织单位的图示:
注意
请注意,objectClass: top
在这些记录中已被省略,接下来的所有记录也同样省略。在所有记录中,默认假设它们是top
对象类的实例,因此不需要显式地包含objectClass: top
属性。
description
属性是可选的,除此之外还有二十多个其他(可选)属性可以添加——其中大多数提供组织单位的联系信息,例如telephoneNumber
、postOfficeBox
和postalAddress
。
在组织单位(OUs)已经设置好后,我们准备在目录树中添加第三层。在开始创建单独的记录之前,让我们先了解一下下一层的结构。下面是包含一个组、一个系统账户和一对用户的目录树结构:
这是我们将在本节余下部分中创建的目录信息树。接下来,我们将继续构建 LDIF 文件,首先添加用户记录,然后是系统记录,最后是组记录。
添加用户记录
我们将保留Users
组织单位用于描述组织中人员的记录。在这些账户中,我们希望存储有关用户的信息——例如名字、姓氏、职称和部门。由于目录还将作为应用程序信息的中央资源,我们还希望存储用户 ID、电子邮件地址和密码。
一个基本的用户记录如下所示:
# Barbara Jensen:
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: Barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword: secret
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
芭芭拉的用户记录属于三个对象类:person
、organizationalPerson
和 inetOrgPerson
。这三个都是结构化对象类,其中 inetOrgPerson
是 organizationalPerson
类的子类,而 organizationalPerson
又是 person
对象类的子类。芭芭拉记录中的属性是这三个对象类中的必需属性和允许属性的混合。下图展示了芭芭拉记录中的属性:
由于 inetOrgPerson
继承自 organizationalPerson
,因此拥有 inetOrgPerson
对象类的记录必须同时拥有 organizationalPerson
对象类。而 organizationalPerson
又继承自 person
对象类,所以 person
也是必需的。
这意味着所有 inetOrgPerson
记录都将需要 cn
(用户的全名)和 sn
(用户的姓氏)属性,因为所有的 inetOrgPerson
记录也是 person
记录。这也意味着该记录可以有三者对象类中定义的四十九个可选属性的任何组合。
注意
由于 uid
和 ou
属性在 DN 中被使用,因此它们实际上也是必需的属性。此外,OpenLDAP 会要求记录中必须有 uid
和 ou
属性,并且这两个属性的值与 DN 中的值匹配——换句话说,由于 DN 中的 ou
是 Users
,记录中的 ou
属性必须有 Users
这个值。这种行为是由 LDAP 标准决定的。
提示
不同的对象类,不同的架构
虽然 person
和 organizationalPerson
定义在核心架构文件(core.schema
)中,inetOrgPerson
是在它自己的架构文件(inetOrgPerson.schema
)中定义的,并且在 RFC 2798 中进行了标准化(rfc-editor.org/rfc/rfc2798.txt
)。之所以这样,主要是历史原因:person
和 organizationalPerson
在 inetOrgPerson
之前就已被定义(并且是由不同方定义的)。
使用更多可用属性的 inetOrgPerson
记录可能看起来是这样的:
# Matt Butcher
dn: uid=matt,ou=Users,dc=example,dc=com
ou: Users
# Name info:
uid: Matt
cn: Matt Butcher
sn: Butcher
givenName: Matt
givenName: Matthew
displayName: Matt Butcher
# Work Info:
title: Systems Integrator
description: Systems Integration and IT for Example.Com
employeeType: Employee
departmentNumber: 001
employeeNumber: 001-08-98
mail: mbutcher@example.com
mail: matt@example.com
roomNumber: 301
telephoneNumber: +1 555 555 4321
mobile: +1 555 555 6789
st: Illinois
l: Chicago
street: 1234 Cicero Ave.
# Home Info:
homePhone: +1 555 555 9876
homePostalAddress: 1234 home street $ Chicago, IL $ 60699-1234
# Misc:
userPassword: secret
preferredLanguage: en-us,en-gb
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
在这个例子中,我们仍然使用相同的三个对象类,但选择了更多的可选属性。在芭芭拉和马特的记录中,有一件事可能会显得特别突出,那就是有许多属性仅仅用于指定个人的姓名;cn
、sn
、givenName
和 displayName
都是与个人姓名相关的字段。为什么会有这么多呢?提供多种姓名字段有两个好处:
-
这减少了应用程序在解析姓名时需要进行的猜测工作。姓名可能会有歧义——例如,约翰·斯图尔特·密尔的姓是密尔,而玛丽·斯图尔特·马斯特森的姓是斯图尔特·马斯特森。明确指定这些内容可以减少歧义。
-
不同的属性允许指定附加信息。多个
cn
和givenName
值可以指定一个人姓名的不同形式,而displayName
(只能有一个值,并且在同一记录中不能多次使用)确保应用程序始终一致地显示相同的姓名。
提示
常见名称
cn
字段被目录中的许多不同对象类使用,其中许多并不描述人员。因此,cn
并不总是包含一个人的全名。组、设备和文档等事物可能会使用 cn
(或 commonName
)属性。
在之前的示例中,包含个人密码的 userPassword
字段是明文的。当该文件加载到目录时,密码的值会进行 base-64 编码,但它并没有被 加密。将明文密码存储在目录中是极不安全的(而且 base-64 编码并不能提高密码的安全性)。在本节后面,我们将查看 ldappasswd
工具,它在将密码存储到目录之前会对密码进行加密。生产环境中的目录应该始终将 userPassword
值以加密形式存储。
注意
你可能会注意到,在 homePostalAddress
字段中,美元符号($
)被用来代替通常期望看到的换行符。OpenLDAP 不会自动将这些符号转换为换行符。但是,使用美元符号是一种较老的方式,表示换行符而不使用 base-64 编码。通常,它仅用于与邮政地址相关的字段——并且由实现应用程序来正确解释这些美元符号。
这两个示例都使用 inetOrgPerson
对象类作为其主要结构化对象类。这是因为这些记录描述的是一个人,并且使用了 uid
属性(并将其作为 DN 的一部分)。此外,inetOrgPerson
提供了许多对现代信息基础设施有用的属性;如 jpegPhoto
、preferredLanguage
和 displayName
(等等)主要是供现代计算机代理使用,而非人类使用。由于它已被标准化并广泛部署(从 Sun 到 Microsoft 的 LDAP 服务器都在使用它),它成为了描述组织内人员的首选对象类。
到目前为止,我们已经创建了一个基本的 DN 条目、一些组织单位和几个用户。现在我们将添加一个记录,描述一个系统账户。
添加系统记录
我们树中的一些条目——我们需要的条目——并不描述用户,因此不属于用户组织单位(OU)。相反,我们将这些特殊记录放入 System
OU。同样,我们所描述的实体并不是人,因此使用 person
、organizationalPerson
和 inetOrgPerson
对象类并不合适。
在本节中,我们将创建一个新的账户记录,帮助用户进行登录。账户的功能将在第四章详细描述,但这个账户需要能够对目录服务器进行身份验证并执行操作。然而,这个账户并非为特定的人而创建,因此它不会拥有个人数据(如姓氏或名字)。
我们的新系统账户 authenticate
看起来是这样的:
# Special Account for Authentication:
dn: uid=authenticate,ou=System,dc=example,dc=com
uid: authenticate
ou: System
description: Special account for authenticating users
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
该记录有两个对象类:account
和 simpleSecurityObject
。第一个对象类,account
,是结构化对象类。一个 account
对象,定义在 Cosine 模式(cosine.schema
)中,描述了用于访问计算机或网络的账户。我们来看一下这两个对象类:
我们的账户,DN 为 uid=authenticate,ou=System,dc=example,dc=com
,使用了 account
对象类所要求的 uid
属性,以及账户中的 ou
和 description
字段。但账户对象类没有用于存储密码的字段。因此,我们需要在记录中添加辅助对象类 simpleSecurityObject
,它有一个属性:必需的 userPassword
属性。
注意
辅助对象类可以与任何其他结构化或辅助对象类结合使用。虽然在一个记录中使用多个结构化对象类要求这些对象类必须是相关的(例如 organizationalPerson
是 person
的子类),但辅助对象类不需要与它们所使用的对象类相关联。在这种情况下,simpleSecurityObject
与 account
没有直接关系。请参阅第六章获取更详细的解释。
通过添加 simpleSecurityObject
辅助对象类,我们现在使得我们的 account
记录能够拥有密码。同样,在我们的示例中,我们以明文的形式指定了密码(userPassword: secret
)。在目录中存储未加密的密码是不安全的。有关加密 LDAP 密码的信息,请参阅本章稍后部分关于 ldappasswd
的章节。
现在我们在三个组织单位中的两个下创建了一些记录:用户(Users)和系统(System)。接下来,我们将在“组(Groups)”组织单位下添加一个组。
添加组记录
我们将添加到 LDIF 文件的最后一条记录是描述 DN 组的记录。组提供了一种灵活的方法,通过任何所需的标准收集相似的 DN。组中的 DN 不必在结构上相似——它们可以具有完全不同的属性和对象类,甚至可以描述完全不同的内容(例如文档和人员)。因此,目录管理员和目录应用程序可以决定哪些 DN 会被分组到特定的组中。
在我们的例子中,我们将创建一个组来代表我们的目录管理员,该组的所有 DNs 都是用户的 DNs(在 Users
组织单位中,并且具有 inetOrgPerson
结构化对象类)。
# LDAP Admin Group:
dn: cn=LDAP Admins,ou=Groups,dc=example,dc=com
cn: LDAP Admins
ou: Groups
description: Users who are LDAP administrators
uniqueMember: uid=barbara,dc=example,dc=com
uniqueMember: uid=matt,dc=example,dc=com
objectClass: groupOfUniqueNames
我们的组的 DN 是 cn=LDAP Admins,ou=Groups,dc=example,dc=com
。请注意,我们使用 cn
属性,而不是 uid
,来标识该组。因为 groupOfUniqueNames
对象类不允许使用 uid
属性(而 cn
是必需的)。
提示
通常,你应该使用 groupOfNames
对象类,而不是 groupOfUniqueNames
,因为 groupOfNames
是 OpenLDAP 中默认的分组对象类。我们在这里使用 groupOfUniqueNames
来展示后续章节中 LDAP 组管理的一些特性。
groupOfUniqueNames
类是 LDAP 版本 3 核心架构(core.schema
)中定义的三种分组对象类之一。其他两种是 groupOfNames
和 organizationalRole
。
这些已经在下图中显示:
这三种对象类都是为了收集 DNs 而设计的。每个类都有一个属性,用于指定该组成员的 DN。在 groupOfNames
中,该属性简单地被称为 member
。groupOfUniqueNames
类,功能上与 groupOfNames
相同,使用 uniqueMember
作为其成员属性。organizationalRole
分组类旨在表示组织中负责执行特定角色的组,它使用 roleOccupant
属性作为成员属性。
在所有三种分组对象类中,成员属性(member
、uniqueMember
或 roleOccupant
)可以多次指定,正如我们在 LDAP Admins
组的 LDIF 片段中所看到的那样。
提示
我应该使用哪种类型的组?
如何决定是使用 groupOfNames
、groupOfUniqueNames
还是 organizationalRole
?默认情况下,最好使用 groupOfNames
,因为它被 OpenLDAP 视为默认的分组对象类。organizationalRole
对象类用于定义一个人在组织中的角色。groupOfUniqueNames
对象类的用途与 groupOfNames
不同,但在 OpenLDAP 中,它们的功能是相同的。
groupOfUniqueNames
和 groupOfNames
对象类都允许使用 owner
属性,并且可以多次使用(例如,建模某个组有两个所有者的情况)。owner
属性保存的是被认为是该组所有者的记录的 DN。
注意
OpenLDAP 中有一种第四种(但实验性的)通用分组方法,叫做 dynlist/dyngroup。它使用特定的对象类,动态的 groupOfURLs 分组类,并结合特殊的目录覆盖。这种分组方法预计将在 OpenLDAP 2.4 中成熟。
在我们的示例群组 groupOfUniqueNames
中,我们指定了两个 uniqueMember
属性:
uniqueMember: uid=barbara,dc=example,dc=com
uniqueMember: uid=matt,dc=example,dc=com
这两个 DN 都是群组的成员。请注意,SLAPD 不会主动检查这些 DN 是否存在,也不会在 DN 从目录中删除时自动将其从群组中移除。
注意
完整性检查
SLAPD 可以通过第五章中讨论的 RefInt(参照完整性)覆盖层来配置执行记录的完整性检查。这个覆盖层可以确保群组成员的 DN 与目录信息树中的条目保持同步。
因此,目录管理员和目录应用程序在处理群组时必须小心,进行额外的验证和清理。当一个 DN 从目录中删除时,应该进行全目录范围的搜索,查找那些使用 DN 值的属性,以确保像 member
和 roleOccupant
(以及 seeAlso
)等属性不会指向新删除的 DN。
完整的 LDIF 文件
最后,我们完成了 LDIF 文件的构建。我们将它保存在一个名为 basics.ldif
的文件中,因为它包含了我们目录的基本元素。下面是它的样子:
# This is the root of the directory tree
dn: dc=example,dc=com
description: Example.Com, your trusted non-existent corporation.
dc: example
o: Example.Com
objectClass: top
objectClass: dcObject
objectClass: organization
# Subtree for users
dn: ou=Users,dc=example,dc=com
ou: Users
description: Example.Com Users
objectClass: organizationalUnit
# Subtree for groups
dn: ou=Groups,dc=example,dc=com
ou: Groups
description: Example.Com Groups
objectClass: organizationalUnit
# Subtree for system accounts
dn: ou=System,dc=example,dc=com
ou: System
description: Special accounts used by software applications.
objectClass: organizationalUnit
##
## USERS
##
# Matt Butcher
dn: uid=matt,ou=Users,dc=example,dc=com
ou: Users
# Name info:
uid: matt
cn: Matt Butcher
sn: Butcher
givenName: Matt
givenName: Matthew
displayName: Matt Butcher
# Work Info:
title: Systems Integrator
description: Systems Integration and IT for Example.Com
employeeType: Employee
departmentNumber: 001
employeeNumber: 001-08-98
mail: mbutcher@example.com
mail: matt@example.com
roomNumber: 301
telephoneNumber: +1 555 555 4321
mobile: +1 555 555 6789
st: Illinois
l: Chicago
street: 1234 Cicero Ave.
# Home Info:
homePhone: +1 555 555 9876
homePostalAddress: 1234 home street $ Chicago, IL $ 60699-1234
# Misc:
userPassword: secret
preferredLanguage: en-us,en-gb
# Object Classes:
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
# Barbara Jensen:
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword: secret
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
# LDAP Admin Group:
dn: cn=LDAP Admins,ou=Groups,dc=example,dc=com
cn: LDAP Admins
ou: Groups
description: Users who are LDAP administrators
uniqueMember: uid=barbara,dc=example,dc=com
uniqueMember: uid=matt,dc=example,dc=com
objectClass: groupOfUniqueNames
# Special Account for Authentication:
dn: uid=authenticate,ou=System,dc=example,dc=com
uid: authenticate
ou: System
description: Special account for authenticating users
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
在下一节中,我们将介绍 OpenLDAP 工具,并将使用这些工具将我们的 LDIF 文件加载到目录中。
使用工具准备目录
到目前为止,在本章中我们已经查看了服务器操作,并创建了一个表示初始目录信息树的 LDIF 文件。在本章的剩余部分,我们将探讨两组工具。在这一部分,我们将介绍 OpenLDAP 工具。在下一部分,我们将介绍 OpenLDAP 客户端。
与 OpenLDAP 客户端不同,这些工具不使用 LDAP 协议连接到服务器并执行目录操作。相反,它们在更低的层次上工作,直接与 OpenLDAP 目录和数据文件进行交互。OpenLDAP 套件包括八个用于执行管理任务的工具。我们将在创建、加载和验证目录数据的过程中,逐步了解这些工具。
本节的目的是解释这些工具的基本使用方法。每个工具都有一些命令行标志,可以用来进一步修改工具的行为。我们将介绍一些常用的标志,但如果需要详细信息,应该查阅优秀的 OpenLDAP 手册页。
在 OpenLDAP 的新版本中,这些工具实际上并不存在作为独立的程序。相反,它们都被编译进了 slapd
程序,并且创建了符号链接,将工具名称指向 slapd
程序。使用 ls
命令,我们可以查看这些工具,了解它们是如何实现的:
$ ls -og /usr/local/sbin
这就是我们得到的结果:
total 0
lrwxrwxrwx 1 16 2006-08-17 11:37 slapacl -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slapadd -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slapauth -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slapcat -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slapdn -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slapindex -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slappasswd -> ../libexec/slapd
lrwxrwxrwx 1 16 2006-08-17 11:37 slaptest -> ../libexec/slapd
这八个工具都是slapd
程序的符号链接。当执行slapd
时,它会检查执行时使用的程序名称,然后按照该程序的行为运行。例如,当slapd
作为slapadd
调用时,它会作为一个将数据加载到目录中的程序运行。如果作为slaptest
调用,它会作为一个用于验证配置文件格式和指令的程序运行。
在我们继续描述这些工具时,我们将把它们视为独立的程序来讲解,因为它们就是如此被处理的。
由于我们在上一部分创建了一个 LDIF 文件,因此本节将首先介绍将 LDIF 文件加载到目录后端的工具。
slapadd
slapadd
程序用于将格式化为 LDIF 文件的目录数据直接加载到 OpenLDAP 中。它是在操作系统的 Shell 中执行的(例如命令提示符或 Shell 脚本)。
slapadd
程序不会使用 LDAP 协议连接到正在运行的服务器。相反,它直接与 OpenLDAP 后端进行交互。因此,当你运行slapadd
时,必须首先关闭目录服务器。否则,可能会出现slapd
服务器进程和slapadd
进程之间的冲突,因为它们都试图独占管理相同的数据库。
何时应该使用 slapadd?
有许多工具可以将记录加载到目录中,包括 OpenLDAP 客户端ldapadd
(它通过 LDAP 协议连接到服务器并执行一个或多个添加操作)。那么,如何在特定情况下确定使用哪个程序呢?
其实,slapadd
旨在加载大量的目录数据,通常用于创建新目录或从备份恢复目录。由于它要求目录下线,因此这个工具通常不适合用于执行常规更新。ldapadd
程序(稍后在客户端部分中讨论)更适合这种操作。
slapadd
做什么?
slapadd
工具读取slapd.conf
文件(及任何包含的文件),加载适当的后端数据库,然后读取 LDIF 数据(通常来自一个文件)。在读取数据时,它验证所有记录是否构造正确(例如,DN 是否位于服务器管理的树中,记录是否使用正确的对象类属性,是否包含所有必需字段,记录格式是否正确等),然后将记录加载到适当的后端中。
由于slapadd
不会通过 LDAP 协议连接,因此不需要对目录进行身份验证。不过,它确实需要对目录数据库文件的写访问权限。因此,slapadd
通常由运行目录服务的用户(通常是ldap
或slapd
)或根账户从 Shell 中运行。
加载 LDIF 文件
在本章的前面部分,我们创建了一个包含我们目录树中少量记录的 LDIF 文件。现在,我们将把这个 LDIF 文件加载到目录中。这将包括四个步骤:
-
停止
slapd
服务器 -
使用
slapadd
测试 LDIF 文件 -
使用
slapadd
加载目录 -
重启
slapd
服务器
停止服务器
我们在第二章的结尾已经讲解了启动和停止服务器的过程。简而言之,我们可以使用 invoke-rc.d
命令停止通过 Ubuntu 软件包安装的版本:
$ sudo invoke-rc.d slapd stop
使用从源代码编译的版本(请参阅附录 A),可以通过查找 slapd
进程 ID 并终止该进程(或使用 killall
程序)来实现:
$ sudo kill `pgrep slapd`
接下来,我们需要确保我们在上一部分创建的 LDIF 文件格式正确。
在测试模式下运行 ldapadd
在实际加载之前运行测试模式可以大大减少加载新的 LDIF 文件所需的时间,因为它能帮助您在记录写入目录之前捕捉到 LDIF 错误。通常,slapadd
会一条一条地添加记录。所以如果文件中有三条记录,第一条记录会在读取第二条或第三条记录之前被添加到目录中。如果文件中的后续记录有错误,那么目录将会部分加载,您将不得不创造性地修改 LDIF 文件,或者销毁数据库并重新开始。
使用测试模式,我们可以在开始将记录加载到目录之前确保 LDIF 文件没有任何错误。这应该可以消除因为错误记录导致 LDIF 文件只部分导入的情况。
我们可以使用 slapadd
程序在将数据加载到目录之前进行此操作:
$ sudo slapadd -v -u -c -f /etc/ldap/slapd.conf -l /tmp/basics.ldif
该命令使用五个标志:
-
-v
标志:此标志将程序设置为“详细”模式,在该模式下,程序会打印出额外的信息,说明正在发生的事情(如果过程失败,还会显示导致失败的原因)。通常,建议在加载未经测试的 LDIF 文件时以详细模式运行slapadd
。 -
-u
标志:这告诉slapadd
以测试(或 干运行)模式运行。当启用此模式时,slapadd
会评估文件,就像它要将文件加载到目录中一样,但实际上不会将任何记录放入目录。 -
-c
标志:这告诉slapadd
即使遇到错误的记录也继续处理文件。使用此标志,我们可以先运行一遍文件并获取所有格式不正确的记录列表。 -
-f
标志:此标志接受服务器配置文件路径作为参数,指定应该使用哪个配置文件。在大多数情况下,您可以省略此标志,slapadd
会默认查看配置文件的位置(通常是/etc/ldap/slapd.conf
)。 -
-l
标志:此标志指向我们要加载的 LDIF 文件。在本例中,我们正在加载位于系统/tmp
目录中的basics.ldif
文件。
如果 LDIF 文件中存在错误,slapadd
会打印出一些有用的信息。例如,如果我们尝试加载一个明显损坏的文件,它看起来像这样:
# This is the root of the directory tree
dn: dc=example,dc=com
description: Example.Com, your trusted non-existent corporation.
dc: example
o: Example.Com
objectClass: top
objectClass: dcObject
objectClass: organization
Broken
# Subtree for users
dn: ou=Users,dc=example,dc=com
ou: Users
ferble: glarp
description: Example.Com Users
objectClass: organizationalUnit
在这个文件中,损坏的行已被高亮显示。当我们运行slapadd
时,我们将得到一个错误:
added: "dc=example,dc=com"
str2entry: entry -1 has no dn
slapadd: could not parse entry (line=11)
<= str2entry: str2ad(ferble): attribute type undefined
slapadd: could not parse entry (line=18)
在这里,slapadd
成功测试了我们的第一个记录dc=example,dc=com
,没有问题,但在第 11 行遇到了一行没有以 DN 开头的内容,跳过了那个记录。在第 18 行,它遇到了另一个错误:ferble
属性在记录中的任何对象类中都没有定义。
当对本章早些时候创建的 LDIF 文件运行成功时,输出如下:
$ sudo slapadd -v -u -c -f /etc/ldap/slapd.conf -l basics.ldif
added: "dc=example,dc=com"
added: "ou=Users,dc=example,dc=com"
added: "ou=Groups,dc=example,dc=com"
added: "ou=System,dc=example,dc=com"
added: "uid=matt,ou=Users,dc=example,dc=com"
added: "uid=barbara,ou=Users,dc=example,dc=com"
added: "cn=LDAP Admins,ou=Groups,dc=example,dc=com"
added: "uid=authenticate,ou=System,dc=example,dc=com"
没有错误。我们已经准备好进行第三步:将记录导入目录。
使用 slapadd 导入记录
要将记录实际导入目录,我们使用slapadd
命令,并使用上一节中使用的标志的子集。我们省略了-u
标志(用于测试)和-c
标志(以便遇到错误记录时不继续)。
提示
使用 -q 标志
为了更快地加载目录,你可以添加-q
标志,关闭slapadd
在处理数据时执行的一些耗时检查。但是在使用此标志之前,请确保先测试 LDIF 数据(使用刚才描述的方法)。否则,你可能会遇到无法使用的目录。
现在,命令看起来是这样的:
$ sudo slapadd -v -f /etc/ldap/slapd.conf -l basics.ldif
这是我们得到的输出:
added: "dc=example,dc=com" (00000001)
added: "ou=Users,dc=example,dc=com" (00000002)
added: "ou=Groups,dc=example,dc=com" (00000003)
added: "ou=System,dc=example,dc=com" (00000004)
added: "uid=matt,ou=Users,dc=example,dc=com" (00000005)
added: "uid=barbara,ou=Users,dc=example,dc=com" (00000006)
added: "cn=LDAP Admins,ou=Groups,dc=example,dc=com" (00000007)
added: "uid=authenticate,ou=System,dc=example,dc=com" (00000008)
注意,这次的输出只是稍有不同;每行的末尾都有一个括号括起来的 ID 号。这个 ID 号是记录的entryCSN
属性的一部分,内部用于监控记录。
注意
与许多 LDAP 服务器一样,OpenLDAP 会向记录附加特殊的操作属性。在这些属性中,OpenLDAP 存储有关记录的目录中心信息。我们稍后将在讨论slapcat
工具时详细说明这些内容。
我们刚刚用本章前面创建的八个记录填充了我们的目录。现在我们准备好启动目录了。
重启目录
在第二章中,我们讨论了启动和停止目录。这可以通过初始化脚本完成:
$ sudo invoke-rc.d slapd start
或者,如果你按照附录 A 安装,slapd
可以直接运行:
$ sudo /usr/local/libexec/slapd
如果出了问题……
有时,在执行slapadd
的过程中,程序会遇到错误——无论是 LDIF 文件本身的问题,还是外部因素的影响——并且会在导入目录的过程中中止。在这种情况下,你可能需要重新开始。但是,仅仅重新运行slapadd
操作会出现像这样的错误(具体的错误信息可能根据你使用的后端不同而有所不同):
$ sudo slapadd -v -f /usr/local/etc/openldap/slapd.conf -l
basics.ldif
=> hdb_tool_entry_put: id2entry_add failed: DB_KEYEXIST: Key/data
pair already exists (-30996)
=> hdb_tool_entry_put: txn_aborted! DB_KEYEXIST: Key/data pair
already exists (-30996)
slapadd: could not add entry dn="dc=example,dc=com" (line=9):
txn_aborted! DB_KEYEXIST: Key/data pair already exists (-30996)
这里发生了什么?
发生的情况是,basics.ldif
文件中的一些条目已经导入到目录中,但可能并不是全部。你可以尝试各种方法来解决这个问题。你可以尝试将 LDIF 文件精简,只保留那些尚未添加的记录。你也可以尝试以继续模式(使用 -c
标志)运行 slapadd
程序,并希望所有剩余的记录都能正确添加。
但你可能会发现,处理这些情况的最佳方法是简单地销毁并重建目录。虽然这听起来是一种极端的措施,但与其他方法相比,它有一个明显的优势:它避免了由于 slapadd
命令失败而导致的不一致记录的问题。因此,它通常是从失败的目录导入中恢复的最佳方法。
提示
索引文件中的错误也可能是由 slapadd
失败引起的。如果你决定在 slapadd
失败后不销毁并重建目录,确保在向目录加载新记录后运行 slapindex
工具(本章稍后会介绍)。
销毁并重建目录文件
在大多数可以通过 slapadd
加载的 OpenLDAP 后端中,后端将数据存储在文件系统中的某个位置或关系型数据库中。在 slapadd
失败后,你可能会发现恢复的最佳方法是销毁底层后端中的所有数据,然后重新开始。
目前,我们正在使用 hdb
后端(见第二章)。这里使用的方法同样适用于其他 BerkeleyDB 后端(在 bdb
模式下的 bdb
和 ldbm
),并且可以轻松适配(已弃用的)ldbm
和 gdbm
后端。
对于其他类型的后端,例如使用关系型数据库(如 PostgreSQL)或自定义后端(如 back-perl
),你需要查看这些后端的文档,以确定清除目录记录的最佳方法。
对于 hdb
和 bdb
后端,目录数据文件存储在文件系统中。在 Ubuntu 中,这些文件位于 /var/lib/ldap
。如果你按照 附录 A 的指示操作,数据库文件位于 /usr/local/var/openldap-data/
。
以下是 /var/lib/ldap
目录的内容:
alock __db.002 __db.005 dn2id.bdb objectClass.bdb
cn.bdb __db.003 DB_CONFIG id2entry.bdb
__db.001 __db.004 DB_CONFIG.example log.0000000001
在这里,你可以看到所有的目录数据库文件(以 __db.
开头)、目录索引文件(以 .bdb
结尾)和 BerkeleyDB 事务日志(以 log.
开头)。该目录中还有一些其他文件,例如 alock
和 DB_CONFIG
,我们不需要删除它们。要删除文件,我们使用 rm
命令,并配合一个表达式列表,仅匹配我们想要删除的文件:
$ sudo rm __db.* *.bdb log.*
这样就只删除了我们不需要的文件。现在目录中应该只包含几个文件:
alock DB_CONFIG DB_CONFIG.example
这就是销毁数据库所需要做的一切。现在,我们可以通过使用 slapadd
命令加载(如果需要,已修正的)LDIF 文件来重新创建目录:
$ sudo slapadd -v -l basics.ldif
并且返回以下消息:
added: "dc=example,dc=com" (00000001)
added: "ou=Users,dc=example,dc=com" (00000002)
added: "ou=Groups,dc=example,dc=com" (00000003)
added: "ou=System,dc=example,dc=com" (00000004)
added: "uid=matt,ou=Users,dc=example,dc=com" (00000005)
added: "uid=barbara,ou=Users,dc=example,dc=com" (00000006)
added: "cn=LDAP Admins,ou=Groups,dc=example,dc=com" (00000007)
added: "uid=authenticate,ou=System,dc=example,dc=com" (00000008)
这就是销毁并重建目录的全部内容。
slapindex
接下来我们将查看的工具是slapindex
。这个工具管理使用索引的 OpenLDAP 后端的索引文件(例如hdb
、bdb
和已废弃的ldbm
)。
OpenLDAP 维护一组索引文件,以加速搜索记录。这些文件存储在主目录数据库之外,随着记录的添加、修改和删除,slapd
服务器会相应地修改索引文件。
但在某些情况下,slapd
服务器可能没有足够的信息来了解需要对索引文件进行的更改,在这些情况下,索引需要手动重建。
注意
像slapadd
一样,在服务器运行时不应运行slapindex
。在运行slapindex
之前,应停止slapd
。
有三种常见情况需要使用slapindex
命令:
-
当使用工具(通常是
slapadd
)向现有数据库添加记录时。 -
当
slapd.conf
中的索引指令发生更改或添加了新的索引时(参见第二章和第五章的性能 调优部分)。 -
在其他(罕见的)情况下,外部条件或失败的
slapadd
命令可能会导致目录数据库和目录索引不同步。这种同步错误的主要症状是使用ldapsearch
进行搜索时,无法返回已知存在于目录中的记录。
在这三种情况下,应该运行slapindex
:
$ sudo slapindex -q -f /etc/ldap/slapd.conf
这将重建slapd.conf
中定义的第一个数据库的所有索引(我们只定义了一个数据库)。
-q
标志指示 slapindex 执行一些额外的检查操作,这将大大加速重新索引的过程。跳过这些检查通常在使用slapindex
时是安全的,但在使用slapadd
时应谨慎操作。
-f
标志用于指定配置文件的路径,它指定了slapd
配置文件。如果省略此标志(如我们所做的那样),slapindex
将查找默认位置中的slapd.conf
文件。
如果您想监控slapindex
的进度,可以使用-v
标志启用详细输出。
slapcat
slapcat
程序将目录的所有内容转储到 LDIF 文件中。它是创建目录备份的方便工具,也可以用于检查目录中的数据。
当然,也有类似的客户端应用程序 ldapsearch
,它也可以导出整个目录内容。那么你如何判断何时使用每个工具呢?由于 ldapsearch
使用 LDAP 协议来联系服务器、绑定并执行 LDAP 搜索操作,它的开销更大。另一方面,slapcat
直接与后端进行交互。ldapsearch
受时间和大小限制,这些限制在客户端配置文件 ldap.conf
和服务器的配置文件 slapd.conf
中都有设置(请参见第二章)。ldapsearch
命令还受到 ACL 的限制,而 slapcat
不受 ACL 的影响。
显然,对于像备份目录这样的操作,应该使用 slapcat
而不是 ldapsearch
。
从 OpenLDAP 2.3 版本开始,如果你使用的是 hdb
或 bdb
后端,你可以在 slapd
运行时安全地运行 slapcat
;不需要关闭目录服务器就能备份副本。
注意
OpenLDAP 中的 slapcat
手册页错误地指出,在目录服务器运行时运行 slapcat
是不安全的。这只是早期版本的 OpenLDAP(2.2 及更早版本)的遗留问题,在那些版本中,slapcat
不能在 slapd
运行时执行。需要注意的是,在 slapd
运行时,仍然不安全对 ldbm
后端执行 slapcat
。
当我们在本章之前介绍 slapadd
时,我们使用该工具将 basics.ldif
中的记录加载到目录中。现在我们可以使用 slapcat
查看这些记录。
$ sudo slapcat -l basics-out.ldif
-l
标志需要一个路径作为参数,指示输出应该写入到哪个文件。在本例中,它将写入文件 basics-out.ldif
。如果省略 -l
,则 LDIF 数据将被发送到标准输出,通常直接打印到屏幕上。
与其他工具一样,-f
标志可用于指定 SLAPD 配置文件的路径。-a
标志需要一个 LDAP 过滤器,可以用来指定记录必须匹配的模式,才能被导出到输出中。你可以使用此标志仅导出一个子树。例如,我们可以使用以下命令仅导出 Users
组织单位中的记录:
$ sudo slapcat -a "(entryDN:dnSubtreeMatch:=ou=Users,
dc=example,dc=com)"
这将仅返回以下三个 DN 的完整记录:
-
ou=Users,dc=example,dc=com
-
uid=matt,ou=Users,dc=example,dc=com
-
uid=barbara,ou=Users,dc=example,dc=com
操作属性
让我们仔细查看仅关于基础 DN 记录的输出:
$ sudo slapcat -a "(dc=example)"
dn: dc=example,dc=com
description: Example.Com, your trusted non-existent corporation.
dc: example
o: Example.Com
objectClass: top
objectClass: dcObject
objectClass: organization
structuralObjectClass: organization
entryUUID: b1a00a7c-c587-102a-9eb2-412127118751
creatorsName: cn=Manager,dc=example,dc=com
modifiersName: cn=Manager,dc=example,dc=com
createTimestamp: 20060821173908Z
modifyTimestamp: 20060821173908Z
entryCSN: 20060821173908Z#000000#00#000000
高亮显示的属性应该看起来不熟悉,因为它们在我们创建的原始 LDIF 文件中并不存在。这些是 OpenLDAP 自动维护的内部 操作属性。
不同的操作属性在 OpenLDAP 中扮演不同的角色,这些属性对目录管理员和支持 LDAP 的应用程序可能很有用。
例如,creatorsName
、modifiersName
、createTimestamp
和 modifyTimestamp
字段通常非常有用。OpenLDAP 会自动保留以下记录级别的信息:
-
每条记录的创建时间和创建人。
-
每条记录的最后修改时间和修改人。
entryUUID
属性为记录提供了 全局唯一标识符(UUID),该标识符作为比 DN 更稳定的标识符(DN 可能会变化),并且根据 RFC 4122 中的规范(rfc-editor.org/rfc/rfc4122.txt
)应当是“在所有 UUID 空间内,跨越空间和时间的唯一标识符”。请参阅 entryUUID
的 RFC rfc-editor.org/rfc/rfc4530.txt
。
entryCSN
(变更序列号) 属性由 SyncRepl 复制提供程序使用,用于确定需要在 LDAP 服务器之间同步的记录。我们将在第七章详细讨论这一点。
最后,添加了 structuralObjectClass
属性。该属性指定了哪个对象类应作为结构对象类。回想一下,当我们为 Matt 和 Barbara 创建记录时,每个记录有三个对象类:person
、organizationalPerson
和 inetOrgPerson
。这三个都是结构对象类,并且它们之间有关联(inetOrgPerson
是 organizationalPerson
的子类,organizationalPerson
又是 person
的子类)。但每条记录只能有一个结构对象类。如上所述,树结构中最深的一个成为结构对象类,其他的则被视为抽象对象类。如果我们使用 slapcat
导出 Barbara 的记录,就可以看到这一点:
$ sudo slapcat -a '(uid=barbara)'
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword:: e1BMQUlOfXNlY3JldA==
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
structuralObjectClass: inetOrgPerson
entryUUID: b1ae9916-c587-102a-9eb7-412127118751
creatorsName: cn=Manager,dc=example,dc=com
modifiersName: cn=Manager,dc=example,dc=com
createTimestamp: 20060821173908Z
modifyTimestamp: 20060821173908Z
entryCSN: 20060821173908Z#000005#00#000000
注意,structuralObjectClass
属性的值为 inetOrgPerson
。
到目前为止,我们已经研究了 slapcat
工具,以及 slapindex
和 slapadd
工具。这三者是最常用的工具。但在某些情况下,还有一些其他工具也可能非常有用。接下来,我们将看一下 slapacl
。
slapacl
编写 ACL(访问控制列表)可能会令人沮丧且难以测试。为了简化在 slapd.conf
文件中测试 ACL 效果的过程,OpenLDAP 套件包括一个用于直接测试 ACL 的工具。我们将在第四章测试 ACL 时更多地使用这个工具,但在这里我们将先介绍该工具。
在第二章中,我们向 slapd.conf
添加了以下 ACL:
access to attrs=userPassword
by anonymous auth
by self write
by * none
这个 ACL 指定了对于目录中的任何给定记录,如果该记录具有 userPassword
,则应对访问该属性的请求应用以下规则:
-
anonymous
用户应该能够使用userPassword
进行身份验证。 -
它应该允许 DN 修改(和读取)自己的密码。
-
它应该拒绝所有其他 DN 访问该记录的
userPassword
。
这意味着 uid=matt,ou=Users,dc=example,dc=com
不应能够为 uid=barbara,ou=Users,dc=example,dc=com
写入新的 userPassword
值。我们可以使用 slapacl
工具来测试这一点:
$ sudo slapacl -v -D "uid=matt,ou=Users,dc=example,dc=com" -b
"uid=barbara,ou=Users,dc=example,dc=com" "userPassword/write"
这个命令一开始看起来可能很复杂,但实际上非常简单。我们按顺序来看一下这些参数:
-
-v
标志打开详细输出。 -
-D
标志用于告诉slapacl
哪个 DN 正在尝试访问目录。在这种情况下,我们设置了:-D "uid=matt,ou=Users,dc=example,dc=com"
。也就是说,slapacl
正在测试 Matt 的 DN 是否可以访问。 -
-b
标志指示我们希望给定的 DN 尝试访问哪个记录。在这种情况下,它是 Barbara 的 DN,因为我们想测试 Matt 是否能写 Barbara 的密码:-b "uid=barbara,ou=Users,dc=example,dc=com"
。 -
最后,最后一个参数指定了我们希望访问的属性,以及我们请求的权限类型。在这种情况下,我们想要访问
userPassword
属性,并且我们想查看 Matt 是否对其具有write
权限("userPassword/write"
)。
因此,最终我们是在测试 Matt 的 DN 是否能够为 Barbara 的记录写入新的userPassword
。以下是slapacl
命令的结果:
authcDN: "uid=matt,ou=users,dc=example,dc=com"
write access to userPassword: DENIED
这是我们预期的结果。由于这个 ACL,Matt 不能写入 Barbara 的userPassword
属性。
slapauth
slapauth
工具用于测试 SASL 身份验证到目录。当一个应用程序尝试使用 SASL 进行绑定时,它并不指定一个完整的 DN(例如uid=matt,ou=Users,dc=example,dc=com
),而是传递一个用户 ID(u: matt
)以及其他一些信息,如领域标识符和认证机制。
我们将在第四章中讲解 SASL 身份验证。如果你还没有 SASL 的经验,建议先阅读下文,并在阅读完第四章后再回到这一节。
OpenLDAP 可以利用这些信息,并使用正则表达式猜测用户所属的 DN。但理解正则表达式的具体形式可能比较困难。slapauth
工具在测试特定的 SASL 请求在 OpenLDAP 接收到时的表现时非常有用。
例如,我们可以将以下 SASL 配置指令添加到我们的slapd.conf
文件中:
authz-policy from
authz-regexp
"^uid=([^,]+).*,cn=auth$"
"uid=$1,ou=Users,dc=example,dc=com"
authz-regexp
中的正则表达式应该将 SASL authzID 格式转换为 LDAP DN:
$ sudo slapauth -U "matt" -X "u: matt"
ID: <matt>
authcDN: <uid=matt,ou=users,dc=example,dc=com>
authzDN: <uid=matt,ou=users,dc=example,dc=com>
authorization OK
第一个参数-U matt
发送一个包含 SASL authcID 为matt
的测试请求。-X "u: matt"
参数则发送一个包含 authzID 为u: matt
的测试请求。根据authz-regexp
中的正则表达式,它们应该输出一个正确格式的 DN。
我们将在第四章中更多地使用slapauth
,当我们设置 SASL 身份验证时。
slapdn
slapdn
工具用于测试给定的 DN 是否对这个目录服务器有效。具体来说,它将 DN 与已定义的模式进行匹配,确保 DN 是有效的。
这里有一些slapdn
实际应用的示例:
$ sudo slapdn 'cn=Foo,dc=example,dc=com'
DN: <cn=Foo,dc=example,dc=com> check succeeded
normalized: <cn=foo,dc=example,dc=com>
pretty: <cn=Foo,dc=example,dc=com>
$ sudo slapdn 'ou=New Unit,dc=example,dc=com'
DN: <ou=New Unit,dc=example,dc=com> check succeeded
normalized: <ou=new unit,dc=example,dc=com>
pretty: <ou=New Unit,dc=example,dc=com>
在这两个示例中,DN 是有效的。slapdn
测试了这些 DN,然后打印出标准化版本(所有小写,去除多余的空格)和原始格式版本。
这里有一个失败的示例:
$ sudo slapdn 'fakeAttr=test,dc=example,dc=com'
DN: <fakeAttr=test,dc=example,dc=com> check failed 21
(Invalid syntax)
在这种情况下,没有找到包含 fakeAttr
属性的模式。这里是另一个失败的例子:
$ sudo slapdn 'documentSeries=Series 18,dc=example,dc=com'
DN: <documentSeries=Series 18,dc=example,dc=com> check failed 21
(Invalid syntax)
虽然 documentSeries
在模式中被定义为对象类,但它是一个对象类,而不是属性,且对象类名称不能用于构造 DN。
slapdn
程序的实用性仅限于那些你需要在不查看 slapd.conf
文件来确定加载了哪些模式的情况下,测试 DN 与目录的匹配的罕见情况(或者,交替使用 ldapsearch
程序搜索模式)。
slappasswd
slappasswd
实用工具是一个根据 OpenLDAP 支持的模式加密密码的工具,例如 RFC 2307 中描述的模式(rfc-editor.org/rfc/rfc2307.txt
)。
在 OpenLDAP 中存储和使用密码
当我们创建基础的 LDIF 文件时,我们使用了 userPassword
属性来存储密码。例如,我们的身份验证账户记录如下所示:
# Special Account for Authentication:
dn: uid=authenticate,ou=System,dc=example,dc=com
uid: authenticate
ou: System
description: Special account for authenticating users
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
userPassword
字段中存储的是明文密码。当该值被加载到目录中时,userPassword
会使用 base64 编码,看起来像这样:
userPassword:: c2VjcmV0
但是,这并不是加密的——只是以一种容易反转的方式进行了编码。虽然它可能防止目录管理员无意中看到用户的密码,但 base64 编码并不会防止攻击者从中猜出密码。
注意
使用 Python 脚本语言,你可以轻松地通过内置的 base64.b64encode()
和 base64.b64decode()
函数进行字符串的编码和解码。
但是,OpenLDAP 并不要求你以未加密的文本形式存储密码。事实上,最好不要这么做。OpenLDAP 支持多种单向哈希算法,可以用来以无法解密的方式存储密码。
slappasswd
程序提供了创建密码哈希值的工具。然后,可以在 LDIF 文件的 userPassword
字段中使用该哈希值。
OpenLDAP 支持五种不同的密码哈希方案:Crypt(CRYPT
)、消息摘要算法 5(MD5
)、加盐 MD5(SMD5
)、安全哈希算法 SHA-1 版本(SHA
)和加盐 SHA(SSHA
)。默认情况下,OpenLDAP 使用最安全的哈希算法:SSHA
。
密码存储在 userPassword
字段中,格式遵循 RFC 2307 第 5.3 节的规定(rfc-editor.org/rfc/rfc2307.txt
)。加密后的密码如下所示:
{SSHA}71xEB2E59cuoPEQLErY44bYMHwCCgbtR
在密码的开头,花括号中的部分({}
)表示使用了哪种密码方案。在这种情况下,它是默认的 SSHA 算法。字段的其余部分是密码的摘要哈希值。
尽管哈希后的密码无法被解密,但当用户尝试绑定到服务器时,OpenLDAP 会使用与userPassword
值(以及相同的盐值)相同的算法对用户提供的密码进行加密。如果两个哈希后的密码匹配,则 OpenLDAP 会让用户登录。如果不匹配,OpenLDAP 会返回一个错误消息,提示身份验证失败。
使用 slappasswd 生成密码
基于对密码如何使用和存储的基本理解,我们现在可以看看slappasswd
程序。这个程序可以用来加密密码并将其格式化以便插入到 LDIF 文件中。该命令可以在没有任何参数的情况下调用:
$ slappasswd
New password:
Re-enter new password:
{SSHA}71xEB2E59cuoPEQLErY44bYMHwCCgbtR
在这种情况下,由于命令行上没有指定任何参数,slappasswd
会提示输入密码,然后再提示验证密码。接着,它会输出密码的加密值。我们可以在 LDIF 记录中使用这个值:
dn: uid=nicholas,ou=Users,dc=example,dc=com
cn: Nicholas Malebranche
sn: Malebranche
uid: nicholas
ou: Users
userPassword: {SSHA}71xEB2E59cuoPEQLErY44bYMHwCCgbtR
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
在某些情况下,输入和重新输入密码可能过于繁琐,因此更喜欢一种更快速的加密多个密码的方法。你可以使用-T
标志来指定一个包含待哈希明文密码列表的文件,或者可以使用-s
标志在命令行中指定密码:
$ for i in foo bar baz ; do slappasswd -s $i; done
{SSHA}p3zm8Sq/jgAMxYkniwnu+ym954qjIRiG
{SSHA}Fklv7m0n0wIw8sLQOe2IxDRsexZegzUT
{SSHA}FOLOLnR0fgmw7jP8p1WRQEJXoX3fJsyG
在这个 Shell 命令中,三个明文密码foo
、bar
和baz
都被slappasswd
加密。
注意
在多用户系统上,其他用户可能能够访问你的命令历史记录,从而能够看到这些明文密码。在命令行中指定密码(或其他敏感信息)时应小心。
通过使用-h
标志,你可以指定slappasswd
应使用的哈希算法:
$ slappasswd -h {MD5} -s test
{MD5}CY9rzUYh03PK3k6DJie09g==
$ slappasswd -h {SMD5} -s test
{SMD5}vWw5aAcoIbJ1PS9BMnp/KF5XS5g=
$ slappasswd -h {SHA} -s test
{SHA}qUqP5cyxm6YcTAhz05Hph5gvu9M=
在上述命令中,使用三种不同的哈希算法对相同的密码test
进行加密。
接下来,我们将转向最后一个 OpenLDAP 工具——slaptest
。
slaptest
slaptest
工具用于检查slapd.conf
文件(及其包含的任何文件)中使用的格式和指令。
运行slaptest
非常简单:
$ slaptest -v -f /etc/ldap/slapd.conf
-v
标志打开详细输出,而-f
标志(它需要一个参数)指定要检查的配置文件。如果省略-f
,则会检查默认的slapd.conf
文件(通常是/etc/ldap/slapd.conf
)。
注意
如前一章所述,Ubuntu Linux 提供的slaptest
版本在slapd
中的指令未知时不会打印警告。这是一种非标准行为。大多数时候,OpenLDAP 都是在启用这些警告的情况下进行编译的。
如果配置文件格式正确且所有指令有效且可操作,那么slaptest
会打印出基本的成功消息:
config file testing succeeded
然而,如果出现任何问题,slaptest
会打印出诊断信息。例如,如果我在slapd.conf
中添加一个指向不存在文件的 include 指令,slaptest
会打印出错误:
$ sudo slaptest
could not stat config file "/non/existent/file": No such file or
directory (2)
slaptest: bad configuration file!
这个输出应该有助于追踪配置文件中的问题。在这种情况下,问题是由一个看起来像这样的行引起的:
include /non/existent/file
这是 OpenLDAP 实用程序的最后一部分。现在我们将转向包含在 OpenLDAP 套件中的客户端应用程序。
使用客户端执行目录操作
有许多 OpenLDAP 客户端,全部存储在 /usr/bin
(或者如果您根据 附录 A 进行编译,则存储在 /usr/local/bin
)。OpenLDAP 客户端通过 LDAP 协议进行通信。它们都符合标准,并遵循 LDAPv3 协议(该协议最后更新于 2006 年 6 月)。
虽然一些客户端提供基本的标准化 LDAP 操作,如搜索、添加和删除,但其他客户端实现了一个或多个 LDAP 扩展。但由于这套工具遵循标准,这些工具应该可以与任何符合标准的 LDAP 目录服务器一起工作。
本章的这一部分我们将简要介绍每个 OpenLDAP 客户端,并看看它们如何与 LDAP 服务器交互。我们没有足够的空间来详细介绍每个客户端的所有细节,所以我们将重点介绍每个客户端最有用和最常见的功能。OpenLDAP 的手册页(与 OpenLDAP 一起安装)详尽而且信息丰富,它们为这些客户端提供了良好的进一步信息来源。
注意
在上一部分中,大多数实用程序要求 SLAPD 服务器必须不在运行。然而,本节中的所有工具都连接到 SLAPD 服务器。因此,请确保在尝试本部分示例之前,您的服务器正在运行。
常见命令行标志
所有 OpenLDAP 客户端都是使用 UNIX 风格标志将参数传递给程序的命令行应用程序。为了保持连贯性,通用标志(如 -D
、-b
和 -H
)在所有客户端中都一致使用。
在第二章中,我们配置了我们的目录服务器来处理基本的目录操作。然而,我们没有配置它来使用 SASL 认证(这在第四章中介绍)。为了对服务器进行认证,我们将使用所谓的简单绑定。在简单绑定中,客户端通过向服务器发送完整的 DN 和密码来进行认证。
根据是否进行简单绑定或SASL 绑定,客户端需要不同的命令行标志。现在我们将看到简单绑定所需的那些标志。关于 SASL 绑定所需的标志将在第四章中介绍。
常见标志
有关简单绑定过程的命令行标志。以下是一些常见标志:
-
-D
:-D
标志用于指定将绑定到目录服务器的用户的完整 DN(这用于简单绑定)。 -
-W
、-w
、-y
:每个标志表示密码的不同来源。我们逐个来看它们:-
-W
标志指示用户应与服务器交互式提示输入密码。 -
-w
:此标志接受一个密码字符串作为值。我们可以用它在命令行中指定密码。 -
-y
:此标志接受文件名作为参数。它将使用文件的内容作为密码。这些标志是互斥的——每个命令中只能使用一个。
注意
-y
标志使用文件的整个内容作为密码。这意味着如果文件中有换行符,它将被视为密码的一部分。要创建密码文件,可以使用带-n
标志的echo
命令:$ echo -n "secret" > my_pw
。 -
-
-x
:-x
标志指定客户端将使用简单绑定。如果未指定,则客户端将尝试使用 SASL 绑定。 -
-H
,-h
:这两个标志提供了指定连接主机的不同方式。-H
接受一个 LDAP URL(-H 'ldap://example.com:389'
)。-h
仅接受主机名(-h example.com
),并可以与-p
一起指定端口。除非没有选择,否则使用-H
。-h
标志仅为向后兼容而提供,未来版本中可能会删除。 -
-Z
:此标志用于指示客户端应向服务器发出启动 TLS命令,以便根据 TLS 标准加密流量。但是,如果 TLS 协商失败,客户端仍将继续操作。使用两个 Z(-ZZ
)将强制流量加密。如果协商失败,则客户端将断开连接。TLS 相关内容将在下一章中详细介绍。 -
-b
:用于指定基本 DN(-b 'dc=example,dc=com'
)。 -
-f
:-f
标志接受文件名作为参数。客户端将读取该文件的内容,并根据文件内容构建请求。 -
-v
:此标志将开启详细输出。在故障排除时非常有用。
这些是 OpenLDAP 套件中客户端常用的标志。但这些仅代表每个客户端使用的标志的一个子集,因为每个客户端都实现了完成其任务所需的标志。
在 ldap.conf 中设置默认值
在第二章的 配置 LDAP 客户端 部分,我们查看了 ldap.conf
文件。在该文件中,我们设置了一些有用的默认值。特别是我们设置了以下三项:
URI ldap://localhost
BASE dc=example,dc=com
BINDDN cn=Manager,dc=example,dc=com
如果省略了主机设置(-H
,-h
),则将使用 URI
的值。如果客户端需要基本 DN,并且没有使用 -b
标志进行设置,则将使用 BASE
的值。同样,如果客户端使用简单绑定(-x
)且未使用 -D
指定 DN,则将使用 BINDDN
的值。
由于我们已经创建了一个 ldap.conf
文件,因此许多示例将省略 -H
和 -b
标志。
虽然 ldap.conf
是所有客户端共享的,但你可以在你的主目录中创建一个特定于用户的 LDAP 配置文件。LDAP 客户端将在你的主目录($HOME
)中查找名为 ldaprc
和 .ldaprc
的用户特定配置文件。
现在我们已经准备好查看客户端命令。
ldapsearch
我们首先要看的客户端是最常用的工具:ldapsearch
。顾名思义,这是一个用于搜索目录信息树的工具。
ldapsearch
客户端连接到服务器,验证用户身份,然后(以该用户身份)执行一个或多个搜索操作,并以 LDIF 格式返回结果。当完成搜索后,它会关闭连接并退出。由于 ldapsearch
是一个网络客户端,因此可以用于搜索本地目录或远程目录服务器。
一个简单的搜索
让我们来看一个简单的搜索命令。在这个命令中,我们将以目录管理员身份登录,并请求获取用户 ID barbara 的记录:
$ ldapsearch -x -W -D 'cn=Manager,dc=example,dc=com' -b \
'ou=Users,dc=example,dc=com' '(uid=barbara)'
这是结果:
Enter LDAP Password:
# extended LDIF
#
# LDAPv3
# base <ou=Users,dc=example,dc=com> with scope subtree
# filter: (uid=barbara)
# requesting: ALL
#
# barbara, Users, example.com
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword:: c2VjcmV0
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
在这个例子中,我们运行了带有四个标志的 ldapsearch
命令:-x
、-W
、-D
和 -b
。有关这些标志的描述,请参阅《常用命令行标志》部分。不过,简而言之,-x
、-W
和 -D
都是用于目录认证的参数。它们指示客户端使用简单认证(-x
)以 -D
指定的 DN(此例中为目录管理员)进行绑定,然后交互式地提示用户输入密码(-W
)。
-b
标志设置搜索的基础 DN。它被设置为 ou=Users,dc=example,dc=com
。根据这一设置,ldapsearch
将从 Users
组织单位(OU)开始搜索。
注意
如果我们省略了 -b
标志,则会使用 ldap.conf
中的 BASE 值,这会将基础 DN 设置为 dc=example,dc=com
。
在所有命令行标志及其参数之后,我们指定了一个 LDAP 过滤器:
(uid=barbara)
这是服务器用于搜索的过滤器。我们在本章的《搜索操作》部分更详细地讨论了搜索过滤器。不过,在这种情况下,搜索过滤器很简单:它仅匹配属性名为 uid
且属性值为 barbara
的记录。
注意
许多属性有多个名称(这些被称为属性描述)。例如,标记用户 ID 的属性有属性描述 uid
和 userID
。在上面的例子中,搜索 (uid=barbara)
也会匹配具有形式为 userID: barbara
的目录条目。
当执行该命令时,系统首先会提示用户输入密码(因为使用了 -W
标志),然后连接到服务器并尝试以指定的 DN(cn=Manager,dc=example,dc=com
)进行绑定。接着,如果绑定成功,它会请求所有符合过滤器 (uid=barbara)
的记录。如示例所示,服务器将返回用户的整个记录,或者在非管理员用户的情况下返回 ACLs 允许的部分记录。
结果以 LDIF 格式返回,其中夹杂着注释。第一组注释提供了有关搜索的基本信息:
# extended LDIF
#
# LDAPv3
# base <ou=Users,dc=example,dc=com> with scope subtree
# filter: (userID=barbara)
# requesting: ALL
#
第一行表示该记录是扩展的 LDIF 格式。这是 LDIF 版本 1.0,并包含一些注释。下面是搜索的摘要,包含以下内容:
-
使用的 LDAP 版本(v3)
-
基本 DN 是什么(
ou=Users,dc=example,dc=com
)。 -
将执行什么类型的搜索。在此案例中,这是一个子树搜索,意味着服务器会查找基本 DN 下所有记录。
-
操作搜索过滤器是什么(
(userid=barbara)
)。 -
客户端希望返回的属性。
ALL
表示客户端希望返回所有可用的属性。
文件的中央部分包含 Barbara 的完整记录。记录下方是结果的简要总结:
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
第一行 search
表示我们执行了两个搜索操作(一个用于绑定,一个用于执行过滤搜索)。
第二行 result
表示服务器返回的结果代码。0 Success
表示我们的搜索没有遇到任何错误。
扩展(因此带有注释的)结果增加了一些附加信息。numResponses
表示服务器向客户端发送了两个响应(一个用于绑定,另一个用于搜索)。而 numEntries
表示搜索返回了多少条记录。在此例中,只有一条——Barbara 的记录。
限制返回字段
有时我们不想获取整个 DN 的记录。相反,我们只希望获取一些特定的属性。这可以通过在命令的末尾指定属性列表来实现:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -b \
'ou=Users,dc=example,dc=com' -LLL '(userID=matt)' mail cn
这里是结果:
dn: uid=matt,ou=Users,dc=example,dc=com
cn: Matt Butcher
mail: mbutcher@example.com
mail: matt@example.com
请注意,在这个例子中,我们使用了 -w secret
标志在命令行中指定密码。我们还使用了 -LLL
标志来抑制 LDIF 输出中的所有冗余注释。
提示
在命令行中指定密码可能会存在安全风险。系统上的其他用户可能通过命令行历史记录(如 Bash shell 的历史功能)和操作系统构造(如 Linux 中的 /proc
文件系统)访问这些信息。
除了过滤器(userID=matt)
,我还添加了一个我希望返回的属性列表:cn
和 mail
。返回的记录包含四行:dn
、两个 mail
属性和 cn
属性。DN 总是会被返回。
请求操作属性
你可能已经注意到,通过 ldapsearch
返回的 Barbara 记录与通过 slapcat
返回的记录有所不同。
注意
我们在本章名为 使用工具准备目录 的部分中讲解了 slapcat
。
让我们对比一下这两者。首先,这是 ldapsearch
的输出:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -b
'ou=Users,dc=example,dc=com' -LLL '(userID=barbara)'
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword:: c2VjcmV0
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
现在,这是 slapcat
的输出:
$ sudo slapcat -a '(uid=barbara)'
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword:: c2VjcmV0
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
structuralObjectClass: inetOrgPerson
entryUUID: bec561c4-c5b0-102a-81c0-81bc30f92d57
creatorsName: cn=Manager,dc=example,dc=com
modifiersName: cn=Manager,dc=example,dc=com
createTimestamp: 20060821223300Z
modifyTimestamp: 20060821223300Z
entryCSN: 20060821223300Z#000005#00#000000
slapcat
的输出包含许多额外的属性——即目录内部维护的特殊操作属性。我们可以通过 ldapsearch
获取这些操作属性,方法是指定属性名称并与所需属性列表一起使用,或者在 ldapsearch
命令末尾使用特殊的加号(+
)属性列表说明符:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -b
'ou=Users,dc=example,dc=com' -LLL '(userID=barbara)' +
这是我们得到的结果:
dn: uid=barbara,ou=Users,dc=example,dc=com
structuralObjectClass: inetOrgPerson
entryUUID: bec561c4-c5b0-102a-81c0-81bc30f92d57
creatorsName: cn=Manager,dc=example,dc=com
modifiersName: cn=Manager,dc=example,dc=com
createTimestamp: 20060821223300Z
modifyTimestamp: 20060821223300Z
entryCSN: 20060821223300Z#000005#00#000000
entryDN: uid=barbara,ou=Users,dc=example,dc=com
subschemaSubentry: cn=Subschema
hasSubordinates: FALSE
指定 +
列表并不会返回所有属性——仅返回操作属性。要获取所有常规属性和所有操作属性,您需要同时使用 +
说明符和 *
(星号)说明符。*
说明符表示我们想要所有标准属性。以下是输出:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -b
'ou=Users,dc=example,dc=com' -LLL '(userID=barbara)' '*' +
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword:: c2VjcmV0
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
structuralObjectClass: inetOrgPerson
entryUUID: bec561c4-c5b0-102a-81c0-81bc30f92d57
creatorsName: cn=Manager,dc=example,dc=com
modifiersName: cn=Manager,dc=example,dc=com
createTimestamp: 20060821223300Z
modifyTimestamp: 20060821223300Z
entryCSN: 20060821223300Z#000005#00#000000
entryDN: uid=barbara,ou=Users,dc=example,dc=com
subschemaSubentry: cn=Subschema
hasSubordinates: FALSE
现在我们有了完整的属性列表。使用这些参数的组合,我们可以生成适合备份的 LDIF 文件(假设 ACL 不会阻止访问某些内容)。虽然 slapcat
在这个任务上会比 ldapsearch
更高效,但 ldapsearch
可以远程通过网络运行,这在许多情况下非常有吸引力。
注意
请注意,在给定的记录中,ldapsearch
返回了三个 slapcat
未显示的操作属性:entryDN
、subschemaSubentry
和 hasSubordinates
。这些值是在运行时动态生成的,并不存在于 LDAP 后端。因此,它们不会与 slapcat
一起导出。由于它们是动态生成的,它们并不是备份的有用值。
也可以使用 ldapsearch
按顺序运行多个查询。这是通过使用外部文件来存储多个搜索的过滤器信息来实现的。
使用文件进行搜索
ldapsearch
客户端可以使用文件来构建并执行多个查询。假设我们有一个包含用户 ID 的纯文本列表,并且我们想获取每个用户 ID 的姓氏。文件 userIDs.txt
看起来是这样的:
matt
barbara
我们可以使用 ldapsearch
动态构建过滤器并为每个用户的姓氏运行搜索。为此,我们使用 -f
标志,指向 userIDs.txt
文件,然后构建一个特殊的过滤器。以下是要执行的命令行:
$ ldapsearch -x -D 'cn=Manager,dc=example,dc=com' -b \
'ou=Users,dc=example,dc=com' -w secret -f userIDs.txt '(uid=%s)' sn
到现在为止,大部分内容应该已经很熟悉了。但请注意过滤器:'(uid=%s)'
。这个过滤器使用特殊的 %s
占位符来指示文件中的值应该放置的位置。每当 ldapsearch
运行时,它会逐行读取 userIDs.txt
文件,并在每行中执行一个搜索,将该行的值替换为过滤器中的 %s
。结果如下:
# extended LDIF
#
# LDAPv3
# base <ou=Users,dc=example,dc=com> with scope subtree
# filter pattern: (uid=%s)
# requesting: sn
#
#
# filter: (uid=matt)
#
# matt, Users, example.com
dn: uid=matt,ou=Users,dc=example,dc=com
sn: Butcher
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
#
# filter: (uid=barbara)
#
# barbara, Users, example.com
dn: uid=barbara,ou=Users,dc=example,dc=com
sn: Jensen
# search result
search: 3
result: 0 Success
# numResponses: 2
# numEntries: 1
在这个示例中,ldapsearch
客户端实际上运行了两个不同的搜索操作。它首先将 (uid=%s)
扩展为 (uid=matt)
并运行一个搜索;然后,它将 (uid=%s)
扩展为 (uid=barbara)
,并运行另一个搜索。在每种情况下,它只返回 dn
(这是匹配时总会返回的)和请求的 sn
属性。
您还可以在文件中创建过滤器,并运行多个搜索过滤器。例如,我们可以创建一个名为 filters.txt
的文件,其中包含以下几行:
&(ou=System)(objectClass=account)
&(uid=b*)(ou=Users)
由于每一行将被插入到一个过滤器中,因此不需要外部的括号。现在我们可以使用这些行通过 ldapsearch
动态构建过滤器:
$ ldapsearch -x -D 'cn=Manager,dc=example,dc=com' -b \
'dc=example,dc=com' -w secret -f filters.txt '(%s)' cn description
我们将得到以下输出:
# extended LDIF
#
# LDAPv3
# base <dc=example,dc=com> with scope subtree
# filter pattern: (%s)
# requesting: cn description
#
#
# filter: (&(ou=System)(objectClass=account))
#
# authenticate, System, example.com
dn: uid=authenticate,ou=System,dc=example,dc=com
description: Special account for authenticating users
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
#
# filter: (&(uid=b*)(ou=Users))
#
# barbara, Users, example.com
dn: uid=barbara,ou=Users,dc=example,dc=com
cn: Barbara Jensen
# search result
search: 3
result: 0 Success
# numResponses: 2
# numEntries: 1
在这种情况下,过滤器 (%s)
在第一个案例中被展开为 (&(ou=System)(objectClass=account))
,在第二个案例中则展开为 (&(uid=b*)(ou=Users))
。
使用这种技术,就可以通过一个命令执行多个复杂的搜索。
本书中我们将继续使用 ldapsearch
客户端。现在我们对它的基本工作方式有了了解,接下来我们将继续介绍 OpenLDAP 套件中的下一个客户端。
ldapadd
这是一个用于向 LDAP 目录添加新条目的命令行程序。ldapadd
命令实际上并不是一个独立的客户端,它只是 ldapmodify
程序的一个链接。当 ldapmodify
看到它是作为 ldapadd
被调用时,它会假定应该请求服务器执行添加操作,而不是请求修改操作。
在最简单的情况下,ldapadd
可以用来从命令行输入一个新记录:
$ ldapadd -x -W -D 'cn=Manager,dc=example,dc=com'
Enter LDAP Password:
一旦我们成功通过身份验证,光标将移到下一行并等待输入。我们可以直接输入记录。只要我们按下 Enter 两次(创建一个空白行,表示记录结束),ldapadd
就会将记录发送到服务器:
dn: uid=adam,ou=Users,dc=example,dc=com
cn: Adam Smith
sn: Smith
uid: adam
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
adding new entry "uid=adam,ou=Users,dc=example,dc=com"
高亮部分是我们输入的文本。它指定了一个完整的记录(一个名为 Adam Smith 的用户记录)。
当我们按下回车键两次,插入一个空白行时,记录被发送到服务器。客户端显示正在添加记录:adding new entry "uid=adam,ou=Users,dc=example,dc=com"
。没有错误信息出现。这意味着添加成功。
一旦记录被添加,光标将移至空白行,等待下一个记录的 dn
属性。
dn: cn=Foo,dc=example,dc=com
farble: gork
objectClass: account
adding new entry "cn=Foo,dc=example,dc=com"
ldap_add: Undefined attribute type (17)
additional info: farble: attribute type undefined
在这个示例中,我们输入的记录(再次突出显示)包含一个未定义的属性,服务器因而显示相同的错误信息。当服务器发送错误信息时,ldapadd
客户端会打印错误信息并退出。要重新输入记录,您必须重新运行 ldapadd
。
但只要新记录有效,并且服务器没有报告错误,ldapadd
将继续提示(或者说是监听)新记录。当完成时,使用 CTRL-C 键组合退出程序。
从文件添加记录
虽然有时直接在客户端输入记录可能很有用,但在大多数情况下,创建纯文本文件中的记录并使用 ldapadd
程序一次性加载它们要更方便(且更不容易出错)。
和往常一样,文本文件中的记录应该采用 LDIF 格式。例如,以下是文件 user_records.ldif
的内容:
dn: uid=david,ou=Users,dc=example,dc=com
cn: David Hume
sn: Hume
uid: david
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
dn: uid=immanuel,ou=Users,dc=example,dc=com
cn: Immanuel Kant
sn: Kant
uid: immanuel
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
我们可以添加文件中的所有记录:
$ ldapadd -x -w secret -D 'cn=Manager,dc=example,dc=com' -f \
user_records.ldif
adding new entry "uid=david,ou=Users,dc=example,dc=com"
adding new entry "uid=immanuel,ou=Users,dc=example,dc=com"
就像我们交互式地添加记录时一样,这里没有错误信息表示记录已成功添加。
接下来,我们将看一下如何修改目录中已经存在的记录。
ldapmodify
ldapmodify
程序用于修改现有条目。它可以添加、更改和删除目录中的条目属性。它还可以用来添加新条目(以及条目的属性)。
与 ldapadd
类似,ldapmodify
也可以交互式运行。它可以用来添加、修改和删除记录。
使用 ldapmodify
添加记录
添加记录的语法在 ldapmodify
中与 ldapadd
几乎相同:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
结果如下:
dn: uid=nicholas,ou=Users,dc=example,dc=com
changetype: add
cn: Nicholas Malebranche
sn: Malebranche
uid: nicholas
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
adding new entry "uid=nicholas,ou=Users,dc=example,dc=com"
唯一的区别是,在 dn
后添加了 changetype
指令。这告诉 ldapmodify
应对该记录执行哪种 LDAP 操作。
注意
changetype
指令并不是一个属性,尽管它看起来像是。它不是记录的一部分,而是告诉服务器应使用哪种操作的指令(以 LDIF 格式)。
changetype
有四个可能的值:
-
add
-
modify
-
modrdn
-
delete
每一个操作都对应一个 LDAP 操作。add
更改类型用于添加新记录(本质上执行与 ldapadd
相同的添加操作)。modify
更改类型用于修改现有记录(例如,通过添加、替换或删除属性)。modrdn
更改类型用于修改记录的相对 DN(或 RDN)。delete
更改类型用于从目录服务器中删除整个记录。
修改现有记录
通常,使用 ldapadd
添加记录更为简便。而 ldapmodify
客户端的真正亮点在于它能修改现有记录,添加、删除或替换记录中的属性。
让我们为上一节中添加的一个记录增加一个 givenName
字段:
$ ldapmodify -x -W -D 'cn=Manager,dc=example,dc=com'
这会产生以下输出:
Enter LDAP Password:
dn: uid=david,ou=Users,dc=example,dc=com
changetype: modify
add: givenName
givenName: David
modifying entry "uid=david,ou=Users,dc=example,dc=com"
与 ldapadd
一样,一旦认证阶段完成,ldapmodify
会等待提供一个 DN。在指定 dn
属性后,应紧跟 changetype
。
使用 modify
更改类型时,如我们在此所做的,必须明确指定我们将要更改哪些属性,以及如何更改它们。modify
更改类型是唯一需要进一步指定的类型。以下是显示几个更改类型的图示:
在这种情况下,我们想要为 uid=david
、ou=Users
、dc=example
、dc=com
记录添加一个新属性。我们要添加的属性是 givenName
。因此,指定要添加 givenName
属性的行是 add: givenName
。
接下来,我们想要指定属性和属性值:
givenName: David
然后,按Enter两次表示记录已完成。就像使用ldapadd
一样,ldapmodify
会指示它正在修改哪个记录。如果服务器没有返回错误,ldapmodify
将等待另一个修改记录。
add
修改类型是ldapmodify
支持的三种类型之一。只有当更改类型设置为修改时,才能指定操作。三种修改类型分别是:
-
add
:向现有记录中添加新属性 -
replace
:用新属性值替换现有属性值 -
delete
:从记录中删除属性
这些操作中的多个可以在一个事务中进行:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=immanuel,ou=Users,dc=example,dc=com
changetype: modify
add: givenName
givenName: Manny
-
replace: cn
cn: Manny Kant
modifying entry "uid=immanuel,ou=Users,dc=example,dc=com"
在这个示例中,我们首先添加givenName
,然后用新的值替换现有的cn
。在两个修改请求之间,我们使用连字符(-
)表示我们仍在处理同一记录。记住,空行表示我们已完成该记录。现在,如果我们使用ldapsearch
查找该记录,它将如下所示:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -LLL \
'(uid=immanuel)'
dn: uid=immanuel,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
cn
已被替换,并且givenName
属性已被添加。
如果修改是添加多个属性,您可以将它们分组,而不是使用连字符分开添加:
dn: uid=nicholas,ou=Users,dc=example,dc=com
changetype: modify
add: description title
description: This is a test
title: Cartesian philosopher
请注意,在这种情况下,add
行包含两个属性名称(description
和title
),后跟这两个属性。就像使用ldapadd
一样,我们可以将这些变更记录放入一个纯文本文件中,然后使用-f
标志(后接文件路径),让ldapmodify
从文件中读取命令,而不是从交互式提示符中读取:
$ ldapmodify -x -w secret -D 'cn=Manager,dc=example,dc=com' -f \
change-nicholas.ldif
modifying entry "uid=nicholas,ou=Users,dc=example,dc=com"
使用modify
更改类型,我们可以删除一个属性:
dn: uid=nicholas,ou=Users,dc=example,dc=com
changetype: modify
delete: title
从记录中删除一个属性会导致该属性的所有值从记录中被删除。例如,如果 Nicholas 有两个指定的头衔,上面的操作会将它们全部删除。
要删除一个特定的属性,请求中必须指定要删除的属性值:
dn: uid=nicholas,ou=Users,dc=example,dc=com
changetype: modify
delete: title
title: Cartesian philosopher
这将删除任何包含精确字符串"Cartesian philosopher"的title
属性值,保留任何其他属性值不变。
修改相对 DN
第三种更改类型用于修改相对 DN——即标识当前记录的 DN 部分(参见本章开头的讨论)。
例如,我们可以更改用户uid=immanuel,ou=Users,dc=example,dc=com
的 DN 的 RDN 部分:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=immanuel,ou=Users,dc=example,dc=com
changetype: modrdn
newrdn: uid=manny
deleteoldrdn: 0
modifying rdn of entry "uid=immanuel,ou=Users,dc=example,dc=com"
rename completed
在这个示例中,我们使用modrdn
更改类型来指示 SLAPD 更改用户 DN 的 RDN 部分。newrdn
指令提供新的 RDN 部分,而deleteoldrdn
指令决定是否删除或保留旧的属性值(uid=immanuel
)。设置0
表示不删除旧的属性值,而设置1
则会删除旧的属性值。
现在,如果我们搜索该用户,我们可以观察到修改:
$ ldapsearch -x -W -D 'cn=manager,dc=example,dc=com' -LL \
'(sn=kant)' uid
Enter LDAP Password:
version: 1
dn: uid=manny,ou=Users,dc=example,dc=com
uid: immanuel
uid: manny
在某些情况下,我们不希望保留旧的 RDN 属性值。在这种情况下,将deleteoldrdn
值设置为1
将删除旧的 RDN 属性值:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=manny,ou=Users,dc=example,dc=com
changetype: modrdn
newrdn: uid=immanuel
deleteoldrdn: 1
modifying rdn of entry "uid=manny,ou=Users,dc=example,dc=com"
rename completed
这将 RDN 更改回uid=immanuel
,并且由于deleteoldrdn
设置为1
,旧的 UID 值(manny
)应该被删除。我们可以通过ldapsearch
验证这一点:
$ ldapsearch -x -W -D 'cn=manager,dc=example,dc=com' -LL \
'(sn=kant)' uid
Enter LDAP Password:
version: 1
dn: uid=immanuel,ou=Users,dc=example,dc=com
uid: immanuel
请注意,除了更改后的 DN,旧的uid
属性值(manny
)不再出现在记录中,它已被替换。
当我们检查ldapmodrdn
客户端时,我们将再次查看相对 DN 的修改。
使用 modrdn 移动记录
modrdn
变更类型不仅可以用于更改 RDN,还可以用于更改记录的上级条目,实际上是将记录在目录信息树中重新定位。
然而,为了使此操作有效,后端数据库类型必须支持这种修改。目前,唯一支持这种修改的存储数据库是 HDB。在第二章中,我们设置了slapd.conf
以在 HDB 后端存储dc=example,dc=com
树。
现在,我们可以执行一个复合 ModRDN 操作,在此操作中,我们更改记录的 RDN,并将记录移动到另一个 OU:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=manny,ou=users,dc=example,dc=com
changetype: modrdn
newrdn: uid=immanuel
deleteoldrdn: 1
newsuperior: ou=system,dc=example,dc=com
在此示例中,我们将用户的 UID 从manny
更改回immanuel
。由于deleteoldrdn
为1
,旧的 RDN(uid=manny
)将从记录中删除。
newsuperior
指令告诉 SLAPD DN 的新基础部分应该是什么。这将有效地把记录从ou=users
分支移动到我们目录信息树中的ou=system
分支。
注意
与修改用户的 RDN 不同,更改记录的上级不会修改记录中的任何字段。因此,我们上面的记录仍然会保留ou=Users
属性。
再次,我们可以使用ldapsearch
查看已修改的记录:
$ ldapsearch -x -W -D 'cn=manager,dc=example,dc=com' -LL
'(sn=kant)' uid
然后,我们得到:
Enter LDAP Password:
version: 1
dn: uid=immanuel,ou=system,dc=example,dc=com
uid: immanuel
请注意,不仅uid
已更改,DN 中的ou
也发生了变化。
为了使用newsuperior
指令,必须首先指定modrdn
。因此,如果我们想将该用户的记录移动回用户 OU,我们仍然需要指定该用户的新 RDN。
那么,如何在不更改 RDN 的情况下移动记录呢?
由于modrdn
变更类型不要求新 RDN 与旧 RDN 不同,因此只需将newrdn
设置为与旧 RDN 相同,即可使用modrdn
移动记录:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=immanuel,ou=system,dc=example,dc=com
changetype: modrdn
newrdn: uid=immanuel
deleteoldrdn: 1
newsuperior: ou=users,dc=example,dc=com
modifying rdn of entry "uid=immanuel,ou=system,dc=example,dc=com"
rename completed
在此情况下,newrdn: uid=immanuel
实际上并没有更改用户的 RDN。但这是为了更改上级所必须的。
newsuperior
指令表示记录应该被移动(回到)ou=users,dc=example,dc=com
树。对该记录进行最后一次ldapsearch
,我们可以看到该变更的结果:
$ ldapsearch -x -W -D 'cn=manager,dc=example,dc=com' -LL
'(sn=kant)' uid
Enter LDAP Password:
version: 1
dn: uid=immanuel,ou=users,dc=example,dc=com
uid: immanuel
再次,记录回到了Users
OU。
删除整个记录
最后,使用delete
变更类型,我们可以通过ldapmodify
删除整个记录:
$ ldapmodify -w secret -x -D 'cn=Manager,dc=example,dc=com'
dn: uid=nicholas,ou=Users,dc=example,dc=com
changetype: delete
deleting entry "uid=nicholas,ou=Users,dc=example,dc=com"
删除记录时,我们只需指定 DN 和变更类型。
本质上,使用删除更改类型执行的任务与使用ldapdelete
客户端执行的任务相同。
ldapdelete
ldapdelete
工具用于从目录中删除一个或多个记录。它执行与ldapmodify
中使用的delete
更改类型相同的操作。
如果你想使用ldapdelete
删除一条记录,必须知道它的 DN。此工具不会搜索,例如,所有具有指定地址的记录,然后删除它们。
ldapdelete
命令的语法很简单:
$ ldapdelete -x -w secret -D 'cn=Manager,dc=example,dc=com' \
'uid=nicholas,ou=Users,dc=example,dc=com'
在常见的标志(-x
、-w
、-D
)之后,ldapdelete
接受要删除的 DN(这是命令第二行中的uid=nicholas
的 DN)。执行时,它将请求服务器删除该记录。如果记录存在且用户(根据服务器的 ACL)被允许删除记录,那么该记录将从目录中移除。
ldapcompare
该工具用于询问服务器某个特定条目(通过 DN 标识)是否具有与指定属性匹配的属性。如果该条目确实具有匹配的属性,则ldapcompare
返回TRUE
。否则,返回FALSE
。
这里有一对示例:
$ ldapcompare -x -w secret -D 'cn=Manager,dc=example,dc=com' \
'uid=david,ou=Users,dc=example,dc=com' 'givenName:David'
TRUE
$ ldapcompare -x -w secret -D 'cn=Manager,dc=example,dc=com' \
'uid=david,ou=Users,dc=example,dc=com' 'cn:Dave Hume'
FALSE
在第一个示例中,ldapcompare
请求服务器检查uid=david,ou=Users,dc=example,dc=com
的记录,查看它是否具有值为David
的givenName
属性。记录确实具有givenName: David
属性,因此返回值为TRUE
。
第二个示例对相同的记录执行了类似的比较;它查找值为Dave Hume
的cn
属性。虽然该记录具有cn
属性,但该属性的值为David Hume
,而不是Dave Hume
。因此,服务器返回了FALSE
。
提示
使用 ldapcompare 进行 Base-64 编码
在值进行比较时,如果该值不是 ASCII 字符串,应该将该值进行 Base-64 编码,并使用我们在 LDIF 文件中使用的双冒号语法(::
)。例如:givenName::RGF2aWQ=
LDAP 比较操作通常比搜索操作要快得多。在可以通过ldapsearch
和ldapcompare
完成相同任务的情况下,通常使用ldapcompare
更高效。
ldapmodrdn
ldapmodrdn
客户端用于更改 DN 的相对 DN(RDN)部分。该客户端请求一个 ModifyDN 操作。ldapmodrdn
接受一个现有记录的完整 DN 和应该替换现有 RDN 的相对 DN:
$ ldapmodrdn -x -w secret -D 'cn=Manager,dc=example,dc=com'
'uid=immanuel,ou=Users,dc=example,dc=com' 'uid=manny'
这个示例请求将uid=immanual,ou=Users,dc=example,dc=com
的 RDN 从uid=immanuel
更改为uid=manny
。
现在让我们查看更改后的记录。我们将通过sn
字段进行搜索:
$ ldapsearch -x -w secret -D 'cn=Manager,dc=example,dc=com' -LLL \
'(sn=Kant)' uid
dn: uid=manny,ou=Users,dc=example,dc=com
uid: immanuel
uid: manny
在这里,过滤器正在查找姓氏为Kant
的记录,并请求仅返回uid
属性。回想一下,我们从未添加过uid
属性值为manny
的记录——我们只有uid: immanuel
。
但是从结果来看,我们可以看到不仅 DN 已被修改,而且一个新的用户 ID 属性也被添加了。在某些情况下,修改 RDN 导致添加(而不是替换)属性值是可以的。但在其他情况下,这样做会很不方便,甚至是非法的(因为架构的原因)。
例如,我们可能在目录中有一个描述与公司网站相关的记录子树的条目。这样的记录可能如下所示:
dn: dc=www,dc=example,dc=com
dc: www
ou: Website
objectClass: organizationalUnit
objectClass: dcObject
现在,假设我们想将 RDN 从www
更改为web
。像之前那样使用ldapmodrdn
会导致错误:
$ ldapmodrdn -x -w secret -D 'cn=Manager,dc=example,dc=com' \
'dc=www,dc=example,dc=com' 'dc=web'
Rename Result: Constraint violation (19)
Additional info: attribute 'dc' cannot have multiple values
出现此错误的原因是dc
的架构定义指定每个记录中只能有一个dc
属性值。
注意
dc
(或domainComponent
)属性在core.schema
中有定义。
解决此问题的方法是为ldapmodrdn
使用-r
标志。
$ ldapmodrdn -x -w secret -D 'cn=Manager,dc=example,dc=com' -r
'dc=www,dc=example,dc=com' 'dc=web'
-r
标志使得ldapmodrdn
替换现有的属性值,而不是添加它。现在结果记录看起来是这样的:
dn: dc=web,dc=example,dc=com
ou: Website
objectClass: organizationalUnit
objectClass: dcObject
dc: web
列出的只有一个dc
属性,并且它具有新设置的值web
。
使用ldapmodrdn
修改上级 DN
就像我们之前看到的ldapmodify
的modrdn
类型变更一样,我们也可以使用ldapmodrdn
来更改上级 DN(记录 DN 的基础部分)。
提示
正确的后端
不是所有的后端都支持这种类型的重命名。目前,只有 HDB 后端支持更改 DN 中的上级引用。其他非存储后端(如ldap
)可能会将这些操作传递给底层存储机制,而该机制可能会或可能不会支持这种程度的重命名。
同样,与modrdn
类型变更一样,ldapmodrdn
必须指定一个替换的 RDN,即使这个 RDN 与当前的 RDN 相同。换句话说,即使 RDN 不是新的,仍然需要指定 RDN。我们将在下面看到一个例子。
ldapmodrdn
的-s
标志指定了新的上级 DN。因此,要将条目uid=barbara,ou=users,dc=example,dc=com
移动到目录中的ou=system
分支,我们可以使用如下命令:
ldapmodrdn -x -w secret -D 'cn=Manager,dc=example,dc=com' \
-s "ou=system,dc=example,dc=com" -r \
"uid=barbara,ou=users,dc=example,dc=com" "uid=barbara"
这是一个较长的命令,因此分成了三行:
-
第一行包含了用于绑定到目录的标志,这些标志现在应该已经很熟悉了。
-
第二行以
-s
标志开始,后面跟着一个 DN 作为参数。这个标志指定了新的上级 DN 是什么。在这个例子中,它是ou=system,dc=example,dc=com
。如我们之前所见,
-r
标志指示 SLAPD 用新的 RDN 替换旧的 RDN。 -
第三行是我们要修改的条目的 DN,
uid=barbar,ou=users,dc=example,dc=com
,以及新的 RDN。由于我们希望保持相同的 RDN(但将记录移动到新的子树),因此我们将最后一个值设置为uid=barbara
,这是现有记录的 RDN。
运行此命令后,我们可以使用ldapsearch
查看结果:
$ ldapsearch -x -W -D 'cn=manager,dc=example,dc=com' -LL
'(uid=barbara)' uid ou
Enter LDAP Password:
version: 1
dn: uid=barbara,ou=system,dc=example,dc=com
ou: Users
uid: barbara
Barbara 的新记录的基本部分现在是ou=system,dc=example,dc=com
。
就像在ldapmodify
的modrdn
变更类型中一样,修改上级条目不会改变记录中的任何属性。因此,即使这个记录现在位于系统 OU 中,它仍然具有ou: Users
属性。
构建具有多个属性值的相对 DN 是可能的。例如,我可以在 RDN 部分中使用uid
和l
(用于位置)的组合:
dn: uid=matt+l=Chicago,ou=Users,dc=example,dc=com
在这种情况下,使用加号(+
)表示这两个属性都应该被视为 RDN 的一部分。
ldapmodrdn
足够智能,能够处理这些情况。它将添加(或替换)RDN 中使用的所有属性。
在指定了-r
标志的情况下,有一些需要注意的事项。首先,ldapmodrdn
将替换新 RDN 中使用的所有字段。其次,如果初始 RDN 中有被从 RDN 中移除的值,那么该属性值也将从记录中移除。例如,以下是我们的起始记录:
dn: cn=Matt Butcher+l=Chicago,dc=example,dc=com
cn: Matt Butcher
sn: Butcher
l: Chicago
objectClass: person
objectClass: organizationalPerson
请注意,DN 使用了cn
和l
两个属性,这两个属性都出现在记录的主体中。现在,如果我们使用ldapmodrdn
并加上-r
标志,将cn=Matt Butcher+l=Chicago
替换为cn=Matt Butcher
,l: Chicago
属性将从记录中删除:
dn: cn=Matt Butcher,dc=example,dc=com
sn: Butcher
objectClass: person
objectClass: organizationalPerson
cn: Matt Butcher
因此,在使用带有多属性 RDN 的ldapmodrdn
时,应谨慎使用-r
标志。
ldappasswd
在实用工具部分,我们查看了如何使用slappasswd
加密密码。该工具用于生成加密的值,以便包含在 LDIF 文件中。与此不同,ldappasswd
客户端连接到服务器并在目录中更改密码值。如果需要,它还可以用来自动生成密码。
与使用 LDAP v.3 标准的 Add 和 Modify 操作的ldapadd
和ldapmodify
不同,ldappasswd
客户端使用的是扩展操作——LDAP 密码修改扩展操作,该操作在 RFC 3062 中有定义(rfc-editor.org/rfc/rfc3062.txt
)。
注意
当从 LDIF 文件加载密码,或通过ldapadd
或ldapmodify
时,如果你发送给服务器的是明文密码,该密码将以未加密的字符串形式存储在目录中。这是不安全的。你应该使用slappasswd
生成加密密码并将其包含在 LDIF 文件中,或者使用ldappasswd
来设置密码。
只要 ACL 允许,用户就可以使用ldappasswd
客户端更改她或他的密码:
$ ldappasswd -x -W -S -D 'uid=matt,ou=Users,dc=example,dc=com'
New password:
Re-enter new password:
Enter LDAP Password:
Result: Success (0)
-S
标志是这里唯一使用的新标志。它表示ldappasswd
应该提示用户输入(并重新输入)新密码。如你所记得,-W
标志则提示用户交互式地输入当前密码。
用户输入密码的顺序与常规不同。用户首先被提示输入并重新输入新密码,然后再输入当前密码。
管理员(或具有userPassword
属性写入权限的用户)也可以更改其他用户的密码:
$ ldappasswd -x -w secret -D 'cn=Manager,dc=example,dc=com' -s secret \ 'uid=barbara,ou=Users,dc=example,dc=com'
Result: Success (0)
在这种情况下,目录管理员正在更改uid=barbara,ou=Users,dc=example,dc=com
的userPassword
属性值。与使用-S
并在交互提示符下输入密码不同,密码已在命令行中指定:-s secret
。
通过ldappasswd
更改密码时,密码会在存储到记录中之前由服务器自动加密:
# barbara, Users, example.com
dn: uid=barbara,ou=Users,dc=example,dc=com
userPassword:: e1NTSEF9UzFTUnQ1bkkvcHZGOGt3UklVU3J3TkRHZHFSS3hOQ1Y=
如果我们解码userPassword
值,结果是:{SSHA}S1SRt5nI/pvF8kwRIUSrwNDGdqRKxNCV
。该密码以不可逆的 SSHA 哈希格式存储。
提示
设置默认加密方案
你可以指定服务器在加密密码时选择哪种加密方案。要指定算法,请在slapd.conf
中使用password-hash
指令。例如:password-hash {SMD5}
最后,ldappasswd
可以请求服务器为该 DN 生成一个强密码。如果没有设置标志来指示密码的来源(例如-s
、-S
或-T
),那么ldappasswd
将请求生成一个密码。以下是请求:
$ ldappasswd -x -w secret -D 'cn=Manager,dc=example,dc=com' \
'uid=barbara,ou=Users,dc=example,dc=com'
New password: dS9R4Kvc
Result: Success (0)
服务器响应了这个请求,并生成了一个密码New password: dS9R4Kvc
,该密码已被加密并存储在服务器的userPassword
属性中。
ldapwhoami
OpenLDAP 套件中的最后一个客户端是ldapwhoami
。该客户端提供了“我是谁?”扩展操作的客户端实现。该操作提供了当前与目录绑定的 DN 的信息。
ldapwhoami
命令仅需要足够的信息来验证目录服务器:
$ ldapwhoami -x -w secret -D 'cn=Manager,dc=example,dc=com'
dn:cn=Manager,dc=example,dc=com
Result: Success (0)
如你所见,从这个例子中可以看出,这个客户端只会返回我们连接的用户的 DN。这个工具在调试不需要 DN 来连接的 SASL 身份验证时非常有用。我们将在下一章讨论 SASL 配置。
总结
在本章中,我们详细介绍了 OpenLDAP 套件中的工具。我们首先查看了 SLAPD 和 SLURPD 服务器。特别是,我们了解了主要的 LDAP 操作,如绑定(bind)、搜索(search)、添加(add)、修改(modify)和删除(delete)。
接下来,我们在一个 LDIF 文件中创建了一个基本的目录信息树。在此过程中,我们熟悉了 LDIF——用于表示 LDAP 目录数据的文本格式。
然后我们查看了 OpenLDAP 套件中的实用工具和客户端。在此过程中,我们将目录信息树从 LDIF 加载到目录中,然后添加和修改了这些数据。
到此为止,你应该已经能够熟练使用 OpenLDAP 中包含的工具。在下一章中,我们将回到 SLAPD 服务器,并深入了解 LDAP 安全。
第四章。保护 OpenLDAP
在第二章中,我们安装了 OpenLDAP 并创建了 SLAPD 服务器的基本配置文件。然后,在上一章中,我们将注意力转向了 LDAP 操作和 LDAP 客户端。现在,我们将返回到 SLAPD 服务器,但重点将有所不同:安全性。我们将重点关注 OpenLDAP 的三个主要安全考虑因素:保护服务器和客户端之间的连接、验证目录用户的身份,以及指定特定用户可以访问哪些数据(以及他们可以以何种方式访问)。我们将从实际角度来审视这些安全考虑因素,并在此过程中涵盖以下内容:
-
配置 SSL 和 TLS 以保护网络数据
-
使用简单绑定来验证 DNS(域名系统)以使用目录
-
使用 SASL 提供更强大的身份验证服务
-
集成 SASL 和客户端 SSL/TLS 证书进行身份验证
-
配置访问控制列表(ACL)以建立有关用户可以访问哪些数据的规则
LDAP 安全性:三大方面
正如我们已经看到的,目录包含敏感信息。一个例子就是 userPassword
属性。但目录中也可能包含其他被认为是敏感的信息,如个人信息或关于组织的机密信息。这些信息需要得到保护。
我们可能会问,这里所说的保护是什么意思。因为我们显然并不是要阻止所有客户端查看所有内容。我们真正想要的,是允许人们访问特定的目录信息。然而,另一方面,也有些情况是我们希望拒绝某些用户访问特定的目录信息。所以,保护我们的数据成为在某些情况下提供信息,而在其他情况下拒绝信息的一个问题。
虽然可以进行更精细的区分,但在这里我们将考虑保护目录及其信息的三个广泛安全方面。这三个方面如下:
-
连接安全性:这是保护目录信息(以及客户端信息)在客户端和目录服务器之间传递过程的过程。我们将在网络安全的背景下讨论这一点,涉及 SSL 和 TLS。
-
身份验证:这是确保尝试访问目录信息的用户确实是其声称身份的过程。在本章中,我们将介绍两种身份验证类型:简单绑定和 SASL 绑定。SASL 代表简单身份验证和安全层。
-
授权:这是确保已识别或已验证的用户被允许访问目录中某些信息的过程。OpenLDAP 的 ACL 用于指定授权规则。
在本章中,我们将探讨这三个安全方面。通过将这三者结合起来,我们可以为我们的目录信息提供适当细粒度的保护。
使用 SSL/TLS 保护基于网络的目录连接
我们将要研究的第一个安全要素是网络安全。大多数客户端通过网络接口连接到 OpenLDAP,客户端请求和服务器响应都通过网络传输。
LDAP 协议默认情况下以明文形式发送和接收消息。在这种情况下,数据在通过网络传输时不会进行任何隐藏处理。明文传输有几个优点:
-
它更容易配置和维护。
-
LDAP 服务可以更快速地运行。加密和解密消息的过程可能会占用大量处理器资源,去除这些处理过程可以加速操作。
但这些优点是以安全性为代价的。网络上的其他设备可能能够拦截这些未加密的传输并读取其内容,从而可能获取敏感信息。在小型局域网(LAN)中,风险可能较小(尽管依然存在)。而在大规模网络中,例如互联网,风险则要大得多。
在本节中,我们将介绍配置安全套接字层(SSL)和传输层安全性(TLS)加密的过程,以保护数据在网络上传输时的安全。SSL 和 TLS 非常相似,以至于这两个术语经常被作为同义词使用(通常是可以接受的)。不过,TLS 是 SSL 的改进版本,实施方式比典型的 SSL 实现更加灵活。StartTLS 方法就是一种保护连接的例子。
SSL 和 TLS 的基础
OpenLDAP 提供了两种加密网络流量的方法。第一种是让 OpenLDAP 在一个特殊端口上监听请求(默认使用端口 636,即 LDAPS 端口)。这个端口上的传输会自动进行加密。这种方法较旧,是作为 LDAP v2 的一个附加功能引入的,但现在已不再是首选方法。
第二种方法是 LDAP v3 标准的一部分,允许客户端在标准端口(通常是端口 389)上连接时,申请从明文传输切换到加密传输。我将在这里介绍这两种配置。
安全套接字层(SSL)是一种安全过程,最初由 Netscape 通信公司为其网页浏览器开发,旨在提供一种安全的方式,在服务器和任何客户端之间交换可信的信息。SSL 过程的两个主要特性是:建立身份验证和进行安全加密交易。
随着 SSL 的发展和演变,它被移交给了一个标准化组织——互联网工程任务组(IETF),进行标准化和持续开发。IETF 将其重新命名为传输层安全(TLS),并发布了 1.0 版本(作为 RFC 2246)。SSL 3.0 和 TLS 1.0 没有显著差异,大多数支持其中一种的服务器也支持另一种。由于它们的相似性和共同的历史,我通常将它们合并称为 SSL/TLS。
真实性
证明真实性和提供加密是 SSL/TLS 的两个主要特性。关于第一个,SSL/TLS 提供了一种建立服务器真实性的方法(如果需要,也可以验证客户端的真实性)。这意味着 SSL/TLS 使客户端能够合理地确认服务器确实属于它声称的所有者。
考虑一下在线银行的情况。如果我使用浏览器登录到银行的网站并进行一些交易,我希望确保我连接到的网站确实是我的银行网站,而不是一个冒充我银行的网站。SSL/TLS 提供了使用X.509 证书来验证服务器真实性的工具。X.509 证书包含三个重要信息:
-
有关证书所有者的个人或组织的信息
-
一个公钥(我们将在下一部分讨论)
-
数字签名由证书颁发机构(CA)提供
证书被设计为一种保证,确保某个服务器与特定的个人或组织相关联。当我联系我认为是我的银行的服务器时,我希望得到一些保证,确认它确实是我银行的服务器。因此,证书中包含的一项信息是关于谁拥有该证书的信息。我们可以自己检查这些信息,但由于证书包含数字签名,软件也可以以一种比简单读取证书并信任证书更可靠的方式来验证这些信息。
数字签名是加密的信息块。它使用证书颁发机构(CA)拥有的特殊“私钥”进行加密。然后,CA 可以发布一个公钥,客户端软件可以使用它来验证证书确实是由 CA 签署的。CA 在建立信任方面起着非常重要的作用。我们将在加密部分讨论公钥和私钥。
证书颁发机构(CA)负责颁发证书。理想情况下,CA 是一个受信任的来源,可以验证证书的真实性,并提供保证,确保证书确实由声明拥有该证书的组织或个人所有。
有许多商业 CA 提供收费的证书生成服务。为了通过这些服务获取证书,组织或个人必须提供一些信息,用以验证申请证书的人或组织是否合法。一旦对这些信息进行了调查,并且申请人或组织支付了相应的费用,CA 就会签发一个数字签名的证书。
大型 CA 的证书默认包含在大多数支持 SSL 的应用程序中,如流行的网页浏览器(如 Mozilla Firefox)和 SSL 库(如 OpenSSL)。这些证书包含了验证数字签名所需的公钥。因此,当客户端获取到由这些 CA 签名的 X.509 证书时,它就具备了验证证书真实性所需的所有工具。
但是,对于一个组织或个人来说,创建一个本地使用的证书颁发机构(CA)并使用该 CA 为内部应用生成证书是可行的,而且通常是有用的。这就是我们在为 OpenLDAP 创建证书时所做的事情。
当然,这种方式生成的证书可能不会被组织外的用户认为是可靠的,但托管一个个人或组织级别的 CA 可以有效地为自己的网络增加安全性,而无需购买来自商业供应商的证书。
注意
并非所有 CA 都采用相同形式的权威签名(也并非所有 CA 都收取证书费用)。有些 CA,如 Cacert.org,采用一种称为 信任网 的技术来建立身份认证。在信任网中,证书的真实性由同伴验证,他们可以担任确保证书归属所声明的个人或组织的角色。欲了解更多信息,请访问 www.cacert.org/
。
我们已经讨论了 SSL/TLS 的第一个功能:建立身份认证。接下来,我们将讨论 SSL/TLS 的第二个功能:提供加密服务。
加密
SSL/TLS 提供了客户端和服务器之间发送加密消息所需的功能。简而言之,过程如下:服务器将证书发送给客户端,在证书中(以及其他内容中)包含了服务器的公钥。公钥是一对密钥中的第一部分。公钥可用于加密消息,但不能解密消息。第二个密钥,私钥,则用于解密消息。服务器将私钥保密,但会将公钥提供给任何请求的客户端。客户端可以将消息发送给服务器,只有服务器可以解密并解读这些消息。
根据配置,客户端还会向服务器发送其公钥,服务器可以利用这个公钥发送只有客户端能够解密的消息。此时,双方可以互相发送加密消息。
使用公钥/私钥的缺点是:它们速度较慢且资源消耗大。与其通过这些公钥/私钥组合交换所有信息,客户端和服务器会协商一组临时对称加密密钥(使用相同的密钥来加密和解密消息),它们将在会话期间共同使用。客户端之间的所有流量都使用这些密钥进行加密。一旦会话完成,客户端和服务器都会丢弃临时密钥。
注意
关于 SSL 和 TLS 的更详细介绍,以及进一步信息的链接,请参阅维基百科上的传输层安全条目:en.wikipedia.org/wiki/Transport_Layer_Security
。
StartTLS
按照通常的实现方式,SSL 要求服务器在与非加密流量不同的端口上监听加密流量。所有通过 SSL 端口的流量都被认为是 SSL 加密的流量。这意味着,任何需要同时提供明文和加密服务的服务器都必须至少在两个不同的端口上监听。
多端口的需求对一些人来说似乎是多余的、不优雅的且浪费资源。没有理由客户端不能在明文(非 SSL)连接上请求客户端和服务器之间的进一步通信加密。然后,客户端和服务器可以在同一连接上完成所有 SSL/TLS 协商,而不必切换到另一个仅支持 SSL/TLS 的端口。这个建议在 RFC2487 中被标准化为StartTLS。
提示
选择哪个:StartTLS 还是 LDAPS?
在 LDAP v.3 中实现 SSL/TLS 的标准方法是使用 StartTLS 方法。此方法应该尽可能实现。然而,外部因素(例如网络防火墙或不支持 StartTLS 的客户端)可能要求您使用 LDAPS 和专用的 SSL/TLS 保护端口。目前,LDAPS 支持已被列为过时,但尚未从 OpenLDAP 中移除。两种方法可以在同一服务器上同时使用。
在支持 StartTLS 的服务器中,如果客户端向服务器发送命令STARTTLS
,则服务器将开始 TLS 加密过程。如果 TLS 协商成功,客户端和服务器将继续使用加密流量进行通信。
StartTLS 显然的优势是每个服务器只需要一个监听端口。而且,它使得客户端和服务器可以在处理不重要数据时进行明文通信,当安全性变得重要时再切换到 TLS。由于加密是资源密集型的,需要额外的处理能力来加密和解密消息,通过 StartTLS 方式简化服务可以提高性能,并释放资源用于其他任务。
然而,StartTLS 有一个缺点。由于加密流量和明文流量通过同一个端口发送,单纯通过阻止端口来防止不安全的数据传输(例如使用防火墙)在 StartTLS 中并不有效。安全措施必须能够在协议级别检查传输。
为了提高此类情况下的安全服务,OpenLDAP 提供了测试连接安全强度因子(SSF)的方法,以检查连接是否加密(如果是,加密方案是否足够强大)。我们将在本章后面的使用 安全 强度 因子部分更详细地讨论 SSF。
到目前为止,你应该对 SSL 和 TLS 的工作原理有了相当清晰的理解。现在我们将进入更实际的部分。我们将创建我们自己的证书颁发机构(CA),并生成我们自己的证书,然后配置 OpenLDAP 以支持 SSL/TLS 和 StartTLS。
创建 SSL/TLS 证书颁发机构(CA)
为了创建证书颁发机构并生成证书,你需要安装 OpenSSL。由于许多 Ubuntu 包(包括 OpenLDAP 包)都需要 OpenSSL,它应该已经安装好了。
如果你是从源代码构建的,如附录 A 中详细描述的那样,你也可以通过 OpenSSL 库启用 SSL/TLS 支持。
提示
如果你已经有了证书,可以跳过本节并转到配置 StartTLS 部分。OpenLDAP 使用 PEM 格式的证书。
我们需要做的第一件事是创建新的证书颁发机构(CA)。
虽然可以使用 openssl
命令行工具手动配置 CA,但使用随 OpenSSL 附带的 CA.pl
Perl 脚本要简单得多。这个脚本简化了 OpenSSL 的许多配置选项,我们首先使用它来创建我们新的 CA 环境。
注意
Ubuntu 维护了关于以“长方式”创建新的证书颁发机构(手动创建所有文件)的文档。这份文档详细且值得阅读。虽然我会遵循那里建立的惯例,但我将使用 CA.pl
脚本来完成大部分繁重的工作(help.ubuntu.com/community/OpenSSL
)。
你可以将 CA 环境放在系统的任何位置。有些人喜欢将 CA 文件与其他 SSL 配置一起保存在 /etc/ssl/
目录下。也有一些人喜欢将证书颁发机构保存在用户目录中,以避免在系统升级时被覆盖(虽然这不太可能,但也有这种可能)。根据 Ubuntu 的建议,我将把它保存在我的用户主目录下,路径为 /home/mbutcher/
:
$ cd ~
$ /usr/lib/ssl/misc/CA.pl -newca
请注意,CA.pl
脚本不在 $PATH
中,因此你需要输入脚本的完整路径。
提示
查找 CA.pl
不同的操作系统发行版会将 CA.pl
放在不同的位置。如果运行 which CA.pl
没有返回任何结果,你可能需要查阅 SSL 的手册页(man config
或 man CA.pl
),或者使用 find
或 slocate
工具来查找 CA.pl
文件。
参数 -newca
指示 CA.pl
设置一个新的证书颁发机构环境。这将生成一个目录结构,并包含多个文件。
CA.pl
首先会提示你输入一个 CA 文件:
$ /usr/lib/ssl/misc/CA.pl -newca
CA certificate filename (or enter to create)
按下 Enter 创建一个新的 CA 证书。然后,CA.pl
会生成一个新的密钥,并提示你输入密码:
CA certificate filename (or enter to create)
Making CA certificate
Generating a 1024 bit RSA private key
....++++++
...................................++++++
unable to write 'random state'
writing new private key to './demoCA/private/cakey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
一旦你输入并重新输入了密码,CA.pl
将收集一些关于你所在组织的信息:
You are about to be asked to enter information that will be
incorporated into your certificate request.
What you are about to enter is what is called a Distinguished Name or
a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Illinois
Locality Name (eg, city) []:Chicago
Organization Name (eg, company) [Internet Widgits]:Example.Com
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:Matt Butcher
Email Address []:matt@example.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:mypassword
An optional company name []:Example.Com
CA.pl
会引导你完成创建主证书的过程。代码列表中高亮的行是你在交互式提示中需要提供信息的地方。设置好国家、州和城市名称后,我们将 组织名称 设置为 Example.Com。虽然我们将 组织单位 字段留空,但你可以利用这个字段进一步指定这个 CA 属于组织的哪个部分。
注意
你应该考虑在证书中使用与你在前两章设置目录信息树时使用的根 DN 相同的字段。
通常,公共名称 和 电子邮件地址 字段应包含有关组织的信息。有时,公共名称 用于服务器名称(如我们创建证书时)。有时,它也用于联系信息。在接下来的例子中,我们使用了我的名字和电子邮件。如果 CA 是你组织的“官方” CA,你应该将其设置为证书查询的官方联系人。
接下来,CA.pl
将开始生成 CA 证书的证书请求。换句话说,CA.pl
将创建一个新的证书,作为 CA 自己的证书。第一步是创建证书请求。我们需要为证书请求设置一个复杂的密码,也可以设置公司名称。有了上述信息,CA.pl
将继续生成新证书的过程:
Using configuration from /usr/lib/ssl/openssl.cnf
Enter pass phrase for ./demoCA/private/cakey.pem:
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number:
bf:2f:58:47:b1:6d:31:4d
Validity
Not Before: Oct 10 21:34:28 2006 GMT
Not After : Oct 9 21:34:28 2009 GMT
Subject:
countryName = US
stateOrProvinceName = Illinois
organizationName = Example.Com
commonName = Matt Butcher
emailAddress = matt@example.com
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
07:92:9B:35:CB:B7:EE:92:A8:33:61:B0:DC:F7:88:E9:4F:06:9F:7F
X509v3 Authority Key Identifier:
keyid:07:92:9B:35:CB:B7:EE:92:A8:33:61:B0:DC:F7:88:E9:4F:06:9F:7F
Certificate is to be certified until
Oct 9 21:34:28 2009 GMT (1095 days)
Write out database with 1 new entries
Data Base Updated
我们将被提示输入密码短语。这是我们首先创建的密码短语(当提示输入 PEM 密码短语 时)。如果我们正确输入密码短语,CA.pl
将生成新的证书,并在屏幕上显示其内容。
我们现在已经创建了一个证书颁发机构。现在,我们准备开始生成一个供 SLAPD 使用的证书。
注意
由于某些版本的CA.pl
存在 bug,您可能需要cd
进入./demoCA
目录(CA.pl -newca
创建的目录),并为其添加一个符号链接:ln -s ./demoCA
。这是因为CA.pl
有时会期望在当前目录(./
)中找到文件,它假设该目录是demoCA/
,有时它期望在./demoCA
中找到文件(这当然等同于demoCA/demoCA/
)。您也可以通过简单地编辑/etc/ssl/openssl.cnf
文件中[CA_default]
下的dir=
行,并将其设置为绝对路径来解决此问题。
创建证书
创建证书是一个两步过程:
-
我们需要生成证书请求。
-
我们需要用 CA 的签名来签署请求。
让我们详细看看这些步骤。
创建新的证书请求
创建有效的 SSL 证书的第一步是创建证书请求。在这个过程中,我们将指定我们希望在证书上显示的信息。
有几种方式可以生成证书请求。例如,您可以使用openssl
命令行工具并指定一系列命令行参数。但是,按照我们之前的示例,我们将使用CA.pl
并让应用程序在需要时提示我们输入信息。
要生成新的请求,我们将运行CA.pl -newreq
。在下一个示例中,突出显示的行是需要我们输入信息的行:
$ /usr/lib/ssl/misc/CA.pl -newreq
Generating a 1024 bit RSA private key
.....++++++
.....................++++++
unable to write 'random state'
writing new private key to 'newkey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be
incorporated into your certificate request.
What you are about to enter is what is called a Distinguished Name or
a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Illinois
Locality Name (eg, city) []:Chicago
Organization Name (eg, company) [Internet Widgits]:Example.Com
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:example.com
Email Address []:matt@example.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Request is in newreq.pem, private key is in newkey.pem
这应该看起来很熟悉。在大多数方面,它与生成证书颁发机构的过程相似。
首先,我们将被提示输入一个密码短语。我们将在稍后使用这个密码短语。
接下来,我们需要提供有关该证书所代表的组织的信息。和之前一样,字段包括国家名、州/省名、城市、组织名、组织单位、联系人常用名和联系人的电子邮件地址。同样,像之前一样,我们输入了 Example.Com 的相关信息。
然而,这次我们将“常用名称”字段设置为证书所对应的服务器域名——example.com
。正确使用服务器的域名非常重要。在证书协商过程中,客户端会检查“常用名称”字段,看它是否与服务器的域名匹配。如果域名不匹配,用户可能会收到错误信息,或者客户端应用程序可能会直接终止连接。
额外的密码和可选 公司 名称有时会在证书请求过程中使用。由于我们自己在请求和签署过程中进行操作,因此不需要填写这些字段。
现在我们应该在 CA 目录中有两个文件:
-
第一步是创建名为
newreq.pem
的文件,里面包含我们证书请求的 base-64 编码表示。 -
第二步是创建名为
newkey.pem
的文件,里面包含 base-64 编码的私钥。
我们现在可以继续进行第二步了。
签署证书请求
证书请求包含了证书所需的所有信息,但仍然缺少 CA 的数字签名。那么,下一步就是使用我们之前创建的 CA 来签署这个新证书。为此,我们将运行 CA.pl -signreq
:
$ /usr/lib/ssl/misc/CA.pl -signreq
Using configuration from /usr/lib/ssl/openssl.cnf
Enter pass phrase for ./demoCA/private/cakey.pem:
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number:
ba:49:df:f5:8e:7e:77:c2
Validity
Not Before: Oct 12 21:23:49 2006 GMT
Not After : Oct 12 21:23:49 2007 GMT
Subject:
countryName = US
stateOrProvinceName = Illinois
localityName = Chicago
organizationName = Example.Com
commonName = example.com
emailAddress = matt@example.com
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
47:DD:90:8F:79:90:2E:C0:CC:B3:95:62:35:C4:D8:6C:5D:A2:EE:88
X509v3 Authority Key Identifier:
keyid:6B:FB:66:33:5D:DB:CC:40:42:D7:71:F7:F0:D0:7C:94:3E:8F:CD:58
Certificate is to be certified until
Oct 12 21:23:49 2007 GMT (365 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
Signed certificate is in newcert.pem
CA.pl -signcert
命令会查找 newreq.pem
文件,然后开始签名过程。首先,我们需要输入 CA 的密码短语。如果密码正确,CA.pl
会显示 newreq.pem
中的证书,并询问是否要签署它。最后,它会要求我们提交这些更改。
一旦更改被提交,将会创建一个名为 newcert.pem
的新文件。
我们现在有两个重要的文件:
-
newkey.pem
,它包含私钥。 -
newcert.pem
,它包含签名后的证书。
我们只需要处理几个小问题,然后就可以继续配置 SLAPD 以使用 SSL/TLS。
配置和安装证书
我们只需要完成剩下的三个步骤。第一个步骤与我们在证书中设置的密码短语有关。
移除密钥的密码短语
在这里需要非常小心!在生成证书请求时,我们为证书设置了一个密码短语。这使得 newkey.pem
文件使用密码短语进行了加密。
如果你使用的是带有密码短语加密的密钥文件,那么每次使用这个证书时,你都必须输入密码。这意味着,在我们的例子中,每次启动 OpenLDAP 时,我们都需要输入密码短语。除非我们有严格的安全要求(并且愿意忍受每次启动或重启服务器时输入密码短语的麻烦),否则我们可能不希望密钥文件被加密。
因此,我们需要使用 openssl
命令创建一个未加密版本的密钥文件:
$ openssl rsa < newkey.pem > clearkey.pem
这就是我们得到的结果:
Enter pass phrase:
writing RSA key
在这个例子中,命令 openssl rsa
执行了 OpenSSL RSA 工具,它将解密密钥。通过 < newkey.pem
,我们将 newkey.pem
文件的内容传递给 openssl
进行解密。然后,通过 > clearkey.pem
,我们指示 openssl
将明文密钥文件写入 clearkey.pem
文件。为了完成此操作,openssl
会提示输入密码短语。现在,clearkey.pem
文件包含了我们证书的未加密私钥。
注意
clearkey.pem
文件现在包含了一个未加密的私钥。此文件应该受到保护,以防止滥用。你应该为此文件设置严格的权限,以确保系统中的其他用户无法访问它。
提示
OpenSSL 程序
openssl
程序执行了几十个与 SSL 相关的功能,从生成证书到模拟基于网络的 SSL 客户端。其语法通常比较复杂。这也是为什么我们使用 CA.pl
包装脚本来执行常见任务的原因。但有些任务只能通过 openssl
命令来完成。如果需要,openssl
有非常出色的手册页面:man openssl
。
移动证书
第二个任务是将我们的新证书和密钥移动到服务器上的一个有用位置,并给 PEM 文件命名。如果这个证书需要被多个不同的服务使用,可能可以考虑将其放在共享目录中。但在我们的情况下,我们只会将 SSL 证书用于 LDAP,所以我们可以将文件放在/etc/ldap/
(如果您是从源代码构建的,可以放在/usr/local/etc/openldap/
)。
我们关注的两个文件是newcert.pem
和clearkey.pem
。我们需要重命名并移动这两个密钥:
$ sudo mv cacert.pem /etc/ldap/example.com.cert.pem
$ sudo mv clearkey.pem /etc/ldap/example.com.key.pem
现在,我们需要为证书文件设置权限和所有权。由于我们没有给密钥添加密码短语,我们还应确保只有 OpenLDAP 用户能够读取该密钥文件:
$ sudo chown root:root /etc/ldap/example.com.*.pem
$ sudo chmod 400 /etc/ldap/example.com.key.pem
第一行将两个 PEM 文件的所有者和组更改为root
用户和root
组。第二行设置权限,使得只有文件所有者能够读取该文件,其他人无法访问。
如果您以非 root 用户运行 OpenLDAP(这样做是个好主意),那么这些文件应该由该用户而非 root 用户拥有;例如chown oenldap example.com.*.pem
。
安装 CA 证书
第三个任务是安装 CA 的公用证书,以便系统上的其他应用程序可以使用该证书来验证我们刚生成的证书的真实性。首先,我们需要将 CA 证书复制到 Ubuntu 本地的证书数据库。在此过程中,我们将为它提供一个用户友好的名称:
$ sudo cp cacert.pem /usr/share/ca-certificates/Example.Com-CA.crt
然后,编辑/etc/ca-certificates.conf
文件,并在文件末尾添加Example.Com.crt
。
最后,运行update-ca-certificates
:
$ sudo update-ca-certificates
Updating certificates in /etc/ssl/certs....done.
CA 证书现在已经安装。/etc/ssl/certs
目录现在是 CA 证书的权威来源。
注意
除了 Ubuntu 和 Debian 之外的 UNIX 和 Linux 系统可能没有update-ca-certificates
脚本。请查阅系统文档,了解如何在这些系统中更新证书数据库。
可选:清理
如果需要,您可以在 CA 目录中做一些清理。删除加密的密钥文件和证书请求文件,这两个文件都位于demoCA/
目录中:
$ rm newkey.pem newreq.pem
此外,确保clearkey.pem
不再出现在demoCA/
目录中。
现在我们准备好配置 OpenLDAP 以使用我们的新证书了。首先,我们将配置 StartTLS 支持,这是最简单的,然后我们将在 LDAPS 端口 636 上配置 SSL/TLS 支持。
配置 StartTLS
在前面的部分中,我们创建了新证书和密钥,并将这两个文件放置在/etc/ldap
目录中。在本节中,我们将设置 StartTLS(我们在本章的 StartTLS 部分已经介绍过)。配置 StartTLS 只需要在slapd.conf
文件中增加几行。
再次强调,StartTLS 是为 OpenLDAP 提供 SSL/TLS 安全性的一种标准方式(根据 RFC 4511)。出于安全原因,应在实际操作中提供对 StartTLS 的支持。
在 slapd.conf
文件中,在 BDB 数据库配置
部分之前,插入 SSL/TLS 选项:
###########
# SSL/TLS #
###########
TLSCACertificatePath /etc/ssl/certs/
TLSCertificateFile /etc/ldap/example.com.cert.pem
TLSCertificateKeyFile /etc/ldap/example.com.key.pem
基本上,我们只需要指定三个指令就可以使 StartTLS 工作:
-
第一个指令
TLSCACertificatePath
告诉 SLAPD 在哪里找到所有它需要用于验证证书的 CA 证书。绝对位置是,正如我们之前看到的,/etc/ssl/certs/
目录。 -
第二个指令
TLSCertificateFile
指定了已签名证书的位置。 -
第三个指令
TLSCertificateKeyFile
指定了相应密钥文件的位置,该文件具有证书的私有加密密钥。
注意
还有一些其他特定于 TLS 的指令,允许您对 TLS 连接提供详细的约束(例如可以使用哪些密码套件,以及是否需要客户端向服务器提供证书)。关于这些的完整文档可以在 slapd.conf
手册的 TLS 部分找到:man slapd.conf
。
这就是我们使 SLAPD 执行 StartTLS 所需的所有内容。重新启动 SLAPD 以使更改生效。
配置客户端 TLS
我们确实需要在 ldap.conf
中添加一两个指令——这是 OpenLDAP 客户端使用的配置文件。与 SLAPD 类似,我们需要将客户端指向新的 CA 证书的正确位置,以便它们可以验证服务器证书。
在 ldap.conf
文件的底部,我们可以添加适当的指令:
TLS_CACERTDIR /etc/ssl/certs
客户端将使用此指令来定位 CA 证书,以便检查从服务器获取的证书的数字签名。如果您知道您只会使用由特定 CA 签名的证书,可以使用 TLS_CACERT
指令指向特定的 CA 证书文件,而不是包含一个或多个证书的目录。
默认情况下,OpenLDAP 客户端始终对数字签名进行检查。如果服务器发送的证书由 /etc/ssl/certs/
(或 TLS_CACERTDIR
指向的任何目录)之外的 CA 签名,那么客户端将关闭连接并在屏幕上打印错误消息。
但有时,即使无法验证服务器的身份,也值得获得 TLS 的加密支持,因此正确的 CA 证书不可用。
在这种情况下,您可能需要更改 OpenLDAP 客户端执行身份验证检查的方式。例如,可能希望尝试验证证书,但即使没有适当的本地 CA,也希望继续连接。为此,在 slapd.conf
中使用以下指令:
TLS_REQCERT allow
在这种情况下,如果没有 CA 证书或者发送的证书无法验证,会话将继续,而不会显示错误消息。TLS_REQCERT
有几个不同的检查级别,从 strict
(始终验证)到 never
(甚至不尝试验证证书)不等。
此时,我们可以使用 ldapsearch
来测试连接。要指示客户端使用 StartTLS,我们需要使用 -Z
标志。但是,如果仅指定 -Z
,当客户端与服务器的 TLS 协商失败时,它将继续以明文进行事务。换句话说,使用 -Z
时,TLS 是优先的,但不是强制的。要使 TLS 成为必需,我们将向标志中添加一个额外的 z,使其变为 -ZZ
:
$ ldapsearch -LLL -x -W -D 'cn=Manager,dc=example,dc=com' -H \
ldap://example.com -ZZ '(uid=manny)'
这应该会提示输入密码,然后返回一个结果:
Enter LDAP Password:
dn: uid=manny,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
uid: manny
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
如果结果返回如下,则表示 TLS 配置成功。但由于 TLS 在设计上是严格的,配置可能会很困难。配置中的小错误(例如使用与证书中的 CN 字段不同的域名)可能会导致 TLS 无法正常工作。考虑以下示例:
$ ldapsearch -LL -x -W -D 'cn=Manager,dc=example,dc=com' -H \
ldap://localhost -ZZ '(uid=manny)'
ldap_start_tls: Connect error (-11)
additional info: TLS: hostname does not match CN in peer
certificate
在这种情况下,命令行中指定的主机名(localhost
)与证书中的 CN 字段(example.com
)中的主机名不同。尽管在此情况下,这两个域名托管在同一系统上,TLS 仍然不接受不匹配。
TLS 中的其他常见错误包括:
-
反转
TLSCertificateFile
和TLSCertificateKeyFile
指令的值 -
忘记安装 CA 证书(导致出现错误,表示无法验证服务器证书)
-
忘记在
ldap.conf
中正确设置客户端 CA 路径 -
在密钥文件(或证书文件)上设置读/写权限(或所有权),使得 SLAPD 服务器无法读取它
虽然 OpenLDAP 在许多方面可以宽容,但 TLS 配置并不是其中之一。在配置 TLS 和 SSL 时需要特别小心。
配置 LDAPS
现在我们已经配置了 TLS,接下来只需执行几个额外的步骤,以便在其专用端口上启用 SSL/TLS。运行专用的 TLS/SSL 保护的 LDAP 流量的传统端口是 636,LDAPS 端口。
大多数时候,使用 StartTLS 更好。然而,网络因素(如不支持 StartTLS 的客户端或要求强制阻塞允许非加密文本的端口的政策)可能需要使用 LDAPS。
请记住,LDAPS 和 StartTLS 都可以用于同一服务器。SLAPD 可以在专用端口上接受 LDAPS 流量,并继续在 LDAP 端口提供 StartTLS 功能。
注意
与 StartTLS 配置类似,此配置要求 slapd.conf
文件中设置 TLSCertificateFile
、TLSCertificateKeyFile
和 TLSCACertificateDir
指令。
要使 SLAPD 监听此端口,需要在启动 slapd
时传递一个额外的参数。在 Ubuntu 中,与其他基于 Debian 的发行版一样,配置参数可以在 /etc/defaults/slapd
文件中设置。在该文件中,我们只需要设置 SLAPD_SERVICES
。当启动脚本执行时,SLAPD 会启动此处列出的所有服务。
SLAPD_SERVICES="ldap:/// ldaps:///"
给定的代码指示 SLAPD 在所有可用的 IP 地址上监听默认的 LDAP(端口 389)和默认的 LDAPS(端口 636)。如果我们希望 SLAPD 只在一个地址上监听 LDAP 流量,但在所有地址上监听 LDAPS 流量,可以将上面的配置替换为:
SLAPD_SERVICES="ldap://127.0.0.1/ ldaps:///"
在这里,ldap://127.0.0.1/
告诉 SLAPD 仅在回环地址上监听 LDAP 流量,而 ldaps:///
则表示 SLAPD 应该在为该主机配置的所有 IP 地址上监听 LDAPS 流量。你需要重启 SLAPD,才能使这些更改生效。
类似地,如果你是从源代码构建并且想直接启动 slapd
,-h
命令行参数可以让你指定启动哪些服务:
/usr/local/libexec/slapd -h "ldap:/// ldaps:///"
配置 LDAPS 就这么简单。我们现在可以用 ldapsearch
来测试它:
ldapsearch -LL -x -W -D 'cn=Manager,dc=example,dc=com' -H \
ldaps://example.com '(uid=manny)'
这个 ldapsearch
和我们在测试 StartTLS 时使用的有两个关键区别:
-
-H
参数后指定的 URL 协议是ldaps://
,而不是ldap://
。 -
这里没有
-Z
或-ZZ
参数。这些参数告诉客户端发送 StartTLS 命令,而 SSL/TLS 通过专用端口时,不会识别 StartTLS 命令。
如果在进行给定的搜索时遇到错误,但 StartTLS 工作正常,首先要检查的是防火墙设置。通常,防火墙会允许通过 389 端口的流量,但会阻塞 636 端口。还可以确保服务器确实在监听 636 端口。你可以通过命令行使用 netstat –tcp -l
来检查,这会显示正在使用的端口列表。如果 LDAPS(636)没有出现,检查 /etc/defaults/slapd
配置,确保 SLAPD_SERVICES
指令设置正确。
使用 OpenSSL 客户端调试
在某些情况下,能够通过 LDAPS 连接到 SLAPD 并观察证书处理过程是有用的。openssl
程序可以通过其内置的 s_client
客户端应用程序实现这一点:
$ openssl s_client -connect example.com:636
-connect
参数接受主机名,后面跟一个冒号和端口号。当运行此命令时,openssl
将通过 SSL 连接到远程服务器,并执行证书协商。整个协商过程将显示在屏幕上。如果证书协商成功,openssl
将保持连接打开,你可以在命令行输入原始协议命令。要退出,只需按 CTRL+C。
现在我们已经使 StartTLS 和 TLS/SSL 都能正常工作。我们在这一节中只剩下一个简短的内容要讲解,之后我们将进入认证部分。
使用安全强度因子
运行 StartTLS 有其优势。它更容易配置,在许多方面更容易调试,且复杂的事务可以根据需要在明文和加密之间切换。
但有一个明显的缺点:当所有明文流量通过一个端口而所有加密流量通过另一个端口时,我们可以使用标准防火墙来阻止未加密的流量。但当两者都通过同一端口时,许多防火墙无法验证流量是否安全。
但 OpenLDAP 确实提供了一些工具来在 SLAPD 中实现这种安全性,而不是在防火墙中实现。
OpenLDAP 可以检查连接的完整性和加密状态,并根据这些特性为该连接分配一个安全强度因子(SSF)。SSF 是用于表示保护措施强度的数字表示。
大多数 SSF 数字只是反映了加密算法的密钥长度。例如,由于DES的最大密钥长度为 56,当使用 DES 保护连接时,SSF 为 56。Triple-DES (3DES),这是 Ubuntu 的 OpenSSL 配置中默认使用的加密算法,密钥长度为 112。因此,它的 SSF 也是 112。AES加密算法既强大又能快速计算,可以使用不同的密钥大小。AES-128 使用 128 位密钥,而 AES-256 使用 256 位密钥。因此,AES 的 SSF 将反映密钥大小。
有两个特殊的 SSF 数字:0 和 1。SSF 为 0 表示(正如预期的那样)没有实施任何安全措施。SSF 为 1 表示仅对连接进行完整性检查。
OpenLDAP 可以使用 SSF 信息来确定客户端是否可以连接到目录。SSF 信息还可以在 ACL 和 SASL 配置中使用,有效地允许构建复杂的规则,以确定客户端连接必须满足哪些条件,才能在目录上执行某些操作。
我们将在本章后面讨论 SASL 身份验证和 ACL,但现在我们将看看如何在slapd.conf
中的security
指令中使用 SSF,作为指定连接必须多安全才能访问数据库的一种方式。
安全性指令
security
指令可以在slapd.conf
中以两种不同的方式使用。如果它放在文件的顶部,在任何后端数据库定义之前,则它被放置在全局上下文中,并将应用于所有连接。另一方面,如果security
指令放在某个后端定义内,则它只会应用于该特定数据库。例如,考虑一个有两个后端的情况:
include /etc/ldap/schema/core.schema
modulepath /usr/local/libexec/openldap
moduleload back_hdb
# Other configuration directives ...
# DB 1:
database hdb
suffix "ou=Users,dc=example,dc=com"
# More directives for DB 1...
# DB 2:
database bdb
suffix "ou=System,dc=example,dc=com"
# More directives for DB 2...
这个slapd.conf
文件的部分示例定义了两个目录后端。现在,如果security
指令在第一个数据库定义之前使用(即在database hdb
那行之前),则它将全局应用于所有连接。
但是,如果我们想允许对数据库 2 的未加密连接,但只允许对数据库 1(它包含所有用户条目)进行加密连接,我们可以使用不同的security
指令:
include /etc/ldap/schema/core.schema
modulepath /usr/local/libexec/openldap
moduleload back_hdb
loglevel stats
# Other configuration directives ...
# DB 1:
database hdb
suffix "ou=Users,dc=example,dc=com"
security ssf=112
# More directives for DB 1...
# DB 2:
database bdb
suffix "ou=System,dc=example,dc=com"
security ssf=0
# More directives for DB 2...
请注意两个突出显示的行的添加——两个单独的 security
指令,每个后端数据库一个。
现在,重新启动目录(请注意 loglevel
设置为 stats
),我们可以使用 ldapsearch
测试安全参数。首先,我们将尝试使用非 TLS 连接搜索 Users
OU:
$ ldapsearch -x -W -D 'uid=matt,ou=Users,dc=example,dc=com' -b \
'ou=Users,dc=example,dc=com' '(uid=david)' uid
在日志中,我们看到如下条目:
conn=0 fd=12 ACCEPT from IP=127.0.0.1:48758 (IP=0.0.0.0:389)
conn=0 op=0 BIND dn="uid=matt,ou=Users,dc=example,dc=com" method=128
conn=0 op=0 RESULT tag=97 err=13 text=confidentiality required
conn=0 fd=12 closed (connection lost)
connection_read(12): no connection!
第三行指示服务器返回错误编号 13:confidentiality required
。这是因为我们没有采取任何措施来保护连接。使用简单的认证(未加密)并未使用 TLS/SSL 导致客户端连接的有效 SSF 为 0。
接下来,让我们使用启用 TLS 的相同搜索:
$ ldapsearch -x -W -D 'uid=matt,ou=Users,dc=example,dc=com' -b \
'ou=Users,dc=example,dc=com' -Z '(uid=david)' uid
请注意,在此示例中,包含 -Z
标志以发送 StartTLS 命令。现在,服务器日志显示:
conn=1 fd=12 ACCEPT from IP=127.0.0.1:44684 (IP=0.0.0.0:389)
conn=1 op=0 STARTTLS
conn=1 op=0 RESULT oid= err=0 text=
conn=1 fd=12 TLS established tls_ssf=256 ssf=256
conn=1 op=1 BIND dn="uid=matt,ou=Users,dc=example,dc=com" method=128
conn=1 op=1 BIND dn="uid=matt,ou=Users,dc=example,dc=com" mech=SIMPLE ssf=0
conn=1 op=1 RESULT tag=97 err=0 text=
关于此结果有几点需要注意。在第二行,OpenLDAP 报告正在执行 StartTLS。两行后,它报告:TLS established tls_ssf=256 ssf=256
。此行指示 TLS 连接的 SSF 为 256(因为连接正在使用 AES-256),连接的总 SSF 为 256。
如果您向下看几行,可以看到开始 BIND
的第二行,您会注意到另一个报告的 SSF:ssf=0
。为什么呢?
OpenLDAP 在连接的各个方面上测量 SSF。首先,正如我们上面所看到的,它检查网络连接的 SSF。基于它们的密码强度,TLS/SSL 连接被分配一个 SSF。
但在客户端对目录进行身份验证的绑定阶段期间,OpenLDAP 也测量认证机制的 SSF。简单(mech=SIMPLE
)认证机制不会加密密码,因此始终给予 SSF 为 0。
然而,连接的总 SSF 仍为 256,其中 TLS SSF 为 256,SASL SSF 为 0。
一个精细化的安全指令
到目前为止,我们查看的 security
指令很基础。它只要求总 SSF 至少为 112(3DES 加密),但我们可以使其更具体。
例如,我们可以简单地要求任何 TLS 连接至少有一个 128 位的密钥:
security tls=128
这将要求所有传入的连接使用具有强大(128 位或更高)的 TLS 密码。
注意
在某些情况下,定义将使用哪些 TLS/SSL 密码或密码族是可取的。这不能通过 security
指令完成。相反,您将需要使用 TLSCipherSuite
指令,允许您为 TLS/SSL 连接指定可接受的密码详细规范。
或者,如果我们只想为试图执行简单绑定(而不是 SASL 绑定)的连接定义一个强 SSF,那么我们可以为简单绑定指定一个 SSF:
security simple_bind=128
这将要求使用一些强大的 TLS 密码来保护认证信息。
提示
如果你计划允许简单绑定,并且你正在一个不安全的网络上运行,强烈建议你配置 TLS/SSL 并在绑定操作期间通过 security
指令要求 TLS 加密。
你还可以在 security
指令中使用 update_ssf
关键字来设置更新操作所需的 SSF。因此,你可以指定对于读取目录只需要低级加密,但在执行目录信息更新时必须使用高级加密:
security ssf=56 update_ssf=256
在接下来的章节中,我们将讨论 SASL 配置。你也可以使用 security
指令通过 sasl=
和 update_sasl=
关键字来为 SASL 绑定设置 SSF。
最后,在极少数情况下,当 OpenLDAP 监听本地套接字(即 ldapi://
)时,你可以使用 security transport=112
(或任何你需要的加密强度)来确保通过该套接字传输的流量是加密的。
到此为止,我们已完成对 SSL 和 TLS 的讨论。接下来,我们将继续研究安全性三大方面中的第二个:认证。
将用户认证到目录
正如我们在本书前面看到的,OpenLDAP 支持两种不同的绑定(或认证)方式。第一种是使用简单绑定。第二种是使用 SASL 绑定。在本部分中,我们将分别介绍这两种认证方法。
并不需要选择其中一个。OpenLDAP 可以配置同时支持这两种方式,这时由客户端决定使用哪种方法。简单绑定更容易配置(需要的配置非常少)。但 SASL 更安全且更灵活,尽管这些优点伴随着额外的复杂性。
绑定操作和认证过程的基础知识已在第三章的早期部分介绍。虽然我们将在这里回顾一些相关内容,但你可能会觉得回头看看该部分的内容很有帮助。
简单绑定
我们将首先看看的认证形式是简单绑定。从用户的角度来看,它不一定简单,但它肯定更容易配置,而且绑定过程对服务器来说也更简单,因为需要的处理较少。
要执行简单绑定,服务器需要两个信息:一个 DN 和一个密码。如果 DN 和密码字段都为空,则服务器会尝试以匿名用户身份绑定。
在简单绑定过程中,客户端连接到服务器并将 DN 和密码信息发送给服务器,而不会添加任何额外的安全措施。例如,密码并没有特别加密。
如果客户端通过 TLS/SSL 进行通信,那么整个事务将被加密,密码也会因此得到保护。如果客户端没有使用 TLS/SSL,则密码将以明文形式通过网络发送。这当然是一个安全问题,应该避免(可以通过使用上一节中讨论的 security
指令,或者使用 SASL 绑定代替简单绑定)。
客户端应用程序执行简单绑定有两种常见方法。第一种方法有时被称为 快速绑定。在快速绑定中,客户端提供完整的 DN(uid=matt,ou=users,dc=example,dc=com
)以及密码(myPassword
)。它比常见的替代方法(以匿名身份绑定并搜索所需的 DN)更快。
注释
Cyrus SASLAuthd 提供给其他应用程序 SASL 认证服务,它是第一个使用“快速绑定”术语的应用程序。SASLAuthd 是一个为应用程序提供 SASL 认证服务的有用工具。我们将在下一节中再次讨论它。在 OpenLDAP 文档中,根本没有使用“快速绑定”这一术语。
目录首先作为匿名用户,对客户端提供的 DN 的 userPassword
属性执行 auth 访问。在 auth 访问中,服务器会将提供的密码值与存储在目录中的 userPassword
值进行比较。如果 userPassword
值已加密(例如,使用 SSHA 或 SMD5),那么 SLAPD 会将用户提供的密码进行哈希处理,然后比较哈希值。如果值匹配,OpenLDAP 将绑定用户并允许其执行其他 LDAP 操作。
注释
当使用 -x
选项时,OpenLDAP 命令行客户端执行简单绑定。客户端要求你指定完整的用户 DN 和密码,然后它们会执行快速绑定。
这就是快速绑定。但是,还有第二种常见的简单绑定方法——一种旨在消除用户需要知道完整 DN 的要求的方法。
在第二种方法中(顺便说一下,这并不叫“慢绑定”),客户端应用程序要求用户只知道某个特定的唯一标识符——通常是 uid
或 cn
的值。客户端应用程序然后作为匿名用户(或另一个预配置的用户)绑定到服务器,并执行搜索,寻找包含匹配属性值的 DN。如果找到一个(且只有一个)匹配的 DN,它就会重新绑定,使用检索到的 DN 和用户提供的密码。
通常,使用简单绑定的客户端应用程序需要一个基础 DN。执行简单绑定的第二种方法需要一个附加信息,这在快速绑定中是不需要的:一个搜索过滤器。过滤器通常像这样 (&(uid=?)(objectclass=inetOrgPerson))
,其中问号(?
)由用户提供的值替换。
使用认证用户进行简单绑定
虽然当只需要用户 ID 或 CN 时用户更为便捷,但我们所看到的第二种方法可能会引发一个额外的担忧:为了执行搜索,匿名用户必须具有读取目录中所有用户记录的权限。这意味着任何人都可以连接到目录(记住,匿名用户没有密码)并执行搜索。
在许多情况下,这并不是问题。允许某人查看目录中所有用户的列表可能根本不会构成安全隐患。但在其他情况下,这种访问是不可接受的。
解决这个问题的一种方法是使用不同的用户(而不是匿名用户)来执行用户 DN 的查找。在上一章中,我们创建了这样一个账户。以下是我们使用的 LDIF 记录:
# Special Account for Authentication:
dn: uid=authenticate,ou=System,dc=example,dc=com
uid: authenticate
ou: System
description: Special account for authenticating users
userPassword: secret
objectClass: account
objectClass: simpleSecurityObject
这个账户的目的是登录到服务器并执行 DN 查找。换句话说,它执行与匿名用户相同的工作,但它增加了一些安全性,因为使用uid=authenticate
账户的客户端也必须具有相应的密码。
为了明确这一点,我们来看一个案例:一个配置为使用 Authenticate 账户的客户端,将一个自我标识为matt
,密码为myPassword
的用户进行绑定。
下面是逐步分析当以这种方式执行绑定操作时发生的情况:
-
客户端连接到服务器,并开始使用 DN
uid=authenticate,ou=system,dc=example,dc=com
和密码secret
执行绑定操作。 -
服务器作为匿名用户,将 Authenticate 密码
secret
与uid=authenticate,ou=system,dc=example,dc=com
记录中的userPassword
属性值进行比较。 -
如果上述步骤成功,那么客户端(现在已登录为 Authenticate 用户)将使用过滤器
(&(uid=matt)(objectclass=inetOrgPerson))
执行搜索。由于uid
是唯一的,搜索应该返回 0 或 1 条记录。 -
如果找到匹配的 DN(在我们这个例子中是
uid=matt,ou=user,dc=example,dc=com
),那么客户端将尝试以该 DN 重新绑定,并使用用户最初提供给客户端的密码(myPassword
)。 -
服务器作为匿名用户,将用户提供的密码
myPassword
与uid=matt,ou=user,dc=example,dc=com
的userPassword
属性值进行比较。 -
如果密码比较成功,那么客户端应用程序可以继续以
uid=matt,ou=user,dc=example,dc=com
身份执行 LDAP 操作。
这个过程较为繁琐,并且要求客户端应用程序配置绑定 DN 和 Authenticate 用户的密码信息,但它为匿名绑定和搜索增加了额外的安全层。
在本节中,我们探讨了三种执行简单绑定的不同方式。这些方法在特定情况下各有其用,且当与 SSL/TLS 一起使用时,简单绑定在密码通过网络传输时不会构成显著的安全威胁。
提示
slapd.conf 中的简单绑定指令
在 slapd.conf
中只有少数指令与简单绑定相关。默认情况下允许简单绑定。为了防止 SLAPD 接受简单绑定操作,你可以使用 require SASL
指令,这将要求所有绑定操作都为 SASL 绑定操作。此外,security
指令提供了 simple_bind=
SSF 检查,可用于要求对简单绑定操作设置最小的 SSF。这在 安全 指令 部分有更详细的说明。
本书后续章节将介绍几个使用简单绑定连接到目录的第三方应用程序。
但有时我们需要更安全的认证过程,或者简单绑定的绑定-查询-重新绑定方法对客户端来说过于复杂。在这种情况下,使用 SASL 绑定可能更好。
SASL 绑定
SASL 提供了第二种认证 OpenLDAP 目录的方法。SASL 通过用更强大的认证过程取代上述简单绑定方法来工作。
注意
SASL 标准在 RFC 2222 中定义(www.rfc-editor.org/rfc/rfc2222.txt
)。
SASL 支持多种不同的底层认证机制,从登录/密码组合到更复杂的配置,如一次性密码(OTP),甚至Kerberos票证认证。
虽然 SASL 提供了几十种不同的配置选项,但我们只介绍其中的一种。我们将配置 SASL 来进行 DIGEST-MD5 认证。它的设置稍微比某些 SASL 机制复杂,但不需要像 GSSAPI 或 Kerberos 那样详细的配置。
本章后面我们将把 SASL 工作与 SSL/TLS 工作结合起来,并使用SASL EXTERNAL 机制通过客户端 SSL 证书进行目录认证。
注意
Cyrus SASL 文档(位于 /usr/share/doc/libsasl2
或在线查看 asg.web.cmu.edu/sasl/
)提供了实现其他机制的信息。
在 DIGEST-MD5 认证中,用户的密码将由 SASL 客户端加密,只有加密后的密码会通过网络传输,然后由服务器解密并与明文密码进行比较。
使用 DIGEST-MD5 的优点是密码在网络上传输时得到保护。然而,缺点是密码必须以明文形式存储在服务器上。
与简单绑定的工作方式对比。在简单绑定中,密码本身在网络上传输时并未加密,但存储在数据库中的密码副本是以加密格式存储的(除非你对 OpenLDAP 进行了其他配置)。
请记住,当使用 SSL/TLS 时,所有通过连接传输的数据都将被加密,包括密码。
配置 SASL 比配置简单的绑定操作要复杂。配置 SASL 支持有两个部分:
-
Cyrus SASL 配置
-
配置 OpenLDAP
配置 Cyrus SASL
当我们在第二章安装 OpenLDAP 时,我们安装的其中一个软件包是 Cyrus SASL(该库名为 libsasl2
)。我们还需要 SASL 命令行工具,这些工具包含在 sasl2-bin
软件包中:
$ sudo apt-get install sasl2-bin
本软件包中包含了 saslpasswd2
程序以及 SASL 测试客户端和服务器应用程序。
现在我们准备开始配置了。
SASL 配置文件
SASL 库可以被多个应用程序使用,每个应用程序都可以拥有自己的 SASL 配置文件。SASL 配置文件存储在 /usr/lib/sasl2
目录中。在该目录下,我们将为 OpenLDAP 创建一个配置文件。文件 slapd.conf
看起来是这样的:
# SASL Configuration
pwcheck_method: auxprop
sasldb_path: /etc/sasldb2
注意
不要将位于 /usr/lib/sasl2
的 slapd.conf
与位于 /etc/ldap/
的主 slapd.conf
文件混淆。这是两个不同的文件。
和往常一样,所有以 #
开头的行都是注释。第二行决定了 SASL 如何检查密码。例如,SASL 配备了一个独立的服务器 saslauthd,它将处理密码检查。然而,在我们的情况下,我们希望使用 auxprop
插件,它自己进行密码检查,而不是查询 saslauthd
服务器。
最后一行告诉 SASL 密码数据库的位置(该数据库存储所有密码的明文版本)。此数据库的标准位置是 /etc/sasldb2
。
设置用户密码
在开始时,我们将把 SASL 密码存储在 /etc/sasldb2
数据库中。要向数据库添加密码,我们使用 saslpasswd2
程序:
$ sudo saslpasswd2 -c -u example.com matt
请注意,我们必须使用 sudo
来运行上述命令,因为密码文件属于 root 用户。sudo
和 saslpasswd2
都会提示你输入密码。
saslpasswd2
的 -c
参数表示如果用户 ID 尚未存在,则希望创建该用户 ID。-u example.com
设置 SASL 域。SASL 使用域作为划分认证名称空间的一种方式。客户端应用程序通常会向 SASL 提供三项信息:用户名、密码和域。默认情况下,客户端将发送它们的域名作为域。
通过使用域,可以为不同的应用程序或应用程序上下文提供相同用户名的不同密码。例如,example.com
域中的 matt
可以有一个密码,而 testing.example.com
域中的 matt
可以有不同的密码。
对于我们的目的,我们只需要一个域,并将其命名为 example.com
。运行给定的命令时,它将提示输入用户 matt
的密码,然后提示输入密码确认。如果密码匹配,它将把密码以明文形式存储在 SASL 密码数据库中。
现在我们准备配置 OpenLDAP。
为 SASL 支持配置 SLAPD
OpenLDAP 的 SASL 配置在服务器的 slapd.conf
文件中完成,在客户端的 ldap.conf
文件中完成。 本节中,我们将重点关注 SLAPD 服务器。
当 OpenLDAP 接收到 SASL 认证请求时,它会从客户端接收四个信息字段。这四个信息字段是:
-
用户名:此字段包含用户在认证时提供的 ID。
-
领域:此字段包含用户进行身份验证时使用的 SASL 领域。
-
SASL 机制:此字段指示使用了哪种认证系统(机制)。根据我们的 SASL 配置,应该是 DIGEST-MD5。
-
认证信息:此字段始终设置为
auth
,表示用户需要认证。
所有这些信息都被压缩成一个类似 DN 的字符串,看起来像这样:
uid=matt,cn=example.com,cn=DIGEST-MD5,cn=auth
上面字段的顺序与项目符号列表中的顺序相同:用户名、领域、SASL 机制和认证信息。但请注意,领域字段不是必须的,可能并不总是存在。如果 SASL 不使用任何领域信息,领域字段将被省略。
当然,我们的 LDAP 中没有像上面的 SASL 字符串那样的 DN 记录。因此,为了将经过身份验证的 SASL 用户与 LDAP 中的用户关联起来,我们需要设置一种方法,将上述类似 DN 的字符串转换为像目录中那些 DN 一样的结构化 DN。因此,我们希望将给定的字符串变成类似下面这样的格式:
uid=matt,ou=Users,dc=example,dc=com
进行映射有两种方式。我们可以配置一个简单的字符串替换规则,将 SASL 信息字符串转换为像最后一个那样的 DN,或者我们可以在目录中查找一个 uid
为 matt
的条目,然后如果找到匹配项,使用该匹配条目的 DN。
这两种方法各有优缺点。使用字符串替换更快,但不够灵活,可能不足以应对复杂的目录信息树。使用字符串替换时,可能需要连续使用多个 authz-regexp
指令,每个指令有不同的正则表达式和替换字符串。
另一方面,在一个拥有大量子树的目录中,查找用户可以更加灵活。但这会增加额外的 LDAP 树搜索开销,并且可能需要调整 ACL 以允许进行预认证搜索。
两种方法都使用 slapd.conf
中的相同指令:authz-regexp
指令。让我们从字符串替换方法开始,查看每种方法的示例。
在 authz-regexp
中使用替换字符串
authz-regexp
指令接受两个参数:一个正则表达式,用于从 SASL DN 类似字符串中提取信息,和一个替换函数(根据我们是使用字符串替换还是搜索而有所不同)。
对于我们的正则表达式,我们想从 SASL 信息中提取用户名,并将其映射到 DN 中的 uid
字段。我们不需要其他三个 SASL 字段中的任何信息,因此我们的正则表达式非常简单:
"^uid=([^,]+).*,cn=auth$"
这个规则从行的开头 (^
) 开始,查找以 uid=
开头的条目。接下来的部分 ([^,]+)
会将 uid=
后和逗号(,
)前的字符存储在一个名为 $1
的特殊变量中。这个规则的意思是“匹配尽可能多的字符(至少一个字符),这些字符不是逗号,并将它们存储在第一个变量($1
)中。”
此后,规则(使用 .*
匹配任何字符)跳过领域(如果有的话)和机制,然后寻找行尾的匹配项:cn=auth$
(其中美元符号 ($
) 表示行结束)。
一旦正则表达式执行完成,我们应该有一个变量 $1
,它包含用户的名称。现在,我们可以在替换规则中使用该值,将 uid
的值设置为 $1
的值。整个 authz-regexp
行如下所示:
authz-regexp "^uid=([^,]+).*,cn=auth$"
"uid=$1,ou=Users,dc=example,dc=com"
在 authz-regexp
指令之后,我插入了我们刚才查看的正则表达式。正则表达式之后是替换规则,指示 SLAPD 在该模板 DN 的 uid
字段中插入 $1
的值。
authz-regexp
指令可以放在 slapd.conf
文件中任何位置,只要它出现在第一个 database
指令之前。
由于 authz-regexp
是配置 SASL 所需的唯一指令,现在我们可以在命令行上测试 SLAPD,而不需要对 slapd.conf
做任何其他更改:
$ ldapsearch -LLL -U matt@example.com -v '(uid=matt)' uid
ldap_initialize( <DEFAULT> )
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt@example.com
SASL SSF: 128
SASL installing layers
filter: (uid=matt)
requesting: uid
dn: uid=matt,ou=Users,dc=example,dc=com
uid: matt
之前,我们已经使用了 -x
标志,结合 -W
和 -D
,执行了一个简单的绑定操作,使用了完整的 DN 和密码。
然而,使用 SASL 时,我们不需要完整的 DN。我们只需要一个简化的连接字符串。因此,我们不再使用 -x
、-W
和 -D
标志,而是使用 -U matt@example.com
。-U
标志接受一个 SASL 用户名和(可选的)领域。领域与用户名通过 @ 符号连接。所以,在给定的示例中,我们使用用户名 matt
和领域 example.com
进行连接。
接下来,ldapsearch
会提示输入密码(请参见示例中的高亮行)。这不是我们的 LDAP 密码,而是我们的 SASL 密码——也就是在运行 saslpasswd2
时创建的账户密码。
回顾一下,之前命令中发生的事情如下:
-
客户端正在连接到 SLAPD,请求进行 SASL 绑定。
-
SLAPD 使用 SASL 子系统(该子系统检查
/usr/lib/sasl/slapd.conf
文件中的设置)来告知客户端如何进行身份验证。在此案例中,它告诉客户端使用 DIGEST-MD5。 -
客户端将身份验证信息发送到 SLAPD。
-
SLAPD 执行
authz-regexp
中指定的转换。 -
然后,SLAPD 使用 SASL 子系统检查客户端的响应,并与
/etc/sasldb2
中的信息进行匹配。 -
当客户端身份验证成功时,OpenLDAP 执行搜索并将结果返回给客户端。
现在我们准备好使用 authz-regexp
来用特定的过滤器搜索目录了。
在 authz-regexp 中使用搜索过滤器
在这种情况下,我们希望搜索目录中与 SASL 绑定期间接收到的用户名(uid
)匹配的条目。回想一下,SASL 认证信息是以如下字符串的形式传入的:
uid=matt,cn=example.com,cn=DIGEST-MD5,cn=auth
在上一个例子中,我们直接将给定的映射到如下形式的 DN:
uid=<username>,ou=users,dc=example,dc=com.
但如果我们不知道,比如说,用户 matt
是否在 Users OU 或 System OU 中,该怎么办?一个简单的映射函数是行不通的。我们需要搜索目录。我们将通过修改 authz-regexp
指令中的最后一个参数来实现这一点。
我们新的 authz-regexp
指令如下所示:
authz-regexp "^uid=([^,]+).*,cn=auth$"
"ldap:///dc=example,dc=com??sub?(uid=$1)"
这个正则表达式与前一个示例中的相同。但是 authz-regexp
的第二个参数是一个 LDAP URL。
注意
有关编写和使用 LDAP URL 的概述,请参见附录 B。
这个 LDAP URL 指示 SLAPD 在 dc=example,dc=com
基础上进行搜索(使用子树(sub
)搜索),查找 uid
等于 $1
的条目,$1
被替换为从正则表达式第一参数中获取的值。如果用户 matt
尝试进行身份验证,例如,URL 将如下所示:
ldap:///dc=example,dc=com??sub?(uid=matt)
当 SLAPD 对我们的目录信息树进行搜索时,它将返回一个记录——即 DN 为 uid=matt,ou=Users,dc=example,dc=com
的记录。
这是使用 ldapsearch
的一个示例。它与前一节中使用的示例相同,即使我们使用 LDAP 搜索方法,它应该也会得到相同的结果:
$ ldapsearch -LLL -U matt@example.com -v '(uid=matt)' uid
ldap_initialize( <DEFAULT> )
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt@example.com
SASL SSF: 128
SASL installing layers
filter: (uid=matt)
requesting: uid
dn: uid=matt,ou=Users,dc=example,dc=com
uid: matt
关于 ACL 和搜索过滤器的说明
当 SLAPD 读取搜索过滤器时,它会执行对目录的搜索。但搜索是以匿名用户的身份进行的。这意味着我们需要确保匿名用户拥有使用该过滤器进行目录搜索所需的权限。
根据我们之前的示例,匿名用户需要能够在 dc=example,dc=com
子树中搜索 uid
值。我们在第二章中创建的 ACL 并未授予匿名用户此类权限。为了使搜索操作成功,我们需要向 ACL 中添加一条规则:
access to attrs=uid
by anonymous read
by users read
这条规则应该出现在 ACL 列表的顶部,它授予 anonymous
以及系统中任何已认证用户对 uid
属性的读取访问权限。在这个示例中,重要的是匿名用户获得了读取权限。
请记住,通过添加这条规则,我们使得未经身份验证的用户能够看到数据库中存在的用户 ID。根据目录数据的性质,这可能会带来安全问题。如果这是一个问题,您可以使用字符串替换方法(记住,您可以连续使用多个 authz-regexp
表达式来处理更复杂的模式匹配),或者通过构建更严格的 ACL 来减少 uid
字段的暴露。
在本章后续部分,我们将更详细地讨论 ACL。
映射失败
在某些情况下,authz-regexp
的映射可能会失败。也就是说,SLAPD 会使用搜索过滤器在目录中查找,但没有找到匹配项。然而,用户已通过身份验证,SLAPD 不会失败并停止绑定。
相反,发生的情况是用户将以 SASL DN 进行绑定。因此,有效的 DN 可能类似于:
uid=matt,cn=example.com,cn=digest-md5,cn=auth
即使目录中没有与该用户名对应的实际记录,也没有关系。客户端仍然可以访问该目录。
但这个 DN 也受到 ACL 的控制,因此你可以针对那些通过 SASL 认证但没有相应目录记录的用户编写访问控制。
去除指定领域的需要
在我们的配置中,所有用户都位于相同的领域example.com
。为了避免每次都输入用户名和领域,我们可以通过在slapd.conf
中添加以下指令来配置默认领域:
sasl-realm example.com
如果我们用这个新的修改重启服务器,我们现在可以运行ldapsearch
而不需要指定领域:
$ ldapsearch -LLL -U matt -v '(uid=matt)' uid
ldap_initialize( <DEFAULT> )
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
filter: (uid=matt)
requesting: uid
dn: uid=matt,ou=Users,dc=example,dc=com
uid: matt
这一次,传递-U matt
就足以进行身份验证。SLAPD 自动将默认领域插入到 SASL 信息中。
调试 SASL 配置
获取正确的 SASL 配置可能令人沮丧。提高调试能力的一种方法是配置日志记录,以便你可以看到 SASL 事务期间发生的情况。trace
调试级别(1
)可以用来观察 SASL 中的活动。你可以在slapd.conf
中设置调试级别为 trace(或直接设置为数字1
),或者你可以在命令行上将slapd
运行在前台:
$ sudo slapd -d trace
# some of the voluminous output removed...
slap_sasl_getdn: u:id converted to uid=matt,cn=DIGEST-MD5,cn=auth
>>> dnNormalize: <uid=matt,cn=DIGEST-MD5,cn=auth>
<<< dnNormalize: <uid=matt,cn=digest-md5,cn=auth>
==>slap_sasl2dn: converting SASL name uid=matt,cn=digest-md5,cn=auth
to a DN
slap_authz_regexp: converting SASL name
uid=matt,cn=digest-md5,cn=auth
slap_authz_regexp: converted SASL name to
uid=matt,ou=Users,dc=example,dc=com
slap_parseURI: parsing uid=matt,ou=Users,dc=example,dc=com
ldap_url_parse_ext(uid=matt,ou=Users,dc=example,dc=com)
>>> dnNormalize: <uid=matt,ou=Users,dc=example,dc=com>
<<< dnNormalize: <uid=matt,ou=users,dc=example,dc=com>
<==slap_sasl2dn: Converted SASL name to
uid=matt,ou=users,dc=example,dc=com
slap_sasl_getdn: dn:id converted to
uid=matt,ou=users,dc=example,dc=com
通过这个日志,我们可以看到初始的 SASL 字符串uid=matt,cn=DIGEST-MD5,cn=auth
,并观察它是如何被标准化、运行正则表达式并转换为uid=matt,ou=users,dc=example,dc=com
的。
ldapwhoami
客户端和slapauth
工具在调试 SASL 时也非常有用。下一节将给出使用ldapwhoami
评估authz-regexp
结果的示例。
使用客户端 SSL/TLS 证书进行身份验证
SASL 和 SSL/TLS 可以结合使用来执行SASL EXTERNAL 认证。在 SASL EXTERNAL 认证中,SASL 模块依赖于外部来源,在这种情况下是客户端的 X.509 证书,作为身份来源。
使用此配置,拥有适当签名证书的客户端可以绑定到 SLAPD,而无需输入用户名和密码,但这种方式仍然是安全的。
这是如何工作的?就像可以为服务器颁发 SSL/TLS 通信证书一样,也可以为用户或客户端颁发证书。我们已经讨论过,证书可以以安全的方式提供关于服务器的身份信息。客户端证书也可以发挥相同的作用。
认证,使用 SASL EXTERNAL 工作方式如下:
-
客户端和服务器使用 SSL/TLS 保护进行通信,可以使用 LDAPS 或使用 StartTLS
-
当服务器发送其证书时,请求客户端也提供一个证书
-
客户端发送自己的证书,其中包括以下内容
-
身份信息
-
一个公钥
-
服务器将识别的证书颁发机构签名
-
-
服务器在验证证书后,通过 SASL 子系统将身份信息传递给 SLAPD
-
SLAPD 然后使用该信息进行绑定
由于客户端发送的证书包含了验证客户端身份所需的所有信息,因此不需要登录/密码组合。
配置 SASL EXTERNAL 机制需要以下步骤:
-
创建一个新的客户端证书
-
配置客户端以发送证书
-
配置 SLAPD 以正确处理客户端证书
-
配置 SLAPD 以正确转换客户端证书中提供的身份信息
创建一个新的客户端证书
创建新的客户端证书与创建服务器证书没有显著区别。我们将使用本章早期创建的同一证书颁发机构。
首先,我们需要创建一个新的证书请求:
$ /usr/lib/ssl/misc/CA.pl -newreq
Generating a 1024 bit RSA private key
............++++++
..++++++
unable to write 'random state'
writing new private key to 'newkey.pem'
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----
You are about to be asked to enter information that will be
incorporated into your certificate request.
What you are about to enter is what is called a Distinguished
Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:Illinois
Locality Name (eg, city) []:Chicago
Organization Name (eg, company)
[Internet Widgits Pty Ltd]:Example.Com
Organizational Unit Name (eg, section) []:
Common Name (eg, YOUR name) []:matt
Email Address []:matt@example.com
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
Request is in newreq.pem, private key is in newkey.pem
这个过程就像之前的过程一样,只是字段是专门为代表此证书的用户填写的。例如,如果我们为芭芭拉生成这个证书,我们会用她的信息填写通用名称和电子邮件地址字段。
提示
通用名称字段应填写什么?
早些时候,我们使用 CN 字段存储域名。个人的 CN 字段应填写什么?一个选择是使用用户的全名。更实用的选择是使用用户 LDAP DN 中使用的标识符(如用户的uid
属性的值)。这样做可以更轻松地从证书映射到 LDAP 记录。
现在,我们有了新请求(newreq.pem
)和密钥(newkey.pem
)。接下来要做的是使用我们 CA 的数字签名对证书进行签名:
$ /usr/lib/ssl/misc/CA.pl -signreq
Using configuration from /usr/lib/ssl/openssl.cnf
Enter pass phrase for ./demoCA/private/cakey.pem:
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number:
ba:49:df:f5:8e:7e:77:c6
Validity
Not Before: Jul 4 03:28:28 2007 GMT
Not After : Jul 3 03:28:28 2008 GMT
Subject:
countryName = US
stateOrProvinceName = Illinois
localityName = Chicago
organizationName = Example.Com
commonName = matt
emailAddress = matt@example.com
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
Netscape Comment:
OpenSSL Generated Certificate
X509v3 Subject Key Identifier:
9A:97:8F:8C:95:1F:E0:6E:50:BD:DF:F4:C5:71:68:92:3F:A0:30:DD
X509v3 Authority Key Identifier:
keyid:6B:FB:66:33:5D:DB:32:40:42:D7:71:F7:F0:D0:7C:94:3E:8F:CD:58
Certificate is to be certified until
Jul 3 03:28:28 2008 GMT (365 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
unable to write 'random state'
Signed certificate is in newcert.pem
现在,我们将签名的证书存储在文件newcert.pem
中。
接下来要做的是将这些文件移动到用户方便的位置。在这种情况下,我们将在用户的主目录中创建一个新目录,并将文件移动到该目录中:
$ sudo mkdir /home/mbutcher/certs
$ sudo mv new*.pem /home/mbutcher/certs
$ sudo chown -R mbutcher:mbutcher /home/mbutcher/certs
在这三行中,我们为证书创建一个新目录。在这种情况下,新的certs/
目录位于用户的主目录中。
然后,我们将新创建的证书文件移动到新目录中。我们可以重命名这些文件,但目前使用通用名称即可。
最后,我们需要确保用户能够访问自己的证书。这可以通过chown
命令来完成。
证书已准备就绪。
配置客户端
接下来我们需要做的是配置客户端使用证书和密钥。这通过在用户的主目录下创建.ldaprc
文件来完成。
注意
.ldaprc 文件是ldap.conf
文件的“个人”版本。它支持ldap.conf
中通常包含的所有指令,此外还有几个特殊指令,比如TLS_CERT
和TLS_KEY
指令。
由于我是用户mbutcher
,我将在自己的主目录下创建这个文件:
$ cd /home/mbutcher
$ touch .ldaprc
现在我们可以编辑.ldaprc
文件。这个文件需要指明客户端正在使用 SASL EXTERNAL 机制。同时,它必须包含关于证书和密钥文件的指令。此外,指定 CA 证书的位置(甚至指定签署服务器证书的 CA 的特定证书)也是一个好主意,尽管通常这在全局层面通过ldap.conf
文件来完成。
.ldaprc
文件如下所示:
SASL_MECH EXTERNAL
TLS_CERT /home/mbutcher/certs/newcert.pem
TLS_KEY /home/mbutcher/certs/newkey.pem
TLS_CACERT /etc/ssl/certs/Example.Com-CA.pem
第一个指令SASL_MECH
指示客户端使用的 SASL 机制。在我们的例子中,客户端使用的是EXTERNAL
SASL 机制。
TLS_CERT
指令指向客户端签名的 X.509 证书的位置,TLS_KEY
指令指示客户端私钥文件的位置。
TLS_CACERT
指令指向用于签署服务器证书的特定证书。客户端库将使用该证书在 SSL/TLS 协商期间验证服务器的身份。
目前客户端已经准备好。接下来我们需要配置 SLAPD。
配置服务器
SLAPD 需要做一些工作,才能使 SASL EXTERNAL 机制生效:
-
它必须请求客户端的证书(否则客户端将不会提供证书)
-
它需要将客户端证书中给出的身份信息转换为在我们环境中有意义的 DN。
要让服务器请求客户端证书,只需要添加一条指令。在slapd.conf
文件的全局部分,在任何数据库指令指定之前,应添加TLSVerifyClient
指令:
TLSCACertificateFile /etc/ssl/certs/Example.Com-CA.pem
TLSCertificateFile /etc/ldap/example.com.cert.pem
TLSCertificateKeyFile /etc/ldap/example.com.key.pem
TLSVerifyClient try
只有高亮的那一行是新的。其他的行是我们在本章早些时候添加的。
TLSVerifyClient
决定 SLAPD 是否采取步骤请求和验证客户端证书。它有四个可能的值:
-
never
:永远不请求客户端证书。这是默认设置。如果没有请求证书,客户端就不会提供证书。因此,当TLSVerifyClient
设置为never
时,无法使用 SASL EXTERNAL 认证。 -
allow
:这将导致 SLAPD 请求客户端证书,但如果客户端没有提供证书,或者提供的证书无效(例如,无法验证签名),会话将继续进行。 -
try
:在这种情况下,SLAPD 将请求客户端提供证书。如果客户端未提供证书,连接将继续。然而,如果客户端提供的证书无效,连接将终止。 -
demand
:这将导致 SLAPD 要求客户端提供证书。如果客户端没有提供证书,或者提供的证书无效,连接会终止。
在最后的示例中,我们将TLSVerifyClient
设置为try
。这意味着,如果客户端提交了证书,它必须是有效的证书(并且有一个已知的 CA 签名),否则 SLAPD 将不允许连接。但它也允许客户端在没有证书的情况下连接(尽管这些客户端无法使用 SASL EXTERNAL 认证)。
如果我们希望强制客户端提供证书,那么我们应该使用demand
关键字而不是try
。
此时,我们已经正确配置了 SSL/TLS。接下来,我们需要添加一个额外的步骤:我们需要将证书提供的身份(即 DN)映射到目录用户的 DN。
注意
将证书 DN 转换为另一个 DN 不是严格必要的。用户可以使用证书 DN 进行绑定,即使它不在目录中。ACL 可以写成目标这些 DN。
我们创建的客户端证书中的 DN 看起来像这样:
dn:email=matt@example.com,cn=matt,o=example.com,l=chicago,\
st=illinois,c=us
请注意,这是一个长行。
证书中的 DN 包含我们在运行CA.pl -newreq
时输入的信息。我们要做的是将这个 DN 转换为对应的 LDAP 记录的 DN:uid=matt,ou=users,dc=example,dc=com
。
这个翻译是如何完成的?使用我们在 SASL 认证部分中研究过的authz-regexp
指令。
证书的身份字符串中有两个特别有用的字段来识别用户:email
和cn
。因此,简单的正则表达式可以捕获这两个字段:
^email=([^,]+),cn=([^,]+).*,c=us$
这将把电子邮件地址分配给$1
,将 CN 分配给$2
。
从这里开始,我们可以指定一个 LDAP URL,使用邮箱地址过滤器来查找 DN,或者可以将 CN 替换为 LDAP DN 中使用的 UID 字段(因为 CN 可以干净地映射到 UID)。
我们将使用第二种方法,并创建如下的authz-regexp
:
authz-regexp "^email=([^,]+),cn=([^,]+).*,c=us$"
"uid=$2,ou=Users,dc=example,dc=com"
这个指令将证书 DN 的 CN 值映射到 LDAP 授权 DN 中的 UID 属性。因此,当客户端连接并提供一个证书,证书的 DN 是dn:email=matt@example.com,cn=matt,o=example.com,l=chicago,st=illinois,c=us
时,SLAPD 会将其转换为 DN uid=matt,ou=users,dc=example,dc=com
。
现在我们已经准备好测试了。
使用 ldapwhoami 进行测试
测试此过程的理想客户端是ldapwhoami
。它将允许我们使用 SASL EXTERNAL 连接和绑定。此外,它将指示authz-regexp
是否将证书 DN 映射到我们的 LDAP DN。
重启 SLAPD 以加载更改后,我们可以测试服务器:
$ ldapwhoami -ZZ -H 'ldap://example.com'
Enter PEM pass phrase:
SASL/EXTERNAL authentication started
SASL username: emailAddress=matt@example.com,CN=Matt, \O=Example.Com,L=Chicago,ST=Illinois,C=US
SASL SSF: 0
dn:uid=matt,ou=users,dc=example,dc=com
Result: Success (0)
首先,让我们仔细看看输入的命令:
ldapwhoami -ZZ -H 'ldap://example.com'
-ZZ
标志要求 StartTLS 协商必须成功完成。仅使用一个Z
将尝试 StartTLS,但如果协商失败不会关闭连接。使用-ZZ
在尝试使用 SASL EXTERNAL 机制进行身份验证时总是一个好主意。
接下来,-H 'ldap://example.com'
参数提供了 SLAPD 服务器的 URL。记住,StartTLS 协商要正常工作,这里 LDAP URL 中的域名必须与服务器证书中的域名匹配。
当执行这个命令时会发生什么呢?首先,系统会提示用户输入密码短语:
Enter PEM pass phrase:
这个提示实际上是由 SSL/TLS 子系统(OpenSSL)生成的。回想一下,我们生成的密钥是由密码短语保护的。为了读取密钥文件,OpenSSL 子系统需要密码短语。
但我不是说过 SASL EXTERNAL 方法可以避免输入密码吗?是的,的确可以——但要实现这一点,我们需要像生成服务器证书时那样移除密钥中的密码短语:
openssl rsa < newkey.pem > clearkey.pem
然后,.ldaprc
中的TLS_KEY
指令需要调整,指向clearkey.pem
文件。
在某些情况下,移除密码短语可能是需要的,而在其他情况下则不推荐。请记住,移除密钥中的密码短语将使证书更容易被他人劫持。没有密码短语的密钥应该通过权限和其他手段进行严格保护。
一旦用户输入了密码短语,SASL 身份验证开始:
SASL/EXTERNAL authentication started
SASL username: emailAddress=matt@example.com,CN=Matt, \O=Example.Com,L=Chicago,ST=Illinois,C=US
SASL SSF: 0
如这里所见,使用了 SASL EXTERNAL 机制,并且 SASL 用户名被设置为emailAddress=matt@example.com,CN=Matt,O=Example.Com,L=Chicago,ST=Illinois,C=US
。最后,SASL 安全强度因子被设置为 0,因为没有使用 SASL 安全机制。相反,安全机制是外部的,位于 SASL 之外。由于我们使用带有 AES-256 加密证书的 SSL/TLS,整体 SSF 仍然是 256。
一个重要的细节是,SLAPD 会标准化 DN。在标准化形式下,DN 看起来像这样:
email=matt@example.com,cn=matt,o=example.com,l=chicago,st=illinois,\
c=us
emailAddress
属性已经转换为email
,所有大写字符串已经转换为小写。我们上面看到的authz-regexp
在这个标准化的 DN 版本上进行操作。
最后,输出的最后几行是 LDAP Who Am I?操作的结果:
dn:uid=matt,ou=users,dc=example,dc=com
Result: Success (0)
根据 SLAPD,客户端当前正在使用有效 DN 为uid=matt,ou=users,dc=example,dc=com
执行目录操作。这意味着我们的映射成功。
如果authz-regexp
映射没有成功,输出会是什么样的呢?它可能看起来像这样:
$ ldapwhoami -ZZ -H 'ldap://example.com'
Enter PEM pass phrase:
SASL/EXTERNAL authentication started
SASL username:
emailAddress=matt@example.com,CN=Matt,O=Example.Com,L=Chicago,
ST=Illinois,C=US
SASL SSF: 0
dn:email=matt@example.com,cn=matt,o=example.com,l=chicago,st=illinois,c=us
Result: Success (0)
高亮显示的部分展示了“我是谁?”操作的结果。返回的 DN 只是证书 DN 的标准化形式——而不是期望的 LDAP DN。
进一步了解 SASL
SASL 是一个灵活的身份验证处理工具。在这里我们只讨论了两种 SASL 机制:DIGEST-MD5 和 EXTERNAL。但还有许多其他的可能性。它可以与像 Kerberos 这样的强大网络身份验证系统一起使用。它可以利用像 Opiekeys 这样的安全一次性密码系统。还可以作为与更标准的密码存储系统(如 PAM(可插拔身份验证模块))的接口。
尽管这种配置超出了本书的范围,但有很多可用资源。SASL 文档(在 Ubuntu 上本地安装在 /usr/local/doc/libsasl/index.html
),以及 OpenLDAP 管理员指南(openldap.org
),都提供了有关不同 SASL 配置的更多信息。
现在我们将从身份验证转向授权,并关注 ACL(访问控制列表)。
使用 ACL 控制授权
我们已经讨论了连接安全性和身份验证。现在我们准备讨论安全性的最后一个方面:授权。我们特别感兴趣的是控制对目录树中信息的访问。谁应该能够访问记录?在什么条件下?他们应该能看到该记录的多少内容?这些就是我们将在本节中解决的问题。
ACL 基础知识
OpenLDAP 控制目录数据访问的主要方式是通过访问控制列表(ACL)。当 SLAPD 服务器处理来自客户端的请求时,它会评估客户端是否具有访问所请求信息的权限。为进行此评估,SLAPD 会顺序评估配置文件中的每个 ACL,按照适当的规则应用到传入的请求。
注意
在本章之前,我们已经讨论了使用简单绑定和 SASL 绑定的身份验证。ACL 提供授权服务,决定给定 DN 可以访问哪些信息。
在第二章的ACLs部分中介绍了 ACL。本节将扩展那里讨论的基本示例。
ACL 只是 SLAPD 的一个特殊配置指令(access
指令)。像某些其他指令一样,access
指令可以多次使用。SLAPD 配置中有两个不同的地方可以放置 ACL。首先,它们可以放置在数据库部分之外的全局配置中(即,配置文件的顶部附近)。放置在此级别的规则将全局应用于所有后端。在下一章中,我们将讨论一个目录有多个后端的情况。
其次,ACL 可以放置在后端部分(database
指令下的某个位置)。在这种情况下,ACL 仅在处理数据库内的信息请求时使用。在第二章中,我们将 ACL 放在了后端部分,并且没有创建任何全局 access
指令。
这一切在实践中是如何运作的?何时使用全局规则,何时使用特定后端规则?如果一个后端没有特定的 ACL,则将应用全局规则。如果后端确实有 ACL,则只有在没有任何后端特定规则应用的情况下,才会应用全局规则。如果请求是针对存储在任何后端中的记录(例如根 DSE 或 cn=subschema
条目)时,则仅会应用全局规则。
在其上下文中,ACLs 是自上而下进行评估的,从配置文件中的第一条指令开始,直到最后一条。所以,当测试特定后端规则时,SLAPD 会从列表中的第一条规则开始测试,并按顺序继续,直到找到匹配的停止规则,或者 SLAPD 到达列表的末尾。
在第二章中,我们将 ACL 直接放入 slapd.conf
配置文件中。在本节中,我们将把它们放在自己的文件中,并在 slapd.conf
中使用 include
指令,指示 SLAPD 加载 ACL 文件。这将允许我们将可能很长的 ACL 与配置文件的其他部分分开。
让我们快速浏览一下 ACL 的格式,然后我们将继续一些示例,帮助澄清 ACL 方法的复杂性。
一个访问指令如下所示:
访问
[资源]
by
[谁] [授予的 访问类型] [控制]
by
[谁] [授予的 访问类型] [控制]
# More 'by' clauses, if necessary....
一个 访问
指令可以有一个 to
短语,并且可以有任意数量的 by
短语。我们将首先查看 访问
部分,然后是 by
部分。
访问 [资源]
在 访问
部分,ACL 指定了此规则在目录树中要限制的内容。在给定的规则中,我们使用了 [resources]
作为此部分的占位符。ACL 可以通过 DN、属性、过滤器或它们的组合来进行限制。我们将首先查看如何通过 DN 限制访问。
使用 DN 的访问
要限制对特定 DN 的访问,我们可以使用如下内容:
access to dn="uid=matt,ou=Users,dc=example,dc=com"
by * none
注意
by * none
短语简单地拒绝任何人的访问权限。我们将在本章稍后讨论 by
短语时详细介绍这一点及其他规则。
该规则将限制对特定 DN 的访问。每当收到需要访问 DN uid=matt,ou=Users,dc=example,dc=com
的请求时,SLAPD 会评估此规则,以确定该请求是否被授权访问该记录。
限制对特定 DN 的访问有时是有用的,但除了 DN 访问限定符外,还有其他几个支持的选项对于更通用的规则制定非常有用。
可以限制对 DN 子树的访问,甚至通过 DN 模式进行限制。例如,如果我们想编写一个规则,限制对 Users OU 下条目的访问,我们可以使用如下的 访问
子句:
access to dn.subtree="ou=Users,dc=example,dc=com"
by * none
在这个示例中,规则限制了对 OU 及其下属记录的访问。这是通过使用dn.subtree
(或同义词dn.sub
)来实现的。在我们的目录信息树中,Users OU 子树下有多个用户记录。这些记录是 Users OU 的子级。例如,uid=matt,ou=Users,dc=example,dc=com
的 DN 就在子树中,尝试访问该记录将触发此规则。
除了dn.subtree
,还有三个其他关键字,用于为 DN 访问修饰符添加结构性限制:
-
dn.base
:限制对此特定 DN 的访问。这是默认值,dn.exact
和dn.baselevel
是dn.base
的同义词。 -
dn.one
:限制对此 DN 下方的任何条目的访问。dn.onelevel
是其同义词。 -
dn.children
:限制对此 DN 的子级(下属)条目的访问。这与子树相似,唯一不同的是,规则不限制给定的 DN 本身。
dn
子句接受另一个修饰符,可以用于进行复杂的模式匹配:dn.regex
。dn.regex
访问修饰符可以处理 POSIX 扩展正则表达式。以下是一个dn.regex
中简单正则表达式的示例:
access to dn.regex="uid=[^,]+,ou=Users,dc=example,dc=com"
by * none
这个示例将限制对具有uid=SOMETHING,ou=Users,dc=example,dc=com
模式的任何 DN 的访问,其中SOMETHING
可以是任何至少有一个字符长且不包含逗号(,
)的字符串。正则表达式是编写 ACL 的强大工具。在我们讨论by
短语之后,我们将在获取 更多 正则表达式一节中深入讨论它们。
使用 attrs 访问
除了通过 DN 限制对记录的访问,我们还可以限制对记录中一个或多个属性的访问。这是通过使用attrs
访问修饰符来实现的。
在我们看到的示例中,当我们限制访问时,我们是在限制记录级别的访问。attrs
限制作用于更细粒度的级别:它限制对记录中特定属性的访问。
例如,假设我们希望限制对目录信息树中所有记录的homePhone
属性的访问。可以使用以下访问短语来实现:
access to attrs=homePhone
by * none
attrs
修饰符接受一个或多个属性的列表。在给定的示例中,我们仅限制了对homePhone
属性的访问。如果我们还想阻止对homePostalAddress
的访问,可以相应地修改attrs
列表:
access to attrs=homePhone,homePostalAddress
by * none
假设我们想要限制对organizationalPerson
对象类中所有属性的访问。一种方法是创建一个长长的列表:attrs
=title
,x121Address
,registeredAddress
,destinationIndicator
,....但这种方法既费时、难以阅读,又显得笨重。
相反,有一种方便的简写符号表示法:
access to attrs=@organizationalPerson
by * none
然而,应该小心使用这种符号。此代码不仅限制对organizationalPerson
中明确定义的属性的访问,还限制对person
对象类中已经定义的所有属性的访问。为什么?因为organizationalPerson
对象类是person
的子类。因此,person
的所有属性都是organizationalPerson
的属性。
有时,限制对所有未被特定对象类要求或允许的属性的访问是有用的。例如,考虑一个情况,我们只想限制那些在organizationalPerson
对象类中未指定的属性。我们可以通过将at符号(@
)替换为感叹号(!
)来实现:
access to attrs=!organizationalPerson
by * none
这将限制对任何属性的访问,除非它们被organizationalPerson
对象类允许或要求。
有两个特殊的名称可以在属性列表中指定,但它们并不真正匹配任何属性。这两个名称是entry
和children
。所以我们有两种情况:
-
如果指定了
attrs=entry
,则限制该记录本身。 -
如果
attrs=children
,则限制此记录的子记录。
当只使用attrs
指定符时,这两个关键字并不特别有用,但当attrs
和dn
指定符一起使用时,它们会变得更加有用。
有时候,通过属性的值(而不是属性名)来限制访问会很有用。例如,我们可能希望限制对任何givenName
属性值为Matt
的访问。可以通过使用val
(值)指定符来实现:
access to attrs=givenName val="Matt"
by * none
和dn
指定符一样,val
指定符也具有regex
、subtree
、base
、one
、exact
和children
风格。
注意
使用val
指定符时,attrs
列表中最多只能有一个属性。val
指定符也无法对对象类列表起作用。
使用val.regex
,你可以使用正则表达式进行匹配。我们可以修改最后一个例子,限制对任何以字母M
开头的givenName
的访问:
access to attrs=givenName val.regex="M.*"
by * none
在属性值是 DN(例如groupOfNames
对象的member
属性)时,可以使用regex
、subtree
、base
、one
、exact
和children
风格,根据属性值中的 DN 来限制访问。
access to attrs=member val.children="ou=Users,dc=example,dc=com"
by * none
提示
指定替代匹配规则
默认情况下,val
比较使用的是相等匹配规则。然而,你可以选择不同的匹配规则,通过在val
后插入斜杠(/
),然后输入匹配规则的名称或 OID:access to attrs=givenName val/caseIgnoreMatch="matt"
。
使用过滤器进行访问
access
短语的一个较少使用但非常强大的功能是支持使用 LDAP 搜索筛选器来限制对记录的访问。我们在第三章开始时讨论搜索操作时已经看过 LDAP 筛选器的语法。在这里,我们将使用筛选器来限制对记录部分的访问。
筛选器提供了一种支持对整个记录进行值匹配的方法(而不仅仅是像 attrs
中那样匹配属性值)。例如,使用筛选器,我们可以限制访问所有包含 simpleSecurityObject
对象类的记录:
access to filter="(objectClass=simpleSecurityObject)"
by * none
这将限制对目录信息树中所有具有 simpleSecurityObject
对象类的记录的访问。可以在筛选器指定符中使用任何合法的 LDAP 筛选器。例如,我们可以限制对所有具有名字 Matt、Barbara 或姓氏 Kant 的记录的访问:
access to
filter="(|(|(givenName=Matt)(givenName=Barbara))(sn=Kant))"
by * none
这段代码使用了“或”(析取)操作符,表示如果请求需要访问名字为 Matt 或 Barbara 的记录,或者请求需要访问姓氏为 Kant 的记录,则应应用此规则。
组合访问指定符
我们已经看过三种不同的访问指定符:dn
、attrs
和 filter
。在前面的章节中,我们已使用过每种指定符。现在,我们将它们组合起来创建更具体的访问规则。
组合的顺序如下:
access to
[dn] [filter] [attrs] [val]
dn
和 filter
指定符排在前面,因为它们处理的是整个记录。接着是 attrs
(和 val
),它们在属性级别起作用。假设我们希望限制仅在记录具有 employeeNumber
属性的情况下才能访问 Users 组织单位中的记录。为此,我们可以使用 DN 指定符和筛选器的组合:
access to dn.subtree="ou=Users,dc=example,dc=com"
filter="(employeeNumber=*)"
by * none
此 ACL 仅在请求的是 ou=Users,dc=example,dc=com
子树中的记录,并且 employeeNumber
字段存在且有值时,才会限制访问。
类似地,我们可以限制对某个子树中记录的属性的访问。例如,假设我们希望限制对 description
属性的访问,但仅限于 System 组织单位中的记录。我们可以通过组合 DN 和属性指定符来实现:
access to dn.subtree="ou=System,dc=example,dc=com"
attrs=description
by * none
根据此规则,客户端可以访问 DN 为 uid=authenticate,ou=System,dc=example,dc=com
的记录,但无法访问该记录的 description
属性。
通过仔细组合这些访问指定符,可以精确地表述访问限制。我们将在继续研究 by
短语时看到更多应用实例。
通过 [who] [授予的访问类型] [控制]
by
短语包含三个部分:
-
who 字段表示允许访问访问短语中标识的资源的实体
-
访问字段(授予的访问类型)表示可以对资源执行的操作
-
第三个可选部分,通常被省略,是 控制字段。
为了理解这种区别,考虑一下我们在前面部分中使用的 by
子句:by * none
。在这个 by
子句中,who
字段是 *
(星号字符),访问字段是 none
。这个示例中省略了控制字段。
*
是通用通配符。它匹配任何实体,包括匿名和所有 DN。none
访问类型表示不应授予任何权限给 who
指定的实体。换句话说,by * none
表示不应授予任何人访问权限。
注意
在 slapd.conf
文件中通过 rootdn
指令指定的目录管理员(cn=Manager,dc=example,dc=com
)是一个例外。它不能被任何访问控制限制。因此,by * none
不适用于管理员。
我们将详细探讨 who
字段,但在此之前,让我们先来看看访问字段。
访问字段
客户端在访问某个条目或属性时,可以拥有六种不同的权限。此外,还有第七种权限,它表示移除所有权限:
-
w
:写入访问记录或属性。 -
r
:读取记录或属性的访问权限。 -
s
:搜索记录或属性的访问权限。 -
c
:访问以在记录或属性上执行比较操作。 -
x
:执行服务器端身份验证操作来访问记录或属性。 -
d
:访问有关记录或属性是否存在的信息('d' 代表 'disclose')。 -
0
:不允许访问记录或属性。这相当于-wrscxd
。
这七种权限可以在 by
子句中指定。要设置一个或多个访问权限,可以使用 =
(等号)。
例如,为了让服务器将记录的 givenName
字段与客户端指定的 givenName
进行比较,我们可以使用以下 ACL:
access to attrs=givenName
by * =c
这将允许任何客户端尝试执行比较操作。但这就是它唯一允许的操作。根据这个规则,没有人可以读取或写入此属性。实际操作中是怎样的呢?当我们使用 ldapsearch
客户端尝试读取 givenName
属性的值时,无法获取任何有关 givenName
的信息:
$ ldapsearch -LLL -U matt "(uid=matt)" givenName
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=matt,ou=Users,dc=example,dc=com
服务器返回的唯一信息是与过滤器匹配的记录的 DN。不返回 givenName
属性。
然而,如果我们使用 ldapcompare
客户端,我们可以请求服务器告诉我们 DN 是否有一个值为 'Matt' 的 givenName
字段:
$ ldapcompare -U matt uid=matt,ou=Users,dc=example,dc=com \
"givenName: Matt"
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
TRUE
ldapcompare
客户端将一个 DN 和一个属性/值对发送到服务器,并请求服务器将提供的属性值与服务器中该 DN 记录的属性值进行比较。
在这里,ldapcompare
客户端将请求 SLAPD 服务器查找uid=matt,ou=Users,dc=example,dc=com
的记录,并检查givenName
属性是否为'Matt'。服务器将返回TRUE
、FALSE
,或者(如果出现错误)UNDEFINED
。
在这种情况下,服务器回应了TRUE
。这表明服务器执行了比较,并且值匹配。ldapsearch
和ldapcompare
的组合示例应当能够说明 ACL 的工作原理:虽然服务器端允许进行比较操作,但客户端没有权限读取属性值。
可以在一个by
子句中授予多个访问权限。为了修改并允许对givenName
属性进行读取(r
)、比较(c
)和披露(d
),我们可以使用以下 ACL:
access to attrs=givenName
by * =rcd
现在,我们之前运行的ldapsearch
和ldapcompare
命令应该成功。
在某些情况下,权限是从其他 ACL 继承的(我们稍后会讨论)。在这种情况下,我们可以通过使用+
(加号)来添加,使用–
(减号)来移除特定权限,进行有选择的添加或移除。
例如,如果我们知道所有用户已经对所有属性拥有比较(c
)和披露(d
)权限,但我们只想为givenName
属性添加读取权限,我们可以使用以下 ACL:
access to attrs=givenName
by * +r
注意
一个授予比较和披露权限,并继续处理的访问控制可能类似于这样:access to attrs=givenName,sn by * =cd break
。这使用了break
控制指令,告知 SLAPD 继续处理 ACL。如果这个规则出现在 SLAPD 配置文件中,位于规则access to attrs=giveName by * +r
之上,那么对givenName
属性的请求将会有有效权限=rcd
。
同样,如果我们需要仅为givenName
属性移除比较操作,可以使用类似by * -c
的by
子句。
0
访问权限移除所有权限。它不能与+
或–
操作符一起使用,只能与=
操作符一起使用。以下 ACL 移除所有用户对givenName
属性的所有权限:
access to attrs=givenName
by * =0
这与by
子句相同:by * -wrscdx
。
这些访问控制适用于细粒度的权限控制,但有时使用快捷方式会更方便。OpenLDAP 提供了七个快捷方式,用于处理常见的访问控制配置:
关键字 | 权限 |
---|---|
none |
0 |
disclose |
d |
auth |
xd |
compare |
cxd |
search |
scxd |
read |
rscxd |
write |
wrscxd |
我们之前见过的none
关键字与=0
相同。通过查看其他关键字及其相关权限,可以看出一种模式:每个关键字会在前一个关键字的权限基础上添加一个新权限。因此,auth
拥有来自disclose
的=d
权限,加上x
权限,而compare
拥有来自auth
的=xd
权限,并添加了c
权限。底部的write
关键字具有所有权限。
因为这种通用的权限累积方式既能捕获常见的使用案例,又保持了较高的可读性,所以关键词比权限字符串更常用。在接下来的例子中,除非有特别的原因使用权限字符串,否则我们将使用关键词。
注意
在七个关键词中,disclose
、auth
、compare
、search
、read
和 write
可以加上两种前缀之一:self
和 realself
。self
前缀表示如果相关值指的是用户的 DN,那么用户可能会拥有某些权限。因此,selfwrite
表示只有当属性的值为用户的 DN 时,用户才拥有 =wrscxd
权限。
realself
前缀与 self
类似,但它附带了额外的规定,即 DN 不能被代理。这些前缀在处理组和其他基于成员的记录时特别有用。
例如,以下 ACL 只允许用户在 uniqueMember
属性包含该用户的 DN 时对其进行 write
操作:access to attrs=uniqueMember by users selfwrite
。
现在我们已经讲解了访问字段,接下来我们将讨论 who
字段。
who
字段
我们一直在 who
字段中使用 *
。然而,who
字段是 ACL 字段中最丰富的,提供了二十三种不同的形式,其中大部分可以组合使用。为了高效覆盖这些内容,我们将单独讲解主要形式,然后将相似的形式组合在一起作为一个整体来处理。
五种最常用的形式是 *
、anonymous
、self
、users
和 dn
。
*
和 anonymous
修饰符
*
修饰符,如我们所见,是一个全局匹配符。它匹配任何客户端,包括匿名用户。
anonymous
修饰符只匹配那些以匿名用户身份绑定到目录的客户端(有关匿名用户的详细信息,请参见第三章)。这指的是那些没有对目录进行身份验证的客户端。由于认证过程要求客户端以匿名身份连接,然后尝试作为 DN 绑定并使用特定密码,因此匿名用户几乎总是需要权限来执行 auth
操作,即客户端将 DN 和密码发送到目录,并要求目录验证这些信息是否正确。因此,你可能需要一个像这样的 ACL:
access to attrs=userPassword
by anonymous auth
这授予匿名用户执行认证操作的权限。请注意,每个 ACL 以隐式的短语结尾:by * none
。换句话说,如果权限没有明确指定,则不授予任何权限。
请注意,上述 ACL 不允许用户修改自己的密码。这正是 self
修饰符的作用所在。
self
修饰符
self
修饰符用于指定对一个 DN 自身记录的访问控制。因此,我们可以使用 self
修饰符来允许用户修改她或他的 userPassword
值:
access to attrs=userPassword
by anonymous auth
by self write
如果我们以 uid=matt,ou=Users,dc=example,dc=com
登录并尝试修改自己记录的 userPassword
值(dn: uid=matt,ou=Users,dc=example,dc=com
),SLAPD 将允许我们更改密码。但它不会(根据上述规则)允许我们修改其他人的 userPassword
值。
注意
self
指定符可以进一步使用 level
样式进行修饰。level
样式表示是否(以及多少个)父记录或子记录应被视为 self
的一部分。level
样式采用整数索引。正整数表示父记录,而负整数表示子记录。
因此,access to
ou
by
self.level{1}
write
表示当前的 DN 对其父级的 ou
拥有写权限。同样,access
to
ou
by
self.level{-1}
write
表示当前的 DN 对其任何直接子级的 ou
拥有写权限。
users
指定符
users
指定符表示任何已认证的客户端。匿名用户不包括在 users
中,因为它表示尚未进行身份验证的客户端。
当需要允许任何已认证的用户访问某些资源时,dn 指定符非常有用。例如,在企业目录中,我们可能希望允许所有用户查看彼此的姓名、电话号码和电子邮件地址:
access to attrs=sn,givenName,displayName,telephoneNumber,mail
by self write
by users read
dn 指定符
dn
指定符在 by
语句中与其在 access
to
语句中的作用类似。它指定一个或多个 DN。dn
拥有 regex
、base
、one
、subtree
和 children
修饰符,它们在这里的作用与在 access
to
语句中的作用相同。以下是使用几个不同 DN 模式的示例:
access to dn.subtree="ou=System,dc=example,dc=com" attrs=description
by dn="uid=barbara,ou=Users,dc=example,dc=com" write
by dn.children="ou=System,dc=example,dc=com" read
by dn.regex="uid=[^,]+,ou=Users,dc=example,dc=com" read
该规则限制对系统 OU 子树中任何对象的描述属性的访问。用户 uid=barbara,ou=Users,dc=example,dc=com
拥有对描述的写权限,而系统 OU 的任何子级用户则拥有读取权限。DN 形式为 uid=SOMETHING,ou=Users,dc=example,dc=com
的用户也拥有对描述的读取权限。
除了常规的 DN 修饰符外,by
语句中的 dn
还可以带有 level
修饰符。Level 允许 ACL 编写者精确指定 by
语句应该下降多少级。回想一下,dn.one
指定符表示任何直接位于指定 DN 下的记录将获得指定权限。例如,by
dn.one="ou=Users,dc=example,dc=com"
read
会授予 Users
OU 的任何直接后代读取权限。因此,uid=matt,ou=Users,dc=example,dc=com
会被授予读取权限,但 uid=jake,ou=Temp,ou=Users,dc=example,dc=com
不会被授予此权限,因为他位于第二级。dn.level
指定符让我们可以任意指定下降多少级。例如,by
dn.level{2}="ou=Users,dc=example,dc=com"
read
将允许 matt
和 jake
都获得读取权限。
注意
代理认证与真实 DN
如果 SLAPD 被设置为允许代理认证,其中一个 DN 用于认证,然后另一个 DN 用于执行其他目录操作,那么有时根据用于认证的 DN(即真实的 DN)编写 ACL 是很有用的。可以使用realdn
指定符来实现这一点。它的功能与dn
指定符相同,唯一的区别是它作用于真实的 DN。此外,realanonymous
、realusers
、realdnattr
和realself
可以用来基于真实的 DN 进行限制。详情请参阅slapd.access
手册页:man
slapd.access
。
组和成员
有时,授权组成员访问某个对象是很有用的。例如,如果你有一个管理员组,你可能希望授予该组的任何成员对系统 OU 中所有记录的写访问权限。
可能会有人认为,为组成员设置权限的方式就是在 ACL 中使用该组作为dn
指定符的值。但事实并非如此,因为dn
指定符是指整个组记录,且与组成员无关,每个组成员在目录中都有自己的记录。
但我们真正需要的是一种方法,可以搜索特定组记录中的成员属性,然后授予记录中列出的 DN 访问权限。group
指定符正好提供了这种能力。
组评估可以使用group
指定符来完成。其最简单的形式如下所示:
access to dn.subtree="ou=System,dc=example,dc=com"
by group="cn=Admins,ou=Groups,dc=example,dc=com" write
by users read
此 ACL 将授予cn=Admins,ou=Groups,dc=example,dc=com
组的成员对系统 OU 中任何内容的写权限,同时授予所有其他用户只读权限。
提示
顺序很重要
通过短语的 ACL 按顺序进行评估,默认情况下,当 SLAPD 找到匹配项时,它会停止处理by
短语。换句话说,如果上述规则中的by
短语被反转,LDAP 管理员的成员将永远无法获得写权限,因为他们总是会匹配到by
users
read
短语。在检查组成员资格之前,ACL 的评估就会停止。
但是上面的 ACL 只会对那些对象类为groupOfNames
,并且成员属性为member
的组起作用。这是因为groupOfNames
是默认的分组对象类,而member
是默认的成员属性。
当我们在第三章创建 LDAP 管理员组时,它不是groupOfNames
,也没有使用member
属性来表示成员关系。我们的记录如下所示:
dn: cn=LDAP Admins,ou=Groups,dc=example,dc=com
cn: LDAP Admins
ou: Groups
description: Users who are LDAP administrators
uniqueMember: uid=barbara,ou=Users,dc=example,dc=com
uniqueMember: uid=matt,ou=Users,dc=example,dc=com
objectClass: groupOfUniqueNames
我们使用了groupOfUniqueNames
对象类和uniqueMember
成员属性。为了让 ACL 匹配这些约束,我们需要在group
指定符中指定对象类和成员属性:
access to dn.subtree="ou=System,dc=example,dc=com"
by group/groupOfUniqueNames/uniqueMember=
"cn=LDAP Admins,ou=Groups,dc=example,dc=com" write
by users read
请注意高亮行中的更改。通过使用斜杠(/
),我们首先指定了对象类,然后是应该用来确定条目代表哪些成员的成员属性。当评估此by
短语时,SLAPD 将找到 DN cn=LDAP
Admins,ou=Groups,dc=example,dc=com
,检查它是否具有groupOfUniqueMembers
对象类,然后如果在uniqueMember
属性中指定了该 DN,则授予写权限。
使用这种扩展的表示法,你可以将其他基于成员的记录作为组来使用。例如,你可以使用organizationalRole
对象类和roleOccupant
成员属性。
与许多其他说明符一样,组说明符也支持regex
样式的正则表达式。因此,我们可以创建一个规则,允许任何 OU Groups 组的成员对系统 OU 拥有写权限,方法是扩展我们最后的示例:
access to dn.subtree="ou=System,dc=example,dc=com"
by group/groupOfUniqueNames/uniqueMember.regex=
"cn=[^,]+,ou=Groups,dc=example,dc=com" write
by users read
第二行和第三行应该合并为slapd.conf
中的一长行。组说明符中的正则表达式将匹配所有具有 CN 组件的 DN。对于所有此类条目,如果对象类是groupOfUniqueMembers
,则 SLAPD 将为该组中的uniqueMember
用户授予成员资格。
基于成员的记录访问
如果一个组成员需要修改他或她所属组的记录,该怎么办?允许这种操作的一种方法是使用dnattr
说明符。dnattr
说明符仅在客户端的 DN 出现在记录的某个属性中时授予访问权限。例如,以下示例允许一个组(groupOfUniqueNames
对象)的组成员(uniqueMember
)访问该组记录:
access to dn.exact="cn=LDAP Admins,ou=Groups,dc=example,dc=com"
by dnattr=uniqueMember write
by users read
第二行指定,如果客户端的 DN 出现在uniqueMember
属性的值列表中,则该客户端应获得对整个组记录的写入权限。根据第三行,其他用户将只能读取访问权限。
网络、连接与安全
SLAPD 可以在访问控制列表中使用客户端连接的信息(包括网络和安全信息)。该功能提供了一个额外的网络安全层,补充了 SSL/TLS 和 SASL。
以下是网络或连接级别的说明符:
-
peername
:用于指定 IP 地址范围(用于ldap://
和ldaps://
)。 -
sockname
:用于指定 LDAPI 监听器(ldapi://
)的套接字文件。 -
domain
:用于指定ldap://
和ldaps://
监听器的域名。 -
sockurl
:用于指定 LDAPI 监听器的套接字文件的 URL 格式(ldapi://var/run/ldapi
)。 -
ssf
:连接的整体安全强度因子(SSF)。 -
transport_ssf
:网络传输层的安全强度因子(SSF)。 -
tls_ssf
:SSL/TLS 连接的安全强度因子(SSF)。它适用于 LDAPS 监听器上的 SSL/TLS 连接,以及 LDAP 监听器上的 Start TLS。 -
sasl_ssf
:SASL 连接的安全强度因子(SSF)。
SSF 限定符(ssf
,transport_ssf
,tls_ssf
,sasl_ssf
)执行与 SSF 参数对 SLAPD security
指令相同的检查(在本章的第一部分中讨论)。然而,在这种情况下,SSF 可用于有选择地限制(或授予)对目录信息树部分的访问。SSF 限定符需要一个整数值来指定所需的安全级别。例如,使用 ssf=256
将要求连接的整体 SSF 为 256。 但 tls_ssf=56
将要求 TLS/SSL 层的 SSF 至少为 56,无论 SASL 配置的 SSF 是多少。有关 SSF 的更多信息,请参阅本章前面的标题为 使用 安全 强度 因子 的部分。
例如,以下 ACL 仅在客户端使用强大的 SASL 密码连接时,才会授予指定 DN 的 写入 权限:
access to dn.subtree="ou=users,dc=example,dc=com"
by self sasl_ssf=128 write
by users read
此规则仅允许用户在通过 SASL 认证并使用强度为 128(DIGEST-MD5)或更高的安全机制时修改自己的记录。所有其他用户只能获得读取权限。
提示
在 by 短语中组合限定符
如上规则所示,多个限定符可以在同一个 by 短语中使用。当这种情况发生时,所有限定符必须匹配,才能授予(或拒绝)指示的权限。
peername
限定符用于根据 IP 连接信息设置限制。它可以与网络安全中的其他组件(如 SSL/TLS)配合使用。peername
限定符可以接受一个 IP 地址或一系列 IP 地址(使用子网掩码),还可以指定源端口。
以下规则授予本地连接写入权限,授予本地局域网(地址从 10.40.0.0 到 10.40.0.255)上的连接读取权限,并拒绝所有其他客户端的访问。请记住,每条规则以隐式的 by
*
none
结尾。
access to *
by peername.ip=127.0.0.1 write
by peername.ip=10.40.0.0%255.255.255.0 read
请注意,peername
限定符要求使用 IP 风格来指定 IP 地址。它还支持 regex
风格(access
to
*
by
peername.regex="^IP=10\
.40\
.0\
.[0-9]+:[0-9]+$"
write
)以及 path
限定符来复制 sockname
的行为。
提示
IP 地址的正则表达式
对于 IP 地址,在正则表达式评估中使用的字符串格式如下:IP=<address>:<port>
。如果您正在创建精确的正则表达式,请确保处理 IP=
前缀和端口信息。像这样的正则表达式会失败:peername.regex="¹⁰.40.12[0-9]$"
。为什么?因为它缺少 IP=
和端口信息。
上述规则的一个更有用的版本是,如果连接不在特定范围内,则拒绝访问目录中的所有内容,但会将进一步的访问控制留给 ACL 列表中后面的规则。这可以通过使用下节中描述的特殊break
控制来实现。我们还可以添加 SSF 信息,这样通过非本地连接来的连接也必须使用强 SSL/TLS 加密。以下是规则:
access to *
by peername.ip=127.0.0.1 break
by peername.ip=10.40.0.0%255.255.255.0 tls_ssf=128 break
上述规则可能看起来难以阅读,但它的作用如下:
-
如果连接是本地的(来自 127.0.0.1 或
localhost
),则 SLAPD 允许进一步处理 ACL 列表(这就是break
的作用)。用户是否能够访问资源则依赖于其他规则。 -
如果连接来自局域网中的地址并且使用强 SSL/TLS 加密,那么 SLAPD 将继续处理 ACL 列表。
-
在任何其他连接情况下,连接都会被拒绝。例如,如果连接来自局域网,但未使用足够强的 SSL/TLS 加密,连接将被关闭。此行为是由隐式的
by
*
none
短语引起的。
有关break
控制的更多信息,请参见名为控制字段的章节。
有时,能够指定哪些域名(而不是哪些 IP 地址)应被授予访问权限更加有用。这可以通过使用domain
指定符来完成:
access to *
by domain.exact="main.example.com" write
by domain.sub="example.com" read
在上面的示例中,第二行为来自域名main.example.com
的任何客户端连接提供写入权限。第三行为example.com
及其任何子域名提供读取权限。所以,如果域名为test2.example.com
的服务器发起请求,它将在第三条规则下获得访问权限。然而,testexample.com
则不匹配,因为它不是example.com
的子域名——它是一个完全不同的域名。
当 SLAPD 在 ACL 中遇到域名指定符时,它会获取客户端连接的 IP 地址并进行反向 DNS 查找以获取主机名。鉴于此,在使用域名指定符时需要记住两点。
首先,反向 DNS 查找返回的名称可能与正向 DNS 查找返回的结果不同。例如,对ldap.example.com
进行 DNS 查找返回地址 10.40.0.23,而对 10.40.0.23 进行反向 DNS 查找返回mercury.example.com
。为什么会这样?
这是因为ldap.example.com
在 DNS 术语中是一个CNAME 记录,而mercury.example.com
是一个A 记录。实际上,这意味着ldap.example.com
是服务器真实(规范)名称mercury.example.com
的别名。实际结果是:当你使用domain
指定符编写 ACL 时,确保使用 A 记录域名,而不是 CNAME 记录名。否则,SLAPD 会将规则应用到错误的域名。
提示
查找 DNS 信息
有许多工具可以查找 DNS 信息。大多数 Linux 发行版,包括 Ubuntu Linux,都提供了用于命令行 DNS 查找的 host
和 dig
命令。host
命令提供简短的类似句子的信息,例如:ldap.example.com
is
an
alias
for
mercury.example.com
。与此相对,dig
命令提供详细的技术信息。
在考虑域名指定符时,第二点需要记住的是,它的可靠性低于使用 IP 地址信息。DNS 地址可以被伪造,这意味着网络上的另一台服务器可能会假冒 ldap.example.com
,并发送看起来像是来自真实 ldap.example.com
的流量给 SLAPD。
降低这种风险的一种方法是使用客户端 SSL/TLS 证书,并配置 SLAPD 要求客户端发送签名证书进行身份验证,然后才能执行任何其他目录操作。不幸的是,客户端证书不能通过 ACL 有选择地强制使用。相反,你需要在 slapd.conf
文件中使用 TLSVerifyClient
demand
指令。
sockname
和 sockurl
指定符用于使用 UNIX 本地套接字进程间通信(IPC)而不是网络套接字运行的服务器。这些指令可以用于限制使用 IPC 层而非通过 IP 网络连接的本地连接。
注意
运行 LDAPI 并不常见。通常只在不能或不应使用 IP 网络连接的情况下使用。在典型情况下,本地客户端通过 LDAP 连接到 SLAPD,使用 ldap://localhost/
URL,而不是使用 LDAPI。
例如,我们可以使用以下 ACL 仅允许本地(LDAPI)连接写入记录,而通过其他机制连接的用户只能读取记录:
access to dn.exact="uid=matt,ou=Users,dc=example,dc=com"
by sockurl="ldapi://var/run/ldapi" write
by users read
第二行表示只有通过特定 LDAPI 套接字文件连接的 LDAPI 连接才应获得对 DN 的写访问权限。所有其他客户端(用户
)将获得读取权限。
高级步骤:使用 set 指定符
除了我们刚才检查过的语法外,还有一种实验性的 by
短语类型——set 语法。set
语法可以用于创建一个简洁且强大的访问条件集。由于它允许使用布尔运算符,并且具有访问属性值的方法,单个 set
语法规则可以完成原本需要非常复杂的 ACL 才能实现的任务。
set
语法的基本思想是这样的。通过使用由条件组成的规则,SLAPD 创建了一个对象集,这些对象可以访问相关记录。如果对 set
指定符的评估结果是一个包含一个或多个成员的集合,则 by
短语被视为匹配,权限将被应用。另一方面,如果集合为空,SLAPD 将继续评估该规则的 by
短语,以查看是否能找到另一个匹配项。
提示
set
指定符使用与集合论中类似的操作。当使用集合指定符时,您可能会发现以集合论的角度思考非常有帮助,思考集合(项目的列表)和集合操作,如并集(&
)和交集(|
)。
这是一个简单的 ACL,使用set
指定符来复制group
指定符的行为。它只为 LDAP Admins 组中的客户端提供对系统 OU 中记录的写访问权限,其他所有客户端只能获得读访问权限:
access to dn.subtree="ou=System,dc=example,dc=com"
by set="[cn=ldap admins,ou=groups,dc=example,dc=com]/
uniqueMember & user" write
by users none
上面突出显示的第二行包含set
指定符,其中包含set
语句。方括号中的文本指定了一个 DN,即 LDAP Admins 组的 DN。为了访问uniqueMember
属性的值,我们将/uniqueMember
附加到 DN 上。当 SLAPD 展开时,它将包含 LDAP Admins
group
中所有uniqueMembers
的集合。在集合论表示法中(OpenLDAP 未使用此表示法,但有助于理解发生了什么),组成员的集合将如下所示:
{ uid=matt,ou=users,dc=example,dc=com ;
uid=barbara,ou=users,dc=example,dc=com }
LDAP Admins 组中有两个成员(两个uniqueMembers
)。
&
(与符号)运算符对两个集合执行并集操作。user 关键字展开为包含一个成员的集合:当前客户端的 DN。因此,如果我执行搜索,绑定为uid=matt,ou=users,dc=example,dc=com
,那么用户集合将只包含一条记录:
{ uid=matt,ou=users,dc=example,dc=com }
当&
运算符被应用时,它将生成两个集合的交集。也就是说,结果集合只会包含同时出现在原始两个集合中的成员。由于只有 UID 为matt
的记录同时存在于两个集合中,因此结果集合只会包含matt
的 DN:
{ uid=matt,ou=users,dc=example,dc=com }
结果集合非空,因此被视为匹配。集合评估的结果是,uid=matt,ou=users,dc=example,dc=com
将基于set
指定符获得访问权限。
注意
集合是区分大小写的,并且始终使用标准化的 DN 形式。这意味着集合中的 DN 应该始终是小写的。
然而,考虑一个情况,假设用户不是 LDAP Admins 组的成员。如果uid=david,ou=users,dc=example,dc=com
绑定,是否可以执行读写操作?当集合指定符运行时,第一个集合(组成员)将与上述相同:
{ uid=matt,ou=users,dc=example,dc=com ;
uid=barbara,ou=users,dc=example,dc=com }
但是,user 关键字会展开为这个:
{ uid=david,ou=users,dc=example,dc=com }
这两个集合的交集为空集,因此,在应用&
运算符后,结果集合是一个空集:
{ }
没有匹配项,因此这个by
子句不会应用。我们 ACL 中的最后一行(by
users
none
)将会应用,uid=david
将不会获得任何访问权限。
让我们来看另一个例子。我们将使用集合指定符来实现一条规则,即当客户端 DN 尝试访问记录 DN 时,只有当两个 DN 相同,才赋予写访问权限;否则,如果它们在同一 OU 中,则赋予读访问权限。否则,客户端 DN 将被拒绝访问记录 DN。以下是 ACL:
access to dn.subtree="dc=example,dc=com"
by set="this & user" write
by set="this/ou & user/ou" read
第一行表明这条规则将应用于dc=example,dc=com
的记录以及其下的所有内容。
第二行取自两个关键词生成的集合的交集:this
和user
。this
关键词扩展为包含请求记录 DN 的集合。user
关键词,如我们所见,扩展为客户端的 DN。
因此,如果客户端uid=david,ou=users,dc=example,dc=com
请求访问其自己的记录,结果集合操作如下:
{ uid=david,ou=users,dc=exampls,dc=com } &
{ uid=david,ou=users,dc=example,dc=com }
由于两个集合包含相同的成员,结果集合(两者的交集)为{
uid=david,ou=users,dc=example,dc=com
}
。最终集合非空,因此该用户将被授予写访问权限。
现在让我们来看一下给定 ACL 的第三行。只要请求的 DN 和客户端的 DN 在ou
属性上具有相同的值,这条规则就会返回一个非空集合。如果uid=david,ou=users,dc=example,dc=com
请求uid=matt,ou=users,dc=example,dc=com
的记录,SLAPD 将检查它们各自的 OU 属性值。
this/ou
标识的集合将被扩展,包含请求记录中所有 OU 属性的值(uid=matt,ou=users,dc=example,dc=com
的记录)。该集合为:
{ 'Users' }
请注意,在这种情况下,值不是 DN,而是字符串。集合可以对字符串以及 DN 执行匹配操作。
user/ou
标识的集合将被扩展,包含客户端记录中所有 OU 属性的值。uid=david,ou=users,dc=example,dc=com
的记录包含一个ou
属性值,结果集合将包含该一个属性值:
{ 'Users' }
SLAPD 将计算{
'Users'
}
与{
'Users'
}
的交集,结果为{
'Users'
}
。由于集合非空,uid=david,ou=users,dc=example,dc=com
将被授予访问uid=matt,ou=users,dc=example,dc=com
记录的权限。
set
指定符提供了一种在记录包含特定属性时仅授予访问权限的方法。如果我们只想对具有 title 属性的记录授予写访问权限,可以使用以下规则:
access to dn.child="ou=Users,dc=example,dc=com"
by set="this/title" write
在这个 ACL 中,如果请求的记录具有一个title
属性,那么上述规则的评估结果将是一个包含一个元素的集合。然而,如果记录没有 title 属性,那么结果集合将为空,写访问权限将不会被授予。
在我们的目录中,uid=matt,ou=users,dc=example,dc=com
的记录有以下 title 属性:
title: Systems Integrator
但uid=barbara,ou=users,dc=example,dc=com
的记录根本没有 title 属性。因此,如果请求的是uid=matt
的记录,基于上述 ACL,结果集合将是:
{ 'Systems Integrator' }
所以,如果一个经过认证的用户尝试访问uid=matt
的记录,SLAPD 将授予访问权限。相反,uid=barbara
的集合将是{}
,即空集合。因此,尝试访问uid=barbara
记录的用户将被拒绝访问。
使用类似的集合指定符,我们可以根据属性的存在性以及其值来授予对记录的访问权限:
access to dn.child="ou=Users,dc=example,dc=com"
by set="this/objectclass & [person]" write
根据上述规则,只有当条目具有 objectclass
属性且其值为 person
时,才会授予对 Users OU 中任何内容的写访问权限。请注意,在这种情况下,方括号用于定义字符串文字。
如果客户端尝试访问记录 uid=barbara,ou=users,dc=example,dc=com
,我们 set
语句的第一部分将计算出以下集合:
{ 'person' ; 'organizationalPerson' ; 'inetOrgPerson' }
这些是 uid=barbara
记录的三个对象类。另一部分 [person]
将扩展为以下集合:
{ 'person' }
当计算联合时,结果将是集合 {'person'}
,因此会授予写访问权限。
这些只是使用 set
指定符可以执行的一些基本操作。不幸的是,set
在 slapd.access
手册页中没有文档记录。然而,OpenLDAP 官方 FAQ-O-Matic 上有一篇详细且内容丰富的文章介绍了如何使用 set
:www.openldap.org/faq/data/cache/1133.html
。
控制字段
by
短语中的最后一个字段是控制字段。控制字段只有三种可能的值:stop
、break
和 continue
。如果未指定控制字段,则默认为 stop
。例如,by
*
none
与 by
*
none
stop
是相同的。
第一个值 stop
表示如果该特定的 by
条件匹配,则不应继续检查与之匹配的其他 ACL。考虑以下(虽然是人为构造的)情况:
access to attr=employeeNumber, employeeType, departmentNumber
by users=cd
by dn="uid=matt,ou=Users,dc=example,dc=com" +r
access to attr=employeeNumber
by users +w
如果我以 uid=matt,ou=Users,dc=example,dc=com
身份绑定并尝试修改我的 employeeNumber
,我会被允许吗?不会,我不会被允许。
我无法修改记录的原因是因为我只会拥有第一个 by
短语所授予的权限:by
users
=cd
(记住,by
users
=cd
与 by
users=cd
stop
是相同的)。一旦 SLAPD 看到我匹配第一个 ACL 的第一个 by
短语,它就会停止测试 ACL。因此,它永远不会执行授予我 DN +r
访问权限的规则,也不会执行授予所有用户对 employeeNumber
属性 +w
权限的规则。
这是 stop
控制的一个例子,它被所有三个规则隐式使用。
现在,如果我想确保在第一个 by
短语之后,SLAPD 继续评估 ACL 内的短语,我可以使用 continue
控制重新编写 ACL:
access to attr=employeeNumber, employeeType, departmentNumber
by users-=cd continue
by dn="uid=matt,ou=Users,dc=example,dc=com" +r
access to attr=employeeNumber
by users +w
在对这些规则进行相同测试后,DN uid=matt,ou=Users,dc=example,dc=com
将拥有 =cdr
权限。
continue
控制指示 SLAPD 继续处理 当前 ACL 中的所有 by
短语。然而,一旦它完成了对该 ACL 的评估,它将不再继续查找其他 ACL 中的匹配项。
为了告诉 SLAPD 查看不同的规则来进行匹配,我们必须使用break
控制。当 SLAPD 遇到以break
控制结尾的适用子句时,它会停止处理当前的 ACL,但会继续查看其他 ACL,看看它们是否适用。
因此,为了通过 ACL 获得写权限,我们希望使用以下 ACL:
access to attr=employeeNumber, employeeType, departmentNumber
by users=cd continue
by dn="uid=matt,ou=Users,dc=example,dc=com" +r break
access to attr=employeeNumber
by users +w stop
现在,当 UID 为matt
的用户尝试访问employeeNumber
时会发生什么呢?
首先,第一条 ACL 的by
短语将被评估,matt
将被授予=cd
权限。由于continue
控制,SLAPD 接着会检查第二个by
子句,这个子句也会匹配到matt
用户。因此,当第一条 ACL 处理完成时,matt
将拥有=rcd
权限。
由于break
控制,第二个 ACL 也会被评估,并且matt
将被授予+w
权限,因此他的最终权限将是=wrcd
。
使用continue
和break
控制语句是逐步处理权限的一种方法。在复杂的配置中,合理使用continue
和break
可以让维护 ACL 变得更加容易,并且减少 ACL 的总数。
从正则表达式中获取更多信息
在前面的章节中,我们已经看过了如何在access
to
短语和by
短语中使用正则表达式。但我们也可以将它们结合使用。我们可以在access
to
短语中存储匹配到的信息,然后在by
短语中使用这些信息。
为了临时存储access
to
短语中的匹配信息,我们可以用括号将正则表达式包裹起来。以下是一个示例:
access to dn.regex="ou=([^,]+),dc=example,dc=com"
by dn.children,expand="ou=$1,dc=example,dc=com" read
这个 ACL 只会在客户端的 DN 和记录的 DN 位于同一个目录树部分(即它们位于同一个 OU 中)时,才授予客户端读取记录 DN 的权限。
在给定的 ACL 的第一行中,我们使用括号捕获了正则表达式[^,]+
的匹配结果,它将成为 DN 的ou=
部分的值。再说一次,[^,]+
的意思是“匹配所有不是,
的字符”。
在第二行中,我们使用了dn.children
指定符,但加上了一个额外的关键字:expand
。expand
关键字告诉 SLAPD 将access
to
子句中的匹配项替换到这个短语中。
由于expand
关键字,变量$1
将被替换为第一行匹配的值。正则表达式中(
和)
之间捕获的所有内容将存储在$1
中。
变量名称按顺序分配。正则access
to
短语中的第一组括号将存储在$1
中。如果有第二组括号,里面的匹配信息将存储在$2
中,以此类推,对于每一组括号都如此。
例如,我们可能想要像这样的 ACL:
access to dn.regex="uid=([^,]+),ou=([^,]+),dc=example,dc=com"
by dn.children,expand="uid=$1,ou=$2,dc=example,dc=com" write
这条规则将授予客户端 DN 读取和写入其自己记录下属条目的权限,但禁止其他用户读取这些条目。
注意
地址簿有时通过将用户的地址存储为用户条目下的从属条目来在 OpenLDAP 中实现。在 OpenLDAP FAQ-O-Matic 中有一个示例:www.openldap.org/faq/data/cache/1005.html
请注意,第一行存储了两个变量。UID 存储在 $1
中,OU 存储在 $2
中。这些变量在第二行中被展开。
也可以在 by
短语中使用来自 access
to
短语的匹配项,作为正则表达式的一部分:
access to dn.regex="uid=[^,]+,ou=([^,]+),dc=example,dc=com"
by dn.regex="uid=[^,]+,ou=$1,dc=example,dc=com" write
在第一行中,仅捕获并存储第二个正则表达式的结果,并将其存储在变量中。第二行也包含一个正则表达式,并利用 $1
变量从第一行中获取 OU 的值。请注意,dn.children,expand
已被 dn.regex
替代。正则表达式不需要添加 expand
关键字。
该规则授予客户端 DN 对目录树中同一 OU 下的任何用户记录的写入访问权限。
我们已经在这些 ACL 中看了一些简单但有用的正则表达式。但还可以构建更复杂的正则表达式,使 ACL 更加强大。当你编写更高级的正则表达式时,你可能会发现一些其他的信息源很有帮助。除了 slapd.access
手册页外,POSIX 扩展正则表达式手册页(man
regex
)也可能很有用。
调试 ACL
调试 ACL 可能令人沮丧。它们复杂、安全敏感,并且需要详细的测试。但有三种工具可以使调试和测试过程变得更容易。
第一个工具就是 ldapsearch
命令行客户端。它可以用于精心编写过滤器,专门用于测试 ACL 的处理。ldapcompare
工具在需要测试比较操作时也很有用。
但充分利用 LDAP 的日志指令也是很有帮助的。trace
和 acl
调试级别都提供有关 ACL 处理的详细信息。例如,acl
级别会记录每次 ACL 评估。这对于确定哪些规则被执行以及何时执行非常有用。我们发现 trace
调试级别也很有用,因为它提供了每次评估是如何执行的的信息,包括正则表达式是如何展开的。
提示
在前台运行 SLAPD
有时通过将 SLAPD 以前台模式运行,而不是作为守护进程运行,并将调试和日志信息打印到标准输出上来测试 ACL 会更容易。例如,我们可以通过这种方式打印 ACL 和 trace 调试信息:slapd
-d
"acl,trace"
。请注意,你需要以合适的用户身份(如 openldap
)运行此命令。要终止该进程,可以使用 Ctrl-C 键盘组合。
最后,slapacl
命令行工具提供了一个注重细节的工具,用于直接评估 ACL。由于它不通过 LDAP 协议连接到 SLAPD 服务器,因此它允许直接测试 ACL。
例如,我们可以检查特定的 SASL 用户matt
是否可以访问记录cn=LDAP
Admins,ou=Groups,dc=example,dc=com
并读取description
属性的值:
$ slapacl -U matt -b "cn=LDAP Admins,ou=Groups,dc=example,dc=com" \
"description/read"
-U
matt
参数指定了 SASL 用户名。-b
"cn=LDAP
Admins,ou=Groups,
dc=example,dc=com"
参数指示我们希望测试的记录,最后一个字段"description/read"
指示属性和访问级别。如果 ACL 允许读取访问,它将返回ALLOWED
,否则返回DENIED
。
同样,我们可以测试其他 LDAP 操作。例如,我们可以测试用户是否有权限进行compare
操作:
$ slapacl -U matt -b "uid=matt,ou=Users,dc=example,dc=com"
"uid/compare"
authcDN: "uid=matt,ou=users,dc=example,dc=com"
compare access to uid: ALLOWED
在这个例子中,我们已经包含了响应内容。第一行响应显示了 SASL DN 是如何解析的,第二行显示了对uid
的比较访问被允许。
slapacl
程序本质上运行自己的 SLAPD,因此可以设置为将完整的处理日志打印到屏幕上。例如,要启用跟踪调试,我们只需将-d
trace
参数添加到给定命令中:
$ slapacl -U matt -b "uid=matt,ou=Users,dc=example,dc=com" -d trace
"uid/compare"
slapacl init: initiated tool.
slap_sasl_init: initialized!
hdb_back_initialize: initialize HDB backend
hdb_back_initialize: Sleepycat Software: Berkeley DB 4.3.29:
(September 6, 2005)
bdb_db_init: Initializing HDB database
>>> dnPrettyNormal: <dc=example,dc=com>
# LOTS of lines deleted...
<<< dnPrettyNormal: <uid=matt,ou=Users,dc=example,dc=com>,
<uid=matt,ou=users,dc=example,dc=com>
entry_decode: ""
<= entry_decode()
compare access to uid: ALLOWED
slapacl shutdown: initiated
====> bdb_cache_release_all
slapacl destroy: freeing system resources.
如您所见,slapacl
在这种情况下提供了详细的评估信息。
使用 LDAP 命令行客户端、详细日志记录和slapacl
命令,可以有效地调试和测试 ACL。
一个实际的例子
在本章的这一部分,我们对 OpenLDAP 中的 ACL 进行了低级别的研究。我们已经覆盖了 ACL 系统的许多细节。现在是时候将我们所学的内容应用到实践中,为我们的目录信息树创建一组通用的 ACL 了。
在第二章中,我们在slapd.conf
文件中创建了一组基础的 ACL。以下是我们当时创建的内容:
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
by * none
access to *
by self write
by * none
现在,我们将创建一组新的、更实际的 ACL。
我们首先要做的是将 ACL 从slapd.conf
中移出来,放到一个单独的文件acl.conf
中。这将使得 ACL 的长列表与我们其余的配置分开。为此,我们将用include
指令替换上面的 ACL:
########
# ACLs #
########
include /etc/ldap/acl.conf
当 SLAPD 启动时,它会在include
语句出现的位置包含/etc/ldap/acl.conf
的内容。请记住,ACL 是与后端特定的。每个不同的数据库可以有自己的 ACL(并且多个数据库可以在同一个slapd.conf
文件中定义)。因此,将include
指令放在slapd.conf
文件中的数据库directive
之后是很重要的。
现在我们将开始编辑acl.conf
文件。我们将编写的规则将是简单的,适用于大多数目录用户都被允许查看目录中大多数信息的目录。一个更高安全性的目录可能会有一份更复杂的 ACL 列表。
由于 ACL 是按从上到下的顺序进行评估的,我们需要仔细制定规则,以确保重要的限制能够立即生效。
如果存在基于网络的访问规则,它们通常应该出现在 ACL 列表的顶部,以便首先评估。例如,如果我们希望限制当主机不在我们的局域网内时对整个数据库的访问,我们可以使用以下规则:
access to *
by peername.ip=127.0.0.1 none break
by peername.ip=10.40.0.0%255.255.255.0 none break
根据此规则,只有来自本地主机(127.0.0.1)和我们 10.40.0.0 子网内部的访问将被允许访问目录。由于指定了break
控制,后续规则可能会修改none
权限,从而授予客户端更多权限。所有其他连接将立即关闭。
接下来,我们希望授予 LDAP 管理员组成员对dc=example,dc=com
树中所有内容的写访问权限:
access to dn.subtree="dc=example,dc=com"
by group/groupOfUniqueNames/uniqueMember=
"cn=LDAP Admins,ou=Groups,dc=example,dc=com" write
by * none break
这立即授予 LDAP 管理员组成员写访问权限。然而,对于所有其他客户端,SLAPD 将继续处理。
注意
目录管理器不需要编写 ACL,slapd.conf
指令rootdn
中指定的 DN 始终具有对目录信息树的完全访问权限,ACL 对该用户没有任何作用。
接下来,我们希望确保userPassword
字段对匿名用户可用,以便进行身份验证。我们还希望允许用户修改自己的密码,但除此之外,我们希望userPassword
对其他人不可读写。请注意,根据前面的规则,LDAP 管理员也能够修改用户的密码。
access to attrs=userPassword
by anonymous auth
by self write
在某些情况下,其他用户可能也需要对密码进行auth
访问,这时您可能需要将by
users
auth
添加到给定的列表中。
如果我们在authz-regexp
指令中使用ldap://
URL 形式进行 SASL 绑定,我们还需要授予uid
属性的访问权限。这是因为 LDAP URL 中的过滤器是以匿名身份运行的(参见为 SASL 支持配置 SLAPD小节的讨论)。
此外,我们不希望允许用户尝试修改他们自己的uid
,因为uid
在 DN 中被使用:
access to attrs=uid
by anonymous read
by users read
现在,匿名用户和所有经过身份验证的用户将能够访问他们有权限访问的目录中任何记录的uid
属性。
还有一些其他属性,我们不希望用户能够修改——即使是在他们自己的记录中。
我们不希望用户尝试修改他们的 OU 属性,因为 OU 属性也用于 DN 中。我们也不希望他们能够修改他们的employeeNumber
或employeeType
:
access to attrs=ou,employeeNumber,employeeType by users read
我们有一个特殊账户uid=Authenticate,ou=System,dc=example,dc=com
,该账户偶尔用于帮助进行绑定请求。此用户不应有权访问除了我们指定的内容之外的任何其他内容:
access to *
by dn.exact="uid=Authenticate,ou=System,dc=example,dc=com"
none
by users none break
同样,最后一行指示 SLAPD 继续处理没有认证账户的用户的 ACL。这一行还将阻止匿名用户浏览树中的其他部分,因为末尾的隐式规则 by
*
none
会拦截匿名用户。
注意
uid=Authenticate
用户在之前的规则中已被授权访问 uid
属性,这个属性是该账户用于查找绑定所需的用户信息的。
假设我们不希望普通用户(位于 Users OU 中的 DNs)能够访问目录中的 System OU 记录(通常用于系统账户)。我们可以通过以下规则实现这一点:
access to dn.subtree="ou=System,dc=example,dc=com"
by dn.subtree="ou=Users,dc=example,dc=com" none
by users read
这拒绝了用户 OU 中的用户访问权限,但允许其他用户(如系统账户)访问这些记录。
我们还希望给予每个用户读取和写入自己记录的权限,但限制其他人访问这些记录。这使得用户可以在目录中存储自己的信息(如通讯录):
access to dn.regex="^.*,uid=([^,]+),ou=Users,dc=example,dc=com$"
by dn.exact,expand="uid=$1,ou=Users,dc=example,dc=com write
最后,我们需要的最后一条规则是默认规则。这个规则应该回答“当没有其他规则匹配时,我们希望发生什么?”的问题。我们希望用户能够修改自己的记录并查看他人的记录:
access to *
by self write
by users read
现在我们的 ACL 列表已完整。总体来看,它们是这样的:
#################################################
# ACLs
# These are ACLs for the first database section
# of the slapd.conf file found in this directory
#################################################
##
## Restrict by IP address:
access to *
by peername.ip=127.0.0.1 none break
by peername.ip=10.40.0.0%255.255.255.0 none break
## Give Admins immediate write access:
access to dn.subtree="dc=example,dc=com"
by group/groupOfUniqueNames/uniqueMember="cn=LDAP
Admins,ou=Groups,dc=example,dc=com" write
by * none break
## Grant access to passwords for auth, but allow users to change
## their own.
access to attrs=userPassword
by anonymous auth
by self write
## This rule is needed by authz-regexp
## (Note: Since uid is used in DN, user cannot change its own uid.)
access to attrs=uid
by anonymous read
by users read
## Don't let anyone modify OUs, employee num or employee type.
access to attrs=ou,employeeNumber,employeeType by users read
## Stop authentication account from reading anything else. This also
## stops anonymous.
access to *
by dn.exact="uid=Authenticate,ou=System,dc=example,dc=com"
none
by users none break
## Prevent DNs in ou=Users from seeing system accounts
access to dn.subtree="ou=System,dc=example,dc=com"
by dn.subtree="ou=Users,dc=example,dc=com" none
by users read
## Allow user to add subentries beneath its own record.
access to dn.regex="^.*,uid=([^,]+),ou=Users,dc=example,dc=com$"
by dn.exact,expand="uid=$1,ou=Users,dc=example,dc=com" write
## The default rule: Allow DNs to modify their own records. Give
## read access to everyone else.
access to *
by self write
by users read
尽管这些规则肯定无法满足所有需求,但它们为平衡目录的安全性和可用性提供了一个良好的起点。此外,它们为本书后续内容的操作奠定了基础。
在本书的后续章节中,我们将再次讨论并微调这些 ACLs,以支持更多功能,如目录复制。
总结
本章的重点是 OpenLDAP 的安全性,我们已经覆盖了很多内容。我们从连接级别的安全性开始,配置了我们的目录服务器的 SSL/TLS 加密。我们使用了标准 LDAP 端口上的 StartTLS,并且在端口 636 上配置了较旧的(LDAP v2)LDAPS 协议。接着,我们探讨了 LDAP 身份验证的过程。在这一部分,我们讨论了简单绑定和 SASL 绑定。最后,我们详细审视了访问控制列表(ACLs),并以一组基本的 ACLs 结束了本章。
在下一章中,我们将深入探讨 OpenLDAP 的 SLAPD 服务器的高级配置。我们将配置服务器以托管多个后端数据库,并通过目录覆盖来为我们的 SLAPD 服务器添加强大的附加功能。
第五章:高级配置
在上一章中,我们讨论了如何通过 SSL/TLS、简单认证和 SASL 认证以及基于 ACL 的授权规则来保护我们的 OpenLDAP 服务器。所有这些措施都通过 SLAPD 的配置文件实现。在本章中,我们将介绍 SLAPD 的一些其他高级功能,包括:
-
配置多个数据库后端
-
调整目录性能
-
使用目录覆盖
-
添加完整性检查
-
添加唯一性约束
多个数据库后端
迄今为止,在操作 OpenLDAP 时,我们一直使用一个目录树(dc=example,dc=com
)和一个后端数据库(在slapd.conf
中配置的 HDB 数据库)。这种配置适用于大多数小型目录服务器。它简单易配置,所有数据都存储在同一个地方。
但是,在一些更复杂的使用场景中,拥有一个可以处理多个目录树的目录服务器是有意义的,每个目录树都存储在自己的后端数据库中。以下是一些可能需要这种配置的情况:
-
一个目录服务器托管多个组织的目录信息树
-
一个大型目录服务器被拆分成多个较小的树和子树,以提高性能和复制的效率。
-
两个或更多先前存在的目录信息树正在逐步合并(如企业并购的情况)。
当然,还有其他可能需要使用多个后端的 LDAP 服务器的场景。这些只是一些常见情况的示例。
一个带有多个后端的 SLAPD 是如何工作的?让我们通过一个简单的例子来理解。假设我们有两个目录信息树,一个是我们在之前章节中使用过的dc=example,dc=com
树,另一个是dc=demo,dc=net
。
我们希望在同一个 SLAPD 服务器上托管这两个目录树。但我们不希望dc=example,dc=com
的数据与dc=demo,dc=net
存储在相同的数据库文件中(如果以后需要拆分数据库,这可能会带来问题)。当然,我们也不希望一个目录树中的记录查询返回另一个目录树中的条目。
配置一个新的数据库主要是通过在slapd.conf
中定义新的数据库来完成的。完成这一步之后,我们只需要创建一些数据并将其加载到新数据库中。
slapd.conf 文件
我们在第二章中创建了slapd.conf
文件。在之前的章节中,我们修改了slapd.conf
的一小部分,但现在我们将回顾一下slapd.conf
文件的整体结构。
如第二章所述,slapd.conf
文件可以分为几个组成部分。最初,我们创建了三个部分,分别叫做Basics、Database和ACLs。在上一章中,我们详细讨论了 ACLs 以及大多数情况下在第一个Basics部分中定义的安全指令。让我们来看看我们slapd.conf
文件的结构:
现在是时候稍微完善一下模型了。基础部分包含全局配置参数。也就是说,在那里定义的参数对整个 SLAPD 服务器有效,无论它有多少个数据库后端。
数据库部分包含与特定数据库后端相关的指令,每个后端通常只托管一个目录信息树。此部分中的参数定义了使用哪个后端(例如 BDB、HDB、LDIF、SQL)、该后端的具体参数和叠加层、哪个 DN 将是该数据库的管理员,等等。一个slapd.conf
文件中可以有多个数据库部分。实际上,配置多个数据库部分是我们在一个 SLAPD 服务器上托管多个数据库后端的方式。
最后,ACL部分实际上是数据库部分的一个子部分(虽然正如我们在上一章所看到的,ACL 也可以在全局级别使用)。每个数据库可以有自己的访问控制集。所以,slapd.conf
文件的一个更准确的表现应该是这样的:
这个图更能代表slapd.conf
文件的组成。前面的例子展示了两个独立的数据库(虽然数据库的数量当然不限于两个),每个数据库都有自己的指令和访问控制列表(ACL)。
虽然全局 ACL 在基础 配置部分中提到,但它们没有被单独分成一个部分,部分原因是它们在这里的作用不像在后端上下文中使用 ACL 那样重要。全局 ACL 主要用于保护根 DSE、cn=Config
和cn=Subschema
树的部分(请参阅附录 C),但不止于此。大多数 ACL 应放置在适当的数据库 配置部分中。
现在我们准备转向配置文件本身,看看前面的图示是如何付诸实践的。
一个基本的多数据库设置可以通过在我们的slapd.conf
文件中增加十几行轻松完成。我们将从第二章创建的现有后端配置开始,并在其下方添加一个新的数据库后端:
##############################
# BDB Database Configuration #
##############################
# Database 1: Example.Com
database hdb
suffix "dc=example,dc=com" "o=My Company,c=US"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
#directory /usr/local/var/openldap-data
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
########
# ACLs #
########
include /etc/ldap/acl.conf
##############################
# Database 2: Demo.Net
database hdb
suffix "dc=demo,dc=net"
rootdn "cn=Manager,dc=demo,dc=net"
rootpw secret
directory /var/lib/ldap/demo.net
#directory /usr/local/var/openldap-data/demo.net
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
########
# ACLs #
########
access to attrs=userPassword
by anonymous auth
by self write
access to dn.sub="dc=demo,dc=net" by users read
我们刚刚配置了两个数据库:
-
Example.Com
目录由第一个数据库处理。 -
Demo.Net
目录由第二个数据库处理。
关于此配置,有几点需要注意:
-
每个目录都有一个单独的管理员帐户。这在每个目录由不同的个人或小组管理时非常有用。
-
第二个数据库的目录与第一个数据库的目录不同。请记住,目录是存储数据库文件的位置。每个后端必须有自己的存储目录。
-
正如我们之前讨论的,每个数据库部分可以(并且应该)有自己的 ACL,并且可以为
slapd.conf
中定义的每个数据库指定不同的 ACL。在之前的示例中,ACL 是最小的。
创建和导入第二个目录
在我们导入数据之前,需要创建存储数据的位置。在slapd.conf
文件片段中,directory
指令指向/var/lib/ldap/demo.net
。然而,这个目录还不存在。我们需要创建它:
$ sudo mkdir /var/lib/ldap/demo.net
注意
如果 SLAPD 是以非root
用户身份运行的,请确保更改demo.net/
目录的所有权。SLAPD 用户应该拥有该目录。例如,如果ldap
用户运行slapd
,可以执行以下操作:
chown ldap /var/lib/ldap/demo.net
接下来,我们需要创建一个包含新目录基本记录的 LDIF 文件。在第三章中,我们为dc=example,dc=com
目录信息树创建了一个 LDIF 文件,其中包含主要的树结构。在这里,我们将创建一个最小的目录结构,并将其保存在一个名为demo.net.ldif
的文件中:
# This is the root of the directory tree
dn: dc=demo,dc=net
description: Demo.Net
dc: demo
o: Demo.Net
objectClass: top
objectClass: dcObject
objectClass: organization
# Subtree for users
dn: ou=Users,dc=demo,dc=net
ou: Users
description: Demo.Net Users
objectClass: organizationalUnit
# George Berkeley
dn: uid=george,ou=Users,dc=demo,dc=net
ou: Users
uid: george
sn: Berkeley
cn: George Berkeley
givenName: George
displayName: George Berkeley
mail: george@demo.net
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
这个文件创建了顶级条目——一个单一的子树分支(用于用户)和一个单一的用户。
现在我们已经有了一个 LDIF 文件,我们可以通过slapadd
导入它。如果你还没有这样做,请在运行slapadd
时停止 SLAPD。我们运行以下命令进行导入:
$ sudo slapadd -b 'dc=demo,dc=net' -l demo.net.ldif
默认情况下,slapadd
会尝试将数据导入到slapd.conf
中指定的第一个目录。然而,在我们的案例中,我们希望将数据存储在第二个目录中。因此,在之前的示例中,我们使用了-b
标志来指定第二个目录的基准 DN。我们本可以使用-n
2
,这样就指示slapadd
将记录放入第二个数据库中,而不是使用-b
'dc=demo,dc=net'
。
现在我们有了一个包含少量条目的第二个数据库。我们可以启动服务器并使用ldapsearch
进行测试:
$ ldapsearch -LLL -x -W -D 'cn=Manager,dc=demo,dc=net' -b \
'dc=demo,dc=net' '(objectclass=*)' description
这是我们将得到的结果:
Enter LDAP Password:
dn: dc=demo,dc=net
description: Demo.Net
dn: ou=Users,dc=demo,dc=net
description: Demo.Net Users
dn: uid=george,ou=Users,dc=demo,dc=net
作为该目录的管理员绑定到dc=demo,dc=net
目录树,我们可以验证我们添加的三个记录是否存在。请注意,只有description
属性会被返回。这就是为什么只显示dn
和description
的原因。
在slapd.conf
中的demo.net
部分没有设置 ACL 来阻止example.com
数据库的用户查看demo.net
目录中的信息。例如,用户uid=matt,ou=users,dc=example,dc=com
可以从demo.net
目录中检索信息:
$ ldapsearch -LLL -U matt -b 'dc=demo,dc=net' '(uid=george)' mail
这是输出:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=george,ou=Users,dc=demo,dc=net
mail: george@demo.net
如果我们想防止这种行为,可以通过 ACL 来实现。例如,我们可以将规则access
to
dn
.sub="dc=demo,dc=net"
by
users
read
替换为一个限制仅允许dc=demo,dc=net
树内条目读取的规则:
access to dn.sub="dc=demo,dc=net"
by dn.sub="dc=demo,dc=net" read
这将拒绝dc=demo,dc=net
树以外的条目访问这些记录。必须在dc=example,dc=com
部分的 ACL 中添加类似的规则,以阻止dc=demo,dc=net
树中的用户访问。
现在我们有一个包含两个不同数据库的目录。在本书的后续部分中,我们将研究使用多个数据库的其他方面。例如,在本章的后面,我们将看看如何使用 glue
叠加来连接两个数据库进行搜索。在第七章中,我们将研究在多个数据库上进行复制。但接下来,我们将看一些 SLAPD 的性能调优选项。
性能调优
在第二章中,我们创建了一个基本的 slapd.conf
文件。我们在那里的重点是让基本服务器运行起来。在上一章中,我们详细查看了与安全相关的指令。刚刚创建第二个数据库后端时,我们更高层次地查看了 slapd.conf
文件。
在这部分中,我们将继续在 slapd.conf
上工作,但这里我们将专注于帮助您根据组织的性能需求调整服务器的参数。稍后在这一部分,我们将看看 Berkeley DB 后端(BDB 和 HDB)使用的 DB_CONFIG
文件。在该文件中进行的优化可以显著提升 OpenLDAP 的性能。
提示
术语:数据库和后端
数据库和后端之间的区别非常微妙,通常这两个术语可以互换使用。这里是区别。
-
数据库是存储目录信息树的位置(文件、关系数据库、网络资源)。
-
后端是用于存储数据库(或者在某些情况下,指导 SLAPD 连接到远程数据库)的特定机制。后端被编码为模块,这意味着它们可以在启动时动态加载。
性能指令
我们已经创建了一个 SLAPD 使用的 slapd.conf
文件来管理目录服务器。随着我们查看下一批指令,我们将继续构建这个配置文件。
我们将把指令分成两个不同的类:
-
那些属于全局的指令应该放在
slapd.conf
文件顶部的基本配置部分。 -
仅适用于单个数据库后端的指令
对于数据库后端适用的指令,一些适用于所有后端类型(如 BDB、SQL、Shell、LDIF 等),还有一些仅适用于特定后端类型。由于我们使用的是 HDB 后端(默认),我们将专注于可以由该后端使用的指令。
全局指令
全局指令必须放在 slapd.conf
文件的顶部部分,在定义任何数据库部分之前。这些指令适用于整个 SLAPD 服务器,而不仅仅是该服务器内的特定目录信息树。
我们将首先看到的前三个指令用于优化客户端与 LDAP 服务器之间的交互。这些指令是 timelimit
、sizelimit
和 idletimeout
指令。之后,我们将看看 threads
指令,该指令用于调整 SLAPD 的线程。
注意
可以使用limit
指令为每个数据库设置精细的大小和时间限制,稍后会讨论该指令。例如,可以使用此指令为每个用户或组设置时间和大小限制。
时间限制
timelimit
指令用于指定 SLAPD 在停止操作并返回消息给客户端之前,针对特定操作所能花费的最大时间。
一些操作,例如在一个没有索引的属性上搜索一个大目录,可能需要很长时间。其他时候,客户端通过慢速网络连接并请求大量数据也可能会占用大量时间。这类漫长的搜索会拖慢整个服务器的速度,在繁忙的服务器上,它还可能阻止其他客户端连接并获取及时的响应。当然,并非所有客户端应用程序都能很好地处理长时间的等待。
为了避免这些问题,存在一个timelimit
指令,它允许你设置服务器在结束操作并返回消息给客户端之前,等待操作完成的最大时间。
默认时间限制是 3600 秒。在这个例子中,我们将其降低到仅五分钟:
timelimit 300
请记住,这个指令是一个全局指令,必须放在配置文件中的database
指令之前。
有时,取消所有时间限制是有用的。这样做的缺点是允许连接占用资源一段不确定的时间,如果有太多连接这样做,可能会导致客户端出现长时间延迟(在极端情况下,甚至是拒绝服务)。但是,在受控环境中,这可能是一个可以接受的风险。要关闭时间限制,请使用关键字unlimited
:
timelimit unlimited
使用此设置,服务器在操作完成之前不会向客户端返回任何消息。
这些例子展示了时间限制的基本用法,但有时需要更复杂的时间限制配置。OpenLDAP 开发人员创建了一种更高级的timelimit
指令形式来处理这种复杂的时间限制设置。在这种形式中,timelimit
指令可以设置两种不同类型的时间限制:
-
软限制:软限制是默认的时间限制,服务器在客户端请求中没有包含所需时间限制时,会使用该限制。
-
硬限制:硬限制是服务器处理请求时所能花费的最长时间。
理解这一差异有助于了解客户端和服务器如何处理时间问题。
当客户端连接到目录并执行搜索时,它可能会发送自己的时间限制请求,指示服务器在该时间限制内完成搜索。例如,如果客户端发送了 30 秒的时间限制,它将期望服务器在 30 秒内响应。如果服务器的硬时间限制高于客户端发送的时间限制,那么服务器将为该请求设置客户端请求的时间限制。然而,如果服务器的硬时间限制低于客户端的时间限制,它将使用自己的硬限制来处理该请求。
所以,如果服务器的硬时间限制是 60 秒,而客户端请求的是 30 秒时间限制,服务器将使用 30 秒的限制。然而,如果服务器的硬时间限制是 10 秒,而客户端请求的是 30 秒时间限制,服务器将使用其硬 10 秒限制,因为它较低。
提示
设置客户端时间限制
对于像ldapsearch
这样的 OpenLDAP 客户端,可以通过编辑ldap.conf
(或你的.ldaprc
文件)并添加TIMELIMIT
指令来设置客户端时间限制。在ldap.conf
文件中,TIMELIMIT
只有一个参数:以秒为单位的时间限制。例如,设置时间限制为 30 秒:TIMELIMIT
30
。
软时间限制在哪儿起作用?客户端并不总是提供时间限制,在这些情况下,你可能希望设置一个低于硬时间限制的限制。也就是说,如果默认的硬时间限制是一小时,这可能是一个完全合法的最大限制,但对于那些不需要更长时间限制的客户端来说,设置一分钟或两分钟的默认限制会更合适。
注意
如果你设置了一个高于硬限制的软限制,将使用硬限制。
现在我们可以查看timelimit
指令的扩展形式,了解如何设置硬时间限制和软时间限制的示例。通常,两者是在同一命令中设置的(虽然你可以只设置一个,而不设置另一个):
timelimit time.soft=30 time.hard=300
在这个例子中,软时间限制是 30 秒,而硬时间限制是 300 秒。这允许请求更长时间限制的客户端获得更长的处理时间,同时为那些在请求时没有提供时间限制的客户端设置较低的默认时间限制。
如果时间限制到达,客户端会得到什么?服务器将返回它能够完成的最大处理量,但也会包含一个警告,说明时间限制已被超出。
请注意,在繁忙的服务器上,请求可能会被排队,但实际上可能直到线程可用时才会执行。在这种情况下,请求等待线程的时间不会算入时间限制。时间限制的计时器从工作线程开始处理请求时开始,而不是从服务器接收到请求时开始。
注意
本章后面讨论的后端特定限制指令提供了更细粒度的时间和大小限制支持。例如,你可以为特定用户或组成员设置时间限制。
空闲超时
除了限制 SLAPD 处理请求所需的时间外,您还可以限制 SLAPD 允许客户端保持连接但空闲的时间。如果一个连接已连接到 SLAPD,但没有执行任何操作,则该连接为空闲状态。例如,客户端可能连接到 SLAPD,执行绑定操作,然后保持连接打开,可能是在等待用户输入。
在许多情况下,允许客户端保持连接但处于空闲状态并不会造成危害。空闲的客户端不需要服务器线程的关注,因此不会消耗宝贵的资源。因此,服务器的默认行为是简单地允许空闲连接无限期地保持连接。
但有时(有时是由于系统其他部分的限制),希望防止客户端连接并保持空闲。使用 idletimeout
指令来设置超时时间。与 timelimit
的简单形式类似,idletimeout
只需要一个参数,即连接空闲前 SLAPD 关闭连接的秒数:
idletimeout 3600
大小限制
除了设置操作可以持续的时间限制外,还可以设置搜索操作返回记录的数量限制。客户端可以轻松执行广泛的搜索,这些搜索会返回大量记录。如果没有设置大小限制,使用过滤器 (objectclass=*)
的搜索(如果不受 ACL 限制)将返回搜索基准中的每一条记录。如果在包含数百万条记录的数据库上执行此类搜索,SLAPD 会将所有这些记录返回给客户端。
在大多数情况下,为任何一次搜索设置记录返回数量的上限是有意义的。默认情况下,SLAPD 只会返回前 500 条记录。但这个数字可以通过 sizelimit
指令进行更改。
在其简单形式中,sizelimit
指令只需要一个参数,即返回的最大记录数:
sizelimit 1000
与 timelimit
类似,sizelimit
指令也有扩展形式,并且像 timelimit
一样,sizelimit
具有软限制和硬限制。扩展形式的 sizelimit
指令还可以设置第三个属性,这个属性称为 unchecked
。
在 sizelimit
中,硬限制和软限制的功能与 timelimit
中相似。硬限制决定了在任何搜索中返回的最大搜索结果数。就像时间限制一样,客户端也可以发送信息告诉服务器客户端希望返回的最大条目数。如果没有设置此类信息,则会使用软限制的值。
如果服务器发现的记录超过了 sizelimit
允许的数量,它将返回最大数量的记录并附带错误信息:Size limit exceeded
。
unchecked
条件稍微复杂一些。在搜索请求的属性未被索引时,SLAPD 可能会找到大量记录,需要测试这些记录是否符合客户端的筛选条件。有时候候选记录的数量非常大。unchecked
属性可以用来设置一个最大记录数限制,限制可以作为匹配候选项的记录数。这可以防止调优不良的数据库在搜索匹配的记录时消耗大量时间和资源。
注意
对常用的搜索属性进行索引是避免这种情况的最佳方法。索引将在本章后面讨论。
如果客户端的请求产生的候选项超过了unchecked
属性允许的数量,服务器将返回错误(行政限制超出
),并且不会执行搜索。
unchecked
属性将防止服务器在此类任务上花费过多时间,但代价是客户端无法对数据库运行查询。这样,完全合法的搜索也可能会被阻止。因此,unchecked
属性应谨慎使用。默认情况下,未对候选记录数设置限制。这相当于指定size.unchecked=unlimited
。
这是一个在一个指令中设置所有三个限制的示例:
sizelimit size.soft=500 size.hard=1000 size.unchecked=2000
在此示例中,软大小限制设置为 500,硬限制设置为 1000,最大未检查记录数为 2000。请注意,未检查的大小限制应当实际设置为比硬限制更大的值。
线程
最后的几个指令涉及设置服务器执行请求操作的限制。这些限制可以有效地防止资源的浪费或误用。现在,我想转向一个指令,它控制服务器处理请求的能力。
SLAPD 是一个多线程应用程序。与其他服务器不同,SLAPD 不会启动子进程来处理搜索。相反,SLAPD 服务器是一个单一进程,在该进程内有多个不同的线程并发执行。
每个线程可以执行自己的任务。因此,如果一个服务器有十六个线程(OpenLDAP 的 SLAPD 服务器的默认配置),那么它可以同时执行十六个不同的任务。粗略地说,线程执行操作。一个客户端可以建立一个连接,然后请求多个不同的操作,每个操作可能由不同的线程执行(尽管不超过一半的线程会被分配给单个客户端)。
默认的 16 个线程是过多的。最近的性能测试表明,在负载较高的情况下,运行 8 个线程的繁忙服务器的表现要优于运行 16 个线程的服务器。为什么?简而言之,更多的线程会导致更多的资源竞争。SLAPD 足够高效,通常将工作委托给较小的线程池会比使用大型线程池更快,并且可以减少线程调度的开销。
降低线程数还有其他好处。据估计,每个线程至少需要 13MB 的内存(根据 SLAPD 的配置和机器硬件,可能需要更多的内存)。企业级 LDAP 目录能够承受这种开销,但如果主机同时运行 LDAP 和其他许多服务,减少线程数可能会提升服务器在其他领域的性能,且仍然能保持与使用 16 个线程时相同(或更好的)性能。
注意
在未来版本的 OpenLDAP 中,默认的线程数很可能会从 16 降低到 8。
threads
指令用于设置 SLAPD 创建的最大线程数。它需要一个整数值:
threads 8
在典型的 OpenLDAP 配置中,这个设置是最优的,尽管流量较小的服务器可能会通过将线程池减少到 4 来受益。
提示
代理与线程
如果你正在运行一个忙碌的 SLAPD 代理服务器(使用 proxy
或 ldap
后端,在第七章中介绍),该服务器会查询远程目录服务器,你可能会通过拥有更大的线程池来提高性能。由于工作线程会一直占用,直到远程 LDAP 服务器响应,因此一个线程可能会长时间被占用。为了防止客户端被拒绝服务,你可能需要添加更多的线程。
请注意,允许的最低线程数是 2。这个线程数是 OpenLDAP 提供基本服务所需的最小线程数。
数据库部分中的指令
一些指令应该放在数据库部分,而不是配置文件的主部分。并且其中有些数据库指令是特定于所使用的后端的。除了与后端无关的指令外,我们还会看到一些可以在 BDB/HDB 后端使用的指令。
限制
我们已经看过了 sizelimit
和 timelimit
指令,它们都用于全局部分。但在数据库部分,还有另一种用于设置限制的指令,这种指令提供了更精细的控制,可以限制特定用户。你可以例如为单独的 DN、子树或某个组的成员设置更低或更高的限制。用来做这些事情的指令是 limit
指令。
limit
指令类似于 ACL。它有三个部分:指令本身、who-phrase 和一个或多个 limit-phrase。下面是一个例子:
limits users size=20
此指令为所有经过身份验证的用户(使用users
关键字)设置了限制。在 SLAPD 返回消息Size limit exceeded
之前,只会返回二十条记录。
limit
指令支持两种限制短语:size
和time
。与上面讨论的sizelimit
指令一样,size
可以使用soft
、hard
和unchecked
样式。同样,time
可以使用soft
和hard
样式。由于可以使用多个限制短语,我们可以创建更强大的限制集。以下是一个示例,将匿名用户限制为只返回短结果集,并且仅在操作可以迅速完成时才返回:
limits anonymous
size.soft=5 size.hard=15 size.unchecked=100
time.soft=5 time.hard=30
这将为匿名用户设置所有三个大小限制,以及两个时间限制。这将防止匿名用户进行长时间的搜索。
正如我们所看到的,anonymous
和users
关键字可以用于 who-phrase。但是,就像在 ACLs 中一样,dn
说明符以及它的修饰符(exact
、base
、onelevel
、subtree
、children
和regex
)也可以使用。
注意
dn
字段及其修饰符在上一章的访问控制列表部分中已详细介绍。
使用dn
字段,我们可以为特定的 DN、DN 模式或子树设置限制。例如,我们可以为特定用户设置大小限制:
limits dn="uid=matt,ou=Users,dc=example,dc=com" size=50
这将仅为该特定用户设置大小限制为 50。如果这是唯一的限制声明,则 SLAPD 会将sizelimit
中设置的大小限制应用于所有其他 DN。
同样,我们可以使用类似的指令为子树中的所有 DN 设置大小限制:
limits dn.sub="ou=Users,dc=example,dc=com" size=50
上述限制将适用于uid=matt,ou=Users,dc=example,dc=com
以及该目录信息树同一分支下的所有其他用户。
最后,限制还可以通过组来设置。在这种情况下,限制将适用于该组的任何成员。与 ACLs 一样,限制指令的 who-phrase 使用group
字段来指示 SLAPD 应该基于组成员身份进行限制:
limits group="cn=Admins,ou=Groups,dc=example,dc=com" size=unlimited
此指令为Admins
组的成员设置了unlimited
的限制,这意味着不会对这些组成员实施限制。
与 ACLs 一样,只有具有对象类groupOfNames
的记录才会自动被视为组。但是,其他对象类也可以作为组使用。例如,在第三章中,我们创建了一个对象类为groupOfUniqueNames
的组。该组的 DN 是cn=LDAP
Admins
,ou=Groups,dc=example,dc=com
。
为了将该记录作为一个组使用,我们需要在limits
子句中指定更多信息:
limits group/groupOfUniqueNames/uniqueMember="cn=LDAPAdmins,ou=Groups,dc=example,dc=com" size=unlimited
当将指令(如给定的指令)放入slapd.conf
文件时,请注意整个组字段(从group
到 DN 的末尾)必须写在一行上。
这个限制
指令将允许cn=LDAP
Admins
,ou=Groups,dc=example,dc=com
组成员的搜索结果大小不限。该组类型明确指示了记录的对象类(groupOfUniqueNames
)以及应视为该组成员字段的字段(uniqueMember
)。因此,当
SLAPD 检查限制时,会查看LDAP
Admins
记录,检查是否具有groupOfUniqueNames
对象类,然后评估连接的用户是否在记录中的某个uniqueMember
值中。如果是,那么该用户的大小限制将设置为unlimited
。
只读和限制指令
提高繁忙服务器性能的一种方法是限制客户端在服务器上可以执行的操作。例如,如果目录中的信息是静态的(即没有用户应该能够更改数据),那么最好将目录服务器设置为只读模式。或者,也许仅限制特定的操作(例如添加新记录或删除记录)就足够了。
有两个指令可以放在slapd.conf
文件中以实现这些结果:readonly
和restrict
。
readonly
指令很简单。它只有on
或off
两种状态。默认情况下是off
,因此目录允许写入操作(添加、修改、删除等)。以下是将 SLAPD 配置为只读目录服务器的方式:
readonly on
当设置此指令时,尝试修改目录信息树中信息的客户端将从服务器收到错误信息:
ldap_modify: Server is unwilling to perform (53)
additional info: operation restricted
注意
当readonly
开启时,甚至管理员也无法对目录进行修改。
然而,绑定、搜索以及其他不涉及更改目录信息的操作仍然可以正常运行。
注意
扩展操作,如密码修改扩展操作,不会受到readonly
指令的影响。因此,ldappasswd
客户端(例如)即使在readonly
开启时仍然能够更改目录中的密码。
为了防止这种情况发生,可以使用restrict
操作来限制一个或所有扩展操作。密码修改扩展操作在 RFC 3062 中定义(www.ietf.org/rfc/rfc3062.txt
)。
有时候将服务器设置为只读模式过于严格。可能只需要阻止某些操作。这可以通过restrict
指令来实现。
restrict
指令接受一个或多个 LDAP 操作的列表,这些操作应该被禁止。restrict
支持以下操作:
-
添加
-
绑定
-
比较
-
删除
-
修改
-
重命名
-
读取
(一个特殊的伪名,防止所有读取操作,如搜索、比较和绑定) -
搜索
-
写入
(一个特殊的伪名,防止所有写入操作,等同于将readonly
设置为on
)
除了这九种类型外,还有一种特殊类型用于处理扩展操作:extended=<OID>
。在扩展类型中,<OID>
应替换为你想要限制的扩展操作的对象标识符(OID)。
例如,我们可以使用以下指令防止用户添加、重命名和删除整个条目:
restrict add delete rename
这将阻止用户添加新条目、重命名现有条目(即更改 DN)或删除条目。根据在 slapd.conf
数据库部分中的配置,我们不能使用命令行工具添加或删除条目:
$ ldapadd -U matt -f john_locke.ldif
这是我们得到的结果:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
adding new entry "cn=John Locke, ou=users,dc=example,dc=com"
ldap_add: Server is unwilling to perform (53)
additional info: operation restricted
$ ldapdelete -U matt "uid=manny,ou=users,dc=example,dc=com"
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
ldap_delete: Server is unwilling to perform (53)
additional info: operation restricted
请注意,在这两种情况下,服务器的响应都是:Server unwilling to perform
。然而,仍然允许修改记录中的属性,以及进行搜索、比较和绑定操作。
如前所述,扩展操作可以通过 restrict
指令与 extended
类型进行限制。不过,与其他类型不同,extended
需要一个值——我们可以指定要限制的扩展操作。不幸的是,值必须使用不友好的 OID 格式。要找到正确的 OID,你可以检查服务器的 Root DSE 条目(参见 附录 C),或者阅读所需扩展操作的 RFC。
一旦你获得了 OID 编号,就可以轻松设置限制。例如,为了防止客户端执行 Password Modify 扩展操作,使用以下内容:
restrict extended=1.3.6.1.4.1.4203.1.11.1
尝试使用 ldappasswd
客户端修改密码将会导致错误:
$ ldappasswd -x -W -D 'cn=Manager,dc=example,dc=com' -S
'uid=barbara,ou=users,dc=example,dc=com'
以下是我们得到的错误:
New password:
Re-enter new password:
Enter LDAP Password:
Result: Server is unwilling to perform (53)
Additional info: extended operation restricted
restrict
指令提供了一种方便的方式来限制客户端可以执行的操作。
索引(仅限 BDB/HDB 后端)
如果你运行的是带有 BDB 或 HDB 后端的 SLAPD 服务器(最常用的后端),那么 index
指令是最重要的性能相关指令。
index
指令在每个 BDB 或 HDB 数据库的数据库部分中指定,表示 SLAPD 应该为哪些字段构建并维护索引。索引是一个单独的数据库文件,优化用于在 LDAP 读取操作时进行快速访问。
当客户端使用带有未索引属性的搜索过滤器时,SLAPD 会在目录中查找每一条记录,寻找所需的属性,然后将该属性的值与客户端提供的属性值或过滤器进行对比。
如果该属性已经建立索引,那么 SLAPD 服务器会直接在相应的属性索引中查找该值,并迅速返回匹配记录的列表。
索引搜索比全目录搜索要快,而且目录越大,差异越明显。
确定需要索引的属性的任务由你来完成,你应该根据在目录信息树中使用的对象类以及针对你的目录服务器执行的读取操作(搜索、绑定、比较)来决定需要索引的属性。主要面向人员信息的目录(使用person
、organizationalPerson
和inetOrgPerson
对象类)可能需要为常用属性如cn
、sn
和uid
创建索引。
在第二章创建基础的slapd.conf
文件时,我们配置了以下索引:
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
上面指定了三个索引:一个是针对objectClass
的,一个是针对cn
的,还有一个是针对uid
的。
第一行创建了一个针对objectClass
属性的索引。该索引针对相等(eq
)匹配进行了优化(即像objectclass=person
这样的搜索,但不是像objectclass=*son
这样的搜索)。这个索引应该始终包含,因为绝大多数的读取操作都会使用objectClass
属性。
第二行是针对cn
属性的索引。除了配置此索引以有效处理相等(eq
)匹配外,还配置了有效执行子字符串(sub
)和近似(approx
)匹配,并快速测试该属性是否存在(pres
)。以下是每种索引优化类型的简要说明:
-
approx
:这优化了近似匹配的搜索。如果搜索操作请求近似匹配(cn~=mat
),则可以使用此索引加速近似匹配。 -
eq
:这优化了相等匹配。请求精确匹配的过滤器,如(uid=matt
)或(objectclass=person
),都会使用eq
优化。确保objectclass
属性有针对相等匹配优化的索引非常重要。当使用目录复制或其他覆盖时,可能还需要对其他常用属性进行索引。 -
sub
:这优化了子字符串匹配。当搜索请求发送一个字符串的部分,并请求返回包含该部分的属性值时,就会发生子字符串搜索。例如,过滤器(uid=*ar*
)应该匹配任何包含字符串ar
的 UID。用户mark
和karen
都会匹配此过滤器。 -
subinitial
:这是一种特殊的sub
优化,仅优化匹配字符串的第一部分。它适用于处理像(uid=mar*
)这样的过滤器,但不适用于像(uid=*ark
)这样的过滤器。 -
subfinal
:这也是一种特殊的sub
优化。它优化了匹配字符串最后一部分,并且对于像(uid=*ark
)这样的过滤器表现良好。 -
pres
:pres
类型优化了索引,用于仅需检查某个属性是否存在的情况。
然而,并不是所有属性都支持所有的索引选项。例如,objectclass
属性不支持 approx
、sub
或任何 sub
的变体,也无法从 pres
索引中获益。
提示
索引与模式
一个对象类的模式定义了一个属性支持的匹配规则,而匹配规则的类型决定了该属性是否能支持某种特定类型的索引。请参见第六章。
一般来说,为常用属性添加索引是一个好主意。它加快了搜索和其他读取操作的速度,并且由于大多数 LDAP 操作是读取操作,这对性能来说是一个提升。
但是,维护索引会减慢涉及索引属性的写入操作,因为这些属性不仅需要在主数据库中维护,还需要在索引数据库文件中维护。此外,每个索引还需要额外的缓存空间来高效地进行搜索,这意味着添加更多索引将消耗更多的内存。基于这些原因,最好只对经常用于搜索操作的属性进行索引,而不是对所有属性都进行索引。
当添加或修改 index
指令时,SLAPD 并不会自动重新索引目录中的所有条目。你需要手动执行。例如,在查看我们系统上的常见搜索后,我们确定为 sn
和 member
属性添加索引会是个不错的选择。其他应用程序经常进行搜索,以查找特定 DN 所在的组,索引该属性将加快这些搜索。
为了满足这些需求,我们将添加以下新的 index
指令:
index sn eq,sub,approx
index member eq
但是,一旦我们将这些添加到 slapd.conf
后,我们需要停止 SLAPD 并运行 slapindex
程序来重建索引文件:
$ sudo slapindex -q
这将重建所有的索引。-q
(快速)标志将大大加速该过程,因为它跳过了数据库一致性检查。
提示
避免重建索引
slapindex
程序将重建所有索引。在向一个大型目录中添加索引时,你可能希望避免重建所有其他索引。一种方法是注释掉 slapd.conf
中现有的索引(只保留新的索引行未注释),然后运行 slapindex
,再从现有的索引中删除注释。OpenLDAP 的下一个版本将支持一种更方便的添加索引的方式。
如果某些优化类型不允许某个属性使用(例如,如果尝试为 objectclass
添加子字符串索引),slapindex
程序将打印错误信息。但当运行成功时,它会悄悄退出,并且不会打印任何信息。
一旦 slapindex
完成,SLAPD 就可以重新启动。
控制缓存(仅限 BDB/HDB)
在 BDB 和 HDB 后端中,SLAPD 将经常访问的记录存储在缓存中,这样它就不需要每次请求时都从磁盘读取目录信息。默认情况下,SLAPD 在缓存中保留一千条记录。但是,对于拥有几千条或更多条目且繁忙的目录服务器来说,增加缓存大小会有所帮助。这可以通过cachesize
指令来实现:
cachesize 2000
上面的指令将默认缓存大小加倍,指示 SLAPD 将 2000 条记录保存在内存中。
当缓存满了会发生什么呢?默认情况下,SLAPD 会直接删除缓存中的最后一个项目(使缓存保持 2000 个条目,但只有 1999 个条目已满)。在繁忙的服务器上,每次只清空一个缓存项可能会对性能产生轻微的负面影响,因为如果多个搜索迅速连续执行,每个搜索都未命中缓存,缓存的最后一个条目将被清空,并在每个请求中填充。这种情况在缓存大小与数据库中的条目数量不成比例时更有可能发生。
cachefree
指令可以用来指示 SLAPD 在缓存满时删除多个项目:
cachefree 5
这个示例指示 SLAPD 从缓存中删除最后五个条目。
理想情况下,缓存大小应该尽量接近数据库中实际条目的数量,前提是内存限制允许。至少,缓存应该足够大,以便频繁请求的记录能够保持在内存中。例如,如果你的目录服务器作为通讯录使用,那么缓存应该足够大,以便用户记录及其祖先记录可以同时保存在缓存中。
注意
这些缓存指令并不是 SLAPD 中唯一重要的指令。请参阅DB_CONFIG
文件部分中的set_cachesize
指令。
第三个缓存指令是idlcachesize
。idlcachesize
指令用于缓存经常执行的搜索结果,在这里,较大的缓存将使经常使用的搜索变得更快。对于 HDB 数据库,建议将此值设置为cachesize
的三倍:
cachesize 2000
idlcachesize 6000
我们现在已经完成了对slapd.conf
配置选项的查看。接下来,我们将转向另一个可以用来调整 SLAPD 性能的配置文件。
降低磁盘 I/O 延迟(仅适用于 BDB/HDB)
当 LDAP 操作将新数据写入目录,并且 SLAPD 使用 BDB 或 HDB 后端时,数据首先存储在内存中,然后刷新到操作系统中的数据库文件。
在非常繁忙的目录服务器(或磁盘 I/O 非常慢的服务器)上,有时会希望在速度和数据安全性之间进行权衡。特别有两个指令可以指示 SLAPD 进行这种权衡:
-
其中第一个,且风险较小的是
dirtyread
指令,它不接受任何参数。假设有一个客户端执行写操作来修改一个记录,然后另一个客户端在 SLAPD 将第一个客户端的更改写入磁盘之前,执行对该记录的读取操作。此时服务器应当返回未修改的磁盘数据,还是未提交的修改数据呢?通常,服务器会返回前者,将干净但即将过时的记录发送给客户端。
“脏读”一词描述了第二种情况,即服务器将尚未提交的信息发送给客户端。虽然返回这些数据可能更快,但它可能不准确;即使服务器已将修改后的数据发送给第二个客户端,也可能会拒绝或中止第一个客户端的修改请求。
dirtyread
指令只会增加客户端获取不准确数据的风险。 -
第二个指令
dbnosync
风险更高。通常,当一个操作更改了目录信息时,修改会尽快写入磁盘。存储在内存中的数据会被刷新到 Berkeley DB 子系统中的文件中。但进行磁盘 I/O 操作可能会减慢服务器速度。加速此过程的一种方式是指示 SLAPD 延迟将信息写入磁盘上的日志文件,这可以通过
dbnosync
指令来实现。使用
dbnosync
运行的风险在于,如果服务器在没有正常关闭的情况下崩溃,对目录所做的修改但尚未写入磁盘的部分将会丢失。然而,这并不会导致数据库损坏的风险增加——数据库仍然可以恢复,尽管最近的更改可能会丢失。你可以通过同时使用
checkpoint
指令来降低(但不能消除)使用dbnosync
运行的风险。设置检查点会使 SLAPD 定期将数据写入磁盘。checkpoint
指令有两个参数:最大大小(以千字节为单位)和时间限制(以分钟为单位)。当写入的数据量大于最大大小或经过指定时间间隔后,SLAPD 将执行数据库检查点。以下是checkpoint
指令的示例:checkpoint 1024 30
这指示 SLAPD 在向数据库写入超过一兆字节的数据并且每隔 30 分钟时,执行数据库检查点(将所有新数据从内存刷新到文件系统)。
由于这些指令增加了风险,通常最好先尝试其他提升性能的方法(例如调整缓存或调优 DB_CONFIG
文件),再实施这些指令。
DB_CONFIG 文件
DB_CONFIG
文件在技术上根本不是一个 OpenLDAP 配置文件。它是一个 Berkeley DB 配置文件,仅针对 BDB 和 HDB 后端。它为 Berkeley 数据库引擎提供了一系列设置。
注意
Berkeley DB 是一个开源的嵌入式数据库,目前由 Oracle 维护。由于它稳健可靠,且得到积极维护并广泛支持,因此它在开源和专有应用中都非常流行。如需了解有关 Berkeley DB 的更多信息,请访问 Oracle 的网站:www.oracle.com/database/berkeley-db/index.html
由于整个目录信息树以及索引都存储在 Berkeley DB 数据库中,因此正确配置的 DB_CONFIG
文件是影响目录性能的最重要因素。
在实验 DB_CONFIG
文件并尝试新配置时,最好使用非生产服务器,并在做任何更改之前,使用 slapcat
做一次目录数据的完整备份。
DB_CONFIG
文件并没有与 OpenLDAP 配置文件一起存储。相反,它与数据库文件一起存储在 /var/lib/ldap
(或 /usr/local/var/openldap-data
)目录下。与其他配置文件不同,它只有在创建或恢复数据库时才会被读取。从 OpenLDAP 2.3 版本开始,如果 SLAPD 在启动时检测到 DB_CONFIG
的变化,它会尝试进行数据库恢复以融入这些变化,你可能会在日志文件中看到类似的条目:
bdb_db_open: DB_CONFIG for suffix dc=example,dc=com has changed
Performing database recovery to activate new settings
同样地,当你创建一个新的目录时,Berkeley DB 子系统会读取 DB_CONFIG
文件,并根据其中的指令创建数据库。
提示
确保你的数据库中有一个 DB_CONFIG
文件。如果你的数据库目录中没有 DB_CONFIG
文件,系统将使用 Berkeley DB 的出厂默认设置,这些默认设置非常保守。在除了小型(<100 条目)目录服务器外的任何情况下,这些默认设置都不足以提供足够的性能,并且会导致性能问题。
OpenLDAP 发行版包括一个默认的 DB_CONFIG
文件,已针对一般用途进行了调整。它应该已经位于 /var/lib/ldap
(尽管有时会标记为 DB_CONFIG.example
,此时你需要将其重命名为 DB_CONFIG
)。在 Ubuntu Linux 中,一个针对 Ubuntu 定制的 DB_CONFIG
文件位于 /usr/share/doc/slapd/examples/DB_CONFIG
。我们将从 OpenLDAP 源代码发行版中包含的版本开始使用(该版本已为企业使用配置)。默认版本大致如下:
# one 0.25 GB cache
set_cachesize 0 268435456 1
# Data Directory
#set_data_dir db
# Transaction Log settings
set_lg_regionmax 262144
set_lg_bsize 2097152
#set_lg_dir logs
我们已经从文件的头部和尾部删除了一些注释,但保留了所有的设置。
对于中型目录的标准使用,这些设置是合适的。如果你的目录性能足够快,且系统资源充足,那么无需强迫自己更改默认设置。
DB_CONFIG
文件包含与底层 Berkeley DB 文件性能直接相关的指令。我们将依次介绍这六个设置,最重要的指令是第一个。
本节末我们还将查看三个额外的指令,用于调优 Berkeley DB 锁定处理。
注意
我们之前检查过的一些指令是 DB_CONFIG
指令的同义词。例如,dbnosync
的作用与 DB_CONFIG
指令 set_flags
DB_TXN_NOSYNC
相同。
设置缓存大小
BDB/HDB 后端会尽量将尽可能多的目录信息保存在内存中作为缓存。这使得目录读取更快速,因为 SLAPD 不必从磁盘读取信息。
尽管在具有其他服务的系统上(作为一种良好的经济权衡)可能无法将整个目录保存在缓存中,但如果至少将最常用的条目保存在缓存中,服务器的运行速度会更快。
set_cachesize
指令决定 SLAPD 为目录缓存分配多少内存。该指令接受三个参数:
-
分配给缓存的千兆字节数
-
分配给缓存的字节数
-
用于缓存的段数
第一个和第二个值相加后,不应超过 4 GB。第三个值决定了 Berkeley DB 后端将缓存分割成多少个数据段。值 1 和 0 都会导致单个缓存段(通常是需要的)。
在默认的 OpenLDAP DB_CONFIG
文件中,set_cachesize
指令如下所示:
set_cachesize 0 268435456 1
缓存的总大小为 256 兆字节(268435456/1024/1024),整个缓存存储在一个段中。对于我们的小型目录来说,这远远超过了我们的需求。虽然完整的 256 兆字节不会被分配,但这是一个安全设置。
对于小型或中型目录,估算所需最小缓存量的一个经验法则是:每 100 兆字节的 LDIF 数据分配 2 兆字节的缓存,每个索引分配 1 兆字节的缓存。然而,较大的目录一定会从精心调整的缓存中受益。有关更精确的计算,参见 OpenLDAP FAQ-O-Matic 上关于设置缓存大小的条目:www.openldap.org/faq/data/cache/1075.html
。
配置数据目录
set_data_dir
指令接受一个参数,即包含数据库文件的目录路径。在之前的示例中,这个指令被注释掉。由于 DB_CONFIG
文件与 BDB 文件本身存储在同一目录中,因此通常不需要设置此指令。仅当 DB_CONFIG
文件从数据库目录外的某个位置加载时,才需要设置此指令。
优化 BDB/HDB 事务日志
最后三个指令与事务日志相关。随着对 Berkeley DB 的修改,事务的完整细节会被写入日志文件,日志文件名为log.XXXXXXXXXX
,其中十个X
会被 0-9 之间的数字替换。第一个日志文件是log.0000000001
,一旦它变得过大,一个新的日志文件会通过递增数字创建:log.0000000002
。
日志文件包含了数据库中所有发生过的记录。实际上,它们如此完整,以至于可以用来重建一个损坏的数据库。日志文件格式不是纯文本,不能通过常规工具(如cat
、more
或less
)读取。要读取它,您需要使用db_printlog
命令(或dbX.Y_printlog
,其中X.Y
是数据库的主版本号和次版本号,如db4.2_printlog
)。这将显示每个事务的记录。
提示
恢复损坏的 BDB/HDB 数据库
Berkeley DB 子系统写入的日志文件可以用来恢复损坏的数据库。Berkeley DB 发行版包括一个名为db_recover
的工具(或dbX.Y_recover
,其中X.Y
是主版本号和次版本号,如db4.3_recover
)。db_recover
工具使用日志文件来修复损坏的数据库。有关更多信息,请查看db_recover
的手册页。
启动时,SLAPD 会自动对 BDB 目录执行恢复,以确保数据库处于稳定状态。只有在极少数情况下,系统管理员才需要手动处理日志文件。
由于这些事务日志文件在 SLAPD 数据安全性中起着如此重要的作用,因此确保环境已正确调优是很有必要的。
set_lg_regionmax
指令控制分配给存储 Berkeley DB 文件名称的内存量。它需要一个参数:要分配的空间量(以字节为单位)。上述文件为存储名称分配了 256 KB 的空间,这对几乎所有应用程序来说都足够了。只有在极少数有大量索引文件的情况下,才需要提高此限制(我至今从未遇到过这种情况)。
下一个指令,set_lg_bsize
,用于分配用于缓冲数据的内存量,直到数据被写入事务日志。它也需要一个参数:用于缓冲区的空间量(以字节为单位)。我们文件中的设置分配了两兆字节的空间。当对 BDB/HDB 数据库进行修改时,关于该修改的信息不会写入日志,直到事务完成。在它被写入之前,它会暂时存储在一个内存缓冲区中,缓冲区的大小不超过set_lg_bsize
的值。
由于大多数 LDAP 数据相对较短,通常两兆字节已经足够。但如果你的目录经常存储大量数据(如图像文件),你可以考虑增加事务日志的缓冲区大小,以适应最大的文件块。例如,如果目录存储的图像最大为十兆字节,set_lg_bsize
应该设置为 10485760
(即 10 * 1024 * 1024)。
OpenLDAP 开发者之一 Howard Chu 指出,当将 set_lg_bsize
标志的值增大到如此大的值时,你还必须通过 set_lg_max
标志提高日志文件的最大大小限制。日志文件的最大大小必须至少是 set_lg_bsize
值的四倍。
set_lg_max 41943040
最后一条指令 set_lg_dir
指向 BDB 的日志文件。默认情况下,这些日志文件与其他数据库文件存储在同一目录下(如果你从源代码编译,则存储在 /var/lib/ldap/
或 /usr/local/var/openldap-data/
)。然而,由于日志对于数据库恢复至关重要,最好将日志文件存储在与数据库文件不同的位置。例如,你可能希望将日志存储在与数据库文件不同的硬盘上。为此,可以取消注释 set_lg_dir
指令,并将其设置为目标目录的绝对路径:
set_lg_dir /usr/local/var/ldap/
这条指令将指示 Berkeley DB 子系统将日志文件写入 /usr/local/var/ldap
,而不是写入与 BDB 文件所在目录相同的位置。
注意
定期备份 Berkeley DB 文件(包括日志文件)是一个好主意。备份数据的更便捷方式是使用 slapcat
工具导出目录的副本。这将会把数据库导出为 LDIF 格式,可以轻松导入到 SLAPD 服务器中,无论后端格式如何。
调整锁定文件
DB_CONFIG
文件中应该包括三个附加参数。这三条指令用于调整 Berkeley DB 中的锁机制。
数据库上的某些操作需要锁定数据,以防止数据不一致的情况发生。例如,允许两个不同的线程同时修改同一条记录是不好的。Berkeley DB 使用锁定机制来防止这种情况发生。
有三条指令用于调整锁定子系统。这些指令是:
-
set_lk_max_objects
:一次可以锁定的最大对象数 -
set_lk_max_locks
:一次可以请求的最大锁定数 -
set_lk_max_lockers
:最大同时锁定请求数
在默认的 Ubuntu DB_CONFIG
文件中,这些值都设置为 5000,但较低的值(介于 1500 和 3000 之间)可能更为理想:
# Number of objects that can be locked at the same time.
set_lk_max_objects 5000
# Number of locks (both requested and granted)
set_lk_max_locks 5000
# Number of lockers
set_lk_max_lockers 5000
将这些值设置为足够大的值,可以防止数据库用尽锁定,从而拒绝数据库访问。
注意
要查看您的 Berkeley DB 锁定设置是否足够,可以使用以下命令,该命令会打印关于锁和锁定器的详细信息:
db4.2_stat -c
更多关于 Berkeley DB 的信息
我们在本节中介绍的指令是 OpenLDAP 中最受关注的那些。然而,还有其他指令,合理使用这些设置也可以提高 BDB 和 HDB 后端的性能和可靠性。
这些参数的一些信息可以在 OpenLDAP 的 FAQ-O-Matic 中找到(www.openldap.org/faq/data/cache/1072.html
)。然而,要全面理解,最好的资源是Berkeley DB 参考 指南。最新版本可以在这里找到:www.oracle.com/technology/documentation/berkeley-db/db/ref/toc.html
到目前为止,我们已经查看了slapd.conf
和DB_CONFIG
文件,检查了这些文件可以通过哪些方式进行修改,以提高 SLAPD 的性能。接下来,我们将讨论一个不同的话题:使用目录覆盖扩展 SLAPD 的功能。
目录覆盖层
随着 OpenLDAP 项目的发展,越来越多的功能被添加进来。最初,这些功能直接被添加到 SLAPD 服务器的代码库中。但是随着功能的不断集成到 OpenLDAP 中,代码和配置变得越来越复杂。
为了解决这个问题,OpenLDAP 开发者在 OpenLDAP 2.2 中引入了一个新概念,使得在减少底层代码复杂性的同时更容易引入新功能。开发者引入了一个名为覆盖层(overlays)的模块化系统。覆盖层是可以修改 SLAPD 行为的一段代码。
当 SLAPD 收到一个配置为使用覆盖层的数据库请求时,覆盖层会在从底层数据库检索任何信息之前有机会对请求进行处理。因此,覆盖层可以用于对请求进行额外的处理。
如何将覆盖层添加到目录服务器中?通过在 slapd.conf
文件中的特殊指令。overlay
指令放置在数据库配置部分,尽管某些覆盖层会拦截与后端无关的操作。
一个数据库中可以使用多个覆盖层。当覆盖层以这种方式使用时,它们被称为堆叠。正如我们在本章后面将看到的那样,覆盖层指令的顺序非常重要,因为 SLAPD 会依次遍历覆盖层堆栈,一次调用一个覆盖层。
官方覆盖的简要介绍
在 OpenLDAP 2.3 中,包含了十六个官方覆盖层以及一些贡献的和非官方的覆盖层。几乎所有的官方覆盖层都在手册页中进行了描述。在这里,我们简要描述了这十六个覆盖层;我们还将更详细地讨论一些有用的覆盖层。在后续章节中,我们也会使用这些覆盖层。
官方覆盖层如下:
-
accesslog
:访问日志覆盖层用于记录有关目录访问和利用的信息。信息不是记录在文件系统中,而是作为记录存储在一个特殊的日志目录中。然后,可以通过 LDAP 客户端检索日志,或者使用如slapcat
之类的工具将日志导出到平面(LDIF)文件中。在下一章中,我们将实现此覆盖层,并在第七章中再次使用它来改进复制。 -
auditlog
:审计日志覆盖层记录关于目录变更的信息。与功能更强大的访问日志覆盖层不同,审计日志将信息存储在文件系统中的文件中。 -
chain
:在复杂的目录环境中,一个目录可能包含另一个目录没有的信息。第二个目录可能被配置为引用第一个目录的客户端。通常,引用涉及发送客户端有关重定向查询的信息,然后客户端需要追踪该引用。chain
覆盖层处理服务器端的引用追踪;服务器会自行跟踪引用并将完整信息返回给客户端。 -
denyop
:拒绝操作覆盖层执行的功能与本章早些时候讨论的限制指令相同。它不允许客户端执行某些 LDAP 操作。在下一部分,我们将使用此覆盖层。 -
dyngroup
:dyngroup
覆盖层提供了一种基于对象中特定属性创建动态组的方法。这提供了一种强大的记录分组方法。 -
dynlist
:它类似于dyngroup
覆盖层。 -
glue
:glue
覆盖层是内建的,并且默认加载,它使得将两个数据库连接起来成为可能,从而使它们看起来像是一个大的目录信息树。例如,如果一个数据库包含dc=example,dc=com
,而另一个数据库包含ou=Users,dc=example,dc=com
,glue
覆盖层使得对dc=example, dc=com
的搜索可以返回来自ou=Users,dc=example,dc=com
数据库的条目。必须在slapd.conf
的数据库部分使用subordinate
指令,来指示哪些数据库应该被连接。 -
lastmod
:最后修改覆盖层在目录信息树中创建一个特殊的记录,包含有关最近修改的记录是什么以及何时修改的信息。 -
pcache
:代理缓存覆盖层缓存 LDAP 搜索的结果。此覆盖层主要与ldap
后端一起使用。通过这种组合,SLAPD 可以配置为使用另一个 LDAP 服务器作为其后端,但通过在一个特殊的数据库中保持数据的缓存副本,从而加速客户端请求。 -
ppolicy
:密码策略覆盖层允许您强制执行某些限制,如密码过期日期和密码长度。密码策略覆盖层将在下一章进行描述。 -
refint
:参照完整性覆盖层用于在记录删除或 DN 修改时保持目录条目的一致性。例如,如果从目录中删除一个 DN,并且使用了refint
覆盖层,SLAPD 会在目录中搜索其他与该 DN 相关的引用(例如,组成员关系),并将这些引用也一并删除。我们将在本章稍后讨论此内容。 -
retcode
:此覆盖层旨在帮助 LDAP 客户端实现者测试他们的代码如何响应异常的服务器响应。 -
rwm
:重写与映射覆盖层提供了一种机制,可以重新编写或映射客户端请求中的某些部分到其他值。可以与代理 LDAP 服务器配合使用,重写属性名称和 DN。 -
syncprov
:同步提供者覆盖层用于作为提供者的 SLAPD 服务器,这些服务器向其他 SLAPD 服务器复制数据。我们将在第七章中详细讨论这个话题。 -
translucent
:translucent
覆盖层类似于代理覆盖层。当客户端请求记录时,它会从远程服务器检索该记录。但它可以做更多的事情——它可以存储该记录的本地副本,并可以覆盖远程记录的部分内容。 -
unique
:unique
覆盖层强制属性唯一性。它用于确保对于指定的属性,某个属性值在目录中仅存在于一个记录中。这对于避免多个用户具有相同的电子邮件地址(mail
)或用户 ID(uid
)属性值非常有用。
本文档中记录的每个覆盖层(denyop
除外)都有相应的 man 页面,可以通过命令man
slapo-<overlay 名称>
访问,其中<overlay 名称>
替换为覆盖层的简称。例如,要查看translucent
覆盖层的 man 页面,可以运行命令:man
slapo-translucent
。
在本章的剩余部分,我们将详细介绍一些简单的覆盖层。在接下来的几章中,我们将介绍几个复杂的覆盖层,并使用它们来解决常见的目录服务器需求。
配置覆盖层:denyop
由于我们在查看restrict
指令时已经涵盖了denyop
覆盖层背后的基本概念,并且denyop
实现起来较为简单,因此我们将以它作为如何使用覆盖层的示例进行探讨。
注意
restrict
指令实际上是限制操作的首选方法。denyop
覆盖层主要是作为其他覆盖层作者的示例。
覆盖层在slapd.conf
文件中进行配置。通常配置覆盖层有三个步骤:
-
使用
moduleload
指令加载动态对象。 -
使用
overlay
指令将覆盖层添加到数据库 部分。 -
将任何特定于覆盖层的指令添加到数据库部分。
让我们详细看看每个步骤。
加载模块
第一个任务是加载包含覆盖层的模块。这部分并不总是必要的。某些版本的 OpenLDAP 已经将所有模块静态编译,这意味着它们与服务器一起加载。然而,更多情况下,SLAPD 被编译为动态加载模块,这些模块在 SLAPD 启动时加载,几乎所有的覆盖层都作为模块实现。
注意
参见附录 A,进一步讨论这两种构建 OpenLDAP 方式的区别。
moduleload
指令应放在配置文件的顶部,位于第一个database
指令之前。要加载denyop
动态对象,我们需要添加以下高亮显示的行:
modulepath /usr/lib/ldap
moduleload back_hdb
moduleload denyop
当 SLAPD 启动时,它将搜索模块路径中的denyop
对象,并在找到时加载它。
注意
如果你需要加载一个不在模块路径中的模块,可以指定模块的完整路径。例如/usr/local/libexec/openldap/my_module
。
如果 SLAPD 在启动时未能找到模块,它将无法启动,并且会显示类似这样的错误:
lt_dlopenext failed: (/tmp/lastmod) /tmp/lastmod.so: cannot open
shared object file: No such file or directory
这表明在给定的模块路径中没有找到模块lastmod
,在此情况下路径错误地设置为/tmp
。
确保模块位于modulepath
列出的目录之一,或者模块的完整路径是正确的。
添加覆盖层
下一步是将覆盖层添加到覆盖层堆栈中。由于尚未指定任何覆盖层,这将是堆栈中的第一个项。glue
覆盖层会自动应用,但除非存在subordinate
指令,否则它不会执行任何操作。操作的后端处理(实际的目录查找)始终是堆栈中的最后一项。
要添加我们的覆盖层,我们需要将指令放在slapd.conf
文件的相应数据库部分中。如果有多个后端,可以在每个数据库部分重复同样的覆盖层指令,以便为每个数据库加载覆盖层。以下示例中突出显示了新的指令:
database hdb
suffix "dc=example,dc=com" "o=My Company,c=US"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
overlay denyop
现在,我们准备进行第三步。
添加特定于覆盖层的指令
一个覆盖层可能有其特定的指令。这些指令通常在该覆盖层的手册页中有文档。
denyop
覆盖层仅支持一个指令,即同名的denyop
指令。像我们之前看到的restrict
指令一样,denyop
指令接受一个操作列表。客户端将被禁止执行该列表中的任何操作。
在本章前面,我们使用了restrict
指令来防止客户端执行add
、delete
和rename
操作:
restrict add delete rename
我们可以使用denyop
指令实现相同的功能:
denyop add,delete,modrdn
这两个指令之间有一些小的区别:
-
denyop
接受一个以逗号分隔的操作列表。 -
denyop
使用modrdn
作为名称,而不是使用rename
一词。
如果客户端尝试执行其中一个不允许的操作,denyop
将阻止 SLAPD 执行该操作,并且客户端将收到Unwilling to perform
错误。
denyop
覆盖层是简单的,并且由于restrict
指令的存在,在生产服务器中不太可能被广泛使用。但我们接下来将要查看的下一个覆盖层提供了有用的功能,尽管伴随的指令稍微复杂一些。
参照完整性覆盖层
我们将要查看的第二个覆盖层是 RefInt(参照完整性)覆盖层。RefInt 旨在处理修改或删除记录时,可能导致其他记录中的属性值不准确的情况。
LDAP 组为说明 RefInt 覆盖层旨在解决的问题提供了一个很好的示例。在第三章中,我们创建了一个看起来像这样的 LDAP 组:
dn: cn=Admins,ou=Groups,dc=example,dc=com
objectClass: groupOfNames
cn: Admins
ou: Groups
member: uid=matt,ou=users,dc=example,dc=com
member: uid=david,ou=users,dc=example,dc=com
这个组有两个成员,uid=matt
和uid=david
。这两个成员属性分别引用其他记录(由它们各自的 DN 标识),这些记录也位于目录中。例如,这是uid=david
的记录:
dn: uid=david,ou=Users,dc=example,dc=com
cn: David Hume
sn: Hume
uid: david
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
如果我们从目录信息树中删除了uid=david
的记录,cn=Admins
组会发生什么?什么也不会发生!cn=Admins
组仍然会包含一个成员属性,指向uid=david
的 DN。默认情况下,SLAPD 不会对修改或删除的 DN 进行任何引用搜索。为什么?通常的假设是,这类任务应该由访问和修改目录的应用程序负责。
但是,保持目录中没有无效引用并不是每个人都愿意交给外部应用程序的任务。因此,OpenLDAP 开发人员创建了 RefInt 覆盖层,使得维护参照完整性的任务变成 SLAPD 的责任。
RefInt 覆盖层会在两种情况下启动:
-
当一个 DN 被修改(通过
modrdn
操作)时:RefInt 覆盖层会搜索目录(仅搜索配置中指定的属性值),并将旧的 DN 替换为新的修改后的 DN。 -
当记录被删除(通过
delete
操作)时:RefInt 覆盖层会搜索目录(仅查找指定的属性),并删除它找到的所有引用该 DN 的项。
我们将查看这些例子的实际情况,但首先让我们配置覆盖。
配置覆盖
配置覆盖的第一步是确保模块已加载。与往常一样,方法是在slapd.conf
文件的基本部分中,在第一个数据库部分之前添加moduleload
指令:
modulepath /usr/lib/ldap
moduleload back_hdb
moduleload denyop
moduleload refint
这个例子基于我们之前的moduleload
例子。只有高亮的那一行被添加了进来。
接下来,我们希望将覆盖添加到栈中,并为其配置操作。这些配置指令应该放在我们希望使用该覆盖的每个数据库部分中:
overlay refint
refint_attributes member uniqueMember seeAlso
refint_nothing cn=EMPTY
第一行的overlay
指令将 RefInt 添加到覆盖栈中。记住,它相对于其他overlay
指令的位置将决定它在覆盖栈中的位置。
下一行是refint_attributes
指令。此指令接受一个由空格分隔的属性列表,每当执行modrdn
或delete
操作时,这些属性将被搜索。我们希望包括所有希望 SLAPD 维护引用完整性的属性。
由于我们有groupOfNames
和groupOfUniqueNames
对象类的记录,我们希望 RefInt 覆盖检查member
和uniqueMember
属性。seeAlso
属性是一个允许用于organization
、organizationalUnit
和person
对象的属性(这些对象都在我们的目录信息树中使用),它的值是一个 DN,所以我们希望 RefInt 也检查它。
提示
seeAlso 属性
seeAlso
属性仅允许值为 DN,用于表示包含seeAlso
属性的记录与seeAlso
属性指向的记录或多个记录之间的连接。还有其他属性,如inetOrgPerson
对象的manager
属性,也允许使用 DN 值。
最后的指令refint_nothing
用于特殊情况,当 RefInt 响应delete
操作时。
有时候,RefInt 无法删除一个属性值。这发生在根据模式要求,记录必须至少拥有一个此类属性值的情况下。例如,任何groupOfNames
对象必须至少有一个member
属性值。模式不允许没有成员的组。
但是,如果删除一个条目需要 RefInt 删除某个组的唯一member
属性,该怎么办呢?我们不希望 RefInt 违反服务器的模式约束。
RefInt 通过这种方式避免了问题:RefInt 将refint_nothing
中的 DN 作为该属性的值,然后删除其他属性。实际上,它用已知的占位符值替换了被删除的值。
在前面的例子中,我们将refint_nothing
DN 设置为cn=EMPTY
。我们的目录信息树中没有名为cn=EMPTY
的条目(虽然如果有,也不会引发任何问题)。
修改记录
现在,我们将向我们的目录中添加两条记录:
dn: uid=marcus,ou=users,dc=example,dc=com
uid: marcus
sn: Tullius
cn: Marcus Tullius
givenName: Marcus
ou: users
objectclass: person
objectclass: organizationalperson
objectclass: inetOrgPerson
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectclass: groupOfNames
cn: Public Relations
ou: Groups
member: uid=marcus,ou=users,dc=example,dc=com
第一条记录是一个新的 inetOrgPerson
,UID 为 marcus
。第二条记录定义了 cn=Public
Relations
组,该组当前有一个成员 uid=marcus
。如果我们使用以下命令删除 uid=marcus
的记录,那么 cn=Public
Relations
的 member
属性会发生什么变化呢?
$ ldapdelete -U matt uid=marcus,ou=users,dc=example,dc=com
现在,我们搜索 cn=Public
Relations
组:
$ ldapsearch -U matt -LLL '(cn=Public Relations)'
该记录看起来像这样:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectClass: groupOfNames
cn: Public Relations
ou: Groups
member: cn=EMPTY
正如代码的最后一行所示,仍然有一个成员(groupOfNames
模式要求至少有一个成员),但是得益于 RefInt 覆盖,它不再指向已删除的 uid=marcus
记录。相反,它指向我们在 refint_nothing
中指定的 DN。
通常,记录将包含多个成员属性,像之前的 cn=Admins
示例一样。在这种情况下,当其中一个 DN 被删除时,属性值将被完全移除。考虑我们修改后的 cn=Public
Relations
组:
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectclass: groupofnames
cn: Public Relations
ou: Groups
member: uid=david,ou=users,dc=example,dc=com
member: uid=marcus,ou=users,dc=example,dc=com
如果在这种情况下删除了 uid=marcus
的记录,那么 RefInt 覆盖将简单地删除第二个成员属性值,使得该组看起来像这样:
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectclass: groupofnames
cn: Public Relations
ou: Groups
member: uid=david,ou=users,dc=example,dc=com
refint_nothing
的值仅在需要时使用。
这最后两个示例涉及的是使用 delete
操作的情况。但是 RefInt 覆盖还处理使用 modrdn
操作更改 DN 的情况。例如,如果我们不是删除 uid=marcus
的记录,而是更改了 DN 会怎么样?使用之前的示例,让我们从相同的两条记录开始:
dn: uid=marcus,ou=users,dc=example,dc=com
uid: marcus
sn: Tullius
cn: Marcus Tullius
givenName: Marcus
ou: users
objectclass: person
objectclass: organizationalperson
objectclass: inetOrgPerson
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectclass: groupofnames
cn: Public Relations
ou: Groups
member: uid=marcus,ou=users,dc=example,dc=com
让我们将第一条记录的 DN 改为 Marcus Tullius 更为人知的名字:
$ ldapmodrdn -U matt uid=marcus,ou=users,dc=example,dc=com
uid=cicero
在之前的示例中,我们更改了 DN uid=marcus,ou=users,dc=example,dc=com
,用一个新的相对 DN uid=cicero
替换了相对 DN 部分 (uid=marcus
)。现在第一条记录看起来像这样:
dn: uid=cicero,ou=users,dc=example,dc=com
uid: marcus
uid: cicero
sn: Tullius
cn: Marcus Tullius
givenName: Marcus
ou: users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
ldapmodrdn
客户端添加了新的 uid
属性值 (cicero
),然后将条目的 DN 从 uid=marcus,ou=users,dc=example,dc=com
更改为 uid=cicero,ou=users,dc=example,dc=com
。那么 cn=Public
Relations
组呢?它现在看起来像这样:
dn: cn=Public Relations,ou=Groups,dc=example,dc=com
objectClass: groupOfNames
cn: Public Relations
ou: Groups
member: uid=cicero,ou=users,dc=example,dc=com
RefInt 属性将 member
属性的值更改为指向新修改的 DN。记住,如果没有 RefInt 覆盖,cn=Public
Relations
组将指向现在已经不存在的 DN uid=marcus,ou=users,dc=example,dc=com
。
缺点
使用 RefInt 覆盖是否存在任何缺点?性能是一个问题。对于每次删除或 DN 修改,RefInt 覆盖将检查 refint_attributes
指令中列出的所有属性的所有值。大量的删除或 DN 修改可能会影响系统性能。但是在大多数情况下,大规模的 delete
和 modrdn
操作并不是常见的(而且在进行此类操作时,可以随时关闭该覆盖)。
还有一个值得考虑的缺点。一些应用程序确实会自行处理引用检查。可能存在一个写得不好的客户端试图删除不存在的属性值,从而生成虚假的错误消息。当然,这不会对目录信息树产生负面影响,但可能会引起用户的警觉。然而,绝大多数客户端,包括许多执行自身完整性检查的客户端,都不应该受到 RefInt 覆盖层的影响。
有用的备注
在安装新覆盖层后启动 SLAPD 时,出现以下警告信息并不少见:
WARNING: No dynamic config support for overlay refint.
这条信息是什么意思?问题严重吗?
在使用slapd.conf
文件配置 OpenLDAP 时,可以忽略此警告信息。这只是一个通知,表示一旦服务器启动后,无法更改此覆盖层的配置选项。但当然,所有在slapd.conf
文件中的指令都是如此。
该警告信息仅适用于将配置加载到目录作为 LDIF 文件并在目录服务器内管理配置(使用cn=Config
记录)的安装。这一功能比较新,并且由于不支持 OpenLDAP 的所有特性(如许多覆盖层),因此它不是大多数客户端推荐的配置。
唯一性覆盖层
本节中我们将要检查的最后一个覆盖层是唯一性覆盖层。唯一性覆盖层强制执行目录中特定属性集的唯一性。它防止不同记录中的属性包含相同的值。例如,当处理uid
属性时,这是我们所期望的,因为显然我们不希望系统中多个用户拥有相同的 UID。默认情况下,SLAPD 只强制执行 DN 的唯一性——没有两个 DN 可以相同。但其他属性值则不受检查。通过使用唯一性覆盖层,我们可以指定希望 SLAPD 强制唯一性的属性。
配置唯一性覆盖层的第一步是加载该模块:
modulepath /usr/local/libexec/openldap
moduleload back_hdb
moduleload denyop
moduleload refint
moduleload unique
在slapd.conf
的基本设置部分,我们添加了一个moduleload
指令。我们希望加载的模块名为unique
。
接下来,我们希望将此覆盖层和一些特定的指令添加到相关的数据库部分:
overlay unique
unique_base dc=example,dc=com
unique_attributes uid
这是唯一性覆盖层的非常基础的配置。unique_base
指令指明了我们希望在其中强制实施唯一性的目录信息树的部分。对于我们的练习,我们希望在整个目录树dc=example,dc=com
中强制实施唯一性。
unique_attributes
指令接受一个以空格分隔的属性列表,唯一性覆盖层将在这些属性上强制执行唯一性约束。在此示例中,我们只希望在 UID 属性上强制执行唯一性。
备注
唯一性叠加层的行为预计将在下一个版本的 OpenLDAP(版本 2.4)中发生变化。特别是,它将支持在单个数据库中使用多个基。
因此,根据我们的配置,dc=example,dc=com
目录信息树中任何记录的 UID 值都不应该相同。
现在让我们看看这个叠加层在实际中的表现。
在讨论 RefInt 叠加层时,我们创建了以下记录:
dn: uid=cicero,ou=users,dc=example,dc=com
uid: marcus
uid: cicero
sn: Tullius
cn: Marcus Tullius
givenName: Marcus
ou: users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
请注意,这个记录的 UID 是 marcus
,即使该属性在 DN 中没有被使用。现在让我们尝试添加以下记录:
dn: uid=marcus,ou=users,dc=example,dc=com
uid: marcus
sn: Aurelius
cn: Marcus Aurelius
givenName: Marcus
ou: users
objectclass: person
objectclass: organizationalperson
objectclass: inetOrgPerson
该记录也使用了 UID marcus
。如果没有唯一性叠加层,SLAPD 会允许这两个记录拥有相同的 UID。当然,这会导致假设唯一 ID 确实是唯一的应用程序出现问题——对 UID 属性进行搜索时只会返回零个或一个结果。
但是,使用我们配置的唯一性叠加层,SLAPD 将阻止客户端添加与现有 UID 值匹配的 UID 值。唯一性叠加层通过检查 add
、modify
或 modrdn
操作中的属性来实现这一点。如果我们尝试为 uid=marcus
添加记录,会收到一个错误:
$ ldapadd -U matt -f unique-example.ldif
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
adding new entry "uid=marcus,ou=users,dc=example,dc=com"
ldap_add: Constraint violation (19)
additional info: some attributes not unique
SLAPD 返回 约束违反 错误,因为唯一性叠加层不允许重复的 UID 属性值。为了解决这个问题,我们必须删除 uid=cicero
记录中的额外 UID 属性,或者为 Marcus Aurelius 的记录使用一个不同的 UID。
我们刚才看到的示例配置代表了唯一性叠加层最典型的使用方式。还有两个额外的唯一性指令可以提供更复杂的配置:
第一个是 unique_ignore
指令。通常情况下,这是 替代 unique_attributes
使用的。
提示
虽然你可以同时使用 unique_attributes
和 unique_ignore
,但不推荐这样做,因为这可能会导致意外的行为。有关更多详细信息,请参阅手册页:man
slapo-unique
。
unique_ignore
指令接受一个由空格分隔的属性列表,这些属性 不应该 被测试是否唯一。某些属性,如 ou
、sn
和 objectclass
,可能会在目录中合法地被多次使用。例如,组织中多个员工可能有相同的姓氏,因此具有相同的 sn
属性值是完全可能的。
但是,当没有指定 unique_attributes
时,默认情况下所有 非操作性 属性 都会被假定为需要唯一性。请考虑这个示例配置:
overlay unique
unique_base dc=example,dc=com
unique_ignore objectclass sn ou description
根据这个配置,目录信息树中除了 objectclass
、sn
、ou
和 description
外,所有属性值都必须具有唯一值。显然,这个配置比我们第一个示例更为严格,使用时需要小心。
注意
操作参数——那些用于内部 SLAPD 的参数——在任何情况下都不会自动添加到唯一性列表中。这样做可能会导致难以调试的错误,从而阻止 SLAPD 正常工作。
最后,关于唯一性覆盖层,还有一个额外的指令。unique_strict
指令不带参数,可用于开启“严格”的唯一性强制。
默认情况下,唯一性覆盖层允许多个属性具有空(null)值。例如,如果我们强制执行uid
属性的唯一性,SLAPD 仍然允许多个记录具有空值的 UID 属性。但这并不总是可取的。在某些情况下,可能需要确保只有一个属性具有空值。unique_strict
指令用于此目的。
当unique_strict
指令存在时,如果相同属性的另一个实例已经存在且其值为空(null),唯一性覆盖层将不允许客户端将该属性值设置为空。
到此为止,你应该对如何使用覆盖层有了一个较好的了解。我们已经看过了三种不同的覆盖层,但在接下来的章节中,我们将研究更多的覆盖层。
总结
本章的重点是 SLAPD 服务器的高级配置。我们从重新审视slapd.conf
文件开始。然后,我们向目录服务器添加了一个额外的数据库,支持第二个目录信息树。从那里,我们探讨了使用slapd.conf
文件中的指令来提升 SLAPD 性能的方法,还调优了 Berkeley DB 的DB_CONFIG
文件。在最后一部分,我们研究了 SLAPD 的覆盖层引擎,介绍了三种特定的覆盖层。
到目前为止,你应该已经能够舒适地使用slapd.conf
文件,并且掌握覆盖层的使用。
在下一章,我们将研究 LDAP 模式,添加一些新的覆盖层的模式,然后创建我们自己的模式。稍后,在第七章中,我们将在讨论如何配置多个 OpenLDAP 服务器协同工作时,扩展本章中的一些主题。
第六章:LDAP 架构
本章的重点将是 LDAP 架构。架构是描述可能存储在目录中的对象结构的标准方式。本章的前几节旨在提供架构的基本知识,解释架构的作用和工作原理——这是我们在本章后续部分使用和实现架构所需的基础知识。但我们将继续探讨一些更实际的主题,包括添加预定义架构和定义我们自己的自定义架构。
我们将从对架构的一般性检查开始。从那里,我们将深入探讨架构的层次结构。像目录信息树本身一样,架构也被组织成层次结构。接下来,我们将研究 OpenLDAP 中包括的一些基本架构。我们还将查看需要自己架构的两个覆盖。最后,我们将创建一个自定义架构,由一对新的对象类组成,每个类都有新的属性。本章我们将讨论的主要主题包括:
-
架构定义的基础
-
三种类型的对象类
-
在 OpenLDAP 中使用不同的架构
-
配置访问日志和密码策略覆盖
-
获取和使用对象标识符(OID)
-
手动创建新架构
LDAP 架构简介
我们已经看过 OpenLDAP 中使用的各种属性和对象类。例如,我们使用 person
、organizationalPerson
和 inetOrgPerson
对象类为用户创建了条目,并在此过程中使用了诸如 cn
、sn
、uid
、mail
和 userPassword
等属性。我们还使用 groupOfNames
和 groupOfUniqueNames
对象类创建了组,并特别注意了 member
和 uniqueMember
属性。我们甚至简要地(在第三章中)看过用于描述文档和文档集合的对象类和属性(分别为 document
和 documentCollection
)。
每个对象类和属性都有严格的定义。属性和对象类的定义被捆绑在一起,形成较大的集合,称为架构。OpenLDAP 应用程序使用这些架构来确定记录应该如何结构化,以及每个条目在层次结构中的位置。
为什么它们看起来如此复杂?
LDAP 架构有一个不好的声誉。它们被认为是复杂的、神秘的、超技术的,并且难以实现。本章的目标是克服这种看法。
尽管如此,这种声誉仍然是可以理解的。我认为,LDAP 架构中有几个方面对于新手来说是令人生畏的。
首先,LDAP 架构基于多代技术规范,这些规范来源于复杂的 X.500 系统。由于这种遗产,LDAP 架构频繁使用一些不特别适合人类理解的设备,例如像这样看起来的对象标识符:1.3.6.1.4.1.1466.115.121.1.25
。然而,掌握一点背景知识就能克服这个障碍。
其次,LDAP 模式定义语言与 SQL 开发人员熟悉的定义语言(DDL)显著不同。这主要是由于后端数据库的性质不同。LDAP 不像关系型数据库那样固有地是表格化的,尽管它经常使用像继承这样的概念(在 SQL DDL 语言中是罕见的,尽管一些支持这种概念)。最后,虽然 SQL DDL 是以 SQL 命令的形式出现,但 LDAP 模式定义则是纯粹的描述性。
但是,LDAP 模式语言实际上相当简洁,通常只需要两个指令(attributetype
和objectclasstype
),每个指令有一些参数,就可以创建自定义模式。因此,学习曲线较短,到了本章结束时,你应该能够轻松创建自己的模式。
通常,模式是以纯文本文件形式编写并存储在 OpenLDAP 配置文件夹的子目录中。在 Ubuntu 中,这些文件位于/etc/ldap/schema
。如果是从源代码构建,模式文件默认位于/usr/local/etc/openldap/schema
。
SLAPD 不会自动使用模式目录中的所有模式。SLAPD 启动时,它只加载在slapd.conf
文件中指定的模式。
注意
这个规则有一个例外:某些重要的 LDAP 模式组件,如objectclass
,是硬编码到 OpenLDAP 中的,因为它们对服务器的操作至关重要。
通常,模式是通过include
指令来包含的。在第二章中,我们在slapd.conf
文件中包含了三个模式文件。文件顶部附近的 include 部分如下所示:
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
第一行导入核心模式,其中包含标准 LDAP 使用所必需的属性和对象类的模式。第二行导入了一些常用的对象类和属性,包括用于存储文档信息和 DNS 记录的属性。inetorgperson.schema
文件包含了 inetOrgPerson 对象类定义及其相关的属性定义。
在接下来的章节中,我们将查看这些文件的格式,实现一些现有的模式,最后创建我们自己的模式。
模式定义
LDAP 模式用于正式定义属性、对象类和各种结构化目录信息树的规则。术语模式指的是一组(概念上相关的)模式定义。例如,inetOrgPerson
模式包含inetOrgPerson
对象类的定义,以及所有由inetOrgPerson
对象类允许或要求的额外(非核心)属性。
模式定义是一种特殊类型的指令,用于提供关于如何构建 SLAPD 中某个特定实体的信息。可以在slapd.conf
(或包含的模式定义)中包含四种不同类型的模式定义:
-
对象类定义:这定义了一个对象类,包括它的唯一标识符、名称以及它可能或必须具备的属性。
-
属性定义:这定义了一个属性,包括它的唯一标识符、名称或名称、允许作为值的内容类型规则,以及如何执行匹配操作。
-
对象标识符:这将一个字符串名称附加到唯一标识符。它主要用于加速创建模式。
-
DIT 内容规则:这指定了具有特定结构对象类的条目可以拥有的附加(辅助)对象类的规则。
除了这四种,还有其他不通常放置在模式中的模式定义。大多数这些是由 OpenLDAP 代码生成的。以下是每种的简要描述(更多信息请参见定义 LDAP 模式语言的 RFC 4512):
-
匹配规则定义:这些定义了用于匹配操作的规则。搜索可以使用匹配规则(例如相等匹配和子串匹配)来查找特定的属性值。例如,
distinguishedNameMatch
匹配规则(唯一标识符为2.5.13.1
)定义了用于精确匹配 DN 的匹配规则。该规则被像member
(用于组成员)和seeAlso
这样的属性使用。使用此规则进行搜索时,只有当属性值与给定的 DN 匹配时,才会返回成功的结果。任何属性的匹配规则决定了可以为该属性创建哪些索引。 -
匹配规则使用:这些将属性映射到匹配规则,通常由 SLAPD 动态创建。根据此定义,客户端可以判断某个特定的匹配规则适用于哪些属性。例如,它可以用来查找支持精确 DN 匹配的所有属性值(
distinguishedNameMatch
匹配规则)。匹配规则使用的模式定义(matchingRuleUse
)包含一个唯一标识符、匹配规则名称以及该匹配规则适用的所有属性。 -
LDAP 语法:这些描述了允许用于属性值内容的语法。定义属性时,可以指定属性值的确切类型和语法。SLAPD 定义了多个支持的语法(
ldapSyntaxes
),包括 DN 结构的语法、二进制数据的语法、几种纯文本数据的语法等。当我们查看属性定义时,我们会进一步讨论支持的语法。 -
结构规则:这些定义了给定条目可以在目录信息树中位于何处。它基于条目的结构化对象类。结构化对象类和对象类层次结构将在后面的对象类层次结构部分进行讨论。
-
名称格式:这些指定了在条目的 DN 的 RDN 部分(基于条目的结构对象类)中可能或必须使用的属性。
SLAPD 通过代码构建架构的这一部分。例如,匹配规则的使用是基于现有的匹配规则以及哪些属性实现了这些匹配规则。像架构的其他部分一样,匹配规则、LDAP 语法、结构规则和名称格式都可以通过 LDAP 协议进行访问。有关更多信息,请参见从 SLAPD 检索架构部分。
目前,我们将主要关注可以包含在slapd.conf
文件中的四个架构定义。特别是,我们将重点讨论如何创建新的对象类和属性。
对象类和属性
我们需要两种不同类型的架构定义,以便扩展我们的目录服务器将存储的信息类型:
-
属性类型定义:属性类型定义定义一个属性,包括该属性可能拥有的名称(例如,
cn
和commonName
)、属性可能包含的值类型(数字、字符串、DN 等)、匹配值时使用的规则,以及该属性是否可以有多个值。每个属性可能要求其值或多个值由特定的字符或数据类型组成。例如,
description
属性允许长字符串字符,这使得可以将一句或两句信息作为描述字段的值。 -
对象类定义:对象类定义指定对象类的名称、必须拥有的属性、可拥有的属性以及它是何种类型的对象。
我们将逐一查看这些内容。首先,回顾一下在第三章中介绍的一个架构。以下是person
对象类的图示:
person
对象类有两个必需的属性(cn
和sn
),以及四个允许的但非必需的属性:userPassword
、telephoneNumber
、seeAlso
和description
。
一个新的person
对象类记录(且没有其他对象类)可能如下所示:
dn: cn=Thomas Reid, dc=example,dc=com
objectclass: person
cn: Thomas Reid
sn: Reid
userPassword:: DSFSUYJKHGH=
telephoneNumber: 555-555-5555
seeAlso: uid=david,ou=users,dc=example,dc=com
description: A basic user.
该记录包含person
对象类中的所有属性,并且仅包含这些属性。尝试添加架构中未提到的其他属性类型会导致错误。同样,试图删除cn
或sn
属性的所有值也会导致错误,因为这些属性是必需的。
那么,OpenLDAP 是如何知道哪些属性是必需的,哪些是允许的呢?这些信息存储在person
对象类的架构定义中。
对象类定义
架构定义存储在core.schema
(以及core.ldif
)文件中,路径为/etc/ldap/schema
(如果从源代码编译,则为/usr/local/etc/openldap/schema
)。请查看以下内容:
objectclass
(
2.5.6.6
NAME 'person'
DESC 'RFC4519: a person'
SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)
这是一个简单的对象类定义。它以描述符objectclass
开头,告诉模式解释器正在创建什么类型的定义。其余部分被圆括号括起来。额外的空白字符,包括换行符,通常会被忽略(除非被引用的字符串包含其中),但请记住
由于objectclass
是slapd.conf
文件格式中的指令,因此除了第一行外,每一行必须以空白字符开头。
定义中的第一个字段是对象类的数字标识符:2.5.6.6
。这个唯一标识符称为对象标识符(OID)。每个模式定义都有一个唯一的 OID,它将该定义与世界上任何其他定义区分开来。因为这个 OID 应是全球唯一的,所以为定义指定唯一标识符有一个官方程序。稍后会在章节中描述这一点。现在,值得注意的是,这些 OID 必须是全局唯一的。
任何 LDAP 应用程序都可以通过 OID 引用定义。对象类、属性、匹配规则以及许多其他 LDAP 实体都有 OID。
注意
根 DSE 记录是 LDAP 客户端如何根据服务器提供给客户端的 OID 了解服务器能力的一个好例子。请参阅附录 C 中的示例。
定义中的第二个字段是NAME
字段。虽然 OID 易于计算机使用,但人类并不容易理解它。因此,除了 OID 外,还可以指定服务器唯一的名称(以字符字符串形式)。上面的对象类只有一个名称:person
。一个对象类可以有多个名称,但通常一个名称就足够了。
在模式定义中,字符串名称应始终用单引号括起来。在字符串值的列表中,每个值必须用单引号括起,并且整个列表必须用圆括号括起来。例如,如果人员定义指定了两个名称,person
和humanBeing
,则NAME
字段应如下所示:
NAME ( 'person' 'humanBeing' )
还请注意,NAME
字段的值中不允许有空格,因此'human
being'
将是一个非法名称。
注意
在属性定义中,通常会给属性指定一个长名称和一个简称。例如,cn
和commonName
都是属性2.5.4.3
的名称。
大多数时候,对象类和属性是通过NAME
字段中的值来引用,而不是通过 OID。
注意
按惯例,由多个单词组成的名称通过大写每个单词的首字母来连接,首个单词除外。例如,commonName
由两个单词组成:common
和name
,只有第二个单词的首字母大写。通常不会使用下划线、连字符或其他特殊字符来连接单词。因此,您不应使用像common_name
或common-name
这样的名称。
DESC
字段是对该模式定义用途的简要描述。在此情况下,描述字段指的是 RFC(RFC 4519),该 RFC 详细解释了对象类。当然,并不需要为正式定义模式创建 RFC,但如果计划广泛分发该模式,编写 RFC 是一个好主意。
接下来的字段是SUP
,即“上级”的缩写,指示该对象类的父对象类是什么。person
对象类的父对象类是名为top
的对象类。对象类像目录信息树一样按层次结构组织,top
对象类位于对象类层次结构的顶端。STRUCTURAL
关键字也与该模式定义如何适应模式层次结构相关。我们将在下一部分讨论模式层次结构。
最后两个字段比较简单。它们定义了person
对象必须(MUST
)包含的属性,以及该对象可以(MAY
)包含的属性。
MUST
和MAY
字段的语法很简单。每个描述都包含一组属性:
MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
属性值的列表(可以通过 OID 或属性名称指定)被括在圆括号内。值之间用美元符号($
)分隔。上面的例子表示,userPassword
、telephoneNumber
、seeAlso
和description
四个值是一个人对象可以具有的属性。
属性应该仅在两个列表中的一个中指定。无需将一个属性同时放入MAY
和MUST
列表。
当然,名称也可以用 OID 替代。因此,以下两行是等价的:
MUST ( sn $ cn )
和
MUST ( sn $ 2.5.4.3 )
cn
属性的 OID 是2.5.4.3
,无论使用哪个标识符都可以。
对象类定义中可能包含一些字段,但这些字段在前面的代码中没有出现。第一个是OBSOLETE
关键字,它出现在DESC
字段之后。这个关键字用于标记一个对象类已经过时,但仍然(暂时)支持。
第二部分是扩展部分,用于为模式提供特定于实现的扩展。在模式的末尾可以指定一个或多个扩展。扩展是一个关键字,后跟括号中的列表。默认情况下,OpenLDAP 的schema/
目录中包含的所有模式都没有扩展。
总结来说,对象类定义以objectclasstype
指令开始,可以包含以下字段:
-
一个唯一的 OID,用于标识该对象类(例如:
2.5.6.6
)。 -
一个
NAME
字段,带有一个唯一的名称(NAME
'person'
)。 -
一个
DESC
字段,用于简要描述该对象类的目的(DESC
'RFC4519:
a
person'
)。 -
如果该类已经过时并不应再使用,可以选择性地包含
OBSOLETE
标签。 -
SUP
行表示此对象类的父类(上级)。此外,该行应指定对象类的类型(STRUCTURAL
、ABSTRACT
或AUXILLIARY
)。例如:SUP
top
STRUCTURAL
。抽象类没有上级。在定义抽象类时,可以省略SUP
。 -
MUST
字段列出必须为该对象类实例指定的属性。例如:MUST
(
sn
$
cn
)
。 -
MAY
字段列出可以选择性地添加到此对象类记录中的属性。例如:MAY
(
userPassword
$
telephoneNumber
$
seeAlso
$
description
)
。 -
一个或多个扩展。
对象类定义是模式的重要部分,我们将在本章中多次回顾这些概念。在涵盖其他定义类型后,我们将详细研究对象类层次结构。在这个过程中,SUP
行的作用将变得更加明确。
稍后我们将查看一些特定的对象类,并编写我们自己的自定义对象类。但在我们继续这些内容之前,我们将查看其他模式定义。接下来,我们将查看属性定义。
属性定义
我们现在检查的person
对象类可以拥有六个不同的属性——两个必要的sn
和cn
属性,以及可选的userPassword
、telephoneNumber
、seeAlso
和description
属性。正如对象类在模式中被定义一样,每个属性也都有定义。属性定义的语法类似,但定义中允许的字段不同且更多。
telephoneNumber
属性的模式定义是一个基本属性定义的好例子:
attributetype
(
2.5.4.20
NAME 'telephoneNumber'
DESC 'RFC2256: Telephone Number'
EQUALITY telephoneNumberMatch
SUBSTR telephoneNumberSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32}
)
属性定义以attributetype
指令开始。定义的其余部分被括号括起来。
定义中的第一个字段是此属性的唯一 OID。与所有 OID 一样,这个标识符必须是全局唯一的。OID 2.5.4.20
应仅用于表示telephoneNumber
属性。本章稍后,在获取 OID一节中,我们将讨论如何获取和使用基本 OID。
OID 之后是NAME
字段,用于将一个或多个名称与属性关联。
注意
在NAME
字段中给出的名称通常被称为属性 描述(请参见第三章中对搜索操作的讨论)。在讨论模式定义时,这个术语可能会令人混淆,因为属性模式定义中有一个描述字段,而该字段并不是属性描述。
属性通常有两个名称——一个长名称(例如commonName
或surname
)和一个缩写名称(分别为cn
或sn
)。当一个属性有多个名称时,名称列表应放在括号中。例如,考虑fax
属性的NAME
字段:
attributetype
(
2.5.4.23
NAME ( 'facsimileTelephoneNumber' 'fax' )
DESC 'RFC2256: Facsimile (Fax) Telephone Number'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.22
)
请注意高亮行的语法。列表中的每个名称都用单引号('
)括起来,并且整个列表被括号包围。
注意
SLAPD 将通过第一个名称引用属性。因此,如果你搜索fax
属性,SLAPD 会返回匹配的属性facsimileTelephoneNumber
,而不是fax
。
DESC
字段提供了属性目的的简要描述。在telephoneNumber
属性定义中,该字段的值为'RFC2256:
Telephone
Number'
,表示该属性在 RFC 2256 中有定义。
定义属性时,一个重要的方面是指定应用程序应如何测试两个属性值是否匹配。TEST
和test
匹配吗?在某些情况下我们可能希望它们匹配,而在其他情况下则不希望它们匹配。t*st
是否与test
匹配?同样,在某些情况下,这是可以接受的,而在其他情况下则不是。
我们可以在属性定义中确定应使用哪些匹配规则来测试一个值是否匹配另一个值。当我们在第三章讨论搜索操作时,我们看到可以在搜索筛选器中使用四种不同的比较运算符:
-
相等运算符(
=
) -
近似运算符(
~=
) -
大于或等于运算符(
>=
) -
小于或等于运算符:(
<=
)
除了这些,我们还考虑了使用正则表达式字符,如星号(*
),来匹配属性值的部分或子字符串。这些行为在很大程度上由属性定义中的匹配规则决定。
当 LDAP 服务器处理比较操作(如绑定、比较和搜索操作)时,它会使用模式来确定如何处理这些比较。模式指定应使用哪些匹配规则。可以在模式中分配三种不同类型的匹配规则:
-
相等性规则,EQUALITY
-
排序规则,ORDERING
-
子字符串匹配规则,SUBSTRING
属性模式可以为这三种规则之一、两种或三种都指定规则。每个规则的值可以是 OID 或匹配规则的名称。在telephoneNumber
模式中,使用了EQUALITY
和SUBSTRING
规则:
EQUALITY telephoneNumberMatch
SUBSTR telephoneNumberSubstringsMatch
当请求对电话号码进行相等性测试时,例如评估筛选器(telephoneNumber=+1
234
567
8901)
,会使用telephoneNumberMatch
规则。请注意,+
符号是电话号码的一部分,而不是运算符的一部分。如果筛选器包含通配符匹配,例如(telephoneNumber=+1
234
567*)
,则会使用telephoneNumberSubstringsMatch
规则。
注意
如果没有定义ORDERING
规则,SLAPD 将不会处理>=
或<=
运算符的匹配测试。任何比较都会返回假。
这两种匹配规则如何执行呢?我们来看一个例子。当我们定义 UID 为matt
的用户时,我们为该用户分配了电话号码。这里,我们将搜索该条目,只请求telephoneNumber
属性:
$ ldapsearch -LL -U matt '(uid=matt)' telephoneNumber
搜索结果如下:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=matt,ou=Users,dc=example,dc=com
telephoneNumber: +1 555 555 4321
telephoneNumber
属性的值为+1 555 555 4321
。现在让我们使用电话号码进行搜索:
$ ldapsearch -LL -U matt '(telephoneNumber=+1 555 555 4321)' uid \
telephoneNumber
搜索结果如下:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=matt,ou=Users,dc=example,dc=com
uid: matt
telephoneNumber: +1 555 555 4321
如预期的那样,使用准确的电话号码进行搜索返回了一个结果。这看起来与我们预期的字符串匹配规则没有什么不同。不过,使用架构中的特殊telephoneNumberMatch
规则有一些优势。当使用此匹配规则时,SLAPD 会忽略某些电话号码格式化字符。下面是一个使用子字符串搜索的示例:
$ ldapsearch -LL -U matt '(telephoneNumber=+1 555-555-43*)' uid \
telephoneNumber
这是结果:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=matt,ou=Users,dc=example,dc=com
uid: matt
telephoneNumber: +1 555 555 4321
此示例中的过滤器使用了破折号(-
),而之前的过滤器使用了空格。使用telephoneNumberSubstringMatch
规则时,SLAPD 忽略了破折号。使用telephoneNumberMatch
和telephoneNumberSubstringMatch
规则时,+15555554321
、+1 555 555 4321
、1-5-5-55554-3-2-1
和+1 555-555-4321
这些号码都被视为相同的匹配项。
这说明了能够在架构中指定匹配规则的优点。对于cn
、sn
或mail
(电子邮件地址)等属性,我们当然不希望破折号被当作空格字符来处理。我们也不希望Dan
和Forth
与Danforth
匹配。但是,当匹配电话号码时,这显然是一个理想的功能。LDAP 对此问题的解决方案是为存储在属性中的信息类型分配适当的匹配规则。
注意
其他属性,如homePhone
、pagerTelephoneNumber
和mobileTelephoneNumber
(都在cosine.schema
中定义),也都使用telephoneNumberMatch
和telephoneNumberSubstringMatch
匹配规则。由于它们共享相同的格式,因此无需为每个属性分配不同的专用匹配规则。
注意
匹配规则和索引
一些后端,如 BDB 和 HDB,支持索引(使用slapd.conf
中的index
指令)。支持的索引由为属性定义的匹配规则决定。例如,具有相等匹配规则的属性可以拥有相等(eq
)索引。同样,具有子字符串匹配规则的属性支持sub
索引。
telephoneNumber
匹配方案中的最后一个字段是SYNTAX
字段。这与存储在telephoneNumber
属性值中的数据类型和结构有关。
SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32}
SYNTAX
参数的值有两个部分。第一个是 LDAP 语法的 OID(或名称),第二部分是用大括号({
和}
)括起来的最大长度(通常是字符数)。长度说明符是可选的,服务器没有义务强制执行最大长度。
前面提到的 OID,1.3.6.1.4.1.1466.115.121.1.50
,是电话号语法。这表明,telephoneNumber
属性的实例的属性值应包含电话号码所需的字符(整数、破折号、空格等)。SLAPD 会拒绝包含字母和其他特殊字符的电话号码。稍后在本章的创建 模式部分,我们将查看 OpenLDAP 支持的常见 LDAP 语法列表。
就复杂性而言,telephoneNumber
属性属于中等水平。然而,许多属性定义要短得多,利用了在类似属性中设置的字段。因此,许多属性由于从其上级(父级)属性继承了大部分特性,仅具有 OID、NAME
字段和DESC
字段。广受欢迎的cn
属性的模式定义如下:
attributetype
(
2.5.4.3
NAME ( 'cn' 'commonName' )
DESC 'RFC2256: common name(s) for which the entity is known by'
SUP name
)
在这种情况下,SUP
名称字段表明name
属性是cn
属性的父属性。属性和对象类一样,可以按层次结构组织。上级属性是该属性的父级或原型,如果在模式定义中未指定某些属性,它们将从上级继承。例如,语法和匹配规则可以从父级继承。
在之前的示例中没有指定匹配规则和 LDAP 语法。因此,cn
属性类型从其上级继承了这些值。name
属性使用caseIgnoreMatch
EQUALITY
匹配规则和caseIgnoreSubstringMatch
SUBSTR
规则,并使用目录字符串 LDAP 语法(1.3.6.1.4.1.1466.115.121.1.15
)。目录字符串是一个 UTF-8 编码的字符串,用于存储文本。
之前的示例中没有涉及的一些其他字段包括OBSOLETE
、SINGLE-VALUE
、COLLECTIVE
、NO-USER-MODIFICATION
、USAGE
和扩展区。我们来简要看一下这些字段。
OBSOLETE
标志通常出现在DESC
字段后,在属性定义中与在对象类定义中起到相同的作用。它标记一个属性为过时。虽然过时的属性仍然受到支持,并且可以用于目录信息树中的记录,但它们应该被视为已弃用,并可能在未来的模式或软件版本中被移除。OBSOLETE
不接受任何参数。
SINGLE-VALUE
标志表示定义的属性只能有一个属性值。通常,一个属性可以有任意数量的值。但任何包含SINGLE-VALUE
标志的属性,其值不能超过一个。我们在第三章和第四章中看到的域组件(dc
)属性就是一个例子。拥有dc
属性的对象只能为该属性分配一个值。SINGLE-VALUE
不接受任何参数。
COLLECTIVE
标志表示该属性是一个集合属性。条目可以与集合属性一起分组,形成 条目集合。
集合在 OpenLDAP 中通过 collect
覆盖实现,该覆盖默认并未编译或安装,但可以在源代码的 servers/slapd/overlays
目录中找到。支持集合的模式在 OpenLDAP 分发版中也没有默认包含,必须从其他来源(如 RFC 3671)复制。
以下是条目集合如何工作的粗略示意:
-
一个记录是集合记录,必须使用
collectiveAttributeSubentry
对象类。这将成为该集合属性的权威。所有其他下级记录都继承该属性(及其值),并且该属性作为每个记录的属性可见(但为只读)。有关集合的更多信息,请参见 RFC 3671(www.ietf.org/rfc/rfc3671.txt
)。 -
NO-USER-MODIFICATION
标志用于表示该属性是一个操作性属性(由 SLAPD 或覆盖使用),不能被 LDAP 客户端修改。这通常不用于用户定义的模式。只有在编写自定义覆盖并使用其操作属性时,才使用该标志。 -
USAGE
字段提供 SLAPD 该属性的使用信息。该字段有四个可能的值。前三个值directoryOperation
、distributedOperation
和dSAOperation
表示 SLAPD 自身使用该属性。最后一个值userApplication
是默认值,表示该属性主要用于客户端应用程序。由于大多数模式是为客户端应用程序设计的,因此默认值通常是所需的,USAGE
字段很少使用。 -
最后,
attributetype
定义还可以使用扩展,尽管在 OpenLDAP 中包含的主要模式中没有使用扩展。扩展的语法对于属性类型与对象类定义相同。
总结来说,属性模式定义以 attributetype
指令开始,后跟一个括号括起来的模式定义。属性定义中允许包含以下字段:
-
一个唯一的 OID 编号,这是必需的。例如:
2.5.4.15
。 -
一个
NAME
字段,包含该属性的一个或多个名称。例如:NAME
'businessCategory'
。 -
一个
DESC
字段,包含该属性类型的描述。例如:DESC
'RFC2256:
business
category'
。 -
如果属性已弃用,则有一个
DEPRECATED
标签。 -
一个
SUP
字段,包含上级属性类型的名称或 OID。例如:SUP
postalAddress
。 -
一个
EQUALITY
匹配规则的 OID 或名称。例如:EQUALITY
caseIgnoreMatch
。 -
一个
ORDERING
匹配规则的 OID 或名称。 -
一个
SUBSTR
匹配规则的 OID 或名称。例如:SUBSTR
caseIgnoreSubstringsMatch
。 -
一个带有 LDAP 语法 OID 和可选长度的
SYNTAX
字段。示例:SYNTAX
1.3.6.1.4.1.1466.115.121.1.15{128}
。 -
SINGLE-VALUE
标志,如果该属性只能有一个值。 -
COLLECTIVE
标志,如果该属性是一个集合属性。 -
NO-USER-MODIFICATION
标志,如果该属性是一个操作性属性,客户端应用程序不应能够修改它。 -
USAGE
字段,与四个关键字中的一个一起使用(userApplication
、directoryOperation
、distributedOperation
或dSAOperation
),用来指示该属性的用途。 -
属性定义所需的任何扩展。
到此为止,我们已经看过了对象类定义和属性定义。当创建自定义模式时,最可能需要使用的就是这两种类型的模式定义。
我们已经讨论了模式的基础知识,并在文本中看了一些例子。在本章的后面部分,我们将查看一些其他具体的示例。如果你想查看更多属性和对象类模式的示例,可以浏览 OpenLDAP 模式目录中的文件(/etc/ldap/schema
或 /usr/local/etc/openldap/schema
)。最好的起点是 core.schema
模式,它定义了标准的 LDAPv3 模式。
注意
在阅读 core.schema
时,你可能会注意到一些非常重要的对象类和属性类型被注释掉了。为什么?因为它们包含在 系统模式 中,硬编码在 OpenLDAP 中。这个模式可以在 OpenLDAP 源代码中的 slapd/schema_prep.c
找到。
cosine.schema
文件包含了许多其他常用的模式,也是一个很好的参考地方。inetOrgPerson.schema
模式是一个很好的示例,展示了用户自定义模式文件的样子。或者,作为一个简短的用户自定义模式示例,可以查看 openldap.schema
。
虽然 attributetype
和 objectclass
是模式创建中使用的两个主要指令,但还有一些其他指令,我们将在接下来的两节中简要介绍。
对象标识符定义
对象标识符指令(objectidentifier
)是对标准定义语言的扩展。虽然它并没有为模式语言提供额外的功能,但它作为一个省时且人性化的工具,发挥着作用。
objectidentifier
指令用于为 OID 分配字符串别名。当 SLAPD 处理 attributetype
、objectclasstype
和 ditcontentrule
指令中的 OID 字段时,如果遇到的是字符串而不是 OID,它会检查该字符串是否是 OID 的别名,如果是,它将使用 OID 的值。在上一节中我们检查的 telephoneNumber
模式就是一个很好的例子:
attributetype
(
2.5.4.20
NAME 'telephoneNumber'
DESC 'RFC2256: Telephone Number'
EQUALITY telephoneNumberMatch
SUBSTR telephoneNumberSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32}
)
替代使用电话号平等和子串匹配规则的 OID(分别是1.3.6.1.4.1.1466.115.121.1.50
和1.3.6.1.4.1.1466.115.121.1.58
),schema 引用了匹配规则的名称:telephoneNumberMatch
和telephoneNumberSubstringMatch
。这种形式更容易让人类阅读。
objectidentifier
指令使得为 OID 数字定义别名变得容易,可以是整个 OID 或部分 OID。以下是为 OID 分配名称的简单示例:
objectidentifier exampleComDemo 1.3.6.1.4.1.8254.1021.3.1
在 schema 的顶部使用类似的指令使得后续可以通过exampleComDemo
来引用 OID。
注意
给定的 OID 是有效的,并且已经注册给作者。如果你正在开发自己的 LDAP schema,你应该注册你自己的 OID(请参见获取 OID部分)。虽然你可以在重新创建这些示例时使用这个 OID,但不要用它来编写你自己的扩展。否则,就无法确保这些 OID 在全球范围内的唯一性,这就违背了 OID 的目的。
例如,我们可以创建如下的 schema:
objectclass
(
exampleComDemo
NAME 'myPersonObjectClass'
DESC 'My Person Object Class'
SUP inetOrgPerson STRUCTURAL
)
请注意,我们使用的是exampleComDemo
别名,而不是使用该对象的 OID 号。但通常,我们不会为每个对象类分配一个别名。更方便的做法是别名化一个公共的根 OID,然后仅附加 OID 号的最后一部分。例如:
objectidentifier exampleComOC 1.3.6.1.4.1.8254.1021.1
objectclass
(
exampleComOC:1
NAME 'myPersonObjectClass'
DESC 'My Person Object Class'
SUP inetOrgPerson STRUCTURAL
)
在这个示例中,我们使用了objectidentifier
指令为 OID 基础创建了一个别名,所有我的对象类定义将使用这个基础。因此,当 SLAPD 遇到名称exampleComOC
时,它会将其展开为1.3.6.1.4.1.8254.1021.1
。myPersonObjectClass
的对象类定义应该具有 OID 1.3.6.1.4.1.8254.1021.1.1
(注意末尾的额外.1
)。我们使用exampleComOC
别名,并附加一个冒号(:
),然后加上对象类的数字后缀,而不是写出整个数字。
当 SLAPD 遇到exampleComOC:1
时,它会将其展开为1.3.6.1.4.1.8254.1021.1.1
。同样,如果我创建第二个对象类并使用所需的 OID 1.3.6.1.4.1.8254.1021.1.2
,我可以使用exampleComOC:2
,而不必输入整个长的 OID。
注意
使用objectidentifier
属性不仅可以减少打字量,还可以减少在容易出错的地方(且难以发现错误的地方)出现的拼写错误。
关于objectidentifier
指令的更多示例,请参阅 OpenLDAP 的 schema 目录中的openldap.schema
文件。
DIT 内容规则
我们将要查看的最后一个 schema 指令是ditcontentrule
指令,它用于创建DIT 内容规则。
注意
DIT 代表目录信息树(Directory Information Tree)。这是 LDAP 术语中一个常用的缩写。
DIT 内容规则标识特定的结构化对象类,并指示哪些辅助对象类可以(或不可以)包含在使用该对象类的条目中。
例如,我们可以使用第三章中介绍的一些对象类。在《LDIF 文件的结构》部分,我们创建了一个表示文档的条目。它实现了document
对象类,其模式(位于cosine.schema
中)如下所示:
objectclass
(
0.9.2342.19200300.100.4.6
NAME 'document'
SUP top
STRUCTURAL
MUST documentIdentifier
MAY ( commonName $ description $ seeAlso $ localityName $
organizationName $ organizationalUnitName $
documentTitle $ documentVersion $ documentAuthor $
documentLocation $ documentPublisher )
)
这是一个结构对象类。同样在第三章中,在《添加系统记录》部分,我们为uid=authenticate,ou=System,dc=example,dc=com
添加了条目。该条目实现了simpleSecurityObject
对象类。这里是simpleSecurityObject
的模式:
objectclass
(
0.9.2342.19200300.100.4.19
NAME 'simpleSecurityObject'
DESC 'RFC1274: simple security object'
SUP top
AUXILIARY
MUST userPassword
)
这个对象类是一个辅助对象类,意味着它可以添加到已经具有结构对象类的条目中,结果是该条目现在可以使用辅助对象类的属性。
注意
有关不同类型对象类及其功能的更多讨论,请参见第三章中的讨论以及本章中的《对象类层次结构》部分。
根据默认的 OpenLDAP 设置,如果我们有一个使用document
结构对象类的条目,我们可以通过向记录中添加objectclass:
simpleSecurityObject
,然后添加userPassword
属性,来为该文档设置密码(用于绑定到目录)。这样,我们就能得到一个类似这样的记录:
dn: documentIdentifier=011,uid=david,ou=Users,dc=example,dc=com
documentIdentifier: 011
documentTitle: Treatise on Human Nature
userPassword:: c2VjcmV0
objectClass: document
objectClass: simpleSecurityObject
这个条目本质上是一个能够登录的文档!一个使用此记录的 DN 和正确密码的客户端可以以该文档身份登录。
也许在某些情况下这是可取的,但为了这个示例,我们假设这是我们不想允许的配置。
通常,关于哪些条目具有哪些对象类的决定是由外部应用程序来做的。但如果我们想确保没有应用程序能为document
添加userPassword
属性怎么办?
解决这个问题的最佳方法是创建一个 DIT 内容规则,禁止向任何具有document
对象类的条目添加userPassword
属性。这可以通过ditcontentrule
指令来实现:
ditcontentrule
(
0.9.2342.19200300.100.4.6
NAME 'noPWForDocs'
DESC 'Do not allow passwords for documents'
NOT userPassword
)
ditcontentrule
指令的格式现在应该很熟悉了。像objectclass
和attributetype
指令一样,这个指令将 DIT 内容规则定义包裹在括号内。
第一个字段是一个 OID。但与其他模式定义不同,这个 OID 并不是该定义的 OID。相反,它是我们目标的结构化对象类的 OID。
在这种情况下,OID 0.9.2342.19200300.100.4.6
是document
对象类的 OID。你可以通过查看前面几页列出的文档模式,或者浏览 cosine 模式来验证这一点。
NAME
字段应该包含用于引用此规则的唯一名称。在大多数情况下,该字段的值用于报告日志文件中对该规则的引用,并且用于客户端的响应中。
DESC
字段包含该规则功能的简短文本描述。
NOT
字段包含一组 OID 或属性名称,指定应该禁止的内容。名称userPassword
来源于userPassword
属性定义中的NAME
字段。
有了这个内容规则,如果我们尝试将userPassword
属性添加到文档中,会发生什么呢?以下是使用ldapmodify
的一个示例:
$ ldapmodify -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: documentIdentifier=011,uid=dave,ou=users,dc=example,dc=com
changetype: modify
add: objectclass
objectclass: simpleSecurityObject
-
add: userPassword
userPassword: secret
modifying entry
"documentIdentifier=011,uid=dave,ou=users,dc=example,dc=com"
ldap_modify: Object class violation (65)
additional info: content rule 'noPWForDocs' precluded
attribute 'userPassword'
该示例中的高亮部分是尝试的修改。我们尝试将simpleSecurityObject
对象类和userPassword
属性添加到记录中。但服务器响应了一个对象类违规错误,并给出了以下原因:
content rule 'noPWForDocs' precluded attribute 'userPassword'
我们自定义的 DIT 内容规则成功地执行了它的任务——它阻止了将userPassword
属性添加到document
条目中。
我们上面创建的 DIT 内容规则是一个负面规则——它定义了条目不能拥有的属性。但是,ditcontentrule
也可以用来创建正面规则:即指定允许哪些属性(或辅助对象类)的规则。
例如,我们可以编写一个规则,要求每个inetOrgPerson
条目必须拥有userPassword
属性:
ditcontentrule
(
2.16.840.1.113730.3.2.2
NAME 'reqPassword'
DESC 'Require userPassword for inetOrgPerson'
MUST userPassword
)
该规则中使用的 OID 是inetOrgPerson
对象类的 OID。MUST
字段表示任何具有结构性对象类inetOrgPerson
的条目必须设置userPassword
属性。
由于此规则,尝试添加没有userPassword
的inetOrgPerson
条目将导致与我们之前看到的类似的错误:
$ ldapadd -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=Johann,ou=users,dc=example,dc=com
uid: johann
ou: users
cn: Johann Fichte
cn: Johann Gottlieb Fichte
sn: Fichte
givenName: Johann
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
adding new entry "uid=Johann,ou=users,dc=example,dc=com"
ldap_add: Object class violation (65)
additional info: content rule 'reqPassword' requires attribute 'userPassword'
正在添加的记录(已高亮)是一个有效的inetOrgPerson
条目,符合inetOrgPerson
对象类定义。但由于 DIT 内容规则的限制,添加该记录失败,因为没有指定userPassword
属性值。
现在让我们扩展这个规则,利用AUX
字段。AUX
字段可用来明确声明可以与此结构性对象类组合的辅助类。
在我们新修订的 DIT 内容规则中,我们将确保只有pkiUser
和labeledURIObject
辅助对象类可以添加到inetOrgUser
记录中。
注意
pkiUser
对象类是一个辅助对象类,用于指示条目能够执行公钥基础设施(PKI)安全事务。它有一个属性userCertificate
,包含用户的加密证书。请参阅维基百科页面以快速了解 PKI:en.wikipedia.org/wiki/Public_key_infrastructure
。
labeledURIObject
对象类允许添加一个额外的属性labeledURI
,该属性接受一个 URI(如 URL)和一个纯文本描述:
labeledURI: http://aleph-null.tv Home Page
URI 与标签之间用空格分隔。因此,URI 是aleph-null.tv
,标签是Home
Page
。labeledURIObject
在 RFC 2079 中定义(www.ietf.org/rfc/rfc2079.txt
)。
此外,我们将修改NAME
和DESC
元素,以反映我们规则现在做的不仅仅是要求userPassword
。现在的 DIT 内容规则如下所示:
ditcontentrule
(
2.16.840.1.113730.3.2.2
NAME 'inetOrgPersonRules'
DESC 'Restrictions for entries with inetOrgPerson object class'
MUST userPassword
AUX ( labeledURIObject $ pkiUser )
)
请注意AUX
字段的语法。要在字段中列出多个值,必须将值列表(用美元符号$
分隔)括在括号中。
使用此 DIT 内容规则,我们可以成功地将一个 URL(使用labeledURIObject
辅助对象类)添加到我的记录中:
$ ldapmodify -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=matt,ou=users,dc=example,dc=com
changetype: modify
add: objectclass
objectclass: labeledURIObject
-
add: labeledURI
labeledURI: http://aleph-null.tv Home Page
modifying entry "uid=matt,ou=users,dc=example,dc=com"
上面突出显示的条目已成功添加,因为labeledURIObject
(允许labeledURI
属性)是内容规则所允许的。但如果我尝试添加一个不同的辅助对象类——一个在 DIT 内容规则中没有明确允许的类——更改请求将被拒绝:
$ ldapmodify -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=matt,ou=users,dc=example,dc=com
changetype: modify
add: objectclass
objectclass: userSecurityInformation
modifying entry "uid=matt,ou=users,dc=example,dc=com"
ldap_modify: Object class violation (65)
additional info: content rule 'inetOrgPersonRules' does not
allow class 'userSecurityInformation'
DIT 内容规则阻止了添加辅助对象类,因为此类未在规则的AUX
字段中指定。
与其他定义一样,ditcontentrule
指令也允许OBSOLETE
标志。
总结来说,ditcontentrule
指令采用一个括号括起来的 DIT 内容规则定义。支持以下字段:
-
适用于此规则的结构对象类的 OID。
-
NAME
字段,提供用于标识规则的简短名称。 -
DESC
字段,其中包含规则的描述。 -
OBSOLETE
标志,用于标记该规则为过时。 -
AUX
字段,其中包含所有允许此对象类条目实现的辅助类的名称或 OID。 -
MUST
字段,其中包含此对象类条目必须拥有的所有(尚未强制要求的)属性的列表。 -
MAY
字段,其中列出此对象类成员可能拥有的所有字段。自 OpenLDAP 2.3.30 起,这不是排他性的。未在此列表中的属性但被对象类模式定义允许的,仍然允许。换句话说,MAY
并不施加任何限制。 -
NOT
字段,其中包含此对象类条目不能拥有的属性的列表。不能应用于对象类模式定义所要求的属性。
现在,我们已经了解了slapd.conf
文件(或包含文件)中允许的四种不同的模式定义指令。通过这些信息,你应该能够阅读并理解 OpenLDAP 中定义的任何模式。
接下来,我们将简要了解如何使用 LDAP 协议从 SLAPD 服务器获取模式信息。
从 SLAPD 检索模式
当 SLAPD 加载模式时,它将它们存储在目录信息树的一个特殊部分,与 Root DSE 记录一起;一个特殊条目保存模式信息。拥有这些信息对调试很有用,但更重要的是,它为客户端应用程序提供了一种了解此目录服务器中可能存储的对象和属性类型的方式。
从目录中获取信息就像执行一个ldapsearch
命令一样简单。
架构信息存储在一个特殊的记录中,称为subschema subentry。你可以使用ldapsearch
访问 subschema subentry:
$ ldapsearch -U matt -b 'cn=subschema' -s base +
注意
对cn=subschema
记录的访问受全局 ACL(在数据库部分之前出现的 ACL)管理。例如,为了仅允许用户访问 subschema,你可以使用这样的规则:access
to
dn.exact="cn=subschema"
by
users
read
。
这将从服务器中检索整个架构规范,包括不仅仅是属性和对象类定义,还包括匹配规则、匹配规则使用、结构规则、名称格式和 LDAP 语法的定义。
但是,与 LDAP 服务器中的任何其他记录一样,我们可以使用搜索过滤器仅获取特定属性的值。例如,我们可以找出所有现有的 DIT 内容规则:
$ ldapsearch -LL -U matt -b 'cn=subschema' -s base ditcontentrules
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: cn=Subschema
dITContentRules: ( 0.9.2342.19200300.100.4.6 NAME 'noPWForDocs' DESC
'Do not allow passwords for documents' NOT userPassword )
dITContentRules: ( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPersonRules'
DESC 'Restrictions for inetOrgPerson object class.'
AUX ( labeledURIObject $ pkiUser )
MUST userPassword )
此搜索将返回当前包含在此服务器架构定义中的所有 DIT 内容规则。当然,唯一的两个规则就是我们在上一节中创建的规则。
以下与架构相关的属性包含在cn=Subschema
记录中:
-
ldapSyntaxes
:此属性为目录中每种支持的 LDAP 语法提供一个值。示例:ldapSyntaxes:
(
1.3.6.1.1.16.1
DESC
'UUID'
)
。 -
matchingRules
:此属性为目录中的每个匹配规则提供一个值。示例:matchingRules:
(
2.5.13.14
NAME
'integerMatch'
SYNTAX
1.3.6.1.4.1.1466.115.121.1.27
)
。 -
matchingRuleUse
:此属性为每个匹配规则使用提供一个值,该值将匹配规则 OID 与实施该匹配规则的所有属性列表配对。示例:matchingRuleUse:
(
2.5.13.27
NAME
'generalizedTimeMatch'
APPLIES
(
createTimestamp
$
modifyTimestamp
)
)
。 -
attributeTypes
:此属性为目录中每个属性定义提供一个值。示例:attributeTypes:
(
2.5.4.3
NAME
(
'cn'
'commonName'
)
DESC
'RFC2256:
common
name(s)
for
which
the
entity
is
known
by'
SUP
name
)
。 -
objectClasses
:此属性包含每个对象类定义的一个值。示例:objectClasses:
(
2.5.6.2
NAME
'country'
DESC
'RFC2256:
a
country'
SUP
top
STRUCTURAL
MUST
c
MAY
(
searchGuide
$
description
)
)
。 -
dITContentRules
:此属性包含每个已定义 DIT 内容规则的一个值。
其他标准属性,如cn
、objectclass
以及基本操作属性,也是记录的一部分。
以这种方式检查架构是另一种替代直接读取架构文件的方法。虽然它的文档较少(因为没有注释),但使用过滤器仍然是有帮助的。此外,不在标准架构中的信息(例如操作属性的架构定义)也可以在此记录中找到。
在本章的后面,我们将开始在 SLAPD 中实现架构,首先包括一些已编写好的架构,然后编写我们自己的架构。但接下来我们将快速浏览架构的另一个理论组成部分:架构层次结构。
对象类层次结构
LDAP 中的对象类和属性可以组织成层次关系。层次关系是指一个实体与一个或多个下属实体之间存在父级或上级关系。
属性层次结构通常很简单,只需要简短的解释。另一方面,对象类则使用更为复杂的层次模型,并将在本章中成为焦点。
在属性和对象类层次结构的案例中,创建层次结构的机制是架构定义。属性和对象类的架构定义都使用SUP
字段来表示与父级或上级的关系。
我们将从简短的属性层次结构讨论开始,然后转向更复杂的对象类层次结构。
属性层次结构
属性层次结构是简单的关系,其中一个属性可以通过其与另一个属性的从属关系,继承某些特征,例如匹配规则和 LDAP 语法。
属性层次结构的简单性表现为几个方面:
-
并没有要求属性与其他属性有任何关系。换句话说,并没有要求属性必须属于某个层次结构。许多属性,比如我们在前一部分中看到的
telephoneNumber
属性,是独立存在的。 -
属性层次结构在属性的使用中并没有发挥重要作用。属性层次结构主要存在是为了保持属性架构定义的简洁和清晰,减少重复。
name
属性通常不直接用于任何对象类,它是属性定义中使用上级/下级关系的一个很好例子。核心架构中有十三个属性将name
作为它们的上级。cn
属性就是一个例子。
cn
的架构定义只使用了NAME
、DESC
和SUP
字段,其中SUP
表示name
属性是cn
的上级。
由于cn
属性定义没有指定任何匹配规则或 LDAP 语法,因此这些规则是从name
属性继承的。因此,cn
继承了在name
中定义的相等性和子字符串匹配规则,以及 LDAP 语法和长度。
但是,属性层次结构可以做的事情并不多。除了匹配规则和语法外,其他内容不会自动从上级继承,并且使用属性层次结构没有其他好处。
下属属性与搜索
从属性层级中会产生一个有趣的效果。请求一个上级属性时,可能会返回作为匹配项的下级属性。例如,下面是一个仅请求一个属性的搜索:name
:
$ ldapsearch -LL -U matt '(uid=matt)' name
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=matt,ou=Users,dc=example,dc=com
ou: Users
cn: Matt Butcher
sn: Butcher
givenName: Matt
givenName: Matthew
title: Systems Integrator
st: Illinois
l: Chicago
根据搜索参数,搜索应返回uid=matt
的记录中的任何name
属性值。但返回的记录(已高亮显示)包含的不止这些。除了总是返回的 DN,记录中还包含了ou
、cn
、sn
、givenName
、title
、st
和l
等值。
为什么会这样呢?这仅仅是因为所有这些属性类型都有name
作为上级属性。
这种行为也会扩展到搜索过滤器行为中。例如,一个搜索过滤器(如(name=Marcus)
)将会对所有使用name
作为上级属性的属性进行搜索:
$ ldapsearch -LL -U matt '(name=Marcus)'
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=cicero,ou=Users,dc=example,dc=com
uid: marcus
uid: cicero
sn: Tullius
cn: Marcus Tullius
givenName: Marcus
ou: users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
uid=cicero
的记录匹配是因为givenName
字段的值为Marcus
。正如在属性类型定义中所看到的,givenName
属性的上级属性类型是name
:
attributetype ( 2.5.4.42 NAME ( 'givenName' 'gn' )
DESC 'RFC2256: first name(s) for which the entity is known by'
SUP name
尽管这一特性可能会对不熟悉架构的人造成一些意外行为,但有时它也非常有用。
大部分情况下,属性层级相对简单。然而,对象类层级要复杂得多。接下来,我们将对其进行详细探讨。
对象类类型:抽象、结构化和辅助
像属性一样,对象类也可以组织成层级结构。通常情况下,会有一个主要的对象类层级。但尽管对象类的层级组织在目录结构中发挥着重要作用,并非所有的对象类都属于这个层级结构。要理解为什么会这样,我们必须首先检查不同类型的对象类。
有三种类型的对象类:抽象、结构化和辅助,如下所示:
-
抽象对象类位于对象类层级的顶部。它可以为所有位于其下层级的对象类设置所需和允许的属性,但没有记录仅能是该对象类的实例。此外,任何抽象对象类的父类也必须是抽象的。
-
结构化对象类也在层级中占有一席之地,并且是另一个结构化对象类或抽象对象类的子类(或者换句话说,它继承自其他结构化对象类或抽象对象类)。目录中的一项条目是结构化对象类的一个实例。当一个结构化对象类是另一个结构化对象类的子类时,父类会被视作抽象类。因此,从操作角度讲,对于任何给定的记录,它只有一个结构化对象类——结构化对象类在对象类层级中是最低的。
-
辅助对象类不要求成为对象类层次结构的一部分,尽管它可以是。辅助对象类旨在为已经具有结构对象类的记录定义额外的属性。例如,一个描述系统账户的记录可能不在层次结构中的人员部分,但仍然可能需要一个密码。
simpleSecurityObject
是一个辅助对象类,可以添加到其他结构对象类中,以允许(并且实际上要求)设置userPassword
属性。
抽象和结构对象类被组织成一个层次结构,抽象类位于最上层,结构对象类是它们的下级。在核心模式(core.schema
)中,只有一个抽象对象类:top
。这个对象类标志着对象类层次结构的顶端——所有对象类的祖先(最高的上级)。
对象类层次结构:概述
层次结构从抽象对象类 top
开始。它下面有任意数量的结构对象类,所有这些类都是直接或间接的下级。直接下级是指在模式定义的 SUP
字段中列出 top
作为其上级对象类的类。间接下级则在对象类层次结构的更深层次,它列出了另一个抽象或结构对象类作为其上级,但该上级要么自身将 top
作为上级,要么引用另一个间接下级。
注意
在其他 LDAP 参考中,超级类有时称为 超类,而上级属性(在属性层次结构中)称为 超类型。同样,子类 和 子类型 这两个术语可以用来表示类和属性中的从属关系。
结构对象类的上级可以是抽象对象类,也可以是另一个结构对象类。
辅助对象类可能在对象类层次结构中,也可能不在。它们可以有上级,但不要求一定有。
这里是一个简单的对象类层次结构的示意图(由四个对象类组成),以及我们在第三章中创建的目录信息树中的一对记录:
account
和 groupOfNames
结构对象类都将 top
列为它们的上级(如实线所示)。simpleSecurityObject
,作为一个辅助对象类,则没有上级。
在对象类层次结构下方有两个记录,显示了 DN 和对象类属性。虚线表示这些条目所实现的模式。两个记录(uid=authenticate
用户和 cn=Admins
组)与对象类层次结构的不同部分相关。cn=Admins
是一个 groupOfNames
,而 uid=authenticate
是一个账户,也具有 simpleSecurityObject
的属性。
这种对象类层次结构的表示方式旨在展示模式的组织结构与目录中的条目之间的关系。
需要牢记的是,这里有两个不同的层次结构。上面的两个条目属于目录信息树层次结构。它们在该层次结构中的位置由其 DN 表示。例如,uid=authenticate
条目是ou=System
条目的子条目,而ou=System
条目又是dc=example,dc=com
条目(我们目录信息树的根条目)的子条目。
但通过它们的对象类,条目也可以与对象类层次结构相关联,如图所示。目前,我们只对第二个层次结构——对象类层次结构感兴趣。
让我们看看这三种对象类类型。理解它们之间的区别,以及每种类型所扮演的角色,将有助于阐明对象类层次结构中的概念。
抽象类
我们将要检查的三种类型中的第一种是抽象类。尽管抽象类的使用较为罕见,但它们在对象类层次结构的开发中起着重要作用。
我们已经讨论过特殊的top
对象类。最常用的 LDAP 模式除了top
之外不会使用任何其他抽象对象类。top
对象类的定义如下:
objectclass
(
2.5.6.0
NAME 'top'
DESC 'RFC2256: top of the superclass chain'
ABSTRACT
MUST objectClass
)
它只需要一个属性:objectclass
。所有结构化对象类都应该与top
相关联,无论是直接还是间接。而任何抽象对象类,如果其下有结构化对象类,则必须与top
相关联。虽然可以创建没有父类的抽象类,实际上启动一个新的对象类树,但这通常不这么做。
提示
没有父类的抽象类
定义没有父类的抽象类的主要情况是,所有从该抽象类继承的类都将是辅助对象类。根据 RFC 4512,结构化对象类必须与top
对象类(直接或间接)相关联。
但top
并不是唯一常用的抽象对象类。OpenLDAP 中包含了一些常见的模式,特别是java.schema
和corba.schema
,它们使用了抽象对象类,而这些类的父类是top
。如果一个抽象对象类有父类,它必须是一个抽象父类。
抽象对象类可以在其定义的MUST
和MAY
字段中列出属性。正如我们刚刚看到的,top
对象类要求有objectclass
属性。任何实现了结构化对象类且隶属于这个抽象对象类的条目,都会继承父类的MUST
和MAY
约束。
例如,在java.schema
中,javaObject
类是抽象的。以下是其定义:
objectclass
(
1.3.6.1.4.1.42.2.27.4.2.4
NAME 'javaObject'
DESC 'Java object representation'
SUP top
ABSTRACT
MUST javaClassName
MAY ( javaClassNames $ javaCodebase $
javaDoc $ description )
)
根据SUP
字段,该对象类从属于top
。它要求任何实现了javaObject
的记录必须具有javaClassName
属性。它还定义了几个属性——javaClassNames
、javaCodebase
、javaDoc
和description
——记录可以包含这些属性。
注意
Java 架构用于在目录服务器中存储序列化的 Java 对象。它在 RFC 2713 中进行了定义。
javaObject
没有从属的结构性对象类。然而,有几个从属于javaObject
的辅助对象类:javaSerializedObject
和javaMarshalledObject
。以下是javaSerializedObject
架构的定义:
objectclass
(
1.3.6.1.4.1.42.2.27.4.2.5
NAME 'javaSerializedObject'
DESC 'Java serialized object'
SUP javaObject
AUXILIARY
MUST javaSerializedData
)
在这个类中只需要一个属性:javaSerializedData
。在这个定义中没有指定可选属性。
如果某个记录使用了javaSerializedData
对象类,它必须具有哪些字段?它可以具有哪些字段?
它必须具有javaSerializedData
属性。我们可以从javaSerializedObject
架构中看到这一点。但它还必须具有javaClassName
属性,因为这是在上级javaObject
对象类中要求的。而javaSerializedData
记录可以包含javaObject
架构中MAY
字段列出的任何属性:javaClassNames
、javaCodebase
、javaDoc
和description
。
这个例子说明了如何使用抽象对象类作为将对象类组织成层次结构的一种方式,将类似的对象类(这里是javaSerializedObject
和javaMarshalledObject
)归类于一个共同的(更加通用的)祖先javaObject
。然后,javaObject
抽象对象类被用来指定这两个从属对象类需要包含的公共属性。
因此,抽象对象类的主要用途之一是收集应当(或可以)包含在定义为从属的对象类中的公共属性。
抽象类是罕见的。相比之下,最常用的对象类类型是结构性对象类。
结构性对象类
正如我们在多个示例中看到的,每个记录都有一个 DN 和一个或多个对象类。从这些对象类出发,记录的其他属性取决于对象类。然而,记录可以拥有哪些对象类是有限制的。决定记录可以有哪些对象类的一个主要因素是结构性对象类的层次结构。
目录中的每个记录必须至少有一个结构性对象类。结构性对象类决定了记录的类型。例如,具有结构性对象类organization
的记录是一个organization
记录。
注意
一旦记录在目录中创建,其结构性对象类就无法更改。可以添加或移除辅助对象类,但结构性对象类是不可更改的(ipso facto,同样,优先级对象类的链条也是如此)。
一个条目可以实现多个对象类,并且它实现的所有对象类并不一定都是结构性对象类。我们来看一下我们在第三章创建的组织记录:
dn: dc=example,dc=com
description: Example.Com, your trusted non-existent corporation.
dc: example
o: Example.Com
objectClass: top
objectClass: dcObject
objectClass: organization
这个条目有三个对象类:
-
top
—一个抽象对象类 -
dcObject
—一个辅助对象类 -
organization
—一个结构性对象类
注意
top
对象类在此条目中并非严格必要。SLAPD 会隐式地将top
包括在所有条目中,因为所有结构性对象类都源自它。
我们如何知道哪些对象类属于哪种类型?这些对象类的模式定义是此类信息的主要来源。
结构性对象类将该条目定位在对象类的层级结构中,这个层级由抽象对象类和结构性对象类组成。
一个条目可以有多个结构性对象类,只要它们之间存在上级/下级关系。
在一个记录中有多个结构性对象类的情况下,最下级的对象类(距离根对象类top
最远的那个)将具有所有其他结构性对象类作为祖先。也就是说,对于对象类层级中距离top
最远的对象类,所有其他结构性对象类必须是它的上级。然后,这个最下级的对象类将被视为结构性对象类。
例如,在第三章中我们为用户barbara
创建了一个记录:
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
userPassword: secret
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
这个用户属于四个对象类。前三个对象类在之前已经明确提到:person
、organizationalPerson
和inetOrgPerson
。这三者恰好都是结构性对象类。第四个对象类是top
,它是隐式包含的。
这四个对象类在层级结构中是相关的。top
抽象对象类位于对象类层级的顶端。person
对象类直接从属于top
。也就是说,person
对象类的定义将top
列为其父类:
objectclass
(
2.5.6.6
NAME 'person'
DESC 'RFC2256: a person'
SUP top
STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)
虽然person
指向top
作为它的上级,organizationalPerson
指向person
,而inetOrgPerson
指向organizationalPerson
作为它的上级。因此,我们得到了一个对象类的层级结构:
因此,根据这个层级结构,任何作为inetOrgPerson
的条目必须遵守其所有上级定义:organizationalPerson
、person
和top
。这些对象类的任何必需属性都将是inetOrgPerson
条目的必需属性,任何这些类的可选属性对于inetOrgPerson
条目来说是可选的。
因此,inetOrgPerson
的必需字段是sn
和cn
,这两个字段分别来自person
对象类,objectclass
属性来自top
。
注意
有关inetOrgPerson
所需和允许的字段的完整列表,请参见第三章中添加 用户 记录的子节。
在前面的图中,还包括了pilotPerson
对象类,它表示层次结构的另一个分支。与organizationalPerson
和inetOrgPerson
一样,pilotPerson
描述了组织中的一个人,但它包含了一些organizationalPerson
和inetOrgPerson
中没有的属性,包括favouriteDrink
和janetMailBox
属性。
虽然pilotPerson
并未正式废弃,但它通常不被使用;通常使用的是inetOrgPerson
。但是,像organizationalPerson
一样,pilotPerson
将person
列为其上级。因此,它继承了person
和top
的属性。然而,它与organizationalPerson
或inetOrgPerson
没有直接或间接关系,因此不会继承它们的任何属性。
因为pilotPerson
与organizationalPerson
或inetOrgPerson
没有关系,并且这些都是结构化对象类,SLAPD 不允许任何记录实现pilotPerson
对象类与organizationalPerson
或其子类的组合。例如,如果我们尝试添加包含所有四个描述人的对象类的记录,我们会得到一个错误:
$ ldapadd -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=charles,ou=users,dc=example,dc=com
uid: charles
ou: users
cn: Charles Sanders Peirce
sn: Peirce
gn: Charles
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
objectclass: pilotPerson
adding new entry "uid=charles,ou=users,dc=example,dc=com"
ldap_add: Object class violation (65)
additional info: invalid structural object class chain
(inetOrgPerson/pilotPerson)
当客户端请求添加上述记录时,SLAPD 会返回对象类违规错误,表明对象类链不正确。这是因为pilotPerson
与organizationalPerson
或inetOrgPerson
没有关系。
回到我们的uid=barbara
记录,该条目列出了三个对象类:
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
正如我们在前面的图中看到的,inetOrgPerson
是层次结构中最低的对象类——最远离top
的对象类。这意味着 SLAPD 将此对象类视为记录的结构化对象类。它甚至设置了一个特殊的操作属性structuralObjectClass
,用来存储此值。因此,你可以通过ldapsearch
获取有关结构化对象类的信息:
$ ldapsearch -LL -U matt '(uid=barbara)' structuralObjectClass
以下是相关信息:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: uid=barbara,ou=Users,dc=example,dc=com
structuralObjectClass: inetOrgPerson
在处理操作和评估规则时,如 DIT 内容规则,SLAPD 会将此记录视为inetOrgPerson
记录。
在本讨论中,我们已经涵盖了对象类层次结构的要点。一个条目在层次结构中的位置由其结构化对象类决定。但并非所有对象类都会影响记录在对象类层次结构中的位置。让我们来看看第三种类型的对象类:辅助对象类。
辅助对象类
辅助对象类提供了一种机制,可以向具有现有结构化对象类的条目添加一个或多个属性。可以将其视为一种模块化系统,用于定义一组相关的属性集合,这些属性可以附加到其他(在概念上)不相关的对象类。
为了更好地理解其工作原理,我们再看一遍uid=authenticate
条目:
dn: uid=authenticate,ou=System,dc=example,dc=com
uid: authenticate
ou: System
description: Special account for authenticating users
userPassword:: c2VjcmV0
objectClass: account
objectClass: simpleSecurityObject
该条目的结构化对象类是account
。simpleSecurityObject
对象类是辅助对象类。
account
模式,位于cosine.schema
中,类似于:
objectclass
(
0.9.2342.19200300.100.4.5
NAME 'account'
SUP top STRUCTURAL
MUST userid
MAY ( description $ seeAlso $ localityName $
organizationName $ organizationalUnitName $ host )
)
根据 COSINE 标准(RFC 4524),此条目用于在计算机上定义一个系统账户。
无论出于什么原因,标准的创建者没有包括赋予账户密码所需的属性。这是有道理的。系统账户通常不需要通过 LDAP 进行身份验证。然而,我们创建的系统账户需要执行目录操作,因此我们需要该账户具有userPassword
属性。
一种实现方法是创建一个新的结构化对象类,作为账户的下级,但需要一个userPassword
属性。但在core.schema
中也有一个专门用于给目录中的非个人条目提供userPassword
以便允许它们绑定的对象类。换句话说,已经存在一个提供我们所需功能的对象类:simpleSecurityObject
对象类。
注意
simpleSecurityObject
也在 COSINE 模式中定义。
simpleSecurityObject
模式定义如下:
objectclass
(
0.9.2342.19200300.100.4.19
NAME 'simpleSecurityObject'
DESC 'RFC1274: simple security object'
SUP top
AUXILIARY
MUST userPassword
)
这个模式定义向任何实现的条目添加了一个必需的属性:userPassword
。因此,simpleSecurityObject
对象类可以添加到条目中,以允许它绑定到目录(假设 ACLs 允许)。
结合account
结构化对象类和simpleSecurityObject
辅助对象类,我们的uid=authenticate
记录现在有三个必需字段:
-
objectclass
,继承自top
-
uid
,来自account
结构化对象类 -
userPassword
,来自simpleSecurityObject
辅助对象类。
这个例子说明了如何使用辅助对象类向已经属于结构化对象类的条目添加额外的属性。
与其为每组属性创建新的结构化对象类,不如使用辅助对象类机制,它可以定义一个模块化的附加属性集合,根据需要将这些属性附加到条目中。
默认情况下,可以向记录中添加任何辅助对象类,而不管该记录的结构化对象类是什么。
换句话说,默认情况下,将具有person
结构化对象类(显然是用于表示一个人的条目)的条目与javaSerializedObject
辅助对象类(用于描述 Java 二进制类的存储表示的条目)连接起来是合法的。
历史上,合理选择应当添加到条目的辅助对象类的责任一直由 LDAP 客户端应用程序和用户负责。然而,你可以使用 DIT 内容规则(请参见本章前面部分)来正式化给定结构化对象类的条目允许具备哪些辅助对象类。
继续前进
到目前为止,本章主要关注 LDAP 模式系统的细节,理论与实践并重。
在这些页面中,我尝试提供关于 LDAP 模式的简明解释,重点关注最适用于本书目标的方面。这些材料应该提供阅读模式定义、明智选择适合自己目录需求的模式以及编写自定义模式所需的背景知识。
然而,如果你打算在 OpenLDAP 代码上进行工作,编写覆盖模块或模块,甚至编写旨在进行公共标准化的模式,你应该阅读 LDAP 的 RFC,特别是 RFC 4512,它定义了 LDAP 模式语言。
现在我们准备继续处理更实际的事务。在下一节中,我们将实现一些需要额外模式的覆盖模块。在配置这些覆盖模块时,我们将检查模式及其在覆盖模块功能中所扮演的角色。
之后,我们将创建我们自己的简短模式。
模式:Accesslog 和密码策略覆盖模块
在上一章中,我们看到了 OpenLDAP 的覆盖模块技术,并实现了一些简单的覆盖模块。在本章中,我们了解了 LDAP 模式是如何工作的。现在我们将看看几个需要自定义模式的覆盖模块。
我们将要检查的两个覆盖模块是 accesslog
覆盖模块和 ppolicy
(密码策略)覆盖模块。
由于它们需要自己的模式,并且每个模式都提供强大的功能集,这两个覆盖模块的配置更为复杂。然而,由于基本概念已经熟悉,我们将快速进行。
使用 Accesslog 覆盖模块进行日志记录
访问日志覆盖模块(accesslog
)扩展了 SLAPD 服务器的日志记录能力。首先,它使得能够跟踪客户端对目录服务器的访问。其次,它将这些日志数据存储在目录中,使得任何授权的 LDAP 客户端都可以检索访问日志。
由于它将信息存储在目录服务器内,并且由于访问日志条目的格式在任何已知的模式中尚未描述,因此访问日志覆盖模块需要自己的模式。
访问日志模式仍然被视为实验性,尚未最终确定。它也未包含在模式目录(/etc/ldap/schema
或 /usr/local/etc/openldap/schema
)中。对象类在手册页(man
slapo-accesslog
)中定义。
然而,访问日志覆盖模块会自动加载其自己的模式,因此不需要手动配置模式。
安装 accesslog
的过程分为四个步骤:
-
加载
accesslog
模块 -
配置
accesslog
后端部分 -
创建一个数据库来存储访问日志
-
配置目录后端以将日志写入新的数据库
加载 accesslog 模块
到现在为止,这一步应该已经很熟悉了。和slapd.conf
文件顶部的其他moduleload
语句一起,我们需要添加一条来加载accesslog
模块:
modulepath /usr/local/libexec/openldap
moduleload back_hdb
moduleload denyop
moduleload refint
moduleload unique
moduleload accesslog
当 SLAPD 重新启动时,包含accesslog
覆盖模块的accesslog
模块将被加载。
配置访问日志后台
accesslog
覆盖模块需要在目录服务器中有一个位置来写入访问信息。我们将创建一个额外的数据库后台来存储日志数据。
这个后台并没有什么特别之处。它的功能与其他后台一样,我们将使用标准的配置指令集。但实现accesslog
时有一个要注意的地方:存储访问日志的数据库必须出现在slapd.conf
中在将要记录访问数据的数据库之前。
我们希望记录对第一个数据库(即后缀为dc=example,dc=com
的数据库)的访问日志,因此需要在dc=example,dc=com
数据库之前插入访问日志的配置指令。以下是原始 Example.Com 数据库定义的开头部分:
##############################
# BDB Database Configuration #
##############################
# Database 1: Example.Com
database hdb
suffix "dc=example,dc=com" "o=My Company,c=US"
rootdn "cn=Manager,dc=example,dc=com"
我们将把访问日志的配置插入到之前示例中的database
指令上方:
##############################
# BDB Database Configuration #
##############################
# Database 1: Logging DB
database hdb
suffix cn=log
rootdn "cn=Manager,cn=log"
rootpw secret
directory /var/lib/ldap/accesslog
#directory /usr/local/var/openldap-data/accesslog
index reqStart eq
##############################
# Database 2: Example.Com
database hdb
suffix "dc=example,dc=com" "o=My Company,c=US"
高亮部分是访问日志数据库的定义。
与其他数据库一样,这个数据库使用 HDB 后台。我们的日志目录的后缀将仅为cn=log
。
每个日志事件将作为 LDAP 记录存储,并且日志目录中的每一条记录都会有一个由两个属性组成的 DN。RDN 是reqStart
属性(该属性包含表示请求开始时间的时间戳),并以后缀结尾,在我们的案例中是cn=log
。
该数据库还拥有自己的管理帐户和密码(rootdn
和rootpw
)。Berkeley DB 文件将存储在/var/lib/ldap/accesslog
目录中——我们将在下一步中在文件系统上创建该目录。
最后,index
指令为reqStart
属性配置了一个相等(eq
)索引,这个属性是 SLAPD 用来创建 DN 的属性。在执行维护操作时,SLAPD 会使用这个属性,因此为这个属性建立索引是个好主意。
在slapd.conf
中还需要做几件事。但在进行这些之前,我们将为 Berkeley DB 文件创建一个目录。
为访问日志文件创建目录
与其他 HDB 数据库一样,这个新数据库也需要一个存储 Berkeley DB 数据库文件的服务器文件系统位置。在之前的配置中,我们已经将 SLAPD 指向了/var/lib/ldap/accesslog
目录。现在,我们需要创建这个目录并为 Berkeley DB 环境配置它。
首先需要做的就是创建新目录。在 shell 中,可以轻松地完成这项操作:
$ sudo mkdir /var/lib/ldap/accesslog
接下来,我们只需要将DB_CONFIG
复制到新的accesslog/
目录中:
$ sudo cp /var/lib/ldap/DB_CONFIG /var/lib/ldap/accesslog/
根据你服务器的流量和你记录的数据量,你可能需要增加或减少在 DB_CONFIG
中分配的缓存大小。有关如何调优 DB_CONFIG
文件的更多信息,请参见上一章的讨论。
注意
检查 DB_CONFIG 文件
我们在上一章创建的 DB_CONFIG
文件没有任何对文件系统位置的绝对引用。但 DB_CONFIG
文件中的某些指令(如 set_lg_dir
)可能会有绝对路径引用,这可能导致两个数据库使用相同的日志。那样会导致灾难性的后果。确保根据需要调整 DB_CONFIG
文件。
确保新的 accesslog/
目录对运行 SLAPD 进程的用户账户是可读写的,并且确保该用户可以读取 DB_CONFIG
文件。
为主后端启用日志记录
现在我们已经设置好了日志记录环境,接下来的工作是配置我们的 dc=example,dc=com
后端开始使用新的日志记录后端。
回到 slapd.conf
,我们需要在 dc=example,dc=com
后端中添加一些新的特定于覆盖层的指令。这些指令必须放在 Example.Com 数据库的数据库定义之后:
##############################
# Database 1: Example.Com
database hdb
suffix "dc=example,dc=com" "o=My Company,c=US"
# ... a dozen lines omitted ...
overlay accesslog
logdb cn=log
logops all
logold (objectclass=person)
logpurge 7+00:00 2+00:00
logsuccess TRUE
第一个指令 overlay
accesslog
在这个特定数据库的上下文中加载了访问日志覆盖层。接下来的五个指令是与访问日志相关的特定指令。
logdb
指令是 accesslog
覆盖层所必需的,其他的都是可选的。
logdb
指令指定了哪个数据库将被用作访问日志。在我们的例子中,我们希望使用 cn=log
数据库。对于托管多个目录信息树的网站,可以为每个后缀设置单独的日志数据库。
logops
指令用于精确指定哪些 LDAP 操作应该被记录。在这个例子中,关键字 all
表示所有操作都会被记录。但以下选项也被支持:
-
可以按名称指定任何操作:
add
,delete
,modify
,modrdn
,search
,compare
,extended
,bind
,unbind
和abandon
。 -
有一些特殊的关键字包含了一组操作,它们是:
-
read
(搜索,比较) -
write
(添加,删除,修改,modrdn) -
session
(绑定,解除绑定,放弃)
-
-
有一个
all
关键字,表示包含所有操作。
logops
行可以放置多个值,值之间应以空格分隔。例如,logops
modify
modrdn
会记录所有的 modify 和 modrdn 操作。
logold
(“日志旧”)指令带有搜索过滤器。当删除或修改操作成功执行时,accesslog
将检查记录是否匹配该过滤器。如果匹配,accesslog
将存储该变更的完整记录,包括添加了哪些属性,哪些属性被更改或删除。例如,当我使用ldapmodify
命令行工具修改用户时,访问日志目录信息树中会写入一条详细变更的记录:
dn: reqStart=20070117022818.000002Z,cn=log
objectClass: auditModify
reqStart: 20070117022818.000002Z
reqEnd: 20070117022818.000003Z
reqType: modify
reqSession: 4
reqAuthzID: uid=matt,ou=users,dc=example,dc=com
reqDN: uid=barbara,ou=users,dc=example,dc=com
reqResult: 0
reqMod: objectClass:+ labeledURIObject
reqMod: labeledURI:+ http://example.com Home Page
reqMod: entryCSN:= 20070117022818Z#000001#00#000000
reqMod: modifiersName:= uid=matt,ou=users,dc=example,dc=com
reqMod: modifyTimestamp:= 20070117022818Z
reqOld: objectClass: person
reqOld: objectClass: organizationalPerson
reqOld: objectClass: inetOrgPerson
reqOld: entryCSN: 20061228230549Z#000000#00#000000
reqOld: modifiersName: cn=Manager,dc=example,dc=com
reqOld: modifyTimestamp: 20061228230549Z
reqMod
值显示新的修改,而reqOld
属性值显示旧的条目。请注意,添加了两行(对象类和labeledURI
),而两行发生了更改(modifiersName
,modifyTimestamp
)。
为什么要使用logold
?它可能对日志评估没有特别大的用处,但当与 SyncRepl 结合使用时,SLAPD 服务器之间的同步可以更高效地完成。(这种形式的 SyncRepl 称为Delta-SyncRepl。)如果你没有使用 SyncRepl,可能根本不需要使用logold
。我们将在下一章详细讨论 SyncRepl(以及 Delta-SyncRepl)。
logpurge
指令要求 SLAPD 定期检查访问日志并删除旧条目。它需要两个参数来提供以下信息:条目多旧时才是删除候选,并且检查删除条目的时间间隔是多久。
这两个参数的格式是相同的:
[<number of days>+]<hours>:<minutes>[:<seconds>]
天数和秒数是可选字段。我们的logpurge
参数如下所示:
logpurge 7+00:00 2+00:00
这表示七天前的日志将被视为可以删除的条目。并且在运行检查后,SLAPD 将在指定的时间——两天——后再次检查是否有新的删除操作。
最后一个参数是logsuccess
。默认情况下,accesslog
会记录所有尝试的操作,无论成功与否。若只记录成功完成的操作,请将logsuccess
设置为TRUE
。
配置accesslog
就这么简单。需要重新启动 SLAPD 以便添加新的覆盖层。
日志记录
现在我们已经启动了新的日志记录覆盖层,让我们测试一下。第一步是生成一些日志数据。因为我们正在记录所有操作(logops
all
),所以任何 LDAP 操作都可以。
这里有一个简单的ldapsearch
:
$ ldapsearch -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
'(uid=matt)' mail gn sn
这使用了简单的绑定,并搜索我的记录(uid=matt
),检索mail
、gn
(名字)和sn
属性的值。
使用这样的搜索,访问日志中会写入什么?为了找出答案,我们可以使用ldapsearch
:
$ ldapsearch -LL -U matt -b 'cn=log'
即便只有一条命令的结果,这个命令的输出也出奇的大:
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
version: 1
dn: cn=log
objectClass: auditContainer
cn: log
dn: reqStart=20070117044539.000000Z,cn=log
objectClass: auditBind
reqStart: 20070117044539.000000Z
reqEnd: 20070117044539.000001Z
reqType: bind
reqSession: 0
reqAuthzID:
reqDN: uid=matt,ou=users,dc=example,dc=com
reqResult: 0
reqVersion: 3
reqMethod: SIMPLE
dn: reqStart=20070117044539.000002Z,cn=log
objectClass: auditSearch
reqStart: 20070117044539.000002Z
reqEnd: 20070117044539.000003Z
reqType: search
reqSession: 0
reqAuthzID: uid=matt,ou=Users,dc=example,dc=com
reqDN: dc=example,dc=com
reqResult: 0
reqScope: sub
reqDerefAliases: never
reqAttrsOnly: FALSE
reqFilter: (uid=matt)
reqAttr: mail
reqAttr: gn
reqAttr: sn
reqEntries: 1
reqTimeLimit: 3600
reqSizeLimit: 500
dn: reqStart=20070117044540.000000Z,cn=log
objectClass: auditObject
reqStart: 20070117044540.000000Z
reqEnd: 20070117044540.000001Z
reqType: unbind
reqSession: 0
reqAuthzID: uid=matt,ou=Users,dc=example,dc=com
ldapsearch
返回四个不同的条目,每个条目有不同的结构化对象类。我们将依次查看每个条目。
它显示的第一个 LDIF 条目是cn=log
的基础记录:
dn: cn=log
objectClass: auditContainer
cn: log
auditContainer
对象类被设计为一种通用对象类,用于访问日志。它的模式如下所示:
objectClass
(
1.3.6.1.4.1.4203.666.11.5.2.0
NAME 'auditContainer'
DESC 'AuditLog container'
SUP top
STRUCTURAL
MAY ( cn $ reqStart $ reqEnd )
)
基本记录只使用了可选的cn
属性。
在accesslog
模式中,为每个 LDAP 操作定义了对象类:auditAbandon
、auditAdd
、auditBind
、auditCompare
、auditDelete
、auditModify
、auditModRDN
、auditSearch
和auditExtended
。此外,还有一个名为auditObject
的特殊对象类,用于描述一般事件。
实际上(在当前版本中),所有列出的操作对象类都是auditObject
对象类的子类。因为它是这些对象类的父类,所以我们将首先查看auditObject
的模式定义。
auditObject
对象类的定义如下所示:
objectclass
(
1.3.6.1.4.1.4203.666.11.5.2.1
NAME 'auditObject'
DESC 'OpenLDAP request auditing'
SUP top
STRUCTURAL
MUST ( reqStart $ reqType $ reqSession )
MAY ( reqDN $ reqAuthzID $ reqControls $ reqRespControls $
reqEnd $ reqResult $ reqMessage $ reqReferral )
)
必需的三个属性是:
-
reqStart
:表示操作开始时间的时间戳。 -
reqType
:一个字符串,表示正在执行的操作。 -
reqSession
:SLAPD(内部)使用的连接 ID 号。
除了这些必需属性外,还有八个可选属性:
-
reqDN
:记录操作当前操作的记录的 DN。 -
reqAuthzID
:记录执行操作的用户的 DN。如果用户是匿名用户,则该值为空。 -
reqControls
和reqRespControls
:如果客户端设置了任何控制项,这里会显示。 -
reqEnd
:记录操作完成时的时间戳。 -
reqResult
:如果操作遇到错误,这里会包含数字错误代码。如果操作成功,则返回0
。 -
reqMessage
:如果错误代码伴随有文本消息,则该消息会放入此属性中。 -
reqReferral
:如果操作返回了引用,这里会记录该引用。
返回的第二个条目记录了客户端的绑定操作:
dn: reqStart=20070117044539.000000Z,cn=log
objectClass: auditBind
reqStart: 20070117044539.000000Z
reqEnd: 20070117044539.000001Z
reqType: bind
reqSession: 0
reqAuthzID:
reqDN: uid=matt,ou=users,dc=example,dc=com
reqResult: 0
reqVersion: 3
reqMethod: SIMPLE
这一条记录了绑定操作,并且是auditBind
对象类的一个实例。auditBind
对象类是auditObject
的一个子类:
objectClass
(
1.3.6.1.4.1.4203.666.11.5.2.6
NAME 'auditBind'
DESC 'Bind operation'
SUP auditObject
STRUCTURAL
MUST ( reqVersion $ reqMethod )
)
它添加了两个必需的属性:reqVersion
,用于记录用于连接的 LDAP 版本,以及reqMethod
,指示在绑定时使用的方法。
看着绑定条目,我们可以看到它记录了成功绑定操作的详细信息。开始和结束时间分别记录在reqStart
和reqEnd
中。reqType
表示执行的操作是一个绑定操作。reqSession
表示请求的内部 ID(由于这是我们启动 SLAPD 以来的第一次操作,连接 ID 从0
开始递增,因此它的值恰好是 0)。
由于绑定是由匿名用户执行的,reqAuthzID
属性存在,但没有值。reqDN
表示客户端正在尝试以 uid=matt,ou=users,dc=example,dc=com
进行绑定,而 reqResult
的值为 0
,表示绑定操作已成功完成。底部的两个属性属于 auditBind
对象类。reqVersion
属性表示客户端使用的是 LDAPv3 协议,并且根据 reqMethod
,该绑定是简单绑定。
所以,在这个 LDAP 会话中执行的第一个操作是绑定。第二个操作是搜索:
dn: reqStart=20070117044539.000002Z,cn=log
objectClass: auditSearch
reqStart: 20070117044539.000002Z
reqEnd: 20070117044539.000003Z
reqType: search
reqSession: 0
reqAuthzID: uid=matt,ou=Users,dc=example,dc=com
reqDN: dc=example,dc=com
reqResult: 0
reqScope: sub
reqDerefAliases: never
reqAttrsOnly: FALSE
reqFilter: (uid=matt)
reqAttr: mail
reqAttr: gn
reqAttr: sn
reqEntries: 1
reqTimeLimit: 3600
reqSizeLimit: 500
由于描述的是搜索操作,因此此条目使用了 auditSearch
对象类,其具有以下模式定义:
objectClass
(
1.3.6.1.4.1.4203.666.11.5.2.11
NAME 'auditSearch'
DESC 'Search operation'
SUP auditReadObject
STRUCTURAL
MUST ( reqScope $ reqDerefAliases $ reqAttrsonly )
MAY ( reqFilter $ reqAttr $ reqEntries $ reqSizeLimit $
reqTimeLimit )
)
请注意,auditSearch
是 auditObject
的下属,而不是直接下属,而是 auditReadObject
的下属,auditReadObject
是另一个结构性对象类,且它本身是 auditObject
的下属。换句话说,auditSearch
是 auditObject
的间接子类。auditReadObject
(从 OpenLDAP 2.3.30 起)并未添加任何额外的属性。
在大多数情况下,从 auditObject
继承的属性在此处执行的作用与绑定操作中的条目相同。此时的 reqAuthzID
是已认证用户的 DN,而不是空值,reqDN
显示的是搜索操作的基础 DN。
下一组属性提供了有关搜索请求性质的详细信息。
-
reqScope
表示搜索的范围。reqDerefAliases
表示在搜索过程中,别名条目(映射到目录中其他条目的条目,这类似于 Linux 文件系统中的符号链接)永远不会被解除引用。reqAttrsOnly
标志表示搜索没有请求只返回属性名称,而是要求返回名称和值。 -
reqFilter
包含 LDAP 搜索过滤器。这是我们在运行ldapsearch
命令时在命令行中指定的过滤器。 -
reqAttr
有三个值:mail
、gn
和sn
,对应于我在ldapsearch
命令中请求的三个属性。reqEntries
表示在目录中找到的匹配记录的总数。 -
reqTimeLimit
和reqSizeLimit
表示搜索中请求的(软)大小和时间限制。
总体来看,此条目提供了关于我 LDAP 搜索的详细记录,仅凭这一记录,就可以轻松地复制完全相同的搜索。
还有一条最终的(简短的)条目,记录客户端的解除绑定操作。
dn: reqStart=20070117044540.000000Z,cn=log
objectClass: auditObject
reqStart: 20070117044540.000000Z
reqEnd: 20070117044540.000001Z
reqType: unbind
reqSession: 0
reqAuthzID: uid=matt,ou=Users,dc=example,dc=com
由于解除绑定操作没有参数(只是关闭连接),因此没有特定的对象类来表示这一事件。相反,auditObject
对象类被用作此条目的结构性对象类。
当客户端执行其他类型的 LDAP 操作(如添加和修改)时,将使用不同的对象类。对象类定义(和属性定义)可以在cn=sucbschema
记录中找到。有关如何执行此操作的信息,请参见前面的章节从 SLAPD 检索架构。
现在我们已经完成了对accesslog
覆盖层的学习。这个覆盖层不仅在记录保持方面有用,还能在调试问题时帮助发现哪些属性最适合索引,甚至为目录复制添加性能增强功能。在下一章节中,我们将讨论密码策略覆盖层。
实现复杂覆盖层:密码策略
LDAP 的一个提议扩展是为在 LDAP 目录中实现密码策略提供标准化的方法。密码策略(ppolicy
)覆盖层实现了“LDAP 目录的密码策略”IETF 草案,该草案可能很快成为 RFC。
密码策略提供账户老化、密码过期、密码强度检查、宽限登录和多种其他密码维护服务。
这在 OpenLDAP 中是如何工作的?密码策略信息存储在目录信息树内的记录中,这些记录由专门的架构描述。ppolicy
覆盖层监控连接,更新密码信息并根据需要强制执行密码策略。
注意
密码策略作用于userPassword
属性。这意味着,如果你使用 SASL 并将密码存储在目录信息树之外(如存储在sasldb
中),那么ppolicy
覆盖层将无法起作用。在本章中,我们将使用简单绑定。
密码策略架构定义了由密码策略条目实现的对象类pwdPolicy
。没有用于用户记录的对象类。相反,操作属性(SLAPD 内部使用的属性)用于存储用户记录中的密码策略信息。这些操作属性用于存储内部信息(例如用户上次更改密码的时间),通常仅由ppolicy
覆盖层管理。
密码策略扩展具有许多功能,所有这些都在手册页中记录,也在 IETF 草案标准中说明。由于该草案尚未最终确定,并且仍在变化中,因此此模块被标记为实验性。随着标准的变化,可能会添加新功能,或者当前功能可能会被修改甚至移除。但实验性分类并不反映代码的稳定性。大型系统的管理员报告称,该模块具有生产质量。
由于功能丰富,ppolicy
覆盖层并非一个快速简单的安装过程。它需要以下步骤:
-
包括密码策略架构并加载模块
-
创建密码策略
-
配置
ppolicy
覆盖层
一旦密码策略覆盖层实现完成,我们将进行一些测试。
在 slapd.conf 中设置全局指令:模式和模块
我们需要做的第一件事是配置slapd.conf
文件的全局(基础)部分。像其他覆盖层一样,我们需要加载ppolicy
模块。由于我们使用的是存储在schema/
目录中的新模式,我们还需要将其包含进来。
由于指令很接近,我们可以一起查看两个添加项:
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/ppolicy.schema
#pidfile /var/run/slapd/slapd.pid
#argsfile /var/run/slapd/slapd.args
pidfile /usr/local/var/run/slapd.pid
argsfile /usr/local/var/run/slapd.args
loglevel none
modulepath /usr/lib/ldap
# modulepath /usr/local/libexec/openldap
moduleload back_hdb
moduleload denyop
moduleload refint
moduleload unique
moduleload accesslog
moduleload ppolicy
这两行高亮显示的内容展示了必要的更改:
-
高亮显示的
include
指令将ppolicy.schema
文件导入配置中。 -
moduleload
指令加载ppolicy
模块
在第 3 步中,我们回到slapd.conf
文件并进行一些进一步的更改,接下来我们需要创建密码策略并将其加载到目录中。这将需要重新启动 SLAPD 以应用新的模式定义:
$ sudo invoke-rc.d slapd restart
创建密码策略
这一步比之前的更具挑战性。我们的目标是将新的密码策略加载到目录中。为此,我们需要了解ppolicy
模式中的pwdPolicy
对象类,创建所需的 LDIF 条目,并使用ldapadd
将这些条目加载到目录中。
pwdPolicy
对象类包含多个属性,用于存储有关密码策略的信息。密码策略是一组条件,用于确定在 LDAP 服务器中对密码使用施加哪些约束。
这是pwdPolicy
对象类的模式:
objectclass
(
1.3.6.1.4.1.42.2.27.8.2.1
NAME 'pwdPolicy'
SUP top
AUXILIARY
MUST ( pwdAttribute )
MAY ( pwdMinAge $ pwdMaxAge $ pwdInHistory $ pwdCheckQuality $
pwdMinLength $ pwdExpireWarning $ pwdGraceAuthNLimit $
pwdLockout $ pwdLockoutDuration $ pwdMaxFailure $
pwdFailureCountInterval $ pwdMustChange $ pwdAllowUserChange
$ pwdSafeModify )
)
该对象类是一个辅助对象类,因此,在创建存储策略的条目时,它需要一个结构性对象类。
pwdPolicy
只有一个必需的属性:pwdAttribute
。该属性的值应设置为用于存储密码的属性的 OID。由于该模式是提议的标准的一部分,这个属性的目的是使不同的目录服务器能够使用相同的模式(因为不同的目录服务器实现使用不同的属性来存储密码值)。然而,对于 OpenLDAP 的 SLAPD,唯一可以在此使用的属性是userPassword
的 OID,值为2.5.4.35
。
注意
在 RFC 3112 中定义的authPassword
属性是未来 OpenLDAP 版本中替代userPassword
的候选属性。然而,目前它尚未完全实现。
剩余的属性,全部是可选的,用于存储策略信息。以下是每个属性的简要说明:
-
pwdMinAge
:此参数指定在密码最后一次更改和下一次 SLAPD 允许更改密码之间必须经过的时间(以秒为单位)。设置此项可以防止账户在短时间内多次更改密码。 -
pwdMaxAge
:此项指定密码的有效期限(以秒为单位)。该期限从密码上次更改的时间开始计算。超过该时间后,密码将被标记为已过期。 -
pwdInHistory
:如果您将密码以明文(未加密)存储在目录中,那么ppolicy
覆盖可以配置为维护密码历史记录,并防止用户重复使用密码。此属性用于指定ppolicy
为每个用户维护的最大密码数。除非设置此属性且其值大于零,否则不会维护密码历史。 -
pwdCheckQuality
:如果设置为检查密码,ppolicy
会进行两项质量检查。第一项是长度检查(下文讨论)。第二项是运行自定义的质量检查功能。可以通过使用pwdCheckModule
对象类和一些自定义 C 代码,将您自己的密码质量检查模块添加到 SLAPD 中,然后用它来检查密码质量。此属性有三个整数值:0
,1
和2
。现在我们有三种情况:-
如果值为
0
(默认值),则ppolicy
不会尝试进行任何质量检查。 -
如果为
1
,则ppolicy
将尝试检查,但如果密码被加密且某些检查功能无法执行,则会返回成功。 -
如果为
2
,则当密码检查功能无法运行时,它将返回错误消息。
-
-
pwdMinLength
:如果pwdCheckQuality
设置为1
或2
,则ppolicy
将确保新密码满足最低长度要求。此属性为正整数,用于设置密码的最小接受长度。 -
pwdExpireWarning
:当密码接近到期日期(由pwdMaxAge
设置)时,ppolicy
可以在用户登录时向用户提供警告。此属性表示密码过期前多少秒开始提醒用户。换句话说,在从设置密码时的pwdMaxAge
减去pwdExpireWarning
的时间点,用户将开始收到警告消息。如果此值设置为0
(默认值),则不会发送过期警告。 -
pwdGraceAuthNLimit
:默认情况下(或如果将此属性设置为0
),当密码过期时,帐户将被锁定,用户将无法再绑定到目录服务器。但通过使用此属性,我们可以允许宽限期登录。此属性的值应为非负整数,表示在帐户被锁定之前,密码过期的用户可以进行多少次宽限登录。 -
pwdLockout
:此属性允许启用密码锁定。如果启用,当用户连续失败绑定指定次数(pwdMaxFailures
)后,帐户将被锁定一段时间(pwdLockoutDuration
)。要启用pwdLockout
,其默认值为关闭,将此属性的值设置为TRUE
即可。 -
pwdLockoutDuration
:此属性指定账户在pwdLockout
设置为TRUE
且用户登录失败次数过多时(超过pwdMaxFailures
设置的次数)被锁定的持续时间,单位为秒。如果此属性设置为0
或未设置,则账户将被锁定,直到管理员重新启用它。 -
pwdMaxFailures
:此属性指定用户连续失败登录的次数,超过此次数后账户将被锁定。不过,在执行此限制之前,必须先将pwdLockout
设置为TRUE
。 -
pwdFailureCountInterval
:此属性可用于微调密码锁定的时间设置。默认情况下(或当此属性设置为0
时),失败的登录尝试会被存储,直到成功登录为止。但此属性的值可以设置为一个秒数,ppolicy
将在清除密码失败计数之前等待这个时间。 -
pwdMustChange
:此属性决定在管理员设置密码后,用户是否必须更改密码。默认情况下,用户不会被提示更改密码。但如果此设置为TRUE
,当管理员更改(或初次设置)密码时,用户将被提示重置密码。 -
pwdAllowUserChange
:默认情况下,用户可以更改自己的密码。但如果此属性设置为FALSE
,则在此策略下的用户将无法更改自己的密码。由于可以将不同的策略分配给不同的用户组,因此这比访问控制列表(ACL)提供了更细粒度的密码写权限控制。 -
pwdSafeModify
:默认情况下,一旦用户成功执行绑定操作,用户可以更改密码,而无需重新发送原始密码。但如果pwdSafeModify
被设置为TRUE
,那么用户必须同时发送旧密码和新密码才能更改密码值。这为密码更改过程增加了额外的安全层。
一些策略属性——主要是密码检查功能和密码历史——要求密码以明文存储在目录中。之所以如此,是因为比较函数无法对加密值进行操作。如果使用不同的盐值序列,两个相同的密码值将产生不同的密文。即使使用相同的盐值,采用不同的哈希算法(如 MD5 和 SHA)也会对相同的密码生成不同的哈希。同样,考虑到某些哈希算法,两个不同的字符串也可能生成相同的密文(尽管这种情况发生在特定用户身上的可能性微乎其微)。
然而,大多数其他功能无论目录中存储的值如何,都能正常工作。
现在我们准备创建一个 LDIF 文件来存储我们的策略。根据惯例,密码策略通常位于目录信息树中的一个单独的组织单位(OU)中。我们将为此目的添加一个新的 OU。
对于我们的策略,我们将使用大多数可能的属性:
dn: ou=Policies,dc=example,dc=com
ou: Policies
description: Directory policies.
objectclass: organizationalUnit
dn: cn=Standard,ou=Policies,dc=example,dc=com
cn: Standard
description: Standard password policy.
pwdAttribute: 2.5.4.35
pwdMinAge: 60
# 30 days: 60 sec * 60 min * 24 hr * 30 days
pwdMaxAge: 2592000
pwdCheckQuality: 1
pwdMinLength: 7
# Warn three days in advance
pwdExpireWarning: 259200
pwdGraceAuthNLimit: 3
pwdLockout: TRUE
pwdLockoutDuration: 1200
pwdMaxFailure: 3
pwdFailureCountInterval: 1200
pwdMustChange: TRUE
pwdAllowUserChange: TRUE
pwdSafeModify: TRUE
objectclass: device
objectclass: pwdPolicy
第一个条目是我们的组织单位。第二个是我们的密码策略。由于pwdPolicy
对象类是辅助对象类,我们必须为条目指定另一个对象类,一个结构化对象类。通常使用device
对象类(基于 OpenLDAP 源分发中使用的测试模式)。
提示
为什么 pwdPolicy 是辅助对象类?
有几个原因可能导致密码策略规范的创建者做出这样的选择。首先,根据 RFC 4512,结构化对象类必须表示物理实体。其次,将该类设为辅助类使得可以将此模式与其他现有模式集成。然而,对于我们来说,这也带来了一个小小的难题,因为没有合适的候选结构化对象类。
现在我们可以使用ldapadd
添加这个 LDIF。我们已经将上述 LDIF 保存到名为ppolicy.ldif
的文件中,因此可以使用以下命令将其添加:
ldapadd -x -W -D 'uid=matt,ou=users,dc=example,dc=com' -f ppolicy.ldif
这将我们的两个新条目添加到目录中。
注意
确保在添加模式后重新启动服务器。如果未加载ppolicy
模式,以上操作将无法正常工作。
现在我们已经加载了条目,是时候返回slapd.conf
并配置覆盖层了。
配置覆盖层指令
在设置密码策略覆盖层的第一步中,我们向slapd.conf
添加了指令,以包括ppolicy
模式定义并加载ppolicy
模块。现在我们将查看覆盖层的后端配置。
与其他覆盖层一样,所有配置指令都是特定于后端的。此外,由于ppolicy
覆盖层会大量写入目录信息树,因此并非所有功能都能在只读数据库上工作。
尽管这个覆盖层很复杂,但它只有三个指令,并且这些指令都非常直接。在我们的slapd.conf
文件中的相关部分,在dc=example,dc=com
目录树中,像这样:
overlay ppolicy
ppolicy_default cn=Standard,ou=Policies,dc=example,dc=com
ppolicy_use_lockout
ppolicy_hash_cleartext
一旦使用overlay
指令将覆盖层应用于此数据库,就会有三个特定于覆盖层的指令。
第一个,ppolicy_default
,指向目录信息树中将作为默认密码策略条目的条目的 DN。如我们很快将看到的,不同的条目可以使用不同的策略。但由ppolicy_default
指示的条目是ppolicy
在未显式设置其他条目时将使用的条目。对于上面的示例,它被设置为我们在上一步创建的条目的 DN。
第二个指令是ppolicy_use_lockout
。这个指令改变了 SLAPD 如何报告由于帐户锁定导致的错误消息。当用户的帐户被密码策略覆盖层锁定时,用户将无法再次绑定。默认情况下(当未包含此指令时),客户端会被通知绑定失败是因为凭证无效(通用 LDAP 错误),但不会提供更多的错误信息。然而,当此指令存在时,SLAPD 会发送帐户 已锁定错误代码。
注意
虽然这个额外的错误消息可能对用户有帮助,但它可能会带来负面影响。攻击者可能根据这些信息判断出服务器正在使用密码锁定功能。这样的攻击者可以通过尝试在每个已知账户上登录,直到账户被锁定,从而对服务器上的已知账户发起拒绝服务攻击。
最后的ppolicy
指令,ppolicy_hash_cleartext
,修改了 SLAPD 处理密码更改的方式。简而言之,如果存在此指令,则 SLAPD 将在使用 LDAP 修改操作时自动对明文密码进行哈希处理(与 LDAP 密码修改扩展操作不同)。
为了理解这意味着什么,我们来看一个示例。在我们的目录中,我们有以下记录(在第三章创建):
dn: uid=adam,ou=Users,dc=example,dc=com
cn: Adam Smith
sn: Smith
uid: adam
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
这个用户尚未设置密码。设置密码的一种方法是使用ldappasswd
工具,正如我们在第三章看到的,它使用 LDAP 密码修改扩展操作。这是更改密码的最佳方式,因为服务器会处理密码加密。以下是使用ldappasswd
设置密码的示例:
$ ldappasswd -U matt -s secret 'uid=adam,ou=users,dc=example,dc=com'
这将uid=adam
的密码设置为secret
。现在记录会是什么样子呢?像这样:
dn: uid=adam,ou=Users,dc=example,dc=com
cn: Adam Smith
sn: Smith
uid: adam
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword:: e1NTSEF9WlFzZWdrVUdpT3JKNUgwYXFRdisxQ0dpaTNYUFdkMjA=
userPassword
的值是 Base64 编码的。解码后的值是:
{SSHA}ZQsegkUGiOrJ5H0aqQv+1CGii3XPWd20
SLAPD 对该值执行了 SSHA 哈希。
修改密码的第二种方式是使用 LDAP 修改操作(如ldapmodify
客户端所用)。当使用 LDAP 修改更改userPassword
的值时,假定客户端发送的是应该存储的密码值的形式。实际上,LDAP 标准规定,当进行属性值修改时,服务器应当按这种方式操作。因此,SLAPD 不会对密码进行加密。
这是使用ldapmodify
设置密码的示例:
$ ldapmodify -x -W -D 'uid=matt,ou=users,dc=example,dc=com'
Enter LDAP Password:
dn: uid=adam,ou=users,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: secret
modifying entry "uid=adam,ou=users,dc=example,dc=com"
上面的高亮部分是需要修改的 LDIF 信息。userPassword
属性的值被设置为secret
——这是在ldappasswd
示例中使用的相同密码。但这次,如果我们查看条目,userPassword
的值并没有被加密:
dn: uid=adam,ou=Users,dc=example,dc=com
cn: Adam Smith
sn: Smith
uid: adam
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword:: c2VjcmV0
密码没有被哈希处理。相反,它只是被 Base64 编码。解码后的值是secret
。
包括ppolicy_hash_cleartext
指令会修改这种行为。在修改期间,ppolicy
叠加层会检查修改的属性是否为userPassword
,且该值是否为明文。如果值是明文,ppolicy
将对其进行哈希处理。
注意
实际上,启用此功能会导致 SLAPD 以非标准方式执行,但为了额外的安全性。
例如,我们可以重新运行相同的ldapmodify
:
$ ldapmodify -x -W -D 'uid=matt,ou=users,dc=example,dc=com'
Enter LDAP Password:
dn: uid=adam,ou=users,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: secret
modifying entry "uid=adam,ou=users,dc=example,dc=com"
但这次,由于ppolicy_hash_cleartext
已启用,密码被加密了:
dn: uid=adam,ou=Users,dc=example,dc=com
cn: Adam Smith
sn: Smith
uid: adam
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
userPassword:: e1NTSEF9Q0M3QUdSQUlPMG4vYy8rbVZiRE95bC9aYnpqNHcxd1Q=
解码后的userPassword
值是{SSHA}CC7AGRAIO0n/c/+mVbDOyl/Zbzj4w1wT
。当启用明文密码哈希时,LDAP 修改操作(仅针对userPassword
属性)行为更像是 LDAP 密码修改扩展操作。
我们现在已完全配置了覆盖层。然而,SLAPD 需要重新启动才能加载slapd.conf
中的更改。现在我们可以开始测试这些功能。
测试覆盖层
目前的配置是,SLAPD 将对任何具有userPassword
属性的条目执行策略控制。让我们进行一些小测试,看看密码策略是如何工作的。
提示
管理员
在我执行示例时,我使用uid=matt
账户作为管理账户。该账户被 ACL 允许执行管理任务。但它也受ppolicy
覆盖层的约束。
root DN 账户(cn=manager,dc=example,dc=com on this server)
会被特殊处理。例如,管理员可以为用户设置密码,而不需要知道用户的旧密码,即使pwdSafeModify
已开启。
首先,让我们看看策略如何响应一些密码更改。我们从一次ldapmodify
尝试开始:
$ ldapmodify -x -W -D 'uid=matt,ou=users,dc=example,dc=com'
Enter LDAP Password:
dn: uid=adam,ou=users,dc=example,dc=com
changetype: modify
replace: userPassword
userPassword: new_password
modifying entry "uid=adam,ou=users,dc=example,dc=com"
ldap_modify: Insufficient access (50)
additional info: Must supply old password to be changed as
well as new one
修改尝试失败,因为pwdSafeModify
设置为TRUE
。无法通过ldapmodify
满足此要求。相反,我们必须使用ldappasswd
来更改密码,并且必须设置以提供旧密码。这是我们得到的结果:
$ ldappasswd -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
-s new_password -a secret 'uid=adam,ou=users,dc=example,dc=com'
Enter LDAP Password:
Result: Success (0)
-s
标志用于指定新密码,而-a
标志用于提供旧密码(然后ldappasswd
会提示输入绑定 DN 的密码)。设置这两个后,我们就满足了pwdSafeModify
的要求。
由于我们启用了密码检查功能,因此我们应该能够测试密码长度:
$ ldappasswd -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
-s short -a new_password 'uid=adam,ou=users,dc=example,dc=com'
Enter LDAP Password:
Result: Constraint violation (19)
Additional info: Password fails quality checking policy
在这种情况下,新的密码short
(顾名思义)太短了。策略中的pwdMinLength
规定密码必须至少七个字符长,当执行密码质量检查功能时(由于pwdCheckQuality
设置为1
,它会被执行),服务器会返回一个错误,指出验证失败。不幸的是,系统未能提供具体的失败原因。
接下来,让我们看看密码过期警告和密码过期。为了测试,我们需要对策略做一些小的调整——具体来说,我们将把pwdMaxAge
和pwdExpireWarning
的值设置为较低的值(这些值通常对生产环境来说太低)。我们将设置密码每十分钟过期,过期警告将在最后九分钟内显示:
$ ldapmodify -x -W -D 'uid=matt,ou=users,dc=example,dc=com'
Enter LDAP Password:
dn: cn=Standard,ou=Policies,dc=example,dc=com
changetype: modify
replace: pwdMaxAge
pwdMaxAge: 600
-
replace: pwdExpireWarning
pwdExpireWarning: 540
modifying entry "cn=Standard,ou=Policies,dc=example,dc=com"
现在,当uid=adam
绑定时,以下消息会记录在 LDAP 日志中:
ppolicy_bind: Setting warning for password expiry for
uid=adam,ou=users,dc=example,dc=com = 536 seconds
不幸的是,没有消息发送到客户端,因此用户看不到该消息。这可能是因为草案规范没有要求将消息发送到客户端。因此,过期警告主要对管理员有用。
十分钟后,userPassword
值将超过过期点,下次用户登录时,SLAPD 将标记密码为过期。再次提示,用户不会收到此事实的明确警告。日志文件中的一条记录指示帐户过期:
ppolicy_bind: Entry uid=adam,ou=Users,dc=example,dc=com
has an expired password: 3 grace logins
除了这条日志记录外,用户的记录中还添加了一个新的操作属性。pwdGraceUseTime
属性被添加到用户的记录中,其中的时间戳指示用户最后一次执行绑定操作的时间:
$ ldapsearch -LL -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
'(uid=adam)' pwdGraceUseTime
Enter LDAP Password:
version: 1
dn: uid=adam,ou=Users,dc=example,dc=com
pwdGraceUseTime: 20070121172107Z
每次一个具有过期userPassword
的 DN 绑定到目录时,都会在pwdGraceUseTime
属性中添加一个新值。因此,在uid=adam
的密码过期后进行了三次绑定,用户的记录将包含三个pwdGraceUseTime
属性值:
$ ldapsearch -LL -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
'(uid=adam)' pwdGraceUseTime
Enter LDAP Password:
version: 1
dn: uid=adam,ou=Users,dc=example,dc=com
pwdGraceUseTime: 20070121172107Z
pwdGraceUseTime: 20070121173638Z
pwdGraceUseTime: 20070121174603Z
在pwdGraceUseTime
值的数量达到策略中pwdGraceAuthNLimit
属性的数量时,帐户将被视为已锁定,并且该 DN(在此例中为uid=adam
)将不再允许绑定。如果uid=adam
尝试绑定,他将收到一条错误消息:
$ ldapsearch -x -W -D 'uid=adam,ou=users,dc=example,dc=com' \
'(uid=adam)'
Enter LDAP Password:
ldap_bind: Invalid credentials (49)
此外,还会向日志中添加一条消息,指出该问题:
ppolicy_bind: Entry uid=adam,ou=Users,dc=example,dc=com
has an expired password: 0 grace logins
此时,管理员需要采取措施重新启用该帐户。
密码策略操作属性
在上一部分中,我们测试了密码策略的几个不同功能。现在我们将着眼于对帐户执行管理操作。
ppolicy
覆盖层在用户记录中存储有关用户遵守密码策略的信息。这些信息存储在操作属性中。
与常规属性不同,操作属性不会返回给客户端,除非客户端明确请求它们(无论是通过名称,还是使用特殊的加号(+
)属性说明符,该说明符匹配任何操作属性)。而且,SLAPD 可以阻止客户端修改操作属性。
首先,我们将查看启用密码锁定(pwdLockout
)并且帐户被锁定时发生的情况示例。ppolicy
覆盖层使用操作属性来存储失败和锁定的信息。
在我们的策略中,当用户连续三次验证失败(根据pwdMaxFailure
),他们的帐户将被锁定一段时间(由pwdLockoutDuration
决定)。
我们目录中的其他用户之一是uid=dave,ou=users,dc=example,dc=com
。该用户已失败验证三次。下次用户尝试验证时,即使使用正确的密码,也将无法绑定:
$ ldapsearch -x -W -D 'uid=dave,ou=users,dc=example,dc=com'\
'(uid=dave)'
Enter LDAP Password:
ldap_bind: Invalid credentials (49)
该用户记录中的几个操作属性指示了问题所在:
$ ldapsearch -LL -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
'(uid=dave)' +
Enter LDAP Password:
version: 1
dn: uid=dave,ou=Users,dc=example,dc=com
structuralObjectClass: inetOrgPerson
entryUUID: efbf8838-c734-102a-935c-57e457da105f
creatorsName: cn=Manager,dc=example,dc=com
createTimestamp: 20060823205147Z
pwdChangedTime: 20070121180110Z
pwdFailureTime: 20070121180139Z
pwdFailureTime: 20070121180140Z
pwdFailureTime: 20070121180142Z
pwdAccountLockedTime: 20070121180142Z
entryCSN: 20070121180142Z#000000#00#000000
modifiersName: cn=Manager,dc=example,dc=com
modifyTimestamp: 20070121180142Z
entryDN: uid=dave,ou=Users,dc=example,dc=com
subschemaSubentry: cn=Subschema
hasSubordinates: TRUE
请注意,示例中的 ldapsearch
仅查询(且只查询)与过滤器匹配的条目的所有操作属性——这就是加号 (+
) 的作用。
高亮显示的行展示了我们感兴趣的属性:pwdFailureTime
和 pwdAccountLockedTime
。
pwdFailureTime
操作属性包含用户每次登录失败的时间戳。当用户成功登录时,pwdFailureTime
的值会被清除,因此有三个值意味着连续三次登录失败。
pwdAccountLockedTime
指示密码被锁定的时间。根据我们的配置,锁定应只持续二十分钟,之后用户可以重新尝试登录。
如果用户成功登录,pwdFailureTime
和 pwdAccountLockedTime
属性将从用户记录中删除:
$ ldapsearch -LL -x -W -D 'uid=matt,ou=users,dc=example,dc=com' '(uid=dave)' +
Enter LDAP Password:
version: 1
dn: uid=dave,ou=Users,dc=example,dc=com
structuralObjectClass: inetOrgPerson
entryUUID: efbf8838-c734-102a-935c-57e457da105f
creatorsName: cn=Manager,dc=example,dc=com
createTimestamp: 20060823205147Z
pwdChangedTime: 20070121180110Z
entryCSN: 20070121182203Z#000000#00#000000
modifiersName: cn=Manager,dc=example,dc=com
modifyTimestamp: 20070121182203Z
entryDN: uid=dave,ou=Users,dc=example,dc=com
subschemaSubentry: cn=Subschema
hasSubordinates: TRUE
在这种情况下,管理员无需对用户条目进行任何特殊更改。但是如果用户被锁定怎么办?如果将 pwdLockDuration
设置为 0
,且用户登录失败次数过多,就会发生这种情况。如我们在示例中看到的,如果用户的密码已过期且用户已用尽允许的宽限登录次数,也会发生这种情况。
一旦账户被锁定,用户甚至无法更改其密码。这意味着管理员需要代表用户介入,并使用 ldappasswd
、ldapmodify
或其他类似工具来更改密码。
在少数情况下,可能需要直接修改操作属性。例如,pwdAccountLockedTime
、pwdReset
和 pwdPolicySubentry
可以由管理员进行修改:
$ ldapmodify -x -W -D 'cn=manager,dc=example,dc=com'
Enter LDAP Password:
dn: uid=adam,ou=users,dc=example,dc=com
changetype: modify
add: pwdReset
pwdReset: TRUE
modifying entry "uid=adam,ou=users,dc=example,dc=com"
在这个示例中,uid=adam
账户的 pwdReset
标志被设置为 TRUE
。这将要求用户在下一次绑定时更改密码。
但是,SLAPD 可能不允许通过标准 LDAP 修改操作其他操作属性。这是因为 ppolicy
模式在这些模式定义上设置了 NO-USER-MODIFICATION
标志。
这些操作属性能否被修改?使用特殊控制,Relax Rules 控制(以前称为 ManageDIT),管理员可以更改通常不允许更改的操作参数的值。但是,Relax Rules 控制尚未正式发布,并且在 OpenLDAP 中默认未启用。我们需要构建 OpenLDAP 的开发版本来启用该控制。
ppolicy 操作属性摘要
我们查看了 ppolicy
可以附加到绑定记录的一些其他操作属性。以下是所有可能的属性及其简短描述:
-
pwdChangedTime
:这是一个时间戳,表示密码最后一次更改的时间。此属性只能有一个值。没有此属性的条目中的密码将永不过期。 -
pwdAccountLockedTime
:当条目被锁定时,会添加此属性。它包含一个时间戳,表示 SLAPD 标记账户为锁定的时间。我们看到,当用户连续多次身份验证失败时,会使用此属性。 -
pwdFailureTime
:每当用户尝试绑定但未能提供正确密码时,都会将pwdFailureTime
属性值添加到记录中。成功登录时会清除所有pwdFailureTime
属性。 -
pwdGraceUseTime
:如果用户的账户已过期,并且策略允许宽限期登录,则每次用户使用过期密码登录时,都会添加一个新的pwdGraceUseTime
值。重置密码时会清除所有pwdGraceUseTime
值。 -
pwdHistory
:如果开启了密码历史跟踪,那么每次用户更改密码时,旧密码会存储在pwdHistory
属性值中。只有策略中指定的密码数量会保留在历史中。 -
pwdPolicySubentry
:此属性仅允许一个值,采用此记录应使用的密码策略的 DN。如果未找到此属性,SLAPD 将使用默认策略(如slapd.conf
中的ppolicy_default
指令所指定)。 -
pwdReset
:此属性采用布尔值。当管理员更改密码时,标志被设置为TRUE
。如果策略中同时设置了pwdMustChange
为TRUE
,则用户必须在下次绑定时(使用ldappasswd
)更改其密码。
到此为止,我们已经完成了与密码策略覆盖层的工作。接下来,我们将开始创建我们自己的架构。
创建架构
到目前为止,我们已经深入研究了架构定义,并实现了一些使用自定义架构的覆盖层。到现在为止,你应该已经能熟练地操作和阅读架构。接下来我们将创建我们自己的架构。
本节的目标是创建一个小型架构,用于将博客信息添加到我们的目录中。我们希望能够在目录中存储一个表示博客的记录,并将现有条目与这些博客链接起来,例如,表明某个用户维护了特定的博客。
为此,我们将添加两个对象类——一个结构性和一个辅助性——以及一些新属性。结构性对象类blog
将描述一个独立的博客。它将包含描述博客所需的属性。
辅助类blogOwner
将用于向特定条目添加博客所有权信息。由于博客信息将存储在一个blog
条目中,blogOwner
对象类只需要一个属性,用于指向相应的blog
条目。
我们将首先走一遍获取 OID 的过程。然后我们将创建我们的对象类。对象类创建完成后,我们将定义新的属性。最后,我们将尝试使用我们的新架构。
获取 OID
正如我们到目前为止看到的,OID(对象标识符)在定义架构时起着重要作用。
OID 是由整数组成的序列,数字之间用点(.
)分隔。但 OID 并不是任意的数字组合。它们是有结构的,用于表示对象的来源。在这里,我们将它们用于创建新架构时,会将 OID 视为由三个部分组成:
-
基础 OID
-
类型号码
-
项目号码
OID 号码的基础部分由命名机构分配。我们将从 互联网号码分配局(IANA)获得我们的 OID。
注意
IANA 并不是唯一的命名机构。每个国家可能有自己的注册表。例如,在美国,美国国家标准协会(ANSI) 也有一个注册表。
IANA 维护着一个用于私营企业的 OID 注册表。它免费分配号码,只需进行一次注册。然后,IANA 每个企业只分配一个号码,所以如果你的组织已经有了一个,应该使用现有的那个。你可以在 www.iana.org/assignments/enterprise-numbers
上查看注册表。
要获得一个号码,请访问 iana.org/cgi-bin/enterprise.pl
并填写那里的表格。然后,你将分配一个类似于这样的 OID:1.3.6.1.4.1.?,其中问号会被一个整数替换。这个 OID 作为我们在创建架构时使用的 OID 的基础。通过在这个字符串后面附加你自己的数字和点,你可以创建自己的 OID 号码,只要你确保在自己的域内保持 OID 的唯一性,你就可以假设这些 OID 也是全局唯一的(因为你是唯一拥有该基础 OID 的人)。
注意
在这些例子中,我使用的是注册给我的 OID。这些 OID 可以用于复制这里的例子,但不要用我的 OID 来创建你自己的架构。使用他人的 OID 的做法称为 OID 劫持,这种做法是不被提倡的,因为它破坏了 OID 唯一性的假设。
尽管这一串数字有一定的语义含义(大致上意味着拥有者是一个在 IANA 命名空间内运营的私营企业),但在如何构建 OID 方面没有限制。例如,你可以每次需要创建新 OID 时,简单地将一组随机数字附加到基础 OID 上:
1.3.6.1.4.1.8254.78.45146762
1.3.6.1.4.1.8254.57.483729598
但是通常更易于提出一些语义化的组织方案。建议使用基于 OpenLDAP 基金会方案的版本。从基础 OID 开始,创建一个仅用于 LDAP OID 的段:
1.3.6.1.4.1.8254.1021
现在,我们只有一个部分命名空间,它将仅用于 LDAP OID。接下来我们将使用一个简单的子类别标识符。从 OID 区段 1.3.6.1.4.1.8254.1021 开始,我们将创建如下格式的 OID:
1.3.6.1.4.1.8254.1021.x.y
其中,x
表示对象的类型,y
表示我们正在标识的具体对象。OpenLDAP 基金会使用以下类型:
-
LDAP 语法(
1
) -
匹配规则(
2
) -
属性类型(
3
) -
对象类(
4
) -
支持的特性(
5
) -
协议机制(
9
) -
控制(
10
) -
扩展操作(
11
)
我们将只创建对象类和属性,因此我们类的x
值对于附加到属性的 OID 为3
,对于附加到对象类的 OID 为4
。
对于y
值,我们只需从数字1
开始,每次定义一个新类型的对象时递增。例如,我们的第一个对象类将具有以下 OID:
1.3.6.1.4.1.8254.1021.4.1
对于我们的第二个对象类,我们只需将最后一个值从1
递增到2
:
1.3.6.1.4.1.8254.1021.4.2
再次说明,这只是一个约定,不同的组织使用不同的约定。虽然我提倡这种约定,但如果你觉得其他约定更适合你的需求,你可以自由选择。
然而,有两件事需要记住。首先,确保 OID 在你的命名空间内唯一。这意味着你应该在一个所有与 OID 相关的组织成员都能访问的地方维护一个 OID 注册表。其次,为数字添加意义可以提供巨大的实用性,因为它有助于你回忆或推导出一个本来是任意的数字串所代表的内容。
现在我们准备好开始创建我们的架构了。
给我们的 OID 命名
我们的架构定义都放在一个名为blog.schema
的文件中,稍后我们将在slapd.conf
中的include
语句中引用它。
通常,一旦定义了 LDAP 对象的基本 OID,就可以方便地使用slapd.conf
中的objectidentifier
指令来使 OID 更具可读性,并减少创建架构定义时出错的概率。
我们可以在架构文件的前几行中完成这一操作:
objectidentifier blogSchema 1.3.6.1.4.1.8254.1021
objectidentifier blogAttrs blogSchema:3
objectidentifier blogOCs blogSchema:4
第一行将名称blogSchema
映射到 OID1.3.6.1.4.1.8254.1021
。现在我们可以将那个长 OID 称为blogSchema
,这样更容易记住。
第二和第三个objectidentifier
指令添加了更多的别名。第二个指令将名称blogAttrs
设置为指代 OID blogSchema:3
(即1.3.6.1.4.1.8254.1021.3
)。因此,当我们定义属性时,可以使用快捷方式blogAttrs:1
,而不是输入完整的1.3.6.1.4.1.8254.1021.3.1
。
类似地,blogOCs
别名(即“博客对象类”的缩写)可用于指代1.3.6.1.4.1.8254.1021.4
命名空间。
通过这个机制,我们实现了上一节中解释的组织策略,从此我们的 OID 命名应该只是简单地递增 OID 中的最后一个整数。
创建对象类
我们将从对象类开始,然后使用这些定义的对象类来指导我们属性的创建。这通常是创建模式的方式,但它有一个反直觉的结果:对象类必须在它们包含的属性之后定义。实际上,我们是在跳到模式文件的末尾来添加对象类,并且稍后会在对象标识符和对象类之间添加属性定义。
第一个要描述的对象类是blog
类。该对象类将定义定义博客所需的属性。为了我们的目的,我们将创建一个非常简单的对象类,尽管还可以附加更多的属性。
我们希望该类包含以下属性:
-
blogTitle
:博客标题 -
blogUrl
:博客主页的 URL(统一资源定位符) -
blogFeedUrl
:该 URL 指向 RSS 或 Atom 订阅源的地址 -
description
:博客的简要文本描述
在这些属性中,blogUrl
和blogTitle
属性应该是必需的。blogUrl
是博客的重要组成部分,没有它,描述博客的条目将没有多大价值。而blogTitle
属性是必需的,它可以作为命名组件在 DN 中使用。
为了清晰起见,我们在所有新属性前加上了blog
字符串,以便能立即区分这些属性与其他类似属性。
提示
命名对象类和属性
如果您的对象类或属性是为内部使用或特定应用程序使用设计的,建议在属性和对象类的名称前加上组织或应用程序的名称。这有助于明确已定义项的目的。
幸运的是,description
已经被定义。虽然我们可以使用core.schema
中定义的title
属性,但这可能会引起混淆,因为该属性用于指代组织中人的职称。为了避免混淆,我们将避免重用该属性。
我们已经说过这个对象类将是结构类,并且我们有一个确定 OID 号的方案。由于没有类似的对象类,我们将创建一个以top
为父类的类。现在我们已经拥有了创建模式定义所需的所有信息:
objectclass
(
blogOCs:1
NAME 'blog'
DESC 'Describes an online blog accessible by URL.'
SUP top
STRUCTURAL
MUST ( blogUrl $ blogTitle )
MAY ( blogFeedUrl $ description )
)
在 OID 字段中,我们使用了上一节中分配的对象标识符。我们从1
开始,这是我们的第一个对象类。
blogOwner
对象类应标记为辅助类,以便可以将其附加到不同类型的条目上,无论其结构对象类如何。例如,无论博客是企业博客、由某个组织单元维护,还是单纯个人博客,我们都可以将该对象类添加到所需的条目中。
我们希望使用blogOwner
对象类从一个条目插入指向目录信息树中相应blog
条目的指针。既然这是我们所需的全部,因此一个属性就足够了。
blogDN
:描述与此条目相关联的blog
的 DN。
这个对象类实际上比前一个更简单:
objectclass
(
blogOCs:2
NAME 'blogOwner'
DESC 'Indicates that this entry is responsible for a blog.'
AUXILIARY
MUST ( blogDN )
)
这个 OID 号与第一个不同,唯一的区别是最后一个值已经递增。这遵循了我们在前一部分定义的方案。
由于这是一个辅助对象类,因此不需要上级对象。并且由于我们希望这个类用于指向目录中其他地方的blog
条目,因此需要blogDN
属性。
现在我们有了两个对象类。在创建它们时,我们引用了四个目前不存在的属性。现在是时候创建它们了。
创建属性
在创建了blog
和blogOwner
对象类之后,我们初步定义了(在文本中)四个属性:blogTitle
、blogUrl
、blogFeedUrl
和blogDN
。现在我们将定义每个属性,从blogTitle
开始。
为了定义我们的属性,我们需要决定属性的语法以及 SLAPD 将用于该属性的匹配规则。blogTitle
将包含文本数据的字符串值。因此,我们需要的语法是支持这种数据类型的。目录字符串语法,在 RFC 4517 中定义,正是为此目的而设,且支持国际化,能够以 UTF-8 存储字符。
在执行搜索时,我们不希望文本的大小写(大写或小写)产生影响。换句话说,我们希望"My Blog"和"my blog"被视为匹配项。因此,我们需要找到最适合支持此功能的匹配规则。OpenLDAP 支持三十多种匹配规则(您可以通过搜索cn=Subschema
条目查看列表)。我们希望在blogTitle
属性上实现基于字符串的相等性和子字符串匹配,因此我们将使用的匹配规则对是caseIgnoreMatch
和caseIgnoreSubstringsMatch
。
现在,我们拥有了创建新属性类型所需的所有信息:
attributetype
(
blogAttrs:1
NAME 'blogTitle'
DESC 'Title of a blog.'
EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256}
)
OID 字段为blogAttrs:1
,表示这是我们的第一个属性。
LDAP 语法 OID 是目录字符串的 OID。在 OID 的末尾,{256}
表明标题的最大长度应限制为 256 个字符。
注意
字符是 UTF-8 编码的,因此如果每个 256 个字符占用两个字节,这可能会占用最多 512 字节的空间。
接下来的两个属性,blogUrl
和blogFeedUrl
,是类似的,我们可以利用这一点在定义它们时进行简化。
首先需要检查的是这些属性的 LDAP 语法。与blogTitle
不同,我们不希望blogUrl
和blogFeedUrl
的值使用目录字符串语法,因为(根据 RFC 3986 和之前的 URL 标准)URL 应使用 ASCII 字符集的子集。
注意
关于 URL 和国际化的更多信息,请参见 W3C 的Web Naming and Addressing页面:www.w3.org/Addressing/
。相关信息和相关 RFC 可以在该页面找到。
我们应该使用IA5 字符串语法,而不是使用目录字符串语法,它描述了一种扩展的 ASCII 字符集。该语法的 OID 是1.3.6.1.4.1.1466.115.121.1.26
。
同样地,当我们指定匹配规则时,我们希望使用 IA5 匹配规则。而且由于 URL 是区分大小写的,我们希望进行精确匹配。我们不希望忽略大小写。因此,匹配规则我们要使用caseExactIA5Match
和caseExactIA5SubstringsMatch
。
现在我们可以定义这两个属性了:
attributetype
(
blogAttrs:2
NAME 'blogUrl'
DESC 'Uniform Resource Locator (URL) for a blog.'
EQUALITY caseExactIA5Match
SUBSTR caseExactIA5SubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{512}
)
attributetype
(
blogAttrs:3
NAME 'blogFeedUrl'
DESC 'URL to an XML feed for a blog.'
SUP blogUrl
)
由于blogUrl
字段包含了blogFeedUrl
使用的匹配规则和语法,并且这两者在使用上有明显的相似性,因此将blogUrl
视为blogFeedUrl
的超类型是有意义的。所以,blogFeedUrl
继承了blogUrl
的 LDAP 语法和匹配规则。
最后,我们需要定义我们的blogDN
字段,它将保存一个 DN。DN 有语法和特定的匹配规则,我们将使用这些规则。区分名语法,用 OID 1.3.6.1.4.1.1466.115.121.1.12
定义,用于表示 DN 的值。而distinguishedNameMatch
匹配规则则用于对 DN 进行精确匹配。DN 没有子字符串匹配或排序匹配。
我们最后的属性看起来像这样:
attributetype
(
blogAttrs:4
NAME 'blogDN'
DESC 'DN of a blog entry in the directory.'
EQUALITY distinguishedNameMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.12
)
现在我们已经定义了整个模式。我们准备好进行测试了。
加载新模式
与所有其他模式一样,为了加载此模式,我们必须在slapd.conf
中包含它。
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/ppolicy.schema
include /etc/ldap/schema/blog.schema
假设blog.schema
位于/etc/ldap/schema
目录中(这是放置模式的好位置)。如果你选择将模式放置在其他地方,请相应地调整路径。
代码中高亮的行是唯一需要添加的部分(其余部分应该已经存在)。请注意,我们的模式只依赖于core.schema
。其他三个模式对于使我们的模式生效并不是必须的。
重启 SLAPD 将加载该模式。
排查模式加载问题
如果模式中存在错误,SLAPD 将无法启动,而是会显示一个详细的错误信息,例如:
/etc/schema/blog.schema: line 89: Unexpected token before
MUST ( blogDN ) )
ObjectClassDescription = "(" whsp
numericoid whsp ; ObjectClass identifier
[ "NAME" qdescrs ]
[ "DESC" qdstring ]
[ "OBSOLETE" whsp ]
[ "SUP" oids ] ; Superior ObjectClasses
[ ( "ABSTRACT" / "STRUCTURAL" / "AUXILIARY" ) whsp ]
; default structural
[ "MUST" oids ] ; AttributeTypes
[ "MAY" oids ] ; AttributeTypes
whsp ")"
slapd stopped.
connections_destroy: nothing to destroy.
当我们拼写错误AUXILIARY
时,触发了这个错误——这个错误信息并不容易直接看出其原因。但它说明了编写模式定义需要耐心和精确。
处理这种故障的最佳策略是仔细阅读错误的模式定义,寻找错误。有时简化定义也可以帮助排除其他可能的错误。最后,将定义与 RFC 4512 中的规范进行对比,帮助你发现任何不明显的语法错误。
新记录
现在我们可以使用ldapadd
将一个新的blog
条目添加到我们的目录信息树中。我们将添加关于 Example.Com 官方公司博客的信息:
$ ldapadd -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: blogTitle=Example.Com News,dc=example,dc=com
blogTitle: Example.Com News
blogUrl: http://example.com/blogs/main
blogFeedUrl: http://example.com/rss/main
description: The Official Example.Com Blog.
objectclass: blog
adding new entry "blogTitle=Example.Com News,dc=example,dc=com"
上面的高亮部分是我们正在添加的新条目。SLAPD 返回的最后一行表示该条目已成功添加。
我们的用户 uid=barbara
负责维护这个博客,因此我们可以通过使用 ldapmodify
向她的记录中添加 blogOwner
对象类和 blogDN
属性来表示这种关系:
$ ldapmodify -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=barbara,ou=users,dc=example,dc=com
changetype: modify
add: objectclass
objectclass: blogOwner
-
add: blogDN
blogDN: blogTitle=Example.Com News,dc=example,dc=com
modifying entry "uid=barbara,ou=users,dc=example,dc=com"
uid=barbara
的记录现在如下所示:
dn: uid=barbara,ou=Users,dc=example,dc=com
ou: Users
uid: barbara
sn: Jensen
cn: Barbara Jensen
givenName: Barbara
displayName: Barbara Jensen
mail: barbara@example.com
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: labeledURIObject
objectClass: blogOwner
blogDN: blogTitle=Example.Com News,dc=example,dc=com
我们刚刚成功创建并实现了一个包含新属性和对象类的新模式。
总结
本章的重点是模式。我们首先从理论角度探讨了模式的构成及其定义方式。接着,我们看了目录中模式的组织方式,重点介绍了不同类型的对象类及它们如何协同工作以构成一个层次结构的目录。随后,我们转向了更实用的内容。我们研究了 accesslog
和 ppolicy
覆盖层,每个覆盖层都需要其独立的模式。最后,我们通过创建自定义模式、构建一对对象类和一组属性来结束本章。
在下一章,我们将讨论如何处理多个目录,特别关注目录复制,即保持两个或更多目录服务器同步并包含相同内容的过程。
第七章。多个目录
在前几章中,我们专注于使用单个目录服务器。但在网络环境中,您可能需要配置多个目录服务器以实现互操作。在本章中,我们将讨论不同的方式来让目录服务器在网络上互操作。
虽然本书的重点是 OpenLDAP,但这里介绍的许多策略可以应用于将 OpenLDAP 与其他 LDAP 目录服务器集成,例如 Apache Directory Server、Fedora DS、微软的 Active Directory 以及 Novell Directory Server(NDS)。
我们将要查看的两个主要过程是复制(在另一个目录服务器上创建目录信息树的镜像)和代理(允许一个目录服务器作为 LDAP 客户端与另一个目录服务器之间的中介)。在本章中,我们将讨论:
-
同步和复制目录的基础
-
使用 SyncRepl 进行目录复制
-
使用
ldap
后端进行代理 -
使用 Proxy Cache 覆盖层添加缓存
-
使用
transparency
覆盖层创建混合缓存
在本章中,我们将使用两台服务器——一台将托管目录的权威副本,另一台将通过网络与权威副本进行同步。
复制:概述
有时需要多个相同的目录服务器副本。这在 LDAP 服务器承受大量流量、需要故障转移保护,或 LDAP 客户端地理分布广泛的情况下尤其有效,在这种情况下,拥有本地目录副本将大大加快服务速度。这些都是 LDAP 复制可以提供解决方案的场景。
复制是配置两个或更多目录以包含相同的目录信息树(或目录树的部分),并确保随着时间推移,多个目录数据副本保持同步的过程。这是 OpenLDAP 套件自诞生以来的核心特性。事实上,其前身——密歇根大学 LDAP 服务器,早期就实现了复制,因此,复制长期以来一直被视为 LDAP 服务器的标准任务。
在标准的 LDAP 模型中,复制是以层次结构的方式进行的。一个服务器被认为是主服务器(或者主 DSA(目录服务器代理),有时称为提供者)。该服务器负责维护目录信息树的规范版本。
在主服务器之下有一个或多个影子服务器(有时称为消费者、副本或从服务器)。影子服务器保存主服务器的目录信息树的副本,客户端可以连接到影子服务器并执行对目录信息树(DIT)的查询。我们来看一下下面的图:
就实际应用而言,影像服务器具有只读特性。尽管影像服务器可以处理许多 LDAP 操作,但不允许修改复制的目录信息树中的记录。例如,当接收到添加、修改或删除操作时,影像服务器将返回一个引用给客户端,指导客户端转而联系主服务器。引用是一种特殊的响应类型,它引导客户端联系另一个服务器来执行该操作。配置影像服务器指向主服务器的引用,只需要在 slapd.conf
文件中添加一个 referral
指令即可。
当客户端收到一个引用时,它就拥有了重新尝试在正确服务器上执行操作所需的信息。
为什么不允许对从服务器进行写操作呢?允许多个服务器接受所有的修改、添加和删除操作,使得目录信息树可能处于不一致状态。如果两个目录服务器同时修改一个属性会发生什么?或者如果一个服务器修改了另一个服务器正在删除的记录会怎样?通过仅允许在主服务器上进行写操作,更容易保持多个副本的一致性。
注意
在 OpenLDAP 2.4 版本中,将可以配置多主模式,这将允许多个服务器充当主服务器。和所有多主配置一样,可能会出现某些不一致性风险,但这些风险应当被最小化。
在 OpenLDAP 中,有两种不同的方式实现复制。第一种是通过配置主服务器来保持影像服务器的同步更新,这称为推送方法。第二种是配置从服务器定期检查主服务器是否有变化,并根据变化进行更新,这称为拉取方法。
直到 OpenLDAP 2.2,第一种模型才是 OpenLDAP 唯一支持的模型,并且它是通过一个名为 SLURPD 的独立服务器来实现的。但是 SLURPD 存在许多问题和低效之处,现在已被弃用,将在 OpenLDAP 2.4 中被移除。如果你有兴趣继续使用它以保持向后兼容性,请参阅 openldap.org
中的OpenLDAP 管理员 指南。
随着 SLURPD 的老化,OpenLDAP 的开发者开始致力于开发一种更好、更强大的目录复制方式。结果就是新的同步复制(SyncRepl)模型,它使用 LDAP 同步协议来保持影像目录与主服务器的同步。
SyncRepl
在 OpenLDAP 2.2 中,开发者发布了一种新的实验性复制形式,称为LDAP 同步复制,简称为 SyncRepl。这种方法更可靠、更可配置,在 OpenLDAP 2.3 发布时,它被进一步完善并标记为稳定版本。现在,它是处理 OpenLDAP 服务器复制的首选方式。
与 SLURPD 复制过程不同,SyncRepl 不需要第二个守护进程。SLAPD 服务器实现了影子服务器部分的代码,而提供者服务(主服务器的服务)则通过叠加层提供。SyncRepl 可以使用影子从主服务器拉取或混合拉取/推送方法。
在拉取场景中(称为仅刷新操作),影子服务器定期连接到主服务器,并请求自上次检查以来的所有更改。然后,主服务器将所有更改的记录发送给影子服务器(如果是删除操作,则发送被删除记录的 DN)。
SynRepl 的第二种方法(称为刷新并持久化)是推送特性(在 SLURPD 模型中体现)和上述一些拉取特性的混合(因此它不是一个真正的推送方法)。
在这种情况下,影子服务器首先连接到主服务器并拉取一些初始更新。但它保持连接打开。当主服务器修改其目录信息树副本时,它会通过该开放连接将信息推送到影子服务器。如果影子服务器断开连接,主服务器不做任何处理。下次从服务器连接时,它会请求所有的新更改(就像在拉取方法中一样),然后主服务器将其发送。
注意
有关 LDAP 内容同步和 OpenLDAP 中包括的 SyncRepl 实现的更详细信息,请参见 RFC 4533(www.rfc-editor.org/rfc/rfc4533.txt
)和OpenLDAP 管理员指南,网址为openldap.org
。
SyncRepl 模型相对于 SLURPD 具有一些明显的优势:
-
由于影子服务器发起连接并处理更新,网络中断不会影响目录信息树的可靠性。下次从服务器重新连接时,它将检索所有更新。
-
很少需要中断主服务器的服务。当新的影子服务器首次连接到主服务器时,它会下载整个目录信息树,因此无需从主服务器转储数据并将其发送给影子(尽管仍然支持这种方法,并且在目录信息树较大且网络连接较慢的情况下,这种方法可能更为方便)。
-
在选择仅刷新操作和刷新并持久化操作之间的灵活性使您能够选择最适合您需求的模型。
每种复制模式都有其优点。在高度分布的网络中,由于不需要在一个大且不可预测的网络中保持持续连接,仅刷新复制通常效果更好。但由于影子服务器只是定期检查主服务器,因此主服务器更新和影子服务器获取更改之间可能存在延迟。大多数情况下,这不会造成任何问题。
在一个可靠的局域网中,刷新和持久(refreshAndPersist)复制可能是更好的选择——尤其是当从主服务器到影像服务器的更改需要在最短时间内传输时。一旦主服务器发生变化,它将立即将更新发送到影像服务器。这意味着等待时间较短。
注意
即使是在刷新和持久(refreshAndPersist)操作中,网络中断也不会导致灾难性后果。影像服务器将简单地尝试重新连接到主服务器,并在成功连接后立即获取更新。
这些评论旨在作为一般性指南。由于尝试两者都比较容易,你可能希望通过实验来查看哪个对你最有效。通常,在局域网上,刷新和持久是最佳选择,而在较慢的链接上,刷新仅操作更好。在接下来的章节中,我们将介绍如何配置主服务器与影像副本之间的 SyncRepl。
配置 SyncRepl
SLAPD 服务器自带实现影像服务器所需的所有功能,而 syncprov
覆盖层提供了实现主服务器的功能。
注意
SyncRepl 在 OpenLDAP 2.2 中被引入,但当时的配置方式有很大不同。SyncRepl 应避免在运行 OpenLDAP 2.2 的生产环境中使用。
要使 SyncRepl 运行,需要在主服务器和影像服务器上进行配置。两者的配置指令将添加到 slapd.conf
文件的后端部分。
配置主服务器
我们首先要做的是将一台服务器配置为主服务器。该服务器将监听来自影像服务器的复制请求,并根据请求发送更新。在本书中,我们一直在配置一个 SLAPD 服务器。现在,我们将使用该服务器作为主服务器。
主服务器的功能是通过一个名为 syncprov
的覆盖层实现的(syncprov
是 同步提供者 的缩写)。我们需要加载并配置该覆盖层。
由于我们的 SLAPD 服务器是通过模块构建的,第一步是在 slapd.conf
文件的顶部附近添加模块加载指令:
modulepath /usr/local/libexec/openldap
moduleload back_hdb
moduleload refint
moduleload unique
moduleload accesslog
moduleload syncprov
当目录服务器重新启动时,syncprov
模块将被加载。现在,我们需要对我们将要复制的数据库的配置部分进行一些更改。该目录配置的主要部分大致如下所示:
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
#directory /usr/local/var/openldap-data
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
index sn eq,sub,approx
index member eq
现在,我们想要为 SyncRepl 设置这个数据库。
首先要添加的是一些额外的索引。这些索引将跟踪在 SyncRepl 过程中经常访问的一对操作属性:entryCSN
属性和 entryUUID
属性。
entryCSN
属性用于在每个记录中存储变更序列号(CSN)。entryCSN
的值基本上是一个精细的时间戳,表示该属性最后一次修改的时间。第二个属性entryUUID
包含该条目的(全局)唯一标识符,可用于快速识别主服务器和影子服务器上的对应条目。像其他属性一样,这些属性可以通过 LDAP 搜索检索:
$ ldapsearch -LLL -U matt "(uid=matt)" entryCSN entryUUID
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: uid=matt,ou=Users,dc=example,dc=com
entryUUID: bec1eb70-c5b0-102a-81bf-81bc30f92d57
entryCSN: 20070122003136Z#000000#00#000000
当 SyncRepl 搜索这些属性时,它会进行相等性检查,因此我们应该为执行相等性测试配置一个索引:
index entryCSN,entryUUID eq
这个index
指令配置了两个相等性索引—每个属性一个—可以将其添加到slapd.conf
文件中,位于其他index
指令的下方。
接下来,我们需要加载并配置syncprov
覆盖层。这个覆盖层通常使用的配置指令只有两个,因此我们为主服务器配置的完整覆盖层配置如下所示:
overlay syncprov
syncprov-checkpoint 50 10
syncprov-sessionlog 100
第一行加载了syncprov
覆盖层。第二行指定了 SyncRepl 信息应多久写入一次数据库。与 BDB 和 HDB 后端一样,SyncRepl 经过调整,以尽可能快地执行操作。写入底层数据库的代价很高,因此简化这一过程可以提高性能。
syncprov-checkpoint
指令指示覆盖层仅在新的写入请求到来时才将更改写入数据库,并且已经发生了特定数量的写入(在此情况下为50
次),或者已经过去了特定数量的分钟(在此情况下为10
分钟)。
第二个指令,syncprov-sessionlog
,指定了应在会话日志中存储多少次修改和删除。主服务器使用此日志中的信息来确定需要发送给影子服务器的信息。在这种情况下,它将存储最近的 100 次修改和删除。
我们完成的配置看起来像这样:
##############################
# Database 1: Example.Com
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
#directory /usr/local/var/openldap-data
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
index sn eq,sub,approx
index member eq
index entryCSN,entryUUID eq
overlay syncprov
syncprov-checkpoint 50 10
syncprov-sessionlog 100
一旦完成了对slapd.conf
的修改,最好运行slaptest
来确保配置文件可以被解析,然后(为了保险起见)运行slapindex
来更新索引文件。
创建 SyncRepl 用户
准备主服务器的最后一步是创建一个用于同步的特殊帐户。影子服务器将使用这个帐户连接到主服务器。
我们将创建一个类似于用于执行身份验证的帐户:
dn: uid=syncrepl,ou=System,dc=example,dc=com
uid: syncrepl
ou: System
userPassword: secret
description: Special account for SyncRepl.
objectClass: account
objectClass: simpleSecurityObject
我们可以使用ldapadd
客户端加载此记录:
$ ldapadd -U matt -f syncReplUser.ldif
为了使复制帐户正常工作,它还需要有权限更新目录中的必要条目。这意味着 ACL 必须授予此用户相应的权限。虽然我们可以像第四章那样详细列出 ACL,但为了方便起见,我们将只将新的 SyncRepl 用户添加到cn=LDAP
Admins
组中,并使用ldapmodify
:
$ ldapmodify -U matt
SASL/DIGEST-MD5 authentication started
Please enter your password:
SASL username: matt
SASL SSF: 128
SASL installing layers
dn: cn=LDAP Admins, ou=Groups, dc=example,dc=com
changetype: modify
add: uniqueMember
uniqueMember: uid=syncrepl,ou=system,dc=example,dc=com
modifying entry "cn=LDAP Admins, ou=Groups, dc=example,dc=com"
现在,uid=syncrepl
用户是 LDAP 管理员组的成员,并且应用于该组的 ACL 也将应用于我们的新用户。
这就是将目录配置为主服务器的所有步骤。接下来,我们将配置影子服务器。
配置影子服务器
我们将配置影子服务器使用 refreshOnly
复制方式,主服务器会定期检查更新,如果发现有更新,则获取更改并将其加载到自己的目录树中。
我们的影子服务器将是一个全新的 SLAPD 实例,运行在同一局域网中的另一台服务器上。我们从一个基础的 slapd.conf
文件开始。随着配置 SyncRepl,我们将对这个文件进行修改:
# slapd.conf - Configuration file for LDAP SLAPD
##########
# Basics #
##########
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/blog.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
loglevel none
modulepath /usr/lib/ldap
moduleload back_hdb
#############################
# BDB Database Configuration #
##############################
# Database 1: Example.Com
database hdb
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
#rootpw secret
directory /var/lib/ldap
index objectClass,member eq
index cn,uid,sn eq,sub
index entryCSN,entryUUID eq
#include /usr/local/etc/openldap/acl.conf
基于我们在第二章和第三章中汇总的配置,这应该是很熟悉的。不过,有几点需要注意:
-
主服务器使用的所有模式也必须在影子服务器上加载。
-
在这种情况下,我们将把整个主目录复制到这个影子 SLAPD 服务器,因此我们希望后缀保持一致,
dc=example,dc=com
。 -
我们不希望此实例有根密码。所有更新将来自主服务器,我们不希望在本地进行任何更改。
-
对于主服务器和影子服务器,索引不必完全相同(事实上,主服务器和影子服务器甚至可以使用不同的数据库后端),但我们确实希望确保
objectclass
、entryCSN
和entryUUID
都被索引,因为它们对 SLAPD 的性能至关重要。
这个基础的 slapd.conf
文件应该能够运行一个独立的服务器。但我们不想运行一个独立的服务器;我们希望它从主服务器获取信息并与主服务器保持同步。
syncrepl 指令
当影子 SLAPD 服务器执行同步操作时,它充当一种特殊的 LDAP 客户端。它绑定到主服务器并执行 LDAP 操作——通常是 RFC 4533 中定义的特殊 LDAP 同步操作。
因此,配置一个影子服务器作为 SyncRepl 消费者,类似于配置其他 LDAP 客户端。这大部分配置与提供有关影子服务器如何绑定主服务器以及如何执行搜索的信息有关。
实现影子服务器的配置工作大部分通过一个 slapd.conf
指令完成:syncrepl
。该指令采用多个参数,格式为 name=value
,用于指定影子服务器的行为。以下是一个包含执行基本同步所需所有参数的 syncrepl
指令。在 slapd.conf
文件中,该指令位于我们 example.com
后端的数据库配置部分:
syncrepl rid=001
provider=ldap://directory.example.com
type=refreshOnly
interval=00:00:05:00
searchbase="dc=example,dc=com"
binddn="uid=syncrepl,ou=system,dc=example,dc=com"
credentials=secret
该指令提供了使 SyncRepl 工作所需的最小配置。该指令有七个名称/值参数:rid
、provider
、type
、interval
、searchbase
、binddn
和 credentials
。
第一个参数是 rid
,即 副本标识符 (RID)。该三位数字必须在所有使用相同主服务器的影子服务器中是唯一的。主 SLAPD 实例使用 RID 来跟踪哪些消费者服务器在与它连接。通常,最好从较低的 RID 数字开始,并为每个影子服务器递增它。因此,rid=001
表示这是第一个影子服务器。如果我们添加第二个影子副本,则会是 rid=002
。
注意
在早期版本的 OpenLDAP 中,主服务器必须包含其消费者服务器的所有 RID 列表。现在不再需要这样做。
provider
参数应包含主服务器的 LDAP URL。可以使用 ldap://
或 ldaps://
协议。主机部分可以是主机名或 IP 地址,并且可以在末尾添加可选端口,通过冒号分隔。例如,要通过非标准端口使用 LDAPS 连接到主服务器,可以使用如下格式的 provider:ldaps://10.0.1.34:6868
。请注意,这里仅支持这种简单的 LDAP URL 格式。包含基本 DN、搜索过滤器等的完整 LDAP URL 语法在这里不被支持。
提示
使用 StartTLS 替代 SSL/TLS
您可以配置影子服务器通过 LDAP(未加密)连接,然后发出 StartTLS 命令以开始与主服务器之间的 TLS 加密。为此,添加 starttls=yes
(如果 TLS 协商失败应停止事务,则使用 starttls=critical
)。
type
参数确定影子服务器在连接主服务器时将使用哪种复制模式。唯一可接受的值是 refreshOnly
和 refreshAndPersist
。
在我们的示例中,我们使用了 refreshOnly
选项。在刷新并持久化配置中,interval
参数将被忽略。
否则,配置仅刷新和刷新并持久化之间没有显著差异。
interval
参数表示影子服务器在检查主服务器更新之前等待的时间。这适用于 refreshOnly
模式,在该模式下,消费者服务器连接、检查更新然后断开连接。然后,它将等待由 interval
参数指定的时间段后再进行下一次检查。
interval
参数的语法为 dd:hh:mm:ss
,其中 dd
表示等待的天数,hh
为小时,mm
为分钟,ss
为秒。如果未指定该参数,则默认值为一天 (01:00:00:00
)。通常希望选择较短的间隔,特别是当影子服务器需要提供最新信息时。在前面的示例中,影子服务器将在每次检查之间等待五分钟 (00:00:05:00
)。
提示
如果影像服务器必须与主服务器保持紧密同步,并且影像服务器与主服务器位于同一局域网,则 refreshAndPersist
模式可能更适合。
在 refreshOnly
模式下,一个潜在的困难出现在主服务器不可用的情况下(例如,由于网络中断或服务器故障)。这时,影像服务器应如何处理?除了 interval
参数外,还有一个额外的参数,允许调整刷新间隔,但该选项仅在无法连接到主服务器时生效。
此参数 retry
提供了影像服务器在无法联系到主服务器时应如何处理的信息。它的格式如下:retry="120
10"
。这指示影像服务器在主服务器不可用时每 120 秒重试一次,最多重试 10 次。
提示
使用 retry 参数
在 refresh-only 和 refresh-and-persist 配置中设置 retry 参数是一个好主意。这将确保短暂的网络故障不会干扰复制过程。
此参数可以包含多个参数对。例如,我们可以配置它在短时间内检查几次,然后(如果仍然无法连接)在更长的时间间隔内再次测试:retry="30
10
600
20"
。这次,如果影像服务器无法连接到主服务器,它将每 30 秒尝试一次,共尝试 10 次。如果主服务器仍然无法连接,则它将等待十分钟(600 秒)后再尝试一次。它会重复此过程再进行 20 次。但在这些尝试后,影像服务器将放弃继续尝试连接主服务器。
要配置影像服务器无限期测试——即不断尝试直到连接成功——可以插入特殊的 +
(加号)符号代替重试次数。例如,参数 retry="60
+"
会指示影像 SLAPD 每分钟尝试连接主服务器,直到最终成功为止,成功后将恢复到 interval
参数设置的常规时间间隔。
在 interval
参数之后是 searchbase
参数。该参数指示同步请求的基本 DN。通常,searchbase
应与影像服务器的数据库 suffix
指令相同。
影像服务器不必复制主服务器的整个目录信息树。例如,我们可以配置影像服务器仅复制 ou=users
分支,数据库配置如下:
database hdb
suffix "ou=users,dc=example,dc=com"
rootdn "ou=users,dc=example,dc=com"
directory /var/lib/ldap
index objectClass,member eq
index cn,uid,sn eq,sub
index entryCSN,entryUUID eq
include /etc/ldap/acl.conf
syncrepl rid=001
provider=ldap://directory.example.com
type=refreshOnly
interval=00:00:05:00
searchbase="ou=users,dc=example,dc=com"
binddn="uid=syncrepl,ou=system,dc=example,dc=com"
credentials=secret
再次提醒,suffix
和 searchbase
是相同的。
searchbase
指令是构成搜索规范的多个指令之一。我们还可以使用 scope
、filter
、attrs
、attrsonly
、sizelimit
和 timelimit
参数来构建更复杂的搜索规范。不过,如果不使用这些参数,我们只是接受了默认设置,它会执行如下的搜索:
-
scope
设置为sub
-
filter
设置为(objectclass=*)
。 -
attrs
字段设置为*
,+
,这将请求所有常规和操作属性。 -
没有包括
attrsonly
标志,因此返回的既有属性也有值。 -
sizelimit
和timelimit
参数都设置为unlimited
。
syncrepl
指令中的第六和第七个参数是 binddn
和 credentials
。这些用于执行简单绑定到目录。
配置主服务器时,我们创建了 uid=syncrepl
账户。现在,我们将使用相同的 DN 从影像服务器连接到主服务器。如前所述,主服务器不会自动授予此账户任何特殊权限;主服务器上的 ACL 将应用于该账户。
此用户将应用大小和时间限制。配置 SyncRepl 时一个常见的错误是无意中将 SyncRepl 用户设置为过低的大小或时间限制。结果是,影像服务器可能只获取了它应该拥有的目录信息树的部分内容,无法为客户端提供完整的目录信息。
如果系统资源允许,通常建议为 SyncRepl 用户提供无限的时间和请求大小。
credentials
参数在简单绑定的情况下保存密码。
注意
我们的基本配置使用简单绑定和未加密的(纯 LDAP)连接。这不安全。使用 StartTLS、SSL/TLS 或适当强度的 SASL 机制将提供更高的安全性。
简单绑定并不是 SyncRepl 支持的唯一类型。SASL 身份验证也可以启用,尽管这可能需要额外的参数:
-
bindmethod=sasl
:默认情况下,绑定方法设置为简单。要启用 SASL 身份验证,必须手动设置此参数。 -
saslmech=<SASL
Mechanism>
:此项应设置为,例如,DIGEST-MD5
,以便在传输前对密码进行 MD5 哈希。更多信息请参见第四章中的 SASL 部分。 -
authcid=<uid>
:此项应设置为用于身份验证的账户的 SASL ID。类似的authzid
参数可用于配置备用授权账户。 -
credentials=<SASL
Credentials>
:credentials
字段用于 SASL 身份验证,将凭据信息传递给 SASL 子系统。例如,在 DIGEST-MD5 机制中,credentials
保存账户的密码。 -
realm=<SASL
Realm>
:此参数可传递领域信息(参见第四章)。 -
secprops=<SASL
Security
Props>
:可以通过此参数传递额外的 SASL 安全属性。
最后需要注意的是,默认情况下,在 SyncRepl 操作期间,影像服务器不会对从主服务器接收到的记录进行模式检查。换句话说,如果主服务器发送给影像服务器的记录违反了模式约束,影像服务器会简单地存储该不合法记录,而不会尝试验证或拒绝它。
通常,禁用模式检查是可取的。由于主服务器应始终执行模式检查,第二套相同的检查是多余的,并且会减慢复制过程。然而,在极少数情况下,可能希望进行额外的评估。在syncrepl
指令中通过添加schemachecking=on
参数可以启用对复制记录的模式检查。
配置引用
对复制的目录信息树执行写操作只能在主服务器上进行。例如,你不能通过连接到影像服务器并执行 LDAP 添加操作来更改某个属性。换句话说,影像服务器实际上是只读的。
如果客户端尝试在影像服务器上修改条目,该服务器将响应并表示不会执行修改操作:
$ ldapmodify -x -W -D "uid=matt,ou=users,dc=example,dc=com" -H \
ldap://localhost
Enter LDAP Password:
dn: uid=matt,ou=users,dc=example,dc=com
changetype: modify
replace: description
description: testing modify against shadow.
modifying entry "uid=matt,ou=users,dc=example,dc=com"
ldap_modify: Server is unwilling to perform (53)
additional info: shadow context; no update referral
在这个例子中,当我们尝试修改自己的记录的描述属性值时,服务器响应了unwilling
to
perform
错误。
虽然影像服务器不能允许更新其自身的数据,但可以配置为将客户端重定向到主服务器。这是通过在数据库部分(通常就在syncrepl
指令下方)添加额外的指令来实现的,指示请求应被重定向到哪个服务器。指令如下所示:
updateref ldap://directory.example.com
现在,当客户端尝试执行写操作时,不会收到错误,而是会收到一个引用:
$ ldapmodify -x -W -D "uid=matt,ou=users,dc=example,dc=com" -H \
ldap://localhost
Enter LDAP Password:
dn: uid=matt,ou=users,dc=example,dc=com
changetype: modify
replace: description
description: testing modify against shadow.
modifying entry "uid=matt,ou=users,dc=example,dc=com"
ldap_modify: Referral (10)
referrals:
ldap://directory.example.com/uid=matt,ou=users,dc=example,dc=com
许多客户端可以配置为执行所谓的引用追踪。也就是说,当它们收到引用时,可以自动跟随该引用。在此情况下,客户端将自动尝试在directory.example.com
主服务器上执行修改操作。
启动复制
到此为止,我们已经仔细查看了主服务器和影像服务器的 SyncRepl 配置选项。现在我们准备启用这些配置。
一旦主服务器配置完成,必须重新启动才能使配置更改生效。当syncprov
叠加层加载后,SLAPD 将作为主服务器运行。所有这些步骤应在启动配置好的消费者服务器之前完成,否则,影像服务器将尝试从主服务器获取信息,而主服务器将没有必要的 LDAP 操作可用。
在主服务器重新运行后,可以启动影子服务器。对于中小型目录,以及带宽足够的网络,无需手动将任何目录数据加载到影子服务器中。相反,当影子服务器首次连接到主服务器时,它将获取一份新的目录信息树副本(在主服务器的 ACL 允许的范围内),并将其全部存储到本地。
几分钟之内,影子服务器应该会有一份正确且完整的副本,包含主服务器中存储的信息。
对于较大的目录...
从主服务器到影子服务器自动下载目录信息树确实很容易,但当目录信息树非常庞大且包含几 GB 数据时,通过网络执行更新(每个事务都使用 LDAP 协议)可能会消耗大量时间和资源。
在这种情况下,通常最好在主服务器上使用slapcat
来导出目录内容(无需停止 SLAPD 即可执行此操作),然后将 LDIF 文件传输到影子服务器,并通过slapadd
导入。
注意
附录 C 包含使用slapcat
和slapadd
来导出和加载 SLAPD 数据库的指令。
由于slapcat
和slapadd
程序不涉及 LDAP 网络协议的开销,因此在添加新记录时,它们的性能可以超过 SyncRepl。在带宽无法专门用于大规模数据传输的网络中,LDIF 文件也可以通过其他(离线)介质传输。
一旦目录数据库通过slapadd
填充完毕,你可以启动影子服务器。
Delta SyncRepl
默认情况下,当主服务器向影子服务器发送修改或添加的记录时,它会发送整个 记录,而不仅仅是更改部分。这是因为主服务器并不会追踪已经发送到影子服务器的信息。
但是,accesslog
叠加层确实会跟踪发送到影子服务器的信息。通过配置 SLAPD 使用accesslog
叠加层来提供syncprov
叠加层的日志信息,可以简化复制过程,只发送更改过的信息,而不是整个记录。这被称为Delta SyncRepl。在修改频繁的网络或包含非常大记录的目录中,这种简化可能会带来明显的性能提升。
注意
Delta SyncRepl是一种高级配置。由于它涉及多个叠加层的协作,以及一些相当复杂的配置,因此它可能不是所有配置的最佳解决方案。我在处理小型和中型目录通过局域网(LAN)和广域网(WAN)链接复制的经验表明,常规的 SyncRepl 已足够,Delta SyncRepl 并非必需。
配置 Delta SyncRepl 需要在主服务器上进行一些更改,并在影子服务器上进行少量更改。
主服务器的配置
主服务器必须运行第五章中实现的accesslog
覆盖层。我们将从为该覆盖层设置日志数据库开始。这一配置与第五章中创建的配置非常相似:
# Database 1: Logging DB
# This is used by the access
# log overlay
database hdb
suffix cn=log
rootdn "cn=Manager,cn=log"
rootpw secret
directory /var/lib/ldap/accesslog
index reqStart,objectclass,entryCSN,reqResult eq
overlay syncprov
syncprov-nopresent TRUE
syncprov-reloadhint TRUE
本节创建了一个新的日志数据库,名为cn=log
,所有访问日志信息都将写入该数据库。
本节中只有几行与第五章的配置有所不同。首先,索引指令现在会在reqStart
、objectclass
、entryCSN
和reqResult
上建立索引。虽然reqStart
和entryCSN
主要用于内部,但 SyncRepl 消费者会大量使用objectclass
和reqResult
属性,因此对这些属性建立索引将加速复制过程。
最后四个指令是新的。必须将syncprov
覆盖层添加到 accesslog 数据库的配置中,以配置 SyncRepl 的访问日志。这两个标志,syncprov-nopresent
和syncprov-reloadhint
,都必须开启(TRUE
),以使 Delta SyncRepl 正常工作。事实上,syncprov-nopresent
标志仅应在进行 Delta SyncRepl 时启用。
提示
设置限制和 ACL
根据你的sizelimit
和timelimit
设置,你可能需要显式地为uid=syncrepl
用户在cn=log
数据库上授予无限制的时间和大小限制。同时,确保该数据库的 ACL(访问控制列表)为uid=syncrepl
授予read
访问权限。有关 ACL 的更多信息,请参见第四章,关于limit
指令的内容,请参见第五章。
最后,我们希望通过第五章中介绍的limit
指令,为syncrepl
用户提供无限的搜索时间和结果大小。
接下来,我们需要稍微重新配置我们将要复制的数据库。在slapd.conf
文件中,这应该直接放置在给定的 accesslog 定义下方:
##############################
# Database 2: Example.Com
database hdb
cachesize 500
idlcachesize 1500
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
rootpw secret
directory /var/lib/ldap
index objectClass eq
index cn eq,sub,pres,approx
index uid eq,sub,pres
index sn eq,sub,approx
index member eq
index entryCSN,entryUUID eq
overlay syncprov
syncprov-checkpoint 50 10
syncprov-sessionlog 100
overlay accesslog
logdb cn=log
logops writes
# Purge logs for entries one week old, check once every two days.
logpurge 7+00:00 2+00:00
logsuccess TRUE
高亮的部分标记了新增加的数据库部分,该部分属于复制后端数据库中的数据库。这里配置的accesslog
覆盖层将使用之前定义的cn=log
数据库。我们需要记录的操作仅包括写入数据库的操作(添加、修改、删除和 modrdn)。
注意
根据你的大小和时间限制设置,你可能还需要添加一个显式的限制指令,授予uid=syncrepl
无限的时间和结果大小以完成操作。
这些是主服务器需要做的唯一更改。现在我们将看看影子服务器的slapd.conf
文件中的更改。
影子服务器的配置
在消费者(影子服务器)端,启用 Delta SyncRepl 需要在syncrepl
指令中添加几个参数:
syncrepl rid=001
provider=ldap://10.21.77.100
type=refreshOnly
interval=00:00:02:00
searchbase="dc=example,dc=com"
binddn="uid=syncrepl,ou=system,dc=example,dc=com"
credentials="secret"
syncdata=accesslog
logbase="cn=log"
logfilter="(&(objectclass=auditWriteObject)(reqResult=0))"
syncrepl
指令的新部分由给定示例末尾新增的三行构成。这些行指示影子服务器查阅主服务器的 accesslog 数据库以获取同步信息。
syncdata
参数指示 SyncRepl 应该使用哪个源来获取需要更新的记录信息。应该设置为 accesslog
,表示我们使用的是 accesslog 后端。
logbase
指令应该设置为主服务器上 access-log 的基础 DN。在前面的章节中,我们将其设置为 cn=log
。
最后,logfilter
参数定义了在搜索主服务器的 accesslog 时应该使用的过滤器。在复制过程中,我们需要有关数据库任何更改的信息——添加、修改、modRDN 或删除。这些都是写入操作,并将使用 auditWriteObject
对象类记录在 accesslog 中。此外,我们只想同步成功完成的事务(记住,accesslog 会记录更改目录的失败尝试,我们不希望复制这些)。在写入成功的情况下,reqResult
标志将被设置为 0
。因此,我们也将此添加到过滤器中。
注意
有关完整的 Delta SyncRepl 配置文件集,请参见 Connexitor 博客上的以下技术说明:www.connexitor.com/forums/viewtopic.php?t=3
(Connexitor 是 Symas 提供的商业支持版本 OpenLDAP)。
现在主服务器和影像服务器都已配置完毕。首次启动时,你可能希望删除旧的影像数据库(请参见本章前面的说明)并重新开始。再次,启动消费者之前先重启主服务器。
这就是配置 Delta SyncRepl 的所有内容。接下来,我们将探讨一些调试复制问题的策略。
调试 SyncRepl
配置基于网络的服务器到服务器的设置(如 SyncRepl)时,调试的难度是一个令人沮丧的因素。以下是一些使 SyncRepl 调试变得更容易的提示。
从头开始
有时首次配置复制会失败。实际上,非常容易清除影像服务器的整个数据库,并从头开始重新配置。
如果你使用的是 BDB 或 HDB 后端,只需要删除数据库目录中的所有数据文件:
$ sudo /etc/init.d/slapd stop
$ cd /var/lib/ldap
$ rm -f *.bdb __db.* log.*
注意
警告:确保不要删除 DB_CONFIG
文件!
下次重新启动 SLAPD 时,它将从头开始重建数据文件。
类似的步骤也可以用于迁移数据库、修复损坏的后端等。但这些情况需要更多的关注。有关更详细的说明,请参见 附录 C。
策略日志记录
调试复制的另一种方法是将影像 SLAPD 实例以前台模式运行,并启用 sync
日志级别:
$ sudo slapd -d sync
这将打印关于同步过程的详细信息。
增加主服务器上的日志信息也可能有所帮助。acl
日志级别对于评估如何将访问规则应用于 SyncRepl 用户的请求非常有用。对于更复杂的问题,trace
调试级别也非常有帮助。
一些常见错误
配置 SyncRepl 时常见的一些错误。
限制和 ACL:我已经提到过时间限制和大小限制的问题:sizelimit
和timelimit
指令适用于 SyncRepl 用户,就像它们适用于其他非管理员帐户一样。如果数据库中的条目超过了最大大小限制,或者复制连接需要很长时间,那么从主服务器到阴影服务器的复制可能会提前结束,导致同步不完整。
ACL 也可能在复制中产生意想不到的结果。如果 ACL 拒绝 SyncRepl 用户的访问权限,则该用户将无法同步该信息。这也可能导致同步不完整。幸运的是,SLAPD 会尝试自动弥补尽可能多的这些不一致性。不幸的是,这可能会使问题在更长时间内保持不可见。
未调优的 DB_CONFIG:在第五章中,我们查看了DB_CONFIG
文件,这是一个用于调优 BDB/HDB 数据库后端的特殊配置文件。在配置阴影服务器时,重要的是在数据库目录(/var/lib/ldap
)中放置一个DB_CONFIG
文件。如果DB_CONFIG
文件缺失或调优不当,数据库环境将变得更慢。虽然这在执行简短偶尔的搜索时可能不易察觉,但这会对复制产生不利影响。较大的事务(如初始更新或转移重要修改)可能会比使用经过良好调优的数据库环境时慢得多。
有时,这只是增加了更新数据库时的延迟,但当与时间限制结合使用时,它可能导致同步被截断。
SASL 认证失败:在实现 SyncRepl(或 SLURPD)时,SASL 配置有时会导致混淆。如果你通常使用 SASL 进行认证,并且 SASL 信息没有存储在目录信息树中,那么你还需要确保外部 SASL 数据被同步。
在第四章中,我们配置了 SASL 使用外部的/etc/sasldb2
文件进行 DIGEST-MD5 认证,用于存储密码。如果我们要在阴影服务器上使用 SASL DIGEST-MD5 认证,则需要确保每个服务器都有相同的/etc/sasldb2
文件,这就需要使用一些其他非 OpenLDAP 工具,如rsync(samba.anu.edu.au/rsync/
)。
解决这个问题的一种方法是将明文 SASL 密码存储在目录中,而不是存储在sasldb2
文件中。这可以通过使用{CLEARTEXT}
密码哈希而不是{SSHA}
或其他机制来实现。有关更多信息,请参见第三章。OpenLDAP 管理员指南(openldap.org
)也解释了这种配置。
简单绑定(通过 DN 和用户密码)应该能够与复制正常工作,就像我们在第六章配置的 SASL EXTERNAL 认证一样。
配置 LDAP 代理
有时,代替复制目录信息树,可能需要代理与 LDAP 目录的通信。在这种情况下,配置一个 SLAPD 服务器,站在客户端与网络上另一个 LDAP 服务器之间,响应客户端请求,并从其他 LDAP 服务器检索目录信息来响应请求。
OpenLDAP 支持几种不同的配置方式,将 SLAPD 配置为代理。
使用 LDAP 后端
设置两个服务器之间的代理的一种方式是配置一个服务器使用ldap
后端(而不是 BDB 或 HDB)。ldap
后端监听请求,当收到请求时,透明地将请求转发到另一个 LDAP 服务器。例如,假设我们有两个服务器,directory.example.com,存储数据库,和 proxy.example.com,使用ldap
后端将请求代理到 directory.example.com 服务器。
从客户端的角度来看,当客户端连接到 proxy.example.com 时,它看起来是从 proxy.example.com 获取结果的。所有网络流量在客户端和代理之间传输,返回的结果中没有任何内容表明结果是从另一个服务器获取的。此外,ldap
后端会自动跟随引用,而不需要客户端应用程序进行引用追踪。
从 directory.example.com 的角度来看,连接来自 proxy.example.com。
在协议层面,ldap
后端会透明地将客户端的所有请求转发到其他服务器。换句话说,当客户端进行绑定时,它绑定的不是 proxy.example.com,而是 directory.example.com。
注意
这也是可以配置的,且可以实现更高级的绑定配置。有关此类功能的讨论,请参见使用 身份 管理 功能一节。
每个客户端都会获得一个独立的连接,从代理到目录,唯一的例外是,所有作为匿名用户连接的客户端都通过相同的连接代理到远程服务器。
注意
TLS 连接从客户端到代理。代理可以配置为在客户端请求 TLS 时,或每次代理连接到远程服务器时使用 TLS 与远程服务器进行通信。这可以通过ldap
后端的tls
指令来实现。
配置ldap
后端作为代理非常简单。下面是一个完整的slapd.conf
配置,用于ldap
后端:
# slapd.conf - Configuration file for LDAP SLAPD
##########
# Basics #
##########
include /etc/ldap/schema/core.schema
include /etc/ldap/schema/cosine.schema
include /etc/ldap/schema/inetorgperson.schema
include /etc/ldap/schema/blog.schema
pidfile /var/run/slapd/slapd.pid
argsfile /var/run/slapd/slapd.args
loglevel none
modulepath /usr/lib/ldap
moduleload back_ldap
################
# LDAP Backend #
################
database ldap
uri "ldap://directory.example.com"
suffix "dc=example,dc=com"
本例的重点已被突出显示。
一旦加载了back_ldap
模块,后端仅通过三条指令来定义。数据库指令指向ldap
后端(而不是我们在前几章中使用的hdb
后端)。
uri
指令的值为以空格分隔的 LDAP URL 列表。在这个例子中,只有一个 URL。当有多个 URL 时,尤其是在某个服务器故障时会很有用。当 URL 列表存在时,ldap
后端会按照顺序尝试连接服务器。如果第一个服务器无法连接,它会切换到第二个 URL,依此类推,直到所有服务器都尝试过或者最终成功连接。
suffix
指令指定该后端服务的后缀或多个后缀。这应包含远程目录提供的基准 DN 或多个 DN。通过这种方式,使用代理可以仅使远程服务器的某个分支或几个分支可用。例如,远程服务器可能提供dc=example,dc=com
的访问权限。但我们可以将代理上的后缀设置为ou=users,dc=example,dc=com
,这样该服务器的用户就只能通过该代理搜索目录信息树的这一部分。
注意
一些 OpenLDAP 用户报告成功实现了ldap
后端,将请求代理到其他目录服务器,如 Microsoft 的 Active Directory。
ldap
后端还有一些其他可用的配置选项,所有这些都在slapd-ldap
手册页中有记录:man
slapd-ldap
。但我们将只关注其中的一部分:身份管理功能。
使用身份管理功能
ldap
后端可以做更多复杂的操作。例如,你可以将身份验证和授权任务分开,客户端提供的 DN 进行身份验证,但所有操作都以另一个用户的身份执行。
这个名为ID 断言的功能,允许你设置一个代理(可能位于较不安全的网络上),使得用户能够以自己身份进行绑定,但随后使用一个权限较低的帐户(如权限受到 ACL 限制的系统帐户)从目录中获取有限的信息子集。
配置 ID 断言只需要几个额外的指令。在代理上,你需要向ldap
数据库配置中添加两个指令:idassert-bind
和idassert-authzFrom
。
idassert-bind
指令指定代理服务器如何验证远程目录服务器。以下是一个示例配置:
idassert-bind
bindmethod=simple
binddn="uid=authenticate,ou=system,dc=example,dc=com"
credentials="secret"
mode=none
该指令定义了代理用于连接到远程目录以验证客户端的帐户(及其认证方式)。
bindmethod
的支持值有simple
(进行简单绑定)、sasl
(进行 SASL 绑定)和none
。如果使用none
,则不会进行身份声明(这与完全不使用该指令的效果相同)。
binddn
和credentials
参数指定连接到远程目录的 DN 和密码。
mode
参数指定将向远程服务器声明哪个身份。在给定的示例中,我们将mode
设置为none
,这意味着代理将声明binddn
中指定的 DN 作为其身份。换句话说,代理将在远程服务器上以binddn
中的 DN 身份执行所有操作。
对于更复杂的代理,您可以将mode
设置为anonymous
(这将向远程目录声明匿名身份)或self
(这将声明由客户端发送的身份)。这些实现了 RFC 4370 中定义的代理授权(proxyAuth)控制(www.rfc-editor.org/rfc/rfc4370.txt
)。
对于anonymous
或self
,您可能还需要在ldap.conf
中设置authz-policy
指令,并向代理或客户端的 DN(分别)添加authzFrom
或authzTo
条目。有关更多信息,请参阅slapd.conf
和slapd-ldap
的手册页。
idassert-authzFrom
指令用于授权哪些客户端可以使用代理。例如,我们可以设置一条规则,允许用户使用代理,如果他们的 DN 位于ou=users
子树中:
idassert-authzFrom dn.subtree="ou=users,dc=example,dc=com"
与其他使用dn
说明符的指令一样,这个指令支持常规的修饰符列表,如dn.subtree
、dn.one
和dn.regex
。有关这些修饰符的解释,请参阅第五章中的限制讨论。
将简单代理转换为缓存代理
目前我们已经配置了代理,每个请求都被转发到远程目录服务器,代理本身不保存任何结果。因此,当相同的请求被多次执行时,代理每次都会连接到远程目录服务器并转发请求。然而,可以使用pcache
(代理缓存)叠加层为代理添加缓存,将远程目录的一个子集存储在代理上。这在某些情况下可以显著提升性能。
代理缓存通过将经常访问的信息的子集存储在代理 SLAPD 实例的数据库中来工作。当代理收到一个请求,且该请求的信息已存储在缓存中时,它将返回缓存数据,而不是从远程服务器获取记录。
记录存储在LRU(最近最少使用)缓存中,这意味着一旦缓存被填满,访问最少 最近的记录会被移除,以腾出空间供新条目进入。此外,缓存中的条目只会在一定的时间(称为生存时间,TTL)内提供服务,之后代理将重新连接到远程目录以获取该条目的新副本。这可以防止代理提供过时或已经改变的主目录信息。
注意
pcache
不会缓存绑定操作。每个客户端连接仍然需要执行绑定操作,绑定操作的行为取决于ldap
后端的配置。它可以使用 ID 断言,或者将认证请求传递到远程主机。
pcache
覆盖层在代理的slapd.conf
文件中进行配置。实现pcache
覆盖层的前几个步骤很熟悉。在配置文件的顶部附近,我们需要添加moduleload
pcache
行来加载正确的模块。
在数据库部分,我们需要使用常见的overlay
指令添加pcache
覆盖层。然后,有几个指令是必需的,用于配置pcache
覆盖层。以下是包含代理缓存覆盖层的ldap
数据库完整配置部分:
database ldap
uri "ldap://10.21.77.100"
suffix "dc=example,dc=com"
rootdn "cn=Manager,dc=example,dc=com"
idassert-bind
bindmethod=simple
binddn="uid=authenticate,ou=system,dc=example,dc=com"
credentials="secret"
mode=none
idassert-authzFrom "dn.subtree:dc=example,dc=com"
overlay pcache
proxycache bdb 1000 1 50 1200
directory /var/lib/ldap/cache
index objectclass eq
index uid,mail eq,sub
index queryid eq
proxycachequeries 100
proxyattrset 0 uid mail cn sn givenName
proxytemplate (uid=) 0 600
文件的开始部分与我们在上一节中使用的身份断言配置相差不大。然而,有一个不同之处是,添加了rootdn
指令,这是数据库支持的pcache
覆盖层所要求的。它从未用于认证目的,因此使用目录的基本 DN 是可以的。
一旦通过overlay
pcache
将覆盖层添加到覆盖层堆栈中,第一个代理缓存指令就会出现:
proxycache bdb 1000 1 50 1200
该指令处理代理缓存引擎的核心配置。它有五个不同的参数:
-
数据库类型:
pcache
需要一个存储缓存数据的地方,可以使用底层数据库机制中的一个,比如bdb
、hdb
或ldif
。如果你需要高效的存储系统,bdb
或hdb
是最佳选择。稍后在配置中,我们还需要为数据库设置一些指令。 -
缓存中的最大条目数:你可以设置一个上限来控制缓存中的条目数量。你可以根据该数据库中的记录数量以及代理的使用类型来估算需要缓存的条目数。
-
存储的属性集数量:代理缓存存储来自远程目录的一部分信息。哪些属性被缓存通过定义属性集来控制。此参数应设置为已定义的属性集的数量。我们将首先定义一个,所以上面的值为
1
。 -
每次搜索结果的最大条目数。有些搜索可能返回大量条目,这会占用代理服务器上的大量空间(如果这个特定的大搜索不经常执行,这也会引入低效)。为了避免这种问题,参数指定了搜索结果在缓存时可以有的最大条目数。如果搜索返回的条目数超过最大值(此例中为
50
),则该搜索不会被缓存。 -
一致性检查间隔。该参数指定检查记录过期 TTL 的时间间隔(秒)。如果记录的 TTL 已经过期,那么该记录将被视为过期并从缓存中移除。
proxycache
指令中的第一个字段是数据库类型,用于指定将用于存储缓存数据的数据库后端。现在我们需要添加一些指令来配置该数据库后端:
directory /var/lib/ldap/cache
index objectclass eq
index uid,mail eq,sub
index queryid eq
directory
指令(这是我们在第三章配置 HDB 后端时使用过的)指向将存储 BDB 文件的目录。
如果你将 directory
设置为一个尚不存在的位置,请确保在文件系统上创建该目录:mkdir
/var/lib/ldap/cache
。你还应该将 DB_CONFIG
文件的副本放入 cache/
目录,否则将使用默认的 Berkeley DB 设置,而这些通常会导致性能较差。
在数据库指令之后,有几个索引指令,指定应创建哪些索引以及每个索引支持的搜索类型。如同往常一样,这些索引文件可以加速性能。
应该包含两个索引:一个是 objectclass
的等式索引,另一个是 queryid
的等式索引。queryid
索引特定于 pcache
后端,后者使用 queryid
来识别缓存数据库中的查询。应该指定其他索引,以提高代理缓存模板中定义的查询的查找速度(稍后我们将查看这些模板)。
你还可以使用为 BDB 后端定义的其他指令(如 cachesize
)。有关详细信息,请参阅第五章的讨论以及 slapd-bdb
的手册页。
现在我们有一些更多与 pcache
相关的指令需要检查:
proxycachequeries 100
proxyattrset 0 uid mail cn sn givenName
proxytemplate (uid=) 0 600
proxycachequeries
指令指定应缓存多少查询(而非条目)。
proxyattrset
指令指定了应该缓存哪些属性。代理缓存存储了远程目录的一个子集。这个子集不仅是总条目的子集,还包括每个条目的属性子集。这里的示例中,proxyattrset
指定了只缓存 uid
、mail
、cn
、sn
和 givenName
属性(及其值)。对其他属性的请求将被代理到远程服务器。
proxyattrset
指令包含两个部分:
-
第一个是一个整数标识符,
0
代表第一个proxyattrset
,1
代表第二个,以此类推。 -
第二部分是要存储在缓存中的属性列表(以空格分隔)
可以有多个 proxyattrset
,但 proxycache
指令中必须显式指定 proxyattrset
指令的总数。在我们的配置中,只有一个 proxyattrset
指令,因此在 proxycache
指令中的第三个参数(属性集的数量)设置为 1
。
最后的指令是 proxytemplate
指令。过滤器模板指定将存储在缓存中的搜索类型,并指示哪些属性将存储在与搜索过滤器匹配的记录中。该指令有三个参数:
-
一个过滤器模板
-
要使用的
proxyattrset
指令 -
与此模板匹配的条目的 TTL
过滤器模板是常规 LDAP 过滤器的变体。常规过滤器可能如下所示:(uid=m*)
,或 (&(ou=users)(objectclass=person))
。过滤器模板是没有声明值的过滤器;也就是说,它是一个没有等号右侧值的模板。(uid=)
和 (&(ou=)(objectclass=))
是这两种搜索过滤器的过滤器模板。
如果传入搜索的过滤器与过滤器模板匹配(并且返回的结果不超过最大结果数),那么它将由缓存处理。例如,过滤器 (uid=*)
、(uid=mat*)
和 (uid=dave)
都与过滤器模板 (uid=)
匹配。它们可以由缓存处理,但 (&(uid=*)(ou=system))
不能,因为它不匹配任何定义的过滤器模板。
第二个参数是应该使用的 proxyattrset
指令的数字标识符。在我们的示例中,我们将其设置为 0
,这使用 proxyattrset
0
。因此,此过滤器模板缓存了 uid
、mail
、cn
、sn
和 givenName
属性的值。
proxyattrset
指令用于确定是从缓存中提供传入的搜索请求,还是通过连接到远程目录来处理请求。如果请求与搜索过滤器模板匹配,并且客户端提供的属性列表仅包含 proxyattrset
中的属性,那么结果可能会从代理缓存中提供。例如,如果请求使用搜索过滤器 (uid=m*)
(它与 (uid=)
模板匹配),并请求 uid
、mail
和 sn
属性,这些结果可以从缓存中提供。另一方面,如果属性列表是 uid
、mail
和 telephoneNumber
,那么缓存将被跳过,代理将从远程服务器获取信息。为什么会这样?原因很简单,因为其中一个属性 telephoneNumber
根本没有存储在缓存中,因此 pcache
覆盖层无法满足整个请求。
proxytemplate
指令的第三个参数是 TTL。它指定条目可以在缓存中存在多少秒,之后它被认为是过时的,并将被移除或刷新。
还有一个特殊的第四个参数也可以使用:所谓的负 TTL。默认情况下,代理缓存仅缓存成功的请求。也就是说,如果发出搜索请求,并且远程目录返回零条记录,则不会缓存任何信息。
然而,有时缓存一个“未命中”可能会很有用,这样如果相同的查询再次到来,就可以立即从缓存中提供,而不需要再次访问远程目录——这个过程可能会导致相同的空结果集。负 TTL 参数允许你启用未命中的缓存,并设置负结果(未命中的记录)在缓存中应保留的秒数。
关于属性集和模板的说明
代理缓存覆盖中可能令人困惑的一点是属性集和过滤器模板之间的关系(以及proxycache
指令的属性集计数)。
每个属性集应该至少由一个过滤器模板引用。但多个过滤器模板可以使用相同的属性集。例如,以下是合法的:
proxycachequeries 100
proxyattrset 0 uid mail cn sn givenName
proxytemplate (&(mail=)(objectclass=)) 0 600
proxytemplate (uid=) 0 600
在这种情况下,两个过滤器模板都引用了相同的属性集(ID 号为0
的那个)。
同一个模板可以与不同的属性集一起使用。在这种情况下,发生的情况如下所示。考虑以下内容:
overlay pcache
proxycache bdb 1000 2 50 1200
# ... skipped a few lines...
proxyattrset 0 uid mail cn sn givenName
proxyattrset 1 uid description
proxytemplate (uid=) 0 600
proxytemplate (uid=) 1 600
上述是合法的,并且可以正常工作,但具有有趣的结果。
注意
注意,proxycache
的第三个参数现在是2
而不是1
。这反映了现在定义了两个proxyattrset
指令的事实。
如果执行(uid=m*)
的搜索,要求返回uid
和mail
,则会为第一个属性集生成一个缓存条目。
但是,如果执行(uid=m*)
的搜索,要求返回uid
和description
,则会为第二个属性集生成一个条目。
如果执行(uid=m*)
的搜索,要求返回mail
和description
,它将错过两个缓存,并且结果将从远程服务器检索。
代理缓存覆盖可以将ldap
后端转变为不仅仅是一个简单的代理。通过调整属性集和模板以匹配常用的查询,你可以使用pcache
来提高代理的响应速度,并减少对远程目录的流量。
半透明代理
考虑以下情况。一个远程目录包含你所需的基本信息。你想要创建一个到该目录的 LDAP 代理,但有一些值你希望在代理上修改(而不是在远程目录上修改)。
这可以通过translucent
覆盖来实现,它代理对远程目录的请求,但同时允许在不修改远程目录信息树的情况下,本地修改和存储属性。此类混合代理被称为半透明代理。
我们将简要介绍如何配置半透明代理。
如常规操作一样,在代理的slapd.conf
文件顶部附近,我们需要加载透明模块。我们还需要 LDAP 和 BDB 模块,因为将使用这两个后端:
moduleload back_ldap
moduleload back_bdb
moduleload translucent
现在我们可以跳到配置文件中的数据库部分。
对于 translucent 代理,我们需要将其配置为在本地存储一些信息,但同时像代理一样工作并从远程目录服务器检索信息。以下是transparent
叠加层的示例配置:
database bdb
directory /var/lib/ldap/transparent
suffix "dc=example,dc=com"
rootdn "uid=authenticate,ou=system,dc=example,dc=com"
rootpw secret
index objectclass eq
index uid eq,sub
lastmod off
overlay translucent
uri "ldap://10.21.77.100"
idassert-bind
bindmethod=simple
binddn="uid=authenticate,ou=system,dc=example,dc=com"
credentials="secret"
mode=none
idassert-authzFrom "dn.subtree:dc=example,dc=com"
transparent
叠加层使用数据库(在此例中是bdb
后端)将信息本地存储,然后隐式使用ldap
后端连接到远程目录。与pcache
叠加层一样,最好使用 BDB 或 HDB 作为后端数据存储机制。
对于bdb
后端配置,我们需要常规指令:directory
、suffix
、rootdn
、rootpw
,以及一个或多个index
指令(我们至少应该在objectclass
上有一个相等索引)。
我们还关闭了修改时间戳(lastmod
off
),以防止 SLAPD 自动生成相应的modifiersName
和modifyTimestamp
操作属性。如果你希望将这些信息存储在代理的数据库中,可以删除此行,但当客户端从代理请求记录时,它将看到不同的修改信息,而不是连接到远程目录时看到的信息。
rootdn
和rootpw
密码在 translucent 代理中扮演着特殊角色。这个 DN 是唯一可以向代理数据库添加新记录的用户。来自该用户的任何 LDAP 修改、添加或 modRDN 操作只会改变本地数据副本。
注意
根 DN 只能访问它被允许访问的远程服务器上的值,但可以在本地的 translucent 数据库中添加或修改任何记录。这意味着,实际上,它可能能够将条目写入它无法访问的目录树分支(由于远程目录上的 ACL 限制)。
现在我们已经配置好了后端数据库。接下来,我们要配置translucent
叠加层。
在overlay
指令之后,插入translucent
到叠加层堆栈中,我们需要为translucent
叠加层提供关于远程目录的信息。
由于translucent
叠加层使用ldap
后端,因此可以在此处使用任何ldap
后端参数:
overlay translucent
uri "ldap://10.21.77.100"
idassert-bind
bindmethod=simple
binddn="uid=authenticate,ou=system,dc=example,dc=com"
credentials="secret"
mode=none
idassert-authzFrom "dn.subtree:dc=example,dc=com"
uri
指令用于将 translucent 代理指向远程服务器。我们再次使用本章前面讨论的身份验证来处理对远程服务器信息的授权。
现在让我们来看一些 translucent 代理实际应用的例子。首先,我们可以从远程服务器获取一个代理的记录:
$ ldapsearch -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
-H ldap://proxy.example.com -b 'dc=example,dc=com'
-LLL '(uid=manny)'
Enter LDAP Password:
dn: uid=manny,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
uid: manny
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
在这个例子中,我们使用ldapsearch
连接到代理(ldap://proxy.example.com
),并检索具有uid=manny
的记录。
该操作会导致代理从远程服务器检索记录。然后,它将该记录与其自身的修改数据库中的信息进行比较,并且如果该记录存在任何本地修改,修改将插入到结果记录中。
假设我们想要在 Manny 的记录中添加一个description
字段,但我们只希望该字段存在于代理中,而不是在远程目录中。我们可以通过使用ldapmodify
来实现,并以代理的 root DN(uid=authenticate,ou=system,dc=example,dc=com
)进行身份验证:
$ ldapmodify -x -W \
-D 'uid=authenticate,ou=system,dc=example,dc=com'\
-H ldap://proxy.example.com
Enter LDAP Password:
dn: uid=manny,ou=users,dc=example,dc=com
changetype: modify
add: description
description: This was added only to the proxy.
modifying entry "uid=manny,ou=users,dc=example,dc=com"
此修改仅添加了描述属性,并附上消息:此项仅添加到代理中。
注意
请注意,在此示例中,我们以列为透明数据库 rootdn 的 DN 进行绑定。这是因为这是唯一可以写入透明(本地)数据库的 DN。
现在修改应该仅写入透明数据库。因此,我们应该能够在代理上重复进行搜索,并查看新的描述字段:
$ ldapsearch -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
-H ldap://proxy.example.com -b 'dc=example,dc=com' -LLL \
'(uid=manny)'
Enter LDAP Password:
dn: uid=manny,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
uid: manny
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
description: This was added only to the proxy.
当代理收到此搜索操作时,它会从远程目录请求整个uid=manny
的记录。该记录大致如下(加上操作属性,这些属性未显示):
dn: uid=manny,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
uid: manny
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
透明代理随后将该记录与其自己的记录进行比较,结果如下:
dn: uid=manny,ou=users,dc=example,dc=com
description: This was added only to the proxy.
然后,这两条记录会被合并,透明数据库的更改会优先于远程目录的更改。结果是将description
属性附加到返回记录的末尾。
注意
透明数据库可以使用slapcat
工具进行转储,备份可以使用slapadd
工具加载。
但我们如何知道此修改没有写入远程目录呢?我们可以在该目录上运行搜索,查看未更改的记录:
$ ldapsearch -x -W -D 'uid=matt,ou=users,dc=example,dc=com' \
-H ldap://directory.example.com -b 'dc=example,dc=com' -LLL \
'(uid=manny)'
Enter LDAP Password:
dn: uid=manny,ou=Users,dc=example,dc=com
sn: Kant
uid: immanuel
uid: manny
ou: Users
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
givenName: Manny
cn: Manny Kant
透明代理可以用于提供本地修改条目的功能,而这些条目原本由外部控制。与其他形式的代理一样,没有特定于 OpenLDAP 的远程目录,透明代理可以使用任何符合标准的 LDAP v3 目录作为远程目录。
总结
本章我们已经研究了多种配置 LDAP 服务器以便协同工作的策略。我们首先探讨了如何使用 SyncRepl 将目录信息树从主目录同步和复制到一个或多个影像(从属)目录服务器。
在查看了复制功能后,我们转向了代理,研究了三种不同的代理配置:简单代理、缓存代理和透明代理。
本章结束了我们对 OpenLDAP 服务器套件的详细分析。接下来,我们将转向 LDAP 集成和扩展应用程序以利用目录数据的任务。我们将要研究的大多数应用程序使用 OpenLDAP 库来实现其 LDAP 功能。
第八章:LDAP 与 Web
到目前为止,本书的重点是 LDAP 服务本身。在这一章中,我们将探讨如何将 LDAP 与其他服务集成。本章的重点将是将 OpenLDAP 与支持 LDAP 的 Web 服务集成。目标不仅是提供一些具体的 Web 服务示例,还要给出关于支持 LDAP 的应用程序常见特征的一般了解。我们主要将使用 Apache Web 服务器和 phpLDAPadmin 工具。本章将涉及以下主题:
-
支持 LDAP 的应用程序基础知识
-
使用 OpenLDAP 进行 Apache 认证
-
Apache LDAP 模块的其他功能
-
安装和配置 phpLDAPadmin
-
通过 Web 界面管理目录服务器
我们将以一些关于集成 OpenLDAP 和支持 LDAP 的应用程序的一般指导结束。
支持 LDAP 的应用程序
什么是支持 LDAP 的应用程序?支持 LDAP 的应用程序是指能够通过 LDAP 协议与目录服务器进行通信并执行 LDAP 操作,从而利用目录信息的应用程序。
虽然目录服务最常见的用途是身份验证,但 LDAP 的用途绝不仅限于此。一些 DNS 服务器使用目录服务器存储区域信息。Sendmail 和 Postfix 可以使用 LDAP 存储邮件路由信息。Mozilla Thunderbird、Microsoft Outlook 和许多其他邮件客户端将 LDAP 服务器视为地址簿。所有这些应用程序都被视为支持 LDAP 的应用程序。
注意
尽管有许多支持 LDAP 的应用程序,但并不是所有应用程序都支持 LDAP v3 协议,尽管 LDAP v3 已经发布了十年(请参见 RFC 2251)。许多支持 LDAP 的应用程序仍然使用 LDAP 协议的版本 2,版本 2 缺少一些重要功能,如 StartTLS 支持和 SASL 绑定。
支持 LDAP 的应用程序的共同特点是能够连接并绑定到目录服务器。这也是最常需要配置的功能。因此,大多数支持 LDAP 的应用程序至少需要以下信息:
-
一个将用于绑定到目录的 DN。
-
用于绑定时的密码。
-
关于 LDAP 服务器位置的信息。这可以是
ldap
URL(ldap://directory.example.com:389
)或主机和端口对(host=directory.example.com
,port=389
)。
一些应用程序可能需要额外的信息,如搜索过滤器或要请求的属性列表。
注意
如果 DN 是匿名用户的 DN(即空字符串),则密码不需要设置。
当然,要求用户记住完整的 DN,而他们通常只习惯于记住登录 ID,这可能并不是一个成功的策略。为此,许多支持 LDAP 的应用程序将使用传统的两阶段认证,包括执行两次简单的绑定操作。
这样的应用程序会提示用户输入登录 ID(通常映射到 OpenLDAP 中的 uid
属性)和密码。然后,应用程序会作为初始 DN 进行绑定(通常该 DN 为匿名),接着执行对指定登录属性的搜索,以获取完整的 DN。然后,应用程序将使用新找到的 DN 和用户提供的密码重新绑定。
注意
在第五章中,我们介绍了不同的绑定 OpenLDAP 的方法。
在这种情况下,应用程序本身并不进行密码验证。它将密码发送给目录服务器,目录服务器进行适当的身份验证。
在较少见的情况下,应用程序可能会尝试使用 SASL 绑定,而不是简单绑定。这样,应用程序将不需要完整的 DN。相反,它只需要用户的 SASL 特定信息(例如 DIGEST-MD5 的登录 ID 和密码,或 SASL EXTERNAL 机制的 X.509 证书)。
仅使用 LDAP 进行身份验证的应用程序通常只需要执行绑定操作(或多个操作)。一旦应用程序确认用户可以成功绑定,它就从 LDAP 服务器获取了所需的所有信息。
其他应用程序(例如通讯录或 DNS 服务器)可能会继续与 LDAP 服务器进行交互,执行搜索,甚至更改目录信息树。
在本章中,我们将首先查看 Apache web 服务器如何将 OpenLDAP 用作身份验证源。然后,我们将讨论那些与目录服务器进行更多互动的服务。
Apache 和 LDAP
Apache web 服务器 (httpd.apache.org
) 是互联网中最常用的 web 服务器。它可以运行在大多数主要操作系统上,以其稳定性和丰富的功能集而闻名。几乎每个 Linux 发行版都包括 Apache 作为支持的软件包。
在撰写本文时,Ubuntu 发行版中提供的是 Apache 2.2 版本。但 Apache 2.0 仍然广泛使用。由于这两个版本之间的 LDAP 配置略有不同,我将重点介绍 Apache 2.2,但也会包括配置旧版 Apache 2.0 的技巧。
安装 Apache 简明指南
Apache 提供了出色的手册,并且 Ubuntu 提供的基础配置(其他大多数发行版也是如此)几乎可以直接使用,只需很少的配置。因此,在这一部分,我将提供一个非常基本的 Apache 入门指南。
若要了解更多信息,您可能希望查阅 Apache 网站 (httpd.apache.org
)、Ubuntu Apache 配置文档 (help.ubuntu.com/7.04/server/C/httpd.html
),或许多关于配置 Apache 的指南,既有在线的,也有印刷版的。
要在 Ubuntu 上安装 Apache,您只需要运行一个命令:
$ sudo apt-get install apache2
安装 Apache 可能需要安装几个其他依赖项,但apt-get
将解析这些依赖项,并仅提示我们允许安装它们。
注意
如果您从源代码构建 OpenLDAP,则可能需要安装另一个(可能是较旧的)版本的 LDAP 库以满足软件包依赖关系。这样做不会影响当前的 LDAP 应用程序。
在之前的 Apache 版本 1.3 中,需要安装额外的模块(mod_ldap
)才能获得 LDAP 支持,但从 Apache 2.0 开始,LDAP 支持已经包含在核心 Apache 发行版中。稍后,我们将安装 PHP 模块以获取对 PHP 语言的 Web 服务器支持,但目前不需要额外的软件包。
在 Ubuntu 中,Apache 配置文件位于/etc/apache2
目录。目录布局如下:
$ ls -1
apache2.conf
conf.d/
envvars
httpd.conf
magic
mods-available/
mods-enabled/
ports.conf
README
sites-available/
sites-enabled/
ssl/
magic
ports.conf
README
ssl/
对于我们来说,重要的是已经突出显示。
apache2.conf
文件包含 Apache 的基本设置。Apache 可以执行虚拟主机,其中一个服务器实例可以托管多个不同的网站(位于不同的 IP 地址或主机名上)。apache2.conf
文件包含适用于核心服务器和所有托管站点的配置信息。
类似于 OpenLDAP,Apache 的代码是模块化的。除了服务器的基本功能外,功能可以作为单独的模块实现,并在启动时加载到服务器中。安装模块时,模块的配置文件将放置在mods-available/
目录中。要启用模块,只需在mods-enabled/
目录中创建指向mods-available/
目录中模块配置文件的符号链接,当 Apache 重新启动时,它将加载所需的模块。为了进一步简化此过程,还有两个工具,a2enmod
和a2dismod
,用于分别启用和禁用 Apache 模块。
注意
此方法适用于 Ubuntu、Debian 和其他几个 Apache 发行版,但并非通用。请参阅您系统的文档,了解如何在服务器上启用或禁用模块的具体说明。通常只需在其中一个 Apache 配置文件中添加一两行即可。
最后,虚拟主机(或站点特定)的配置文件位于sites-available/
。这些配置文件包含特定于特定虚拟主机的参数,但不适用于整个服务器。例如,假设我们想在 Apache 实例上托管两个网站:www.example.com
和www.anothersite.com
。这两个站点将在sites-available/
目录中分别有一个单独的配置文件(通常分别称为www.example.com
和www.anothersite.com
)。
但是,仅仅将网站放在sites-available/
文件夹中并不足以启用该站点。与模块一样,Apache 会检查sites-enabled
目录,看看哪些站点应在启动时激活。启用站点只需要从sites-available/
中的目标配置文件添加一个符号链接到sites-enabled/
目录。同样,Apache 的工具a2ensite
和a2dissite
可以用来管理这些链接。
Ubuntu 开箱即配了一个默认的网站。配置文件位于sites-available/default
,并且已经链接到sites-enabled/
。我们不需要更改这个配置文件就能运行一个基本的 Web 服务器。我们所需要做的就是启动 Apache:
$ sudo /etc/init.d/apache2 start
现在,你应该能够通过将 Web 浏览器指向服务器的 IP 地址来浏览默认网站,例如http://192.168.0.211
。
配置 LDAP 认证
该网站提供的 HTML 文件位于/var/www/
。让我们在这个文件夹中创建一个新目录,然后为它添加密码保护:
$ sudo mkdir /var/www/private
在这个新目录中,我们创建一个新的 XHTML 页面,命名为index.html
:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html >
<head>
<title>Insiders Only</title>
</head>
<body>
<p>This page is private, and only authenticated users should
be able to access it.</p>
</body>
</html>
这只是一个简单、无花哨的网页,设置标题为Insiders Only
,并显示消息:This
page
is
private
,and
only
authenticated
users
should
be
able
to
access
it
。
提示
授予权限
Apache 以用户 www-data 的身份运行。为了将页面提供给客户端,Apache 需要能够读取该目录和页面。你可能需要使用 chmod 设置正确的文件系统权限。目录需要为 www-data 设置读取和执行权限,HTML 文件则需要读取权限。
此时,您应该能够通过在我们访问的 URL 后面加上目录名称来访问该页面。在我们的示例中,网站的 URL 是http://192.168.0.211
。要访问private/
目录的索引页面,我们应该能够使用http://192.168.0.211/private
的 URL。
当然,由于我们尚未为此目录配置认证,因此在未登录的情况下我们仍然能够查看该页面。
现在我们已经有了新的文件夹和 HTML 页面,我们可以开始为它提供安全保护,防止被窥探。为此,我们将配置 Apache 加载 LDAP 模块,并在sites-available/default
文件中添加几行代码,启用该文件夹及其内容的 LDAP 认证。
加载模块
Apache 的 LDAP 功能都是作为 Apache 模块实现的。默认情况下,它们并没有被启用,尽管已经安装。也就是说,代码已经存在于服务器上,默认的配置文件位于/etc/apache2/mods-available
,但在/etc/apache2/mods-enabled
中并没有指向这些文件的符号链接。
在 Apache 2.0 和 Apache 2.2 之间,这些模块的名称发生了变化,更好地反映了它们的用途。
要在 Apache 2.2 中启用正确的模块,请运行a2enmod
命令:
$ sudo a2enmod authnz_ldap
这将会在 mods-enabled
中添加一个指向 mods-available/auth_ldap.load
的链接。
在较旧的 Apache 2.0 中,我们需要运行类似的命令:
$ sudo a2enmod auth_ldap
提示
为什么会有差异?
Apache 2.2 引入的一个重要改进是“身份验证、授权和访问控制”功能的重新设计。重新设计的结果是将身份验证(AuthN)和授权(AuthZ)更加清晰地分离。这一分离体现在模块名称上。
接下来,我们需要重启服务器,以便它加载并配置该模块:
$ sudo /etc/init.d/apache2 restart
一旦完成这些操作,我们就可以继续编辑站点的配置文件,并为新建的 /var/www/private
目录添加一些保护措施。
编辑默认配置文件
default
配置文件大约有 45 行。它包含了运行一个基本 Web 服务器所需的所有配置指令。Ubuntu 文档解释了此文件中的指令。
我们希望在配置文件中创建一个如下结构的部分:
<Directory "/path/on/file/system">
Parameter Value
Parameter2 Value
#...
</Directory>
<Directory>
部分表示该标签内的配置指令专门应用于指定的目录(在给定示例中的 /path/on/file/system
)及其内容。
<Directory>
标签内包含的路径是文件系统路径,而不是 URL 的相对路径组件。也就是说,我们的 private/
目录位于文件系统的 /var/www/private/
位置,但它的 URL 是 http://192.168.0.211/private
(相对 URL,即服务器部分后的 URL,是 /private/
)。在 <Directory>
标签中,我们将使用 /var/www/private/
。
由于 <Directory>
和 </Directory>
标签之间的参数仅适用于该目录的内容,因此目录部分可以在目录级别上微调权限、功能和服务。我们将创建自己的 <Directory>
部分,以便为 private/
目录添加 LDAP 身份验证。
为了设置这一功能,我们需要结合使用 Apache 的 mod_auth
和 mod_access
模块的参数,它们提供基本的身份验证和授权服务并默认加载,以及我们在上一节中刚加载的 ldap_auth
模块。
再次提醒,Apache 2.0 配置和 Apache 2.2 配置之间有所不同。我们将首先详细查看 Apache 2.2 配置,并简要提供 Apache 2.0 配置的示例。
目录部分——Apache 2.2
现在,我们准备创建一个新的 <Directory>
部分,该部分将应用于 /var/www/private
目录。我们将在 default
配置文件中的 </VirtualHost>
行上方添加以下内容:
<Directory "/var/www/private">
AuthType Basic
AuthName LDAP
AuthBasicProvider ldap
Require valid-user
AuthzLDAPAuthoritative off
AuthLDAPBindDN "uid=authenticate,ou=system,dc=example,dc=com"
AuthLDAPBindPassword "secret"
AuthLDAPURL ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
</Directory>
<Directory>
部分应用于我们新创建的 private/
目录,本节中指定的指令将强制网页用户在尝试访问 private/
目录或其中的任何内容时进行身份验证。
前两个参数是 Apache 内置的 mod_auth_basic
模块的一部分。
<Directory>
部分中的第一个参数是 AuthType
。该参数控制密码信息从客户端传输到服务器的方式,可能的值有两个:Basic
和 Digest
。如果选择 Basic
,则密码将以明文发送到服务器。不幸的是,许多 HTTP 客户端只支持 Basic
。Digest
更加安全(设置它将指示客户端在发送前对密码进行哈希处理),但支持的客户端较少。由于该模块使用 LDAP 简单绑定,因此密码必须以未加密的形式发送,这意味着当前仅支持 Basic
。
提示
加密 HTTP 流量
确保认证过程安全的最佳方式是配置 Apache 使用 SSL/TLS 来与客户端通信。Ubuntu Apache 文档和官方 Apache 项目文档都有相关介绍。
AuthName
字段的值会发送到浏览器,作为指示认证目的的方式。例如,当一个网页浏览器试图访问 private/
目录中的文件时,用户将会看到一个提示要求输入认证信息的对话框,样式大致如下:
AuthName
显示在对话框的第一行:请输入 "LDAP" 的用户名和密码,网址:http://localhost。通常,AuthName
的值应该是一个提示,告知用户他或她正在登录的内容。
在 <Directory>
部分的下一行,AuthBasicProvider
指定用于基本认证的服务。除了 LDAP,Apache 还支持平面文件、哈希式数据库、关系型数据库以及其他来源。
我们希望使用 LDAP 认证。在 Apache 2.2 中,LDAP 认证(AuthN)和授权(AuthZ)服务由 mod_authnz_ldap
模块提供。要使用 mod_authnz_ldap
认证源,AuthBasicProvider
参数应设置为 ldap
。这意味着当客户端尝试对 Web 服务器进行认证时,将使用 LDAP 源来处理认证令牌。换句话说,用户名和密码将与目录中的信息进行比对。
一旦认证成功,下一阶段是 授权。在这一阶段,Web 服务器决定认证过的用户是否有权限访问请求的资源。接下来两个参数适用于授权过程。
Require
指令指定用户获得访问请求资源的条件。稍后,我们将探讨如何要求用户具备特定属性或成为目录信息树中特定组的成员。但在我们的示例中,valid-user
要求仅需要用户存在于指定的来源(此例为目录中)并且用户能够成功认证。
AuthzLDAPAuthoritative
表示是否应仅使用 LDAP 作为授权信息的来源。默认情况下,这是启用的,这将导致 Apache 使用一个 ldap-*
Require
值。但在之前的示例中,我们要做的只是确保用户是有效用户——也就是说,用户已成功通过身份验证。仅此就足够作为我们的授权。在这种情况下,验证检查是在 mod_authnz_ldap
模块外部提供的,因此我们需要关闭 AuthzLDAPAuthoritative
标志:
AuthzLDAPAuthoritative off
为了使用 require
参数的 valid-user
值,我们需要关闭 AuthzLDAPAuthoritative
,以便可以使用另一个模块(mod_auth_basic
)来处理授权。在这种情况下,LDAP 只会执行身份验证步骤。
接下来的三个指令是 LDAP 特定的:
AuthLDAPBindDN "uid=authenticate,ou=system,dc=example,dc=com"
AuthLDAPBindPassword "secret"
AuthLDAPURL ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
AuthLDAPBindDN
和 AuthLDAPBindPassword
指定了 Apache 应使用的 DN 和密码,以执行对 LDAP 服务器的简单绑定。当新的身份验证请求到来时,Apache 将使用此 DN 和密码绑定到 SLAPD,然后搜索目录信息树以获取尝试进行身份验证的用户的 DN。换句话说,绑定的 DN 和密码用于本章第一部分讨论的两阶段身份验证的第一阶段。
注意
如果省略了 AuthLDAPBindDN
和 AuthLDAPBindPassword
,Apache 将作为匿名用户进行绑定。
对于此应用程序,将使用 uid=Authenticate
系统帐户来访问目录。这提供了一定程度的安全性(因为我们不必允许匿名绑定和搜索),同时也可以提供更好的审计跟踪,记录谁访问了目录中的哪些内容。
注意
您的 SLAPD ACL 需要配置成允许此 DN 从 Apache 服务器进行绑定,否则身份验证的第一阶段将失败。
第三个 mod_authnz_ldap
指令是 AuthLDAPURL
。该参数的值是一个 LDAP URL,包含基本 DN、搜索类型、搜索模式以及要返回的属性。
在前面的示例中,我们使用了这个 LDAP URL:ldap://localhost/ou=Users,dc=example,dc=com?uid??(objectclass=inetOrgPerson)
。Apache 使用此 URL 提取它需要的所有信息,以便搜索用户的 DN。
当用户登录时,正如几页之前登录对话框所示,Apache 将获取用户名和密码。用户名应映射到该用户 LDAP 记录中的 uid
属性,密码应与 userPassword
属性的值匹配(当然是在 SLAPD 对其进行哈希处理之后)。
一旦接收到这些信息,Apache 将使用 AuthLDAPBindDN
中的 DN 进行绑定,并根据上述 LDAP URL 执行搜索,目标是获取尝试登录的用户的 DN。
注意
请注意,所有 LDAP 通信都由 Apache 执行,而不是浏览器。网页浏览器从未直接连接到 LDAP 服务器。这意味着目录可以被防火墙保护。只要 Apache 可以访问它,LDAP 认证就可以使用。
虽然 LDAP URL 在附录 B 中有更详细的介绍,我们将简要查看刚才看到的 URL,以理解其功能。协议部分表明 Apache 要建立一个未加密的 LDAP 连接:
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
可以通过使用 ldaps://
代替 ldap://
来建立 SSL LDAP 连接。(您可能还需要 LDAPTrustedGlobalCert
参数来指示 LDAP 证书的证书授权文件的位置。)
提示
使用 StartTLS 代替 LDAPS
StartTLS(而不是 LDAPS)是建立 SSL/TLS 连接到目录的首选方式。要在 Apache 2.2 中使用 StartTLS,请将指令LDAPTrustedMode TLS
添加到<Directory>
部分。再次提醒,您可能需要 LDAPTrustedGlobalCert
参数或其他 SSL/TLS 参数。
URL 的协议部分之后是主机:
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
在这种情况下,SLAPD 与 Apache 运行在同一台服务器上,因此 localhost
(或 127.0.0.1
)将导致 Apache 使用回环接口连接到 SLAPD。
下一部分是基础 DN,即 SLAPD 开始搜索用户的 DN:
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
由于我们的用户都位于 ou=Users,dc=example,dc=com
分支下,因此我们将使用它作为基础 DN。
剩余的参数都由问号(?
)分隔,而不是斜杠。在基础 DN 后是 SLAPD 将搜索的属性:
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
在这种情况下,用户发送到目录的名称应该是她或他的 UID,因此我们需要查找 uid
属性。类似地,您可以使用 cn
或任何其他属性,只要您知道它将返回不超过一个匹配项。
要使 Apache 认证正常工作,必须返回唯一的 DN 作为标识属性。其操作原理如下:如果搜索返回多个条目,Apache 无法确定哪个记录是正确的认证用户。因此,如果搜索返回多个 DN,Apache 将认为认证尝试失败,并且不允许用户访问该站点。
在 uid
后面是一个空参数,表示通过两个连续的分隔符(??)
??:
。
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
这一部分可以用来指定搜索范围。将其留空表示接受默认范围,即 sub
(子树)。子树范围指示 SLAPD 查找出现在基础 DN 或其下属的任何记录。其他选项包括 base
、one
和 children
。
最后一个字段是过滤器:
ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
这表示只有具有inetOrgPerson
对象类的记录才会被搜索。当 Apache 处理 URL 时,它将构造一个结合用户名搜索和给定过滤器的搜索过滤器。结果可能是这样的:(&(uid=matt)(objectclass=inetOrgPerson))
,其中matt
是尝试登录的用户的名称。
针对我们的目录信息树,搜索应该返回一个 DN,uid=matt, ou=users,dc=example,dc=com
。当 DN 返回到 Apache 时,它将执行第二次绑定,这次使用uid=matt,ou=users,dc=example,dc=com
和用户提交的密码。如果该绑定成功,则 Apache 将授予用户访问权限。
在<Directory>
部分中使用这些参数后,我们已经配置了 Apache,仅当 web 用户在目录信息树中存在并能够提供必要的信息成功绑定时,才能查看private/
目录中的信息。
Apache 2.0 的变化
要在 Apache 2.0 中获得基本相同的行为,我们需要对配置进行一些小的修改:
<Directory "/var/www/private">
AuthType Basic
AuthName LDAP
Require valid-user
AuthLDAPBindDN "uid=authenticate,ou=system,dc=example,dc=com"
AuthLDAPBindPassword "secret"
AuthLDAPURL ldap://localhost/ou=Users,dc=example,dc=com?uid?? \
(objectclass=inetOrgPerson)
</Directory>
此文件与 Apache 2.2 配置的不同之处在于,它缺少AuthBasicProvider
和AuthzLDAPAuthoritative
参数。
Require
参数的其他功能
在上一节中,我们使用了Require
valid-user
参数来强制执行授权要求,要求任何尝试访问该网站部分的用户必须在目录信息树中存在,并能够成功绑定。
但是,Require
参数还可以采用其他选项。我们将简要地查看每个选项。Apache 2.0 使用了不同的名称,我已将它们放在 Apache 2.2 使用的名称后面:
-
valid-user
:这要求用户在目录中存在并且可以绑定。这个选项在 2.0 和 2.2 版本中是相同的。 -
ldap-user
(ler
):这要求用户在用户列表中。例如,Require
ldap-user
matt
dave
将只允许有效的用户,且这些用户的 UID 为matt
或dave
。 -
ldap-dn
(dn
):这要求 DN 与Require
参数中的 DN 完全匹配。例如,Require
ldap-dn
uid=matt,ou=users,dc=example,dc=com
要求用户有效,并且具有 DNuid=matt,ou=users,dc=example,dc=com
。 -
ldap-group
(group
):这要求用户有效且是指定组的成员。稍后我们会更详细地讨论这个指令。 -
ldap-attribute
:这个参数在 2.0 和 2.2 中名称相同。若在Require
参数中使用此项,要使用户获得访问权限,用户必须有效,并且必须具有此参数所声明的属性。例如,Require
ldap-attribute
departmentNumber=001
只会授予有效的用户访问权限,并且该用户必须具有departmentNumber
属性,且该属性值为001
。 -
ldap-filter
(新功能,Apache 2.2新增):它接受一个 LDAP 过滤器,如果用户有效且在执行该过滤器的 LDAP 搜索时返回用户记录,则授予访问权限。
配置基于组的访问控制可能比其他Require
指令稍微复杂一些。该要求的基本用法如下:
Require ldap-group cn=Admins,ou=groups,dc=example,dc=com
注意
在 Apache 2.0 中,ldap-group
应替换为group
。
根据此指令,用户必须是cn=Admins,ou=groups,dc=example,dc=com
组的成员,才能进行身份验证。当 Web 用户尝试登录时,Apache 将作为AuthLDAPBindDN
中的用户进行绑定,执行对该用户 DN 的搜索,再次以该用户身份进行绑定,然后(仍然作为AuthLDAPBindDN
中的用户)检查该用户是否在cn=Admins
组中。
为了使该组搜索正常运行,AuthLDAPBindDN
中的用户必须具有访问组条目的权限。(我们在第四章中的 ACL 没有允许这一点。)你可能需要在 ACL 中添加如下规则:
## Allow anyone to read the groups branch. (Needed for group auth)
access to dn.subtree="ou=groups,dc=example,dc=com"
by * read
这将允许任何人(包括匿名用户)读取ou=groups
子树中的条目。
Apache 如何知道要查找哪种类型的组属性?groupOfNames
对象类使用member
属性,而groupOfUniqueNames
对象类使用uniqueMember
属性。两者都是标准的 LDAP 对象类。
Apache 会检查member
和uniqueMember
属性。但可能会出现需要将其他属性视为成员属性的情况。seeAlso
、owner
和roleOccupant
都是可以作为成员属性的标准属性,此外你还可以在自定义架构中定义其他属性。在这种情况下,可以在<Directory>
部分使用AuthLDAPGroupAttribute
参数来告诉 Apache 应该将哪个属性视为成员属性。
phpLDAPadmin
我们已经配置了 Apache,使用内置的 LDAP 模块通过目录服务器进行身份验证。现在我们将转向一个更复杂的基于 Web 的应用程序——phpLDAPadmin。phpLDAPadmin 是一个用 PHP 编写的应用程序,旨在帮助管理目录服务器。虽然它已知可以在其他目录服务器上运行,但它是针对 OpenLDAP 开发的。
前提条件
在我们安装 phpLDAPadmin 之前,需要先安装其他一些软件包。在本章的第一部分中,我们了解了 Apache。要运行 phpLDAPadmin,需要使用某些版本的 Web 服务器(我们使用的是 PHP 5),并且需要 PHP LDAP 模块。
例如,要安装 PHP 5,我们可以运行以下命令:
$ sudo apt-get install libapache2-mod-php5 php5-ldap
安装 PHP 可能需要满足其他一些依赖项,但apt-get
会为你处理这些繁重的任务。
注意
如果你是从源代码构建 OpenLDAP,可能会提示你安装另一个(可能较旧的)版本的 LDAP 库,以满足软件包依赖性。这样做不会对当前的 LDAP 应用程序造成任何影响。
一旦 PHP 安装完成,你可以重启 Apache,然后继续安装 phpLDAPadmin。
安装 phpLDAPadmin
安装 phpLDAPadmin 的最简单方法是使用 Ubuntu 仓库中的软件包。
phpLDAPadmin 被包含在 Ubuntu 的universe 仓库中。这意味着,只要你在源列表中启用了 universe 仓库(请参见/etc/apt/sources.list
),就可以通过简单的apt-get
命令安装它:
$ sudo apt-get install phpldapadmin
phpLDAPadmin 将被安装在文件系统中的/usr/share/phpldapadmin
目录下,Apache 被配置为将对hostname/phpldapadmin
的请求指向 phpLDAPadmin 应用。Apache 配置文件位于/etc/phpldapadmin/apache.conf
。
注意
你也可以从phpldapadmin.sourceforge.net
获取源代码包,轻松安装 phpLDAPadmin。一旦 Web 服务器和 PHP 安装完成,只需将源代码解压到 Web 服务器的根目录下的一个文件夹中(例如/var/www/
)。有关完整的安装说明,请参见官方 phpLDAPadmin 文档 Wiki 上的安装指南:wiki.phpldapadmin.info/tiki-index.php?page_ref_id=6
。
安装完 phpLDAPadmin 后,我们可以继续配置。
你的软件包是否损坏?
一些版本的 Ubuntu phpLDAPadmin(特别是phpldapadmin_0.9.8.3-7
)自带的配置文件丢失了。因为这个原因,在安装过程中,你可能会看到类似这样的错误:
* Forcing reload of web server (apache2)...
grep: /etc/apache2/conf.d/phpldapadmin: No such file or directory
apache2: Syntax error on line 195 of /etc/apache2/apache2.conf: Could
not open configuration file /etc/apache2/conf.d/phpldapadmin: No such
file or directory
[fail]
invoke-rc.d: initscript apache2, action "restart" failed.
问题在于文件/etc/phpldapadmin/apache.conf
(它链接到/etc/apache2/conf.d/phpldapadmin
)丢失了。
幸运的是,我们可以在/etc/phpldapadmin
目录中创建一个合适的单行apache.conf
文件。这个配置文件的目的是将 phpLDAPadmin 映射到文件系统中 phpLDAPadmin 脚本所在的绝对路径。
要创建这个映射,我们只需要在/etc/phpldapadmin/apache.conf
文件中添加以下一行:
Alias /phpldapadmin /usr/share/phpldapadmin/htdocs
保存此更改后,简单地重启 Web 服务器:
$ sudo invoke-rc.d apache2 restart
然后,Apache 应该会在没有错误的情况下重启。
配置 phpLDAPadmin
phpLDAPadmin 配置文件位于/etc/phpldapadmin/config.php
。phpLDAPadmin 使用的配置文件格式在 PHP 和 Perl 应用中较为常见,但对于习惯编辑大多数 UNIX 应用使用的典型名称/值参数文件的人来说,可能会显得有些令人生畏。
这个配置文件与标准类型的主要区别有两个:
-
默认配置选项的处理方式
-
配置参数的形式
关于第一点,phpLDAPadmin 有两个配置文件,一个用于存储所有默认设置(/usr/share/phpldapadmin/lib/config_default.php
),另一个供管理员编辑(/etc/phpldapadmin/config.php
)。管理员应该只修改第二个配置文件。config_default.php
文件不应被修改。
当 phpLDAPadmin 尝试访问某个设置时,它会首先检查是否在自定义设置文件(config.php
)中存在该设置。如果找到了,它将使用该设置。如果没有找到,则使用默认设置的值。
这种技术的优点是,升级 phpLDAPadmin 时无需对自定义配置文件进行任何更改。只需修改默认文件。缺点是有时会添加新参数,但由于管理员的配置文件保持不变,可能会被忽略。
第二个区别,配置参数的形式,部分基于第一个。phpLDAPadmin 并没有使用简单的文本文件来存储参数,而是使用 PHP 变量来存储信息。从这个意义上讲,config.php
配置文件实际上是一个代码片段。
这样做有一些明显的优势:
-
配置文件中可以使用所有内建的 PHP 功能(包括动态评估的脚本)。
-
无需特殊的配置文件解析器,从而使得代码体积更小,运行速度更快。
但是这种方法肯定有一些缺点,主要的问题是文件的可读性可能会大大降低。例如,默认配置文件几乎有 400 行,并且包含了代码(尽管只有少量)与配置参数混合在一起。
另一个缺点是,应用程序的直观配置仍然需要一些 PHP 语言的知识。
在我们查看配置文件时,我不会假设你对 PHP 有工作知识,并且会解释配置文件中的一些构造。
配置参数的基本概览
phpLDAPadmin 中的配置参数一开始看起来可能让人望而却步。在这一部分,我将解释每种配置参数的格式。每个部分都会提供一个简短的示例,展示该参数的形式,之后是对其所做操作的更详细描述。
如果你不是程序员,不必气馁,如果并非所有内容都能理解。最重要的是,你要理解每个配置指令的结构。
注意
由于这不是 PHP 教程,我只会简要介绍一些设置参数时需要理解的概念。如需更多 PHP 信息,PHP 团队维护了一个非常好的在线手册,网址是 www.php.net/manual/en/
。
在 phpLDAPadmin 的 config.php
文件中,配置参数有三种形式:变量设置、函数调用或数组设置。
设置一个变量
设置变量是这三者中最简单的。简而言之,变量赋值看起来像这样:
$variable_name = 'value';
这就是变量定义的工作方式。
在 PHP 中,所有变量名都以美元符号($
)开头。等号(=
)用来给变量赋值。字符串值应当用单引号('
)或双引号("
)括起来。数字(整数或浮动小数)不需要加任何引号。每一行都应以分号(;
)结尾。以下是两个示例:
$name = 'Matt';
$favorite_number = 7;
第一个设置$name
变量的值为字符串Matt
。第二个设置$favorite_number
变量的值为整数7
。
在config.php
中只有少数几个简单的配置参数。大多数参数是以更复杂的 PHP 语句形式出现的。
调用一个函数
phpLDAPadmin 的配置文件中的配置参数的第二种形式使用了函数调用。简言之,函数调用看起来像这样:
$object->function('parameter one', 'parameter 2');
一个函数可以有零个或多个参数,参数的数量由程序员决定。
函数可以附加到对象上。粗略来说,对象是数据和函数的容器。phpLDAPadmin 是一个面向对象的程序,意味着它频繁使用对象来组织源代码的功能单元。
要调用附加到对象的函数,你需要使用箭头(访问)操作符(->
),它由短横线(-
)和大于号(>
)组成。这表示该函数是对象的成员。以下是从 phpLDAPadmin 配置文件中提取的一个示例:
$i = 0;
$ldapservers = new LDAPServers;
$ldapservers->SetValue($i,'server','name','My LDAP Server');
第一行将名为$i
的变量赋值为0
。
第二行创建了一个新的LDAPServers
对象,并将其赋值给变量$ldapservers
。现在,每当我们操作变量$ldapservers
时,实际上是在操作一个拥有LDAPServers
类中定义的所有成员函数和变量的对象。LDAPServers
类描述了 phpLDAPadmin 将连接的服务器。
你可以将类视为定义机器的各个部分,而对象则是该机器的实例。一旦我们拥有了 LDAPServers 机器的副本,就可以访问机器中存储的数据,并使用机器的功能执行特定任务。
根据该对象的类定义,它有一些成员函数,包括SetValue()
函数。该函数将数据存储在$ldapservers
对象中。所以在给定示例中的第三行设置了一些关于 LDAP 服务器的信息:
$ldapservers->SetValue($i,'server','name','My LDAP Server');
这一行使用了$ldapservers
的SetValue()
函数。SetValue()
函数需要四个不同的参数:
-
服务器的编号(此时为
$i
的值) -
一个表示此设置类型的字符串(
'server'
) -
一个表示要设置的属性名称的字符串(
'name'
) -
一个表示属性值的字符串(
'My
LDAP
Server'
)
稍后我们将讨论这些每一项的作用。不过目前重要的是理解函数的一般形式:$object->function(
param_1,
param_2);
。一个函数可以有程序员决定的任意数量的参数。
大部分情况下,配置文件中的注释会引导我们了解每个函数需要什么样的参数。你不需要查看其他任何代码来弄清楚在对象中应该放入什么。
现在我们来看看列表类型的指令。
设置数组值
在 phpLDAPadmin 中,最后一种配置参数是数组。设置数组值有两种基本形式:
$my_array[0] = 'My Value';
$my_map['Key Name'] = 'Value';
数组 是一种组织信息的集合。PHP 有两种不同类型的数组:索引数组(其中元素按编号顺序存储)和映射数组(其中元素按名称/值对存储)。
可以这样创建一个索引数组:
$my_array = array( 'a', 'b', 'c');
这创建了一个包含三项的数组:'a'
、'b'
和 'c'
。第一个 'a'
被存储在数组的第一个槽中,并可以通过索引号访问:
$my_array[0];
请注意,第一个索引号是零,而不是一。这将返回值 'a'
。第二个元素可以通过第二个项的索引号访问:
$my_array[1];
这将返回 'b'
。
在映射类型的数组中,不是使用数字作为索引,可以使用一些字符串(或其他对象)。例如,我们可以这样创建一个映射:
$my_map = array( 'First Name' => 'Matt', 'Last Name' = 'Butcher' );
这创建了一个包含两个项的数组,一个名为 First
Name
,另一个名为 Last
Name
。现在,我可以通过名称而不是索引来访问它们:
$my_array['First Name'];
这将返回 'Matt'
。
一旦通过 array()
函数创建了一个数组,你可以通过给数组槽赋值来向数组中添加元素。对于一个索引数组,这可能像下面这样:
$my_array[3] = 'd';
这将把 'd'
放在数组中的第四个位置(0、1、2、3)。
同样,向映射中添加值也类似,不同的是,你用的是键名而不是索引号:
$my_array['First Name'] = 'Dave';
这将把名字 'Dave'
添加到键名为 'First
Name'
的数组项中。
最后,数组可以互相嵌套。再一次,这里是来自 phpLDAPadmin 配置文件的一个示例:
$q=0;
$queries = array();
$queries[$q]['name'] = 'User List';
$queries[$q]['base'] = 'dc=example,dc=com';
在这个示例中,$queries
数组是一个索引数组,每个值都是一个映射数组。所以 $queries[0]['name']
和 $queries[1]['name']
代表两个不同的名字值。每个名字值被存储在索引数组中的不同位置。可以把这个数组看作是像下面这样结构化的伪代码:
Queries[0]:
'name' => 'User List'
'base' => 'dc=example, dc=com'
Queries[1]:
'name' => 'Another List'
'base' => 'dc=demo, dc=net'
现在我们有两个不同的查询(都存储在同一个索引数组中):查询 0 和 查询 1。每个查询都有自己的名字和基础。
这些是数组的基本特性——我们将用这些特性来配置 phpLDAPadmin。现在我们准备开始实际配置 phpLDAPadmin。
配置 LDAP 服务器设置
我们需要做的第一件事是配置 phpLDAPadmin 以连接到我们的 LDAP 服务器。这是通过 $ldapservers
对象来完成的。
在我的安装中,Apache 和 OpenLDAP 运行在同一台服务器上,因此我将配置 phpLDAPadmin 连接到本地实例。
为了开始这一部分的配置,我们需要在配置文件中找到 $ldapservers
对象。我们关心的行看起来像这样:
$ldapservers = new LDAPServers;
它位于我们默认配置文件的第 63 行。
这定义了 $ldapservers
对象。我们为 LDAP 服务器配置的其他指令需要位于此行下方。
首先需要做的是设置我们的 LDAP 连接信息。我们需要为我们的 LDAP 服务器指定一个名称、主机和端口信息,并指定是否希望通过 TLS 加密此连接:
$ldapservers->SetValue($i,'server','name','Example.Com');
$ldapservers->SetValue($i,'server','host','localhost');
$ldapservers->SetValue($i,'server','port','389');
$ldapservers->SetValue($i,'server','tls',false);
这将我们的服务器命名为 Example.Com
,并将其配置为连接到 localhost
上的默认 LDAP 端口 389
,没有任何 SSL/TLS 加密。
给定函数中的 $i
表示我们正在配置的 LDAP 服务器的编号。$i
被设置为 0
,表示这是我们配置的第一个 LDAP 服务器。如果需要配置第二个 LDAP 服务器,我们会将 $i
改为 1
,然后继续执行第二批相同类型的指令。
第二个参数 'server'
表示我们正在设置服务器参数。第三个参数('name'
、'host'
、'port'
和 'tls'
)表示我们正在设置的具体服务器参数,第四个参数包含要分配给该参数的值。
请注意,TLS 设置用于开启或关闭 StartTLS(见第四章)。如果您希望使用 LDAPS(基于 SSL 的 LDAP),请在主机设置中使用 LDAP URL,'ldaps://example.com'
,并将端口设置为正确的 LDAPS 端口(默认端口为 636
)。
接下来,我们需要告诉 phpLDAPadmin 登录信息存储的位置。该信息存储在 auth_type
参数中:
$ldapservers->SetValue($i,'server','auth_type','session');
当用户登录 phpLDAPadmin 时,用于绑定到 LDAP 的信息会被存储。此信息可以存储在三个地方:
-
Web 浏览器中的 cookie(
'cookie'
) -
服务器会话变量(
'session'
) -
(这些信息可以手动添加到)配置文件(
'config'
)
一般来说,我们应将信息存储在会话变量中(正如给定示例所做的那样)。如果选择基于 cookie 存储,请确保还将 $config->custom->session['blowfish']
设置为一个随机字符的字符串。该字符串用作 Blowfish 加密算法的密钥,且必须至少为 32 个字符长,越长越好。
注意
有关 Blowfish 加密算法的信息,请参见 www.schneier.com/blowfish.html
。
下一个参数设置了 phpLDAPadmin 应显示的命名上下文(基础 DN)列表:
$ldapservers->SetValue($i,
'server','base'(,array('dc=example,dc=com'));
这只设置了一个上下文 DN:dc=example,dc=com
。虽然在某些 LDAP 服务器上此设置是必要的,但 OpenLDAP 不需要它。OpenLDAP 在根 DSE 记录中发布了上下文的列表,phpLDAPadmin 可以从中获取信息。事实上,这就是 phpLDAPadmin 的默认配置,因此该设置可以省略或设置为:
$ldapservers->SetValue($i,'server','base',array());
这将创建一个空的上下文列表(array()
),并使 phpLDAPadmin 在根 DSE 中查找支持的上下文。
只剩下两个参数需要查看:
$ldapservers->SetValue($i,'login','anon_bind',false);
$ldapservers->SetValue($i,'appearance','password_hash','ssha');
我们来看看这两个设置:
-
第一个设置禁用匿名绑定。这将防止用户在未登录的情况下访问 phpLDAPadmin。即使允许这种情况,SLAPD 中的 ACL 仍然会阻止此类用户修改目录信息树。
-
第二个设置指定了要使用的默认密码哈希。phpLDAPadmin 尝试直接修改
userPassword
属性,而不是使用 LDAP 密码修改扩展操作。为了做到这一点,它必须在将更新发送到 SLAPD 之前,执行所有的加密和 Base-64 编码。此设置告诉 phpLDAPadmin 在修改密码时应该使用哪种哈希算法。默认情况下,OpenLDAP 使用 SSHA,因此我们应该将 phpLDAPadmin 设置为相同的算法。如果你在
slapd.conf
中使用 password-hash 指令设置了不同的值,应该在这里设置相同的值。
注意
不是所有 phpLDAPadmin 中的加密选项都被 OpenLDAP(或任何其他 LDAP 服务器)支持。你不应该为密码使用 blowfish 加密。OpenLDAP 不支持这种加密方式,phpLDAPadmin 错误地将其标记为crypt
哈希。
虽然 phpLDAPadmin 配置文件中还有许多其他可配置的参数,但我们已经完成了基本的配置。现在可以使用我们的网页浏览器测试 phpLDAPadmin 工具了。
phpLDAPadmin 初步介绍
安装了 PHP、重启了 Apache 并配置了 phpLDAPadmin 后,我们现在可以连接到 phpLDAPadmin。Ubuntu 安装了 phpLDAPadmin,并且它可以通过 URL[<hostname or IP address>/phpldapadmin/
](http://http://localhost/phpldapadmin
指向 phpLDAPadmin 工具。
当 phpLDAPadmin 第一次加载时,它看起来像这样:
左侧框架是 phpLDAPadmin 的导航框架。带有Example.Com文本的计算机图标表示我们配置的服务器。如果 phpLDAPadmin 已配置了多个主机,则左侧框架将列出它们所有的主机。
这是截图:
在顶部部分,在版本横幅(phpLDAPadmin – 0.9.8.3)下方,有六个链接。主页链接指向此页面。请求功能、捐赠和报告错误分别指向 phpLDAPadmin 外部网站的不同位置。帮助加载一个内部页面,里面的链接将返回 phpLDAPadmin 论坛网站。
最后,清除缓存链接可以用来清除 phpLDAPadmin 用来优化性能的 LDAP 数据副本的内部缓存。如果 phpLDAPadmin 显示的是某个数据的旧副本,而实际上应该显示更近期的更新,则可能需要此操作。
要登录我们的服务器,请点击登录...链接,位于Example.Com图标下方。这将在右侧的主框架中加载登录界面。
请注意,与 Apache 默认设置不同,phpLDAPadmin 默认要求您输入完整的 DN 才能登录。然后,它将直接以该 DN 进行绑定。
注意
警告消息警告:此网页连接未加密表明浏览器与 Web 服务器之间的连接是 HTTP 而不是加密的 HTTPS。对于这样的应用程序,最好配置 Apache 使用 HTTPS。更多信息,请参见httpd.apache.org/docs/2.0/ssl/
。
如果 phpLDAPadmin conf.php
文件中的 anon_bind
参数设置为 true
而不是 false
,用户还可以勾选一个框来以匿名用户身份登录:
$ldapservers->SetValue($i,'login','anon_bind',true);
在这种情况下,用户无需输入 DN 或密码,但 phpLDAPadmin 将允许他们在 ACL 允许的范围内浏览目录信息树。
浏览 phpLDAPadmin
登录后,导航框架将显示此目录服务器上托管的目录信息树列表,如屏幕截图所示:
在Example.Com下方,现在有七个链接列表:
-
架构: 点击此按钮将显示此 LDAP 服务器支持的整个架构(来自
cn=subschema
)。 -
搜索: 这将加载用于执行简单 LDAP 搜索的主搜索表单。
-
刷新: 这将刷新当前显示在下方树形结构中的数据。如果条目已添加但未立即显示,点击刷新应该能够解决问题。
-
信息: 信息链接将加载根 DSE 信息(已解码,以便人类更易读),显示在主框架中。这对于了解目录服务器非常有用。(有关 Root DSE 的更多信息,请参见附录 C。)
-
导入: 这将上传一个 LDIF 文件,并尝试将条目添加到目录服务器(通过 LDAP 添加操作)。
-
导出:通过此链接,你可以下载目录内容的副本。此操作也使用 LDAP 协议,这意味着它受 ACL 限制,可能无法导出所有内容。换句话说,它不是
slapcat
的替代品。不过,它有一个额外的优点:能够导出为 LDIF、DSML(XML 格式)、CSV(逗号分隔版本)和 VCARD 格式。 -
注销:此链接将当前用户从 phpLDAPadmin 中注销。
在这些链接列表下方是当前托管在此服务器上的两个目录信息树的基础条目,分别是 cn=log
树(保存访问日志)和 dc=example,dc=com
树(保存我们在本书中创建的目录条目)。
注意
这两棵树会显示出来,因为在 config.php 中设置的基础 DN 看起来像这样:$ldapservers->SetValue(
$i,
'server',
'base',
array());
。这使得 phpLDAPadmin 使用来自根 DSE 的信息来确定哪些目录信息树在此托管。根 DSE 返回了两个:cn=log
和 dc=example,dc=com
。
点击加号(+
)图标会展开树的那部分,显示其下属条目:
因此,可以通过左侧导航窗格快速有效地浏览目录信息树。
树中的每个条目仅显示 DN 的 RDN 部分。通过查看层级结构,可以构建完整的 DN,但如果你希望默认显示完整的 DN,可以在 config.php
文件中设置以下参数:
$config->custom->appearance['tree_display_format'] = '%dn';
相反,如果你只想显示 RDN 的值,而不显示 attr=
部分,你可以将该参数设置为 %rdnValue
。
查看和修改记录
要查看完整的记录,只需点击左侧导航框中的层级视图中的所需条目。例如,如果我们点击 cn=Admins
,主框中将显示完整的记录:
此屏幕提供了多个工具来操作记录,并显示所有记录的属性。工具如下:
-
刷新:此操作刷新当前记录。这在条目自上次加载此页面以来可能已更改时非常有用。
-
复制或移动此条目:此操作可用于将条目移动(或复制)到目录信息树中的另一个位置。
-
删除此条目:此操作会对记录执行 LDAP 删除,将其从目录信息树中移除。
-
与另一个条目进行比较:此操作显示两个不同记录的并排可编辑视图。它可以用来直观地扫描两个记录,或者将一个记录作为创建另一个记录的参考。
-
创建子条目:此操作将在当前选定条目的基础上创建一个新的子条目。
-
导出:这与左侧导航窗格中的导出链接执行相同的功能,只不过它默认选择当前条目,而不需要用户选择要导出的点。
-
显示内部属性:这将显示所选记录的操作属性。当然,操作属性不能由客户端应用程序修改,因此这些属性将是只读的。
-
重命名:这允许您更改条目的 RDN(就像我们使用
ldapmodrdn
命令行工具所做的那样)。 -
添加新属性:通过这个功能,您可以向条目添加新属性。phpLDAPadmin 允许您从当前记录的对象类所允许的属性列表中选择属性。换句话说,您不必担心意外选择不允许该记录拥有的属性。
在这些工具选择下方,是当前记录的所有属性的显示:
cn=Admins
组记录具有以下(非操作)属性:cn
、member
、objectclass
和ou
。phpLDAPadmin 分析记录并呈现适合该记录的选项。
首先,cn
不能修改,因为它用于 RDN(如最右侧所示)。此外,它被标记为必需。点击重命名链接将与工具列表中的重命名选项做相同的事情:提示我执行modrdn
操作。
在member
属性下,它也是必需的,有两个值:属于此组的用户的 DN。
DN 左侧的箭头 () 是指向这些用户记录的链接。如果点击该链接,它将加载一个类似的页面,允许您编辑该 DN 的记录。
在成员 DN 字段的另一侧是看起来像带放大镜的目录图标 ()。点击此图标将允许您浏览目录树,找到另一个 DN 并将其放入此字段。
我们稍后会查看这个对话框。但首先,我们将通过添加一个新的属性值来向我们的组中添加一个新的组成员。
查看记录显示的member部分,我们可以通过点击添加值链接来添加新成员。这将弹出一个属性编辑屏幕:
属性编辑屏幕用于向现有记录添加新属性。在屏幕顶部,我们可以看到有关我们正在添加的属性(member)以及其所在记录(cn=Admins)的一些基本信息。
接下来,属性编辑器列出了该属性的现有值(因为该组已经有两个成员)。最后,有一个单行文本框,允许我们输入一个新成员。
phpLDAPadmin 会检查该属性的架构并显示架构描述以及语法的可读人描述。
另外,由于此字段的值是 DN,右侧会出现查找图标(带放大镜的文件夹图标)。我们可以点击该图标调出查找对话框,在该窗口中,我们可以浏览目录信息树,找到我们想要添加的 DN。它会像这样显示:
点击加号(+
)图标将展开该树的分支,而点击 DN 本身则会将该 DN 插入到属性编辑屏幕中的字段中。
注意
此查找对话框在 phpLDAPadmin 中经常使用,提供了一个简单的树形导航工具,用于在目录信息树中定位条目。
现在我们在新的member
字段中有了所需的值:
点击添加新值按钮将暂时把此属性添加到我们的cn=Admin
组,并将我们返回到记录视图。我们的新添加项将在主记录视图中显示:
现在我们有三名成员。在页面底部有一个按钮,标注为保存更改。该按钮用于保存直接对页面字段所做的任何更改,但它并不需要用来保存新加入的组成员——用户uid=barbara
已经被添加到该组。
请注意,objectClass
字段不允许修改结构对象类。这是因为 LDAP 不允许更改条目的结构对象类。然而,可以使用添加值链接添加新的对象类(辅助类)。
此外,在每个对象类旁边都有一个信息图标 ()——一个蓝色圆圈,里面有一个白色字母
i
。
点击此图标将加载该对象类的架构查看器,显示有关该对象类的有用信息:
架构查看器展示了 LDAP 架构中存储的所有信息,但比我们在第六章中查看的架构文件要更加人性化。架构查看器提供了一个界面来查看对象类、属性定义、匹配规则和语法信息。在这种情况下,它展示了groupOfNames
对象类。属性和上级对象类是相互链接的,这使得浏览架构变得更加容易。此外,还有一个跳转到对象类下拉列表,提供了一种快速查看其他对象类的方式。
添加新记录
可以在 phpLDAPadmin 的多个位置添加新记录。在任何有星形图标的地方 (),都表示可以在此位置添加一个新的下级记录。
让我们添加一个简单的用户帐户。为此,我们将使用左侧导航窗格中的树状视图定位ou=Users
分支:
点击星形图标(在此处创建新条目)将把记录创建视图加载到主框架中。我们可以在此开始定义新用户的条目。
第一步是为新用户选择一个结构对象类。phpLDAPadmin 提供了一个可供选择的列表:
phpLDAPadmin 系统有许多预定义的模板用于添加新条目,但我们的 LDAP 服务器并未配置 phpLDAPadmin 支持的所有对象类。(这些模式中的许多已经在/etc/ldap/schemas/
目录中定义。)
尝试添加用户帐户(该帐户使用posixUser
对象类,如nis.schema
中定义的)会在创建用户时出现问题。
在 phpLDAPadmin 中定义但在模板定义中被禁用的条目会用黑色圆圈中的白色箭头标记;这些条目无法选择。
注意
可以轻松创建和添加新的自定义模板。模板是存储在/etc/phpldapadmin/templates/
中的简单 XML 文件。要添加一个新的模板,只需创建一个新的 XML 文件(或复制并修改现有文件),将其保存在templates/
目录中,然后使用 phpLDAPadmin 中的清除缓存工具强制重新加载 XML 文件。请参阅本书中附带的示例包(可在 Packt 网站上找到:www.packtpub.com
)。
我们希望创建一个新的inetOrgPerson
对象。由于没有为inetOrgPerson
预定义模板,我们将使用自定义模板。
第一步是创建 DN 并选择一个结构对象类:
我们新用户的 UID 将是mary
,并且一如既往,我们将使用uid
作为 RDN 中的属性。用户将位于ou=Users
组织单位中。我们希望从对象类列表中选择inetOrgPerson
(以及person
和organizationalPerson
)。点击继续 >>将带我们进入下一个页面,在那里我们可以填写一些属性值。以下是下一个页面:
必填属性位于表单的顶部。之后,有一个选择多个可选属性并为其赋值的部分。如果在这里添加userPassword
值,它将被正确加密并存储在目录服务器上。
滚动到此页面底部,有一个标有创建对象的按钮。点击该按钮将在目录服务器上执行 LDAP 添加操作。
一旦新用户创建完成,phpLDAPadmin 将显示该条目。
其他模板通过自动选择正确的对象类,并将可用的属性缩小到最常用的那些,简化了这一过程。
使用 phpLDAPadmin 进行搜索
我们将在 phpLDAPadmin 中查看的最后一个任务是 搜索。phpLDAPadmin 配备了一套搜索工具,可以用来在目录信息树中查找信息。
要进入搜索界面,请点击左侧导航框中的 搜索。这将带你到基本搜索屏幕:
在这里,我们将搜索所有 UID 以 ma
开头的条目。点击 搜索 按钮将执行搜索,对于我们的目录,它返回四条记录:
这返回了所有 UID 以 ma
开头的用户。请注意,默认情况下,搜索会检查所有可用的目录上下文。这可能意味着某个目录信息树没有搜索结果,而另一个可能有一堆匹配项。
有时,拥有更多对 LDAP 搜索的控制也是很有用的。在简单搜索界面顶部点击 高级搜索表单 链接,将加载一个具有更多选项的搜索界面:
这使我们能够明确设置基础 DN、范围和搜索过滤器,并指定我们希望返回的属性列表。简而言之,这个搜索表单包含了我们在其他 LDAP 应用程序中常见的字段,例如 ldapsearch
命令行客户端。
这也将返回符合我们规格的项目列表。
第三个搜索选项是 预定义搜索。这个工具特别适合反复执行带有相同参数的搜索。
搜索在 /etc/phpldapadmin/
目录下的 config.php
文件底部预定义。预定义搜索部分以如下方式开始:
$q=0;
$queries = array();
第一行设置查询计数器,第二行创建一个新的查询数组。我们将向 $queries
数组中添加配置指令。
搜索定义如下所示:
$queries[$q]['name'] = 'Users with Email Addresses';
$queries[$q]['base'] = 'ou=Users,dc=example,dc=com';
$queries[$q]['scope'] = 'sub';
$queries[$q]['filter'] = '(&(objectClass=inetOrgPerson)(mail=*))';
$queries[$q]['attributes'] = 'cn, uid, mail';
每一行都会将一个新的名称/值对添加到 $queries
数组的第一个位置(记住,$q
是 0
,表示数组的第一个位置)。到目前为止,这种过滤器的格式应该已经相当熟悉:
-
name
:预定义搜索的可读名称。 -
base
:搜索将从该基础 DN 开始。 -
scope
:搜索范围(基础、单一、子级、子孙)。 -
filter
:LDAP 过滤器。 -
attributes
:应返回给用户的属性列表。请注意,属性列表被引号括起来,值之间用逗号分隔。
如果我们要创建第二个过滤器,首先会递增 $q
变量,然后定义一组新的参数:
$q++;
$queries[$q]['name'] = 'Entries with SeeAlso attributes';
$queries[$q]['base'] = 'dc=example,dc=com';
$queries[$q]['scope'] = 'sub';
$queries[$q]['filter'] = '(seeAlso=*)';
$queries[$q]['attributes'] = 'cn, description';
这一行 $q++
将 $q
的值从 0
改为 1
,将下一个五个参数放入 $queries
数组的下一个索引位置。
一旦我们定义了过滤器并保存文件,就可以进行测试了。无需重启 Apache 或 SLAPD;phpLDAPadmin 会在每次新的请求时读取其配置文件,并立即加载我们的更改。
这是预定义搜索的屏幕:
使用预定义的搜索,我们只需从页面顶部的下拉列表中选择所需的搜索,并按下搜索按钮即可运行。由于过滤器存储在配置文件中,phpLDAPadmin 不需要我们提供任何额外的信息。
我们现在已经查看了 phpLDAPadmin 的主要功能,它是一个通过网页界面管理 LDAP 目录的成熟工具。
phpLDAPadmin 并不是唯一的开源目录服务器管理程序。还有像 GQ(gq-project.org
)这样的标准桌面工具,以及其他几十种基于网页的 LDAP 工具。还有插件可以将 LDAP 支持带入其他流行的基于网页的应用程序(如 Squirrelmail、Joomla 和 OpenCms)。
还有一些工具可以将 LDAP 服务引入其他身份验证工具。例如,libpam-ldap
包为PAM(可插拔身份验证模块)提供了执行 LDAP 查找的功能。而saslauthd,一个提供身份验证服务的 SASL 守护进程,也可以配置为连接到 LDAP 服务器进行身份验证。
最后,还有许多 DNS 服务器、邮件服务器、文件服务器以及其他可以配置为使用 LDAP 存储和检索信息(尤其是身份验证信息)的软件包。
总结
在本章中,我们已经探讨了配置其他工具与 OpenLDAP 互操作的方法。我们从 Apache web 服务器开始,使用 LDAP 作为身份验证和授权的来源。接着,我们安装了 phpLDAPadmin,这是一个基于网页的目录服务器管理程序。我们查看了其主要功能并进行了自定义调整。
当然,这只是 LDAP 启用应用程序的冰山一角。本章中提供的信息应该能帮助你实施任何 LDAP 启用的应用程序,因为它们都需要相同的基本配置信息:主机、端口、绑定信息和搜索过滤器。
若要了解更多关于启用 LDAP 的应用程序,你可能需要浏览一些开源软件包网站,比如 Freshmeat.Net(freshmeat.net
)和 Source Forge(sourceforge.net
)。
附录 A:从源代码构建 OpenLDAP
在本附录中,我们将逐步介绍从源代码构建 OpenLDAP 的过程。我们将从配置 Linux 平台以编译 OpenLDAP 开始。接着,我们将配置、编译并安装 OpenLDAP。编译 OpenLDAP 可能听起来很复杂,但其实并不复杂,我已尽力提供足够简明的说明,即使是没有 C 语言经验的人,也能快速从源代码编译。
为什么要从源代码构建?
许多 Linux 和 UNIX 发行版迁移到 OpenLDAP 的新版本的速度较慢。具体原因可以推测,但其中一个可能的原因是,发行版维护者对于已经表现良好、已与其他服务集成并且在安全性和功能性上对许多组织至关重要的任务进行快速采纳新版本的软件持保留态度。提供认证服务的 OpenLDAP 正是这样的服务。
由于这种不愿意更新的态度,你可能在选择的 Linux 或 UNIX 发行版中找不到最新的 OpenLDAP 版本。如果你需要(或者想要)OpenLDAP 提供的最新功能,可能需要获取一个全新的源代码副本并从头开始构建。
获取代码
要获取最新版本的代码,请访问官方的 OpenLDAP 网站:openldap.org
。该网站由OpenLDAP 基金会托管,这是一个非盈利组织,负责管理和监督 OpenLDAP 项目。
在主页上,你会在右下角的突出框中找到当前版本的链接,如屏幕截图所示:
你可以直接从那里下载最新的稳定版本,或者可以访问下载页面(在链接表格的中间列标记为Download!)来查找其他版本(过去的版本、当前的实验版和测试版等)。
编译工具
每当你从源代码构建一个应用程序时,都会需要正确的工具和库。OpenLDAP 也不例外。幸运的是,OpenLDAP 对需求的要求比某些服务器应用程序要轻一些。
编译是在命令行中进行的,因此你需要打开终端或以其他方式访问 shell。
构建工具
你将需要标准的工具链来处理 C 和 C++应用程序;C 编译器、连接器和 make 程序。幸运的是,几乎所有 Linux 发行版都会默认包含这些工具。你可以使用which
命令测试系统中是否有适当的工具,这个命令会告诉你工具在文件系统中的位置(前提是它们位于$PATH
环境变量中列出的某个目录下)。
下面是一个快速示例,展示如何检查工具的位置以及每个工具的当前版本。我使用的是 Ubuntu Linux 6.06,您自己系统上的版本号可能会有所不同,没关系。OpenLDAP 应该能够在所有现代 Linux 发行版上编译,可能也可以在所有现代 UNIX 发行版上编译。
$ gcc --version
gcc (GCC) 4.0.3 (Ubuntu 4.0.3-1ubuntu5)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ which ld
/usr/bin/ld
$ ld --version
GNU ld version 2.16.91 20060118 Debian GNU/Linux
Copyright 2005 Free Software Foundation, Inc.
This program is free software; you may redistribute it under the terms of the GNU General Public License. This program has absolutely no warranty.
$ which make
/usr/bin/make
$ make --version
GNU Make 3.81beta4
Copyright (C) 2003 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
This program built for i486-pc-linux-gnu
$
在每种情况下,我使用which
工具来查看工具的位置。我检查的程序有gcc
、ld
和make
——分别是编译器、链接器和构建程序。只要返回了某个路径,就表示该工具已经安装。如果没有找到命令,which
会没有任何输出。因此,如果我搜索一个不存在的命令,比如blah
,输出会像这样:
$ which blah
$
因此,如果你运行which
命令检查任何程序(如gcc
、ld
或make
),如果没有输出,就表示你没有安装所需的工具。
注意
在一些 UNIX 系统上,GCC 编译器(gcc
)可能不存在,但可能存在其他 C 编译器。C 编译器的事实名称是cc
,如果which
gcc
没有返回结果,你可以尝试which
cc
。
在给定示例中的which
命令之后,我使用--version
标志运行每个命令(在version
前有两个破折号),以查看已安装的版本。--version
标志是 GNU 标准,但非 GNU 程序(如其他版本的make
或cc
)可能不支持该标志。
接下来要做的是设置几个环境变量,为给定工具提供一些基本的设置。虽然你可以通过环境变量为工具提供很多选项,但在这里我们只为构建 OpenLDAP 提供基本设置。
注意
一些 Linux 和 UNIX 发行版会为你设置必要的环境变量。在这种情况下,通常最好使用已经定义好的环境变量,这些变量通常是专门为你的系统优化过的,而不是我们现在要设置的通用变量。
要检查是否有必要的环境变量,可以运行env
命令(不带任何参数),然后查看输出,确认CC
、CFLAGS
和PATH
是否已经定义。
设置环境变量的一种方法是使用export
命令。当你使用export
命令时,环境变量会在当前 Shell 会话期间存储(换句话说,直到退出 Shell 或关闭终端窗口)。在这里,我们将使用export
设置必要的环境变量:
$ export CC=gcc
$ export CFLAGS="-O2"
第一个export
命令将$CC
环境变量设置为gcc
。make
程序将使用这个来确定使用哪个编译器。(如果你使用的是cc
编译器,而不是gcc
,那么请调整示例,指向cc
而不是gcc
)。注意,当你设置环境变量时,变量名之前不需要加美元符号($
)。但是,当你引用变量时,必须包含美元符号。
第二行设置了$CFLAGS
变量。$CFLAGS
变量是在编译期间传递给编译器的选项。在这个例子中,我们传递了-O2
选项(这是大写字母 O,不是零)。这告诉编译器在编译代码时使用级别 2 的优化。
$PATH
环境变量也应设置好。然而,通过使用which
命令查看我们的工具所在位置,我们已经验证了必要的目录(即包含我们工具的目录)已在$PATH
变量中指定。
如果你使用的是非标准系统或某些库的非标准版本,或者你希望向构建工具传递其他选项,可能还需要使用一些额外的环境变量。你可以使用$CPPFLAGS
来传递选项给 C 预处理器(cpp
,是 GCC 的一部分)。同样,你可以使用$LDFLAGS
变量传递链接器(ld
)的选项。最后,如果你有存储在非标准位置的库(由其他应用程序使用的已编译代码模块),可以使用$LIBS
变量来指定这些库的位置。如果需要使用这些变量,应该参考工具和库的文档。
在任何时候,你都可以通过一些简单的命令检查你的环境变量。env
命令(无参数执行)将列出当前定义的所有环境变量及其值。你也可以使用echo
命令检查单个环境变量。只需输入echo
,然后跟上环境变量的名称,即可显示该环境变量的值:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin
/X11:/usr/games
在这个例子中,echo
$PATH
显示的是 Shell 搜索程序的目录列表。正如你可能还记得的,which
命令执行时,会输出指定工具的位置。为了找到工具,它会搜索$PATH
变量中指定的每个目录。
到此,我们可以继续下一步:安装必要的依赖项。
安装依赖项
依赖项是 OpenLDAP 编译和运行时所需的包。安装这些依赖项因平台而异(并且因 Linux 发行版而异)。在这里,我将使用 Debian 工具(包含在 Ubuntu Linux 中)来安装这些包。
OpenLDAP 需要标准的 C 库、正则表达式库和 Berkeley DB 4.2(或更高版本)库。这些库几乎总是包含在现代 Linux 发行版中。除了这些库,还需要头文件。这些头文件通常存储在单独的包中(通常称为 DEV 包)。例如,要在 Ubuntu 中安装 Berkeley DB 4.2 开发包,可以从命令行执行以下命令:
$ sudo apt-get install libdb4.2-dev
这将获取并安装所需的包。
还有一些其他有用的软件包需要安装,我们需要它们来构建本书中使用的所有功能。你需要安装:
-
OpenSSL(用于 SSL 和 TLS 支持)
-
SASL 2(用于 SASL 认证支持)
如果你有兴趣将目录存储在关系型数据库引擎中,例如 MySQL 或 Oracle,你还可能需要安装 iODBC2(用于数据库后台支持)。
这些软件包在现代 Linux 系统中非常常见。确保已经安装这些软件包,并且每个软件包的 DEV(或 -dev
)附加组件也已安装。在 Ubuntu 6.06 中,可以通过一条(相当长的)命令安装:
$ apt-get install libssl0.9.8 libssl-dev libiodbc2 libiodbc2-dev
libsasl2 libsasl2-dev
其他发行版可能使用不同的安装程序,甚至使用不同的软件包名称,但根据提供的名称列表,你应该能够轻松找到它们。
注意
OpenLDAP 包含许多可选模块,提供额外的功能(例如调试或与其他服务的集成)。这些模块在本书中没有涉及,但你可以选择自行探索。一些模块需要额外的库。有关更详细的信息,请查阅源代码中包含的 OpenLDAP 文档。
到目前为止,你已经具备了构建 OpenLDAP 所需的所有工具和要求。接下来我们将进入实际的编译过程。
编译 OpenLDAP
在上一节中,我们为构建 OpenLDAP 准备了所有工具和库。在本节中,我们将配置、编译并测试 OpenLDAP。
首先,我们需要将 OpenLDAP 服务器的源代码移动到一个临时目录中进行构建。将 openldap-2.3.x.tgz
文件复制到适当的目录,然后解压该文件:
$ mkdir build/
$ cp openldap-2.3.37.tgz build/
$ cd build
$ tar -zxf openldap-2.3.37.tgz
在这里,我创建了一个新的目录(名为 build/
),将 OpenLDAP 源代码压缩包复制到新目录中,将工作目录切换到 build/
,然后使用 tar
工具解压该文件(-zxf
标志指示 tar 解压缩(z
)并提取文件内容(x
),文件(f
)为 openldap-2.3.37.tgz
)。完成后,build/
目录应包含一个名为 openldap-2.3.37
的目录。使用 cd
切换到该目录:cd
openldap-2.3.37
。
配置
现在,我们需要运行配置脚本,以准备源代码进行编译。这个脚本决定了 OpenLDAP 的构建方式,并确定哪些选项会默认启用或禁用。OpenLDAP 非常可配置,提供了许多不同的选项可以选择。要查看完整的选项列表,可以使用 --help
标志运行配置脚本:
$ ./configure --help
这个命令会打印出所有可用于配置 OpenLDAP 的选项列表。它还会指示每个选项是否默认启用。例如,SLAPD 数据库后台部分的前几行如下所示:
SLAPD Backend Options:
--enable-backends enable all available backends no|yes|mod
--enable-bdb enable Berkeley DB backend no|yes|mod [yes]
--enable-dnssrv enable dnssrv backend no|yes|mod [no]
我们可以看到,每个选项有三种可能的状态:yes、no 和 mod(mod 表示将该组件构建为可插拔模块,而不是将其构建到 slapd 中)。在列出的选项中,只有--enable-bdb
(启用 Berkeley DB 后端)默认是开启的。
在大多数情况下,默认设置是合适的。所有关键选项默认都是开启的。然而,这本书中讨论的一些附加模块默认并未启用,我们需要手动启用它们。它们是:
-
--enable-ldap
:启用 LDAP 后端存储机制(参见第七章) -
--enable-ppolicy
:启用密码策略覆盖(参见第六章)
如果你不打算使用 ODBC 数据库后端,可以添加--enable-sql
,但你需要确保安装前面章节中提到的 iODBC2 包。
注意
默认情况下,OpenLDAP 将在/usr/local
的子目录中安装(通过最终的 make install 步骤)。这是推荐放置“本地”应用程序和库的地方。那些没有作为标准预配置应用程序(如 deb 或 RPM 包)发布的包被视为本地包。如果你希望将包放置在其他位置,可以使用--prefix
和--exec-prefix
标志。
现在我们准备好运行配置命令了:
$ ./configure --enable-ldap --enable-sql --enable-ppolicy
这将启动一个评估过程,可能需要几分钟时间。配置脚本将系统地评估你的系统设置,确定你正在使用的工具、如何构建以及系统是否具备所有必要的库文件。
如果配置过程终止并显示错误,它会说明失败的原因。通常,这种失败表明缺少某个必需的库或工具。例如,如果错误信息显示sql.h
缺失,这表明没有找到 iODBC2 的头文件(在 Ubuntu 中是libiodbc2-dev
)。这通常意味着它们根本没有安装,尽管也可能是它们安装在了非标准位置。
一些缺失的库不会阻止配置过程运行。这些包会生成错误而不是警告。比如 OpenSSL 库和 SASL 库就是这种情况。一旦配置脚本完成运行,回滚查看结果,确保没有类似以下的行:
configure: WARNING: Could not locate TLS/SSL package
configure: WARNING: TLS data protection not supported!
或
configure: WARNING: Could not locate Cyrus SASL
configure: WARNING: SASL authentication not supported!
如果你看到这些,你可能需要确保已安装适当的包(记得是 DEV 包),然后重新运行./configure
脚本。
一旦配置脚本完成运行,并且没有警告或错误,你就可以开始构建 OpenLDAP 的源代码了。
使用 make 进行构建
使用 make 构建是一个两步过程。首先,需要构建辅助库,然后构建主工具和服务器。幸运的是,所有这些繁重的工作都可以通过一个简短的命令完成:
$ make depend && make
这将编译所有的库(make
depend
),然后,如果第一部分成功,它将运行主构建(make
)。编译可能需要很长时间。
通常,配置脚本会在主编译开始前确保一切正常。然而,极少数情况下,其中一个 make
命令可能会失败。如果发生这种情况,您需要评估错误信息并确定采取哪些步骤来修复问题。在大多数情况下,问题与未满足的依赖关系有关——OpenLDAP 所需的某些软件包或工具未安装,且(由于某种原因)配置脚本未注意到这个问题。
有时,OpenLDAP 附带的文档(README
、make
和 docs/
、libraries/
、servers/
目录中的文档)会指出可能的问题。
注意
如果 make
失败且您找不到问题所在,最佳的做法是搜索 OpenLDAP 邮件列表存档(访问 openldap.org
),或者,如果一切都失败了,订阅邮件列表并在那里询问该问题。
一旦编译过程结束,建议运行自动化测试程序以确保代码构建正确。这也是通过 make
来完成的:
$ make test
由于测试包括频繁的程序延迟并执行数十个测试,因此这个过程可能需要几分钟才能完成。完成后,请查看输出并确保没有错误。请注意,由于我们没有使用所有可能的选项编译 OpenLDAP,部分测试将被跳过。跳过的测试是正常现象,无需担心。
现在我们准备好安装全新的 OpenLDAP 服务器。
安装
安装只需执行一个额外的命令:
$ sudo make install
在某些版本的 Linux 或 UNIX 中,您需要切换用户(su
)为 root,并以 root 用户身份运行安装命令:su
-c
'make
install'
。系统会提示您输入帐户密码(或者,如果使用 su
而非 sudo
,则需要输入 root 密码)。输入正确密码后,必要的 OpenLDAP 文件将被复制到 /usr/local
的子目录中。
在某些系统中,包含本地可执行文件的目录(/usr/local/bin
和 /usr/local/sbin
)没有包含在 $PATH
环境变量中。因此,仅在命令行输入 OpenLDAP 命令可能会返回错误。解决此问题的一种方法是输入命令的完整路径:
$ /usr/local/sbin/slapcat
但这可能很繁琐。您还可以将适当的路径附加到您的 $PATH
环境变量中。然后,您将能够直接执行命令,而无需指定命令的绝对路径:
$ export PATH= /usr/local/bin:/usr/local/sbin:$PATH
$ slapcat
在这个示例中,export 命令会重新设置当前会话的 $PATH
。因此,变量 $PATH
会被赋予 /usr/local/bin
、/usr/local/sbin
和当前 $PATH
变量的内容(其中可能包含 /bin
、/sbin
、/usr/bin
和其他目录)。顺序很重要。当 shell 搜索一个命令(在这个例子中是 slapcat
)时,它会从 $PATH
中的第一个目录开始,一直到最后一个目录。只要找到匹配项,它就会停止搜索。因此,例如,如果存在两个 slapcat
命令,shell 会使用它找到的第一个命令。在我们的案例中,最好将两个 /usr/local
目录放在路径的前面,以防文件系统的其他地方安装了旧版本的 LDAP。
通常,export
命令应该添加到 shell 配置文件中(例如 ~/.bash_profile
),以便每次启动 shell 会话时自动添加额外的路径信息。
现在,你可以开始配置新版本的 OpenLDAP。
构建所有内容
在前一节提到的构建中,我们只编译了基础内容。这能让我们运行最基本的功能。但 OpenLDAP 有许多可能有用的后端和覆盖层(书中会介绍许多)。在我们想要构建所有内容时,通常最好编译带模块支持的 OpenLDAP,并将所有覆盖层和后端编译为模块。这样,我们可以获得所有附加功能,但只有在运行时需要(并在slapd.conf
中配置)的模块才会被加载。
注意
许多附加的后端和覆盖层有自己的依赖项。例如,Perl 后端要求安装 Perl 库。Ubuntu 默认安装了大多数必需的依赖项。如果你没有某个模块所需的库,configure
或 make
程序会告诉你缺少哪个库,你需要追踪包含该库的包。对于这个过程,你可以参考 Debian 网站上的包搜索功能(www.us.debian.org/distrib/packages#search_contents
)。
因为我们正在构建带有模块的 OpenLDAP,所以我们需要确保安装了libtool
和 libtool 头文件。在 Ubuntu 中,它默认没有安装。此外,由于 Perl 后端(back_perl
)将被安装,我们还需要安装 Perl 开发包。你可以通过一个命令安装所有这些:
$ sudo apt-get install libtool libltdl3 libltdl3-dev libperl-dev
libltdl3
库通常会默认安装,但其他库也需要用来编译带模块支持的 OpenLDAP。现在,我们已经准备好构建带模块的 OpenLDAP。
要构建带有所有额外模块的 OpenLDAP,我们只需在configure
时使用正确的标志:
$./configure --enable-dynamic --enable-modules --enable-backends=mod
\
--enable-overlays=mod
构建所有内容只需要四个标志。第一个,--enable-dynamic
启用共享库。第二个,--enable-modules
简单地告诉 configure
我们希望使用模块。接下来的两个标志指示我们希望构建的后端和覆盖层:--enable-overlays
,设置为 mod
以构建模块,以及 –enable-backends
(同样设置为 mod
)以构建所有可用的后端。
一旦 configure
完成,你可以运行 make
:
$ make depend && make && make test
这将构建所有依赖项,然后构建 OpenLDAP(以及所有模块),最后测试所有内容。当你准备好安装时,可以按照上一节中的说明进行操作。
总结
在本附录中,我们简要介绍了从源代码构建 OpenLDAP 的过程。此时,你应该已经具备了从源代码构建 OpenLDAP 所需的信息。
我们已经看到了一个非常基础的构建过程,以及一个使用模块的完整构建过程。但还有许多其他可用的选项。你可以通过 OpenLDAP 附带的文档了解更多关于构建 OpenLDAP 的信息。
附录 B. LDAP URLs
要查询目录,客户端必须向服务器发送几种不同的信息。为了将所有这些信息组合成一个符合标准的字符串格式,LDAP 开发者提出了一种标准的 LDAP URL 语法,它遵循 URL 标准(RFC 3986)。在本附录中,我们将了解 LDAP URL 的格式。
LDAP URL
LDAP URL 由八个不同的部分组成:
-
协议,通常是 LDAP(
ldap://
),尽管也使用非标准的 LDAPS 协议(ldaps://
)。 -
服务器的域名(或 IP 地址)。默认值是
localhost
。 -
服务器的端口号。默认值是标准 LDAP 端口
389
。 -
搜索的基础 DN。
-
要返回的属性列表。默认情况下返回所有属性。
-
范围指定符。默认使用
base
范围。 -
搜索过滤器。默认值是
(objectclass=*)
。 -
扩展字段。如果服务器支持扩展,可以在最后的字段中传递这些扩展的参数。
结合这八个部分中的七个(我们将跳过扩展字段),我们可以创建一个类似于以下的 URL:
ldap://example.com:389/ou=Users,dc=example,dc=com?mail?sub?(uid=matt)
该 URL 由以下七个部分组成:
<protocol>://<domain>:<port>/<basedn>?<attrs>?<scope>?<filter>
当我们需要使用扩展时,我们只需在给定的 URL 末尾附加一个问号 (?
和扩展信息。
使用此 URL 执行 LDAP 搜索,结果如下所示:
-
客户端将使用 LDAP 协议通过端口 389 连接到 Example.Com。
-
基础 DN 将设置为
ou=Users,dc=example,dc=com
。 -
客户端将请求
ou=Users,dc=example,dc=com
子树中 UID 为matt
的所有条目的mail属性。
要使用 LDAPS(在专用 SSL/TLS 端口上使用 LDAP 的非标准做法),请使用 ldaps://
而不是 ldap://
。
在许多情况下,简化 URL 并接受默认选项非常方便。例如,默认域是localhost
(或 IP 地址 127.0.0.1
),即 URL 执行的服务器的地址。默认端口是 389(除非协议为 ldaps://
,而不是 ldap://
,在这种情况下,默认端口是 LDAP 的端口 636)。
在大多数情况下,端口可以省略。但是,URL 中的域名部分也可以省略:
ldap:///ou=Users,dc=example,dc=com?mail?sub?(uid=matt)
请注意,开头现在有三个斜杠 ldap:///
。通常出现在第二个和第三个斜杠之间的域名未指定。如果使用此 URL,LDAP 应用程序将连接到 localhost(默认主机),端口为 389(默认的 LDAP 端口),然后执行搜索。
假设现在我们不仅想让 LDAP 服务器返回 mail
属性,而是让它返回所有标准(非操作)属性。为此,我们只需将属性规范留空:
ldap:///ou=Users,dc=example,dc=com??sub?(uid=matt)
现在,属性位置没有值,尽管两个相邻的问号(??
)指示空属性位置。
在前两个示例中,当我们省略了特定字段值时,必须在 URL 中保留设计符。因此,我们将 URL 的域部分写作ldap:///
,并且在属性规范中留有没有值的?
(在给定示例中为??
)。
但当我们从 URL 的末尾去除值时,不需要保留空位置标识符。例如,如果我们去除末尾的过滤器,就不需要在 URL 末尾留下多余的?
。以下是一个示例:
ldap:///ou=Users,dc=example,dc=com?mail?sub
在此示例中,返回了ou=Users,dc=example,dc=com
下每个条目的mail
属性。
LDAP URL 的常见用途
本书中,LDAP URL 被用于各种不同的目的。
在第四章中,我们使用 LDAP URL 在slapd.conf
中的authz-regexp
指令中执行搜索。
正如我们所探讨的,完整的 LDAP URL 可以作为制定搜索的有用方式,但这可能并不是 LDAP URL 的主要用途。更常见的是,LDAP URL 语法被简化,只用于捕获基本信息。
并非所有 LDAP URL 都用于搜索
在第三章中,我们使用 LDAP URL 通过ldapsearch
工具连接到 SLAPD,但当时我们并没有使用 LDAP URL 来指定搜索字符串。事实上,在许多情况下,LDAP URL 可能仅用于在一个方便的字符串中提供协议、主机和端口信息:
ldap://example.com:646
在此示例中,LDAP URL 提供了足够的信息,以便客户端在连接到Example.Com
服务器时使用普通的 LDAP 协议,并且连接端口为非标准端口 646。
目录引用,在slapd.conf
文件中由引用指令处理,也使用 LDAP URL 语法,但只使用协议、域和端口设置。
因此,LDAP URL 有两个主要用途,每个用途决定了其格式:
-
LDAP 搜索 URL 遵循复杂的八字段格式,能够传递 LDAP 代理进行搜索所需的所有信息。
-
LDAP 连接 URL 仅使用协议、主机和端口信息,主要用于传递如何连接到目录的信息。
当前没有用于修改或删除 LDAP 记录的 LDAP URL 形式。
有关 LDAP URL 的更多信息...
LDAP URL 格式在标准化的 RFC 4516 中进行了描述。该 RFC 充满了示例,涵盖了扩展的使用和特殊字符的编码。RFC 可以在线访问:rfc-editor.org/rfc/rfc4516.txt
。
总结
本简短指南概述了 LDAP URL 语法。LDAP URL 被用于各种场合,提供连接信息,并且有时(在更复杂的形式下)提供执行 LDAP 搜索所需的信息。
附录 C. 有用的 LDAP 命令
在本书过程中,我们查看了 OpenLDAP 发布版中所有命令行工具。但本书的范围要求我们简要讨论每个工具。也有一些高级用法,在某些时候可能会很有用。在本附录中,我提供了这些用法的示例。
在本附录中,我们将介绍
-
使用
ldapsearch
获取目录信息 -
使用两种不同策略创建目录备份
-
重建 BDB/HDB 数据库
获取目录信息
许多 LDAP 服务器提供有关其配置和功能的详细信息。这些信息以一种 LDAP 客户端可以直接访问的方式存储,客户端可以通过搜索操作来获取。例如,客户端可以获取 根 DSE 记录,以了解服务器的基本功能。它还可以访问服务器的 子模式,并了解支持哪些对象类、语法、匹配规则和属性。
根 DSE
根 DSE(DSA 特定条目)(其中 DSA 代表 目录服务代理)是一个特殊条目,提供关于服务器自身的信息。根 DSE 的 DN 是一个空字符串("")。为了获取它,我们需要一个精心设计的 LDAP 查询,该查询将设置空的搜索基准并检索该根条目:
$ ldapsearch -x -LLL -b '' -s base -W -D \
'cn=Manager,dc=example,dc=com' '+'
请注意,基准被设置为空字符串,搜索范围仅限于基准记录。这些参数结合起来的效果是仅请求 DN 为空的记录。此外,由于根 DSE 中大多数属性是操作属性,因此我们需要在搜索的末尾指定 '+'
。
运行此查询的结果大致如下:
dn:
structuralObjectClass: OpenLDAProotDSE
configContext: cn=config
namingContexts: dc=example,dc=com
supportedControl: 2.16.840.1.113730.3.4.18
supportedControl: 2.16.840.1.113730.3.4.2
supportedControl: 1.3.6.1.4.1.4203.1.10.1
supportedControl: 1.2.840.113556.1.4.319
supportedControl: 1.2.826.0.1.334810.2.3
supportedControl: 1.2.826.0.1.3344810.2.3
supportedControl: 1.3.6.1.1.13.2
supportedControl: 1.3.6.1.1.13.1
supportedControl: 1.3.6.1.1.12
supportedExtension: 1.3.6.1.4.1.1466.20037
supportedExtension: 1.3.6.1.4.1.4203.1.11.1
supportedExtension: 1.3.6.1.4.1.4203.1.11.3
supportedFeatures: 1.3.6.1.1.14
supportedFeatures: 1.3.6.1.4.1.4203.1.5.1
supportedFeatures: 1.3.6.1.4.1.4203.1.5.2
supportedFeatures: 1.3.6.1.4.1.4203.1.5.3
supportedFeatures: 1.3.6.1.4.1.4203.1.5.4
supportedFeatures: 1.3.6.1.4.1.4203.1.5.5
supportedLDAPVersion: 3
supportedSASLMechanisms: NTLM
supportedSASLMechanisms: DIGEST-MD5
supportedSASLMechanisms: CRAM-MD5
entryDN:
subschemaSubentry: cn=Subschema
除其他事项外,该记录为我们提供了有关服务器理解并启用哪些控件、功能和扩展的信息。例如,记录中有一行 supportedFeature
,内容如下:
supportedExtension: 1.3.6.1.4.1.4203.1.11.1
这一行表示该 LDAP 服务器支持 RFC 3062 中定义的 LDAPv3 扩展的 更改密码 操作(www.rfc-editor.org/rfc/rfc3062.txt
)。
使用这些信息,一个精心设计的 LDAP 客户端将能够执行服务器端的更改密码操作,而不是在客户端更改密码后再使用修改操作将更改发送到服务器。
注意
更改密码操作的优点在于服务器的存储。如果客户端通过修改操作更改密码,它必须事先知道服务器支持哪些加密类型,它必须自己进行加密,然后将加密后的密码提交到服务器。通常,最好让客户端通过安全的方式(例如通过 TLS)与服务器进行连接,然后使用更改密码操作,这样服务器就可以进行存储。
根 DSE 记录还指向配置(cn=config
)和子模式(cn=subschema
)记录。
子模式记录
子模式记录存储在cn=subschema
中。该记录包含有关服务器支持的模式的详细信息,包括它可用的匹配规则类型、允许在属性中使用的语法类型,以及服务器识别的属性和对象类。
客户端应用程序可以使用这些信息正确构造记录或搜索,并正确解释响应。
可以使用以下命令通过ldapsearch
检索子模式记录:
ldapsearch -x -LLL -b 'cn=subschema' -s base -W \
-D 'cn=Manager,dc=example,dc=com' '+'
在此示例中,我们通过将基础 DN 设置为cn=config
,然后请求base
类型的搜索(-b
'cn=subschema'
-s
base
),来请求所需的记录。这将返回具有 DN 为cn=subschema
的确切记录。
此外,我们需要的多数属性是操作属性,这意味着它们不会在正常搜索中返回,因此最后我们指定'+'
,表示我们需要操作属性。
返回的记录如下所示:
dn: cn=Subschema
structuralObjectClass: subentry
createTimestamp: 20061216235843Z
modifyTimestamp: 20061216235843Z
ldapSyntaxes: ( 1.3.6.1.1.16.1 DESC 'UUID' )
ldapSyntaxes: ( 1.3.6.1.1.1.0.1 DESC 'RFC2307 Boot Parameter' )
# ... lots of lines removed
objectClasses: ( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson'
DESC 'RFC2798: Internet Organizational Person'
SUP organizationalPerson STRUCTURAL
MAY ( audio $ businessCategory $ carLicense $ departmentNumber $
displayName $ employeeNumber $ employeeType $ givenName $
homePhone $ homePostalAddress $ initials $ jpegPhoto $
labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $
roomNumber $ secretary $ uid $ userCertificate $
x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $
userPKCS12 ) )
objectClasses: ( 1.3.6.1.4.1.4203.666.11.1.4.2.1.2 NAME
'olcHdbConfig' DESC 'HDB backend configuration'
SUP olcDatabaseConfig STRUCTURAL
MUST olcDbDirectory
MAY ( olcDbCacheSize $ olcDbCheckpoint $ olcDbConfig $ olcDbNoSync
$ olcDbDirtyRead $ olcDbIDLcacheSize $ olcDbIndex $
olcDbLinearIndex $ olcDbLockDetect $ olcDbMode $
olcDbSearchStack $ olcDbShmKey $ olcDbCacheFree ) )
entryDN: cn=Subschema
subschemaSubentry: cn=Subschema
一个子模式记录包含所有的模式信息,因此,它可能会有一千多行。
子模式记录特别有助于了解服务器支持哪些模式,或者在开发和调试自定义模式时,如第六章所讨论的那样。
配置记录
OpenLDAP 2.3 的一个实验性功能(并且这个功能可能在 OpenLDAP 2.4 中达到生产质量)是将 LDAP 配置存储在目录中。要实现这一点,必须首先使用特殊的配置模式将配置重新创建为 LDIF 格式,并指示 SLAPD 从这个新的 LDIF 文件中读取其配置。
配置信息存储在目录中的 DN 为cn=config
。可以通过类似前一节使用的搜索来访问:
ldapsearch -x -LLL -b 'cn=config' -s base -W \
-D 'cn=Manager,dc=example,dc=com' '*'
在 OpenLDAP 2.3 中,并非所有的叠加层和 OpenLDAP 的功能都能与这种新的配置样式正确配合工作,这对于其使用来说是一个重要的缺点。但改进这种替代配置机制是 OpenLDAP 2.4 开发中的一个优先事项。
将配置存储在目录中的优势是什么?以下是几个可能的优势:
-
通过
ldapsearch
和其他 LDAP 客户端轻松访问配置信息。 -
通过目录工具如
ldapmodify
编辑配置信息的能力。 -
SLAPD 配置的复制支持。你可以使用 SyncRepl 同步目录配置到网络上。
如果你想实现新的基于 LDAP 的配置文件格式,可以在 OpenLDAP 网站的 LDAP 管理员指南中了解:www.openldap.org/doc/admin23/slapdconf2.html
。
进行目录备份
有两种常见的备份策略:一种是备份目录数据库,另一种是将目录内容导出到 LDIF 文件中。
目录数据库的备份副本
不同的后端将目录的内容存储在不同的位置。例如,BDB 和 HDB 后端将数据存储在特殊的 Berkeley DB 数据库文件中。基于 SQL 的后端将信息存储在关系型数据库管理系统中。像 LDAP 和 Perl 这样的特殊后端可能根本不存储数据,而只是访问其他数据源。
每个后端将需要不同的备份程序。这里我们只看如何备份 BDB 和 HDB 数据库——这是本书中使用的类型。
注意
这种方法不可移植。BDB/HDB 文件对版本非常敏感。每次 OpenLDAP(或 Berkeley DB)的新版本发布时,可能会使用不同的数据库结构,因此此备份方法仅在备份和恢复使用相同软件版本时有效。
在 Ubuntu 中,这些数据库文件位于 /var/lib/ldap
。该目录中的所有文件,包括索引(以 bdb
扩展名结尾的文件)、主数据库文件(__db.???
)和日志文件(log.??????????
)。最好也备份 DB_CONFIG
文件,尽管它很少变化,且不存储任何目录数据。
在备份这些文件时,最好停止 SLAPD。下面是一个使用常见 shell 工具的简单示例:
$ sudo invoke-rc.d slapd stop
$ sudo cp -a /var/lib/ldap/* /usr/local/backup/ldap/
$ sudo invoke-rc.d slapd start
这将停止 SLAPD 并将 /var/lib/ldap/
下的所有文件复制到 /usr/local/backup/ldap/
。然后,SLAPD 会重新启动。
一个 LDIF 备份文件
第二种,更具可移植性的备份策略是将目录的内容导出到 LDIF 文件。此方法有几个明显的优点:
-
无需停止 SLAPD
-
输出更具可移植性,数据可以从一个数据库后端迁移到另一个,并且可以从一个 OpenLDAP 版本迁移到另一个版本。
数据冗余较少,因此备份文件比 BDB/HDB 文件要小得多。要创建一个仅包含一个数据库的目录服务器的 LDIF 备份文件(即它只有一个目录根),命令非常简单:
$ sudo slapcat -l /usr/local/backup/my_directory.ldif
这个命令使用 slapcat
将目录的内容以 LDIF 格式导出到文件 /usr/local/backup/my_directory.ldif
。可以使用第三章中讨论的 slapdadd
工具将其重新加载到目录中。
如果您的目录包含多个目录信息树,您需要对每个服务器运行一次 slapcat
程序,并使用 -b
标志来指定您要导出的目录信息树的后缀(基础 DN):
$ cd /usr/local/backup
$ sudo slapcat -b "dc=example,dc=com" -l example_com.ldif
$ sudo slapcat -b "dc=test,dc=net" -l test_net.ldif
在这个示例中,我们将每个目录备份到各自的 LDIF 文件中。
重建数据库(BDB, HDB)
有时需要重建后端数据库。这个过程会根据数据库后端的不同而有所不同。例如,使用 SQL 后端时,可能需要转储、删除并重新创建数据库中的表。
注意
移动到新服务器并将内容传输到新从属服务器的过程也类似于重建数据库,文中提到了其中的区别。
OpenLDAP 最常用的后端是 HDB 和 BDB 后端(两者都基于 Berkeley DB 轻量级数据库)。在这一节中,我将讲解重建这些数据库的过程。
该过程包含五个步骤:
-
停止 SLAPD
-
将目录数据转储到文件中
-
删除旧的目录文件
-
创建新数据库
-
启动 SLAPD
这些步骤都不特别困难。实际上,对于一个小型到中型的目录,这个过程可以在不到十分钟的时间内完成。
注意
从服务器迁移到服务器
将目录从一台服务器迁移到另一台服务器的过程与这里描述的非常相似。只有步骤三,如后文所述,有所不同。在这种情况下,LDIF 文件会从原始服务器传输到新服务器,而不是删除目录文件。步骤一和二会在原始服务器上执行,步骤四和五会在新服务器上完成。
步骤 1:停止服务器
停止服务器的目的是为了在我们处理目录信息树时,避免对其进行额外的修改。
注意
如果你只是将主目录的内容转储并导入到将使用 SyncRepl 的影子服务器中,你无需停止服务器。目录转储后发生的任何更改都会在影子服务器的第一次 LDAP 同步操作中被检索到。
这可以通过结束服务器的进程 ID 或运行启动脚本并加上停止命令来完成:
$ sudo invoke-rc.d slapd stop
现在服务器已经停止,我们可以转储数据库。
步骤 2:转储数据库
在第三章中,我讲解了 OpenLDAP 的工具。我们讨论的一个工具是 slapcat
程序,它用于将目录内容转储到 LDIF 文件中。这正是我们在本步骤中使用的工具。
为什么使用 slapcat
而不是 ldapsearch
?有两个原因。
首先,slapcat
会保留 LDAP 服务器使用的所有属性(以及记录),包括存储的操作属性。(那些在运行时生成的操作属性不会被 slapcat
生成,这很好。我们反正不想导入这些属性。)
其次,slapcat
直接访问数据库,而不是打开与服务器的 LDAP 连接。这意味着 ACL、时间和大小限制,以及 LDAP 连接的其他副产品不会被评估,因此不会更改数据。
BDB/HDB 数据库存储在位于 /var/lib/ldap
(如果你是从源代码构建的,则为 /usr/local/var/openldap-data
)的小文件集中。通常只有 SLAPD 用户的 ID 可以访问这些文件,默认情况下该用户为 root
或 ldap
。为了使用 slapcat
提取信息,你需要访问这些文件。
我们有这个命令:
$ sudo slapcat -l /tmp/backup.ldif
该命令以 root 用户身份执行 slapcat
。-l
标志用于传入输出文件的名称。在这种情况下,文件 backup.ldif
将被创建在 /tmp
目录中。
注意
你可能更倾向于将 LDIF 文件放在 /tmp
以外的文件夹中,特别是如果你打算保留 LDIF 文件超过几分钟的话。
在大多数情况下,-l
标志是唯一需要使用的。如果你有多个后端,并且只想转储一个后端,可以使用 -n
标志指定要转储的后端。
一旦 slapcat
完成,我们就完成了这一步。
然而,在继续之前,你可能希望检查 LDIF 文件的内容,以确保它没有损坏。请在删除数据库文件之前执行此操作。
第 3 步:删除旧的数据库文件
如果你正在重建数据库,你需要在构建新数据库之前删除旧的数据库文件。
注意
如果你是从旧服务器迁移到新服务器,或者配置 SyncRepl 阴影服务器,则无需执行此操作。
这些文件存储在 /var/lib/ldap
(如果你是从源代码构建的,则为 /usr/local/var/openldap-data
)目录下。然而,并不是该目录中的所有文件都应该被删除。我们只想删除以下文件:
-
索引文件:以 '
.bdb
' 结尾的文件。 -
主数据库文件:以
__db.???
命名的文件,其中问号被顺序的数字替代(如__db.001
、__db.002
等)。 -
alock
文件:用于内部存储锁定信息的文件。(通常可以保持不变,不会产生负面后果,但如果 SLAPD 崩溃,这个文件可能会处于不稳定状态。) -
BDB 日志文件:以
log.??????????
命名的文件,其中十个问号被顺序的数字替代:log.0000000001
、log.0000000002
等。
有一个文件是我们绝对不想删除的,那就是我们的数据库配置文件 DB_CONFIG
。删除它会导致 BDB 引擎使用默认设置,这些设置没有针对我们的需求进行调整,通常会导致 OpenLDAP 性能不佳。
所以,为了删除文件,我们可以执行以下操作:
$ cd /var/lib/ldap
$ sudo rm __db.* *.bdb alock log.*
为了减少数据丢失的风险,你可能希望在删除这些文件之前备份 __db.*
、*.bdb
和 log.*
文件。或者,你可以使用 mv
命令将文件移动到其他位置,而不是使用 rm
删除它们:
$ cd /var/lib/ldap
$ sudo mkdir backup/
$ sudo mv *.bdb log.* alock __db.* backup/
现在数据库目录已经清理完毕,我们可以开始创建新的数据库文件了。
第 4 步:创建新数据库
新数据库可以通过使用我们在第三章中介绍的 slapadd
工具,在一个步骤中创建并填充数据。仍然在 OpenLDAP 数据目录下,运行以下命令:
$ sudo slapadd -l /tmp/backup.ldif
这将创建所有必要的文件,导入 LDIF 文件,并处理所有的数据索引。
注意
如果你以 root 以外的用户运行 LDAP 服务器(并且这样做是个好主意),你还需要使用chown
命令更改/var/lib/ldap
下所有文件的所有权,使其归 SLAPD 用户 ID 所有:sudo
chown
openldap
*.bdb
log.*
__db.*
。
现在我们需要做的就是重启服务器。
第 5 步:重启 SLAPD
如果你在步骤 1 中停止了服务器,你需要重新启动它。
以常规方式之一重新启动服务器。使用初始化脚本通常是最好的方法:
$ sudo invoke-rc.d slapd start
就是这样。现在你应该已经有了 SLAPD,并且数据库已更新。
故障排除重建
只要通过slapcat
导出的 LDIF 文件没有问题,这个过程通常不会出错。即使需要删除并重新创建多次,只要 LDIF 文件安全,重要数据就不会受到威胁。
如果 SLAPD 是以非root
用户身份运行的,那么导入过程中的主要问题通常是/var/lib/ldap
下数据库文件的权限。/etc/ldap
目录中的配置文件权限也可能是 SLAPD 失败的原因。确保这些文件归适当的用户所有。
在切换 OpenLDAP 版本时,偶尔旧的 LDIF 文件在新服务器中无效(这种情况发生在 OpenLDAP 2.0 和 OpenLDAP 2.2 之间,又发生在 2.2 和 2.3 之间;未来可能还会发生)。虽然标准模式在时间上相对稳定,但操作属性通常没有标准化,变化更为频繁,并且在不同版本之间会有所变化。
通常,修复方法是调整 LDIF 文件中的记录,以匹配新版本中使用的属性。另一个常见问题是与启动服务器有关。有时,在使用初始化脚本时,可能无法启动服务器,但不会向控制台或日志文件发送任何有用的消息。(启动失败的一个常见原因是我之前提到的权限问题)。
解决启动问题的一个好的第一步是从命令行运行slapd
,并启用调试:sudo slapd -d trace
。
摘要
在本附录中,我们介绍了一些有用的命令,包括一些用于获取目录服务器详细信息的命令。此外,我们还介绍了两种制作目录备份的方法,并详细探讨了重建目录数据库的过程。