在Linux上使用slapd搭建LDAP服务

随着服务器托管服务的增加,无论是新建容器导入用户数据还是修改用户权限,用户管理成为了一件耗时且繁琐的工作。因此使用LDAP服务来统一管理用户和用户组显得具有现实意义。这篇文章将介绍使用slapd在Ubuntu Linux上搭建LDAP服务,并面向新手简单介绍LDAP基本原理、Apache Directory Studio的基本用法。

LDAP简介

LDAP,Lightweight Directory Access Protocol,轻量目录访问协议。什么叫“目录”?你可以简单理解成一个号码本或者通讯录,里面记录关于每个账户的详细资料,包括且不限于记录邮箱、认证密码、SSH密钥等信息。

但是需要注意,LDAP本身仅仅是一个协议,只是描述了数据的组织方式以及如何获取这些数据。目录数据存储在目录数据库中,这些数据库才是真正需要我们管理和配置的。因为LDAP在企业管理中实在是太方便,所以许多公司开发了自己的数据库,比较出名的就是微软的Active Directory了。而在Linux下,我们可以使用OpenLDAP组织旗下的slapd

slapd搭建和配置

安装slapd

无论是编译安装还是通过软件仓库安装也好,自己能搞定就好。在Ubuntu上若使用apt安装:

apt install slapd

安装过程中,可能还会提示你创建管理员密码。后面再讨论。

配置slapd

在安装好slapd后,来看看配置文件结构是怎样的:

/etc/ldap
|-- ldap.conf
|-- sasl2
|-- schema
|   |-- README
|   |-- 一大堆xxx.ldif
|   `-- 一大堆xxx.schema
`-- slapd.d
    |-- cn=config
    |   |-- cn=module{0}.ldif
    |   |-- cn=schema
    |   |   |-- cn={0}core.ldif
    |   |   |-- cn={1}cosine.ldif
    |   |   |-- cn={2}nis.ldif
    |   |   `-- cn={3}inetorgperson.ldif
    |   |-- cn=schema.ldif
    |   |-- olcDatabase={-1}frontend.ldif
    |   |-- olcDatabase={0}config.ldif
    |   `-- olcDatabase={1}mdb.ldif
    `-- cn=config.ldif

5 directories, 39 files

第一眼看到这个目录结构,是不是感觉眼花缭乱?不必紧张,我们目前不用修改任何配置。

直接dpkg重新自动配置,配置过程中会提示输入一系列信息,能帮我们直接配置好大部分设置:

dpkg-reconfigure slapd
  • Omit OpenLDAP server configuration?No
  • DNS domain name:输入你的域名称,比如example.com
  • Name of your organization:可以自定义组织名,比如设置成my-example-test
  • 创建管理员密码
  • Do you want your database to be removed when slapd is purged?用apt purge删除slapd软件包时删除数据库?建议No
  • Remove old database?建议Yes

如果不重新配置,LDAP会获取Linux系统配置的域名作为默认Base DN,比如local,localdomain,也可能是从路由器上获取的。

恭喜,slapd基本安装已经完成。下一步就是学习如何管理和维护目录数据库了。

LDAP管理和维护

安装和配置Apache Directory Studio

其实不必安装,直接下载就能运行。官网:https://directory.apache.org/studio/

直接打开Apache Directory Studio
新建连接
新建连接

点击“测试”,如果都没有问题,就可以点击完成了。

双击连接并打开,就可以看到目录数据库里的内容了:

目录数据库内容

现在我们可以自由浏览数据库内容了,接下来需要学习LDAP一些基础概念,以及管理知识。

LDAP基础知识

LDAP结构

LDAP使用树形结构组织数据。比如按照域名—域名—组织—组织单元—uid方式由上到下组织。

objectClass:一个对象的类型。top对象表示总的根结点。根下继续连接多种objectClass最终构成整个树。常见的objectClassdcObjectorganizationalUnituidObject等。LDAP自带了一些类型,用户也可以通过schema文件添加自定义的类型。常见的有:

  • top:表示当前对象是一个结点,还可向下创建分支
  • organizationUnit:组织单元,提供ou、I、st、fax等属性值
  • person:人,提供cn、sn、surname、telephoneNumber等属性值

Attribute:属性。是一个objectClass中的内容。常见的属性有:

  • dn:distinguished name,表示一个对象的唯一标识,类似于cn=admin,dc=example,dc=com
  • rdn:relative dn,表示相对标识
  • uid:用户的登录名
  • sn:surname,姓
  • giveName:全名
  • dc:domain component,域名,形如dc=example,dc=com
  • ou:organization unit,组织单元名称
  • cn:common name,对象名称,人的话使用全名
  • mail:登录帐号的邮箱
  • telephoneNumber:登录帐号的手机地址
  • c:country,国家代号
  • I:地名
  • o:organization,组织
  • objectClass:是一个特殊属性,包含数据存储方式等相关属性信息,同时用来指示这个对象支持哪些属性值。

总的来说,LDAP目录就像一本大文件夹,objectClass就像已经印好的资料卡,上面有许多属性(Attribute)可供填写。不同类资料卡上面可供填写的信息也不一样。每个人可以有多种资料卡,要指示这个人有哪些资料卡,在这个人的“objectClass”属性中定义。注意前面的objectClass是代词,指代一系列不同类型的卡片,后面的objectClass是名词,记录这个人有哪些类型的卡片。

下面的例子可以使你更加容易理解:

实战:尝试组织一个公司架构

打开Apache Directory Studio,建立连接:

Apache Directory Studio主界面

可以看到,这个条目(dc=example,dc=com)具有2个属性,分别是dc和o。并有3个objectClass:dcObject、organization、top。目前dcObject提供dc属性,organization提供o属性,当然它们还可以提供更多其他的属性。top表示可以在这个条目(dc=example,dc=com)下新增结点。

在LDAP Browser中右键你的域名,点击New Entry。我们先添加总的管理机构,比如人事处和技术部:

结果

可以看到成功添加了两个Organizaion:人事和技术。(仅作演示,实际应用中使用非ASCII字符,编码会比较困难)

在技术下继续添加组织单元,名称为项目组A和项目组B,并分别在下面添加几个人:

添加为organizationalPerson
添加common name为Alex

organizationPerson这个objectClass要求sn(surname)为必填项,填上后完成:

Alex添加成功

假设刚刚我们添加的是Alex Green,下面需要添加一个用户Alex Brown,怎么办呢?这样就好了:

同时使用两个值来构建dn

可以看到DN Preview一栏,同时使用了两个值。把之前创建的Alex的DN修改为Alex Brown:

修改之前的Alex结果

需要注意的是,LDAP使用dn来精确定位用户。也就是说,对一个用户进行认证时,需要提供完整的dn,只提供一部分是没法定位的。在添加用户时,无论是使用图形化还是ldif,都需要注意这一点,不要错将一些无关的值加进了dn而导致认证出错。

此外还可以进行移动、查找等操作,这里不再介绍。

slapd动态配置

传统配置slapd的方法是编辑/etc/ldap/slapd.conf文件,但是新版本加入了动态配置的功能并被建议使用。动态配置后无需重启服务,更加灵活。动态配置存储在独立的树形数据库,而访问数据库需要使用LDIF文件。所以我们需要学习编写LDIF文件来控制LDAP数据库行为。

LDIF文件编写

LDIF文件包含了对LDAP数据库的操作。基本格式如下例:

LDIF文件格式例子(密码为“aaa”)

如何将LDIF文件传递给服务器呢?使用以下命令:

ldapadd -Y EXTERNAL -H ldapi:/// -f lalala.ldif
# 或者
ldapadd -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W -f lalala.ldif

这样就新建了一个条目。命令的具体解释见下下节。

当需要修改一个条目时,可以这样编写LDIF文件:

dn: cn=manager,dc=example,dc=com
changetype: modify
add: loginShell
loginShell: /bin/bash

dn: cn=manager,dc=example,dc=com
changetype: modify
replace: sn
sn: newnamelalala

更多可见:https://segmentfault.com/a/1190000002607146

动态配置简介

slapd配置树结构

所有的配置都以树的方式组织并存储在数据库中。默认只能使用LDIF文件并使用EXTERNAL认证来修改:

ldapadd -Y EXTERNAL -H ldapi:/// -f lalala.ldif

比如需要修改slapd日志级别为128,就需要编写lalala.ldif

dn: cn=config
changetype: modify
add: oldLogLevel
olcLogLevel: 128

然后执行上面的ldapadd来应用。

在ApacheDS中查看动态配置

动态配置默认是不允许通过网络访问的,但仍然有方法通过网络或者普通身份认证的方式访问。事实上,要普通地通过用户名密码访问一个数据库,只需配置好这个LDAP数据库下的olcRootDNolcRootPW。如何配置呢?想必你已经有所眉目:将这两个属性添加进LDIF文件,然后通过EXTERNAL认证方式送往服务器,就可以了。

首先生成密码:

slappasswd

记录下加密后的密码,然后编写LDIF文件 (参考资料[1]):

#先检查你的数据库是否有oldRootDN属性,如果有,才需要执行以下注释的
#检查方法:ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config,查看输出有没有olcRootDN
#dn: olcDatabase={0}config,cn=config
#changetype: modify
#add: olcRootDN
#olcRootDN: cn=admin,cn=config

dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: 刚刚生成的密码

执行LDIF文件:

ldapadd -Y EXTERNAL -H ldapi:/// -f foo.ldif

完成后,使用DN和刚刚创建的密码尝试登陆:

ldapsearch -b cn=config -D cn=admin,cn=config -W

在ApacheDS中新建连接,认证处填入新的dn和密码,在第三步注意取消勾选获取Base DN,填入cn=config的DN:

第三步需要手动配置Base DN

完成,可以使用ApacheDS管理动态配置了。注意直接修改动态配置可能会带来不可恢复的影响,所以修改之前一定要做好备份。备份方法很简单,在ApacheDS中直接右键导出即可。

全部可用的动态配置可见https://www.openldap.org/doc/admin24/guide.html#Configuring%20slapd

LDAP访问控制

目前状况下,只有admin用户可以访问并修改数据库。如果直接将admin用户名和密码交给一些客户端,显然非常不安全。或按需给项目负责人一些权限,允许其控制该小组下的成员而不允许访问更高级别的人员。因此对用户权限进行控制是不可或缺的一环。下面介绍一些基础内容。

传统的slapd访问控制在配置文件中设置。但是新版本的已经已经开始使用动态配置,且默认不再加载传统配置文件。我们可以先看看这ACL规则到底长什么样:

使用olcAccess属性定义ACL规则

我们要修改的就是主数据库(mdb)的oldAccess属性来控制访问权限。可以看到,ACL规则可以非常长,所以还是推荐使用LDIF文件编辑ACL规则。

这里记录olcAccess属性的默认值,也是一个样例:

dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcAccess
olcAccess: to attrs=userPassword
    by self write
    by anonymous auth 
    by * none
olcAccess: to attrs=shadowLastChange
    by self write 
    by * read
olcAccess: to *
    by * read

可以看到默认规则的最后一条匹配了所有情况,所以自己的规则一定要放在这之前。这也是不使用静态配置的原因,因为可能会产生冲突。

ACL规则编写

这里只介绍比较基本的,具体可以参考官方文档:https://www.openldap.org/doc/admin24/guide.html#Access%20Control%20via%20Dynamic%20Configuration

语法

olcAccess: to <what>
    [by <who> [<access>] [<control>] ]

what 匹配访问的数据。可以使用以下方式:

to dn="o=GuroupA,dc=example,dc=com"       # 待匹配DN与给定值完全相同
to dn.exact="o=GuroupA,dc=example,dc=com" # 待匹配DN与给定值完全相同
to dn.regex="o=[^,]+,dc=example,dc=com"  # 正则匹配
to dn.base="o=GroupA,dc=example,dc=com" # 待匹配DN与给定值完全相同
to dn.subtree="o=GroupA,dc=example,dc=com"  # 待匹配DN是给定值DN的子节点,或者它本身
to dn.children="o=GroupA,dc=example,dc=com" # 待匹配DN是给定值DN的子节点,不包括它本身
to dn.one="o=GroupA,dc=example,dc=com" # 待匹配DN是给定值DN的一级子节点,比如ou=a,o=GroupA,dc=example,dc=com可以匹配,而ou=ab,ou=a,o=GroupA,dc=example,dc=com 不匹配

to filter=(objectClass=organization)  # 待匹配DN具有给定值子串
to attrs=cn,sn   # 待匹配DN具有某些属性

to dn.subtree="o=admin,dc=example,dc=com" attrs=cn,sn filter=(ou=groupa) # 可同时匹配多个

to *   # 匹配所有DN

who 为授权的用户

by anonymous  # 匿名用户
by users  # 认证的用户
by self   # what自身
by dn.exact|regex|base|subtree... # 匹配用户DN,同上但不包括filter,attrs
by *  # 匹配所有用户

access 为授予权限,从低到高排序为:

none     # 拒绝访问
disclose # 允许返回错误信息
auth     # 允许认证用户访问(bind方式)
compare  # 允许比较属性值
search   # 允许执行搜索,但结果不一定能被读取
read     # 允许读取搜索结果
write    # 允许修改/重命名
manage   # 运行管理

control 控制匹配行为,可选

stop   # 匹配到后停止匹配(默认)
continue # 继续向后匹配
break    # 跳出当前子句

常用正则表达式

[^,]表示忽略一个字符,遇到“,”则停止。后面添加一个“+”表示连续匹配,即[^,]+表示忽略一个字符串,直到遇到“,”停止。

使用“()”将一个字符包围起来,并在最后面加一个“$”,表示将这串字符存储为变量。

以下是一个例子:

olcAccess: to dn.regex="cn=[^,]+,ou=[^,]+,ou=([^,]+),dc=example,dc=com$" attrs=userPassword
    by self write
    by dn.children,expand="ou=manager,ou=$1,dc=example,dc=com" write
    by dn="cn=manager,dc=example,dc=com" write
    by anonymous auth 
    by * none

上面的ACL表示:匹配被访问dn,且访问的属性为用户密码时,应用以下策略:如果是用户自己允许写入,如果是同组管理员允许写入,如果是全局管理员允许写入,如果是匿名允许认证,其余的不允许访问。

注意,LDIF文件中:不允许随意空行,空行表示另一批操作了;不允许使用TAB来缩进;换行后第一个空格会被吞掉,小心不要将字符连起来了。

可以在https://www.runoob.com/regexp/regexp-syntax.html中的实时工具中测试正则表达式:

正则表达式在线测试工具(runoob)

LDAP常用命令和测试

这一节介绍常用的命令来测试LDAP连接和身份验证。

认证方式

身份验证这里简单介绍两种方式,一种是使用简单身份验证,即提供用户名(dn)和密码即可,另一种是使用EXTERNAL认证。外部认证传递系统用户信息,比如通过root用户执行ldap相关命令时,可以将root用户的权限传递到内部。

认证方式还有很多种,这里暂时不谈。

简单身份认证常用认证命令格式:D-绑定的身份,W-使用命令提示来输入密码

ldapsearch -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W ...

外部认证常用认证命令格式:

ldapsearch -H ldapi:/// -Y EXTERNAL ...

常用命令

ldapsearch
-H:指定URI
-D:绑定的身份
-x:使用简单身份验证
-W:使用命令提示符输入密码
-b:要搜索的DN

# 搜索
ldapsearch -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W -b "cn=readonly-manager,dc=example,dc=com"

# readonly-manager, example.com
dn: cn=readonly-manager,dc=example,dc=com
cn: readonly-manager
sn: manager
objectClass: person

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

ldapwhoami

ldapwhoami -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W
Enter LDAP Password: 
dn:cn=admin,dc=example,dc=com

ldapcompare

ldapcompare -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W cn=readonly-manager,dc=example,dc=com sn:readonly
Enter LDAP Password: 
FALSE

ldapcompare -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W cn=readonly-manager,dc=example,dc=com sn:manager 
Enter LDAP Password: 
TRUE

ldapcompare -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W cn=readonly-manager,dc=example,dc=com in:manager
Enter LDAP Password: 
Compare Result: Undefined attribute type (17)
UNDEFINED

ldappasswd
-A:提示输入旧密码
-S:设置新密码
Note:该方式默认使用SSHA(明文密码加盐值再SHA)方式存储密码。

ldappasswd -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W "cn=readonly-manager,dc=example,dc=com" -S
New password: 
Re-enter new password: 
Enter LDAP Password: 管理员密码

ldappasswd -H ldap://127.0.0.1:389/ -D "cn=admin,dc=example,dc=com" -x -W "cn=readonly-manager,dc=example,dc=com" -S -A
Old password: 用户旧密码
Re-enter old password: 
New password: 用户新密码
Re-enter new password: 
Enter LDAP Password: 管理员密码

LDAP Schema管理

Schema文件定义了所有的objectClass,如果需要拓展LDAP功能,如用于sudo认证,则需要导入sudo提供的schema。这里就以sudo为例,导入其schema。

对于Ubuntu,可以通过安装sudo-ldap的方式提取schema文件:

apt install sudo-ldap

schema文件位于/usr/share/doc/sudo-ldap/schema.OpenLDAP,将其复制到/etc/ldap/schema目录下:

cp /usr/share/doc/sudo-ldap/schema.OpenLDAP /etc/ldap/schema/sudo.schema

获取到了schema文件,接下来就要将其导入数据库了。而数据库只能接受LDIF数据输入,所以首先需要将schema文件转换成LDIF文件。这里要迂回一下,使用slaptest工具转换:

# 创建临时工作文件夹
mkdir /tmp/ldap
# 编辑配置文件
echo "include /etc/ldap/schema/sudo.schema" > /tmp/ldap-sudo.conf
# 生成配置文件夹
slaptest -f /tmp/ldap.conf -F /tmp/ldap
# 将文件夹内文件导出
cp /tmp/ldap/cn\=config/cn\=schema/cn\=\{0\}sudo.ldif /etc/ldap/schema/sudo.ldif

编辑sudo.ldif前面三行:

dn: cn={0}sudo
objectClass: olcSchemaConfig
cn: {0}sudo

修改后:

dn: cn=sudo,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: sudo

并删除后面的自动生成信息:

structuralObjectClass: olcSchemaConfig
entryUUID: f8581bba-1e88-103c-972d-af95ffd9847f
creatorsName: cn=config
createTimestamp: 20220210064650Z
entryCSN: 20220210064650.853161Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20220210064650Z

通过动态配置的方式,以EXTERNAL方式认证并传递LDIF文件:

ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/sudo.ldif

通过ApacheDS查看配置,可以看到sudo已经被添加:

sudo已经显示在配置中

但此时ApacheDS并不知道服务器schema已经增加,需要刷新一下:

重新加载schema

现在就可以增加sudoRole了:

添加sudoRole示例

参考资料

[1] https://gos.si/blog/installing-openldap-on-debian-squeeze-with-olc/

[2] https://www.openldap.org/doc/admin24/guide.html#Access%20Control%20via%20Dynamic%20Configuration

[3] slapd官方文档:https://www.openldap.org/doc/

[4] LDIF修改ldap记录或配置示例:https://segmentfault.com/a/1190000002607146

发表评论