随着服务器托管服务的增加,无论是新建容器导入用户数据还是修改用户权限,用户管理成为了一件耗时且繁琐的工作。因此使用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/
点击“测试”,如果都没有问题,就可以点击完成了。
双击连接并打开,就可以看到目录数据库里的内容了:
现在我们可以自由浏览数据库内容了,接下来需要学习LDAP一些基础概念,以及管理知识。
LDAP基础知识
LDAP结构
LDAP使用树形结构组织数据。比如按照域名—域名—组织—组织单元—uid方式由上到下组织。
objectClass
:一个对象的类型。top
对象表示总的根结点。根下继续连接多种objectClass
最终构成整个树。常见的objectClass
有dcObject
,organizationalUnit
,uidObject
等。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,建立连接:
可以看到,这个条目(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,并分别在下面添加几个人:
organizationPerson这个objectClass要求sn(surname)为必填项,填上后完成:
假设刚刚我们添加的是Alex Green,下面需要添加一个用户Alex Brown,怎么办呢?这样就好了:
可以看到DN Preview一栏,同时使用了两个值。把之前创建的Alex的DN修改为Alex Brown:
需要注意的是,LDAP使用dn来精确定位用户。也就是说,对一个用户进行认证时,需要提供完整的dn,只提供一部分是没法定位的。在添加用户时,无论是使用图形化还是ldif,都需要注意这一点,不要错将一些无关的值加进了dn而导致认证出错。
此外还可以进行移动、查找等操作,这里不再介绍。
slapd动态配置
传统配置slapd的方法是编辑/etc/ldap/slapd.conf
文件,但是新版本加入了动态配置的功能并被建议使用。动态配置后无需重启服务,更加灵活。动态配置存储在独立的树形数据库,而访问数据库需要使用LDIF文件。所以我们需要学习编写LDIF文件来控制LDAP数据库行为。
LDIF文件编写
LDIF文件包含了对LDAP数据库的操作。基本格式如下例:
如何将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
动态配置简介
所有的配置都以树的方式组织并存储在数据库中。默认只能使用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数据库下的olcRootDN
和olcRootPW
。如何配置呢?想必你已经有所眉目:将这两个属性添加进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:
完成,可以使用ApacheDS管理动态配置了。注意直接修改动态配置可能会带来不可恢复的影响,所以修改之前一定要做好备份。备份方法很简单,在ApacheDS中直接右键导出即可。
全部可用的动态配置可见https://www.openldap.org/doc/admin24/guide.html#Configuring%20slapd
LDAP访问控制
目前状况下,只有admin
用户可以访问并修改数据库。如果直接将admin
用户名和密码交给一些客户端,显然非常不安全。或按需给项目负责人一些权限,允许其控制该小组下的成员而不允许访问更高级别的人员。因此对用户权限进行控制是不可或缺的一环。下面介绍一些基础内容。
传统的slapd
访问控制在配置文件中设置。但是新版本的已经已经开始使用动态配置,且默认不再加载传统配置文件。我们可以先看看这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中的实时工具中测试正则表达式:
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
已经被添加:
但此时ApacheDS并不知道服务器schema已经增加,需要刷新一下:
现在就可以增加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