早期,公司是没有统一认证这个东西的,所以各自玩各自的。于是, confluence 一个用户体系,gitlab 一个用户体系,Jenkins 一个用户体系等等, 开发中要用到的开源软件数不胜数,每个软件都要认证, 必须想办法统一账号。
第三系统的认证通常都是配置化的, 比如 oauth, openid, ldap。兼容最广泛就是 ldap了,虽然是很老的系统(LDAPv3 was developed in the late 1990’s to replace LDAPv2.),最后还是要使用它。
环境说明
- 宿主机:CentOS 7.7.1908 x86_64
- Docker: 19.03.4
- docker-compose: 1.21.2
- OpenLDAP 镜像: osixia/openldap:1.3.0
- phpldapadmin 镜像:osixia/phpldapadmin:latest
- 默认系统操作用户:root
- phpldapadmin 地址:http://HostIP:8080
一、 LDAP 简介
在开始使用之前再明确一下我们的目标。为了统一公司内部的账号登录体系。公司的人员组织架构假设是这样子的:
.
└── company
├── ceo-Ryan
├── HR
│ └── hr-a
├── 市场
│ └── a
├── 研发
│ ├── cto-Ryan
│ ├── dev1
│ │ ├── dev-Ryan
│ │ ├── dev-Ryan2
│ │ └── manager-Ryan
│ ├── dev2
│ └── dev3
└── 行政
实际上会复杂的多,简化的来看分为部门和成员。ldap就是存储这样的数据结构 tree.
咬牙看了很多博客,依旧云里雾里的感觉,最终决定上手尝试,并完整记录整个过程来加深理解。接下来将以一个什么都不懂的角色开始探索和使用 ldap。
以下概念主要面向理解,去除了不关注的官方完整介绍,旨在能够快速了解我们要用的东西是什么样,如果需要更完整的概念介绍,参考后面的参考文献即可。
什么是 LDAP
LDAP 是轻量目录访问协议,英文全称是Lightweight Directory Access Protocol,一般都简称为 LDAP。按照我们对文件目录的理解,LDAP 可以看成一个文件系统,类似目录和文件树。
LDAP 的软件
LDAP 并不是一款软件,而是一个协议。
现在市场上有关 LDAP 的产品已有很多,各大软件公司都在他们的产品中集成了 LDAP 服务,如:
- Microsoft 的 ActiveDirectory
- Lotus 的 Domino Directory
- IBM 的 WebSphere
LDAP 的开源实现是 OpenLDAP,它比商业产品一点也不差,而且源码开放。
OpenLDAP 是最常用的目录服务之一,它是一个由开源社区及志愿者开发和管理的一个开源项目,提供了目录服务的所有功能,包括:目录搜索、身份认证、安全通道、过滤器等等。大多数的 Linux 发行版里面都带有 OpenLDAP 的安装包。
OpenLDAP 服务默认使用非加密的 TCP/IP 协议来接收服务的请求,并将查询结果传回到客户端。由于大多数目录服务都是用于系统的安全认证部分比如:用户登录和身份验证,所以它也支持使用基于 SSL/TLS 的加密协议来保证数据传送的保密性和完整性。
OpenLDAP 是使用 OpenSSL 来实现 SSL/TLS 加密通信的。
ldap 的信息模型
【重要部分】
LDAP 的信息模型是建立在”条目”(entries)的基础上。一个条目是一些属性的集合,并且具有一个全局唯一的”可区分名称”DN,一个条目可以通过DN来引用。每一个条目的属性具有一个类型和一个或者多个值。
类型通常是容易记忆的名称,比如:”cn”是通用名称( common name ) ,或者”mail”是电子邮件地址。
条目的值的语法取决于属性类型。比如:
- cn 属性可能具有一个值 “Babs Jensen” 。
- 一个mail属性可能包含 “bbs@kevin.com” 。
- 一个 jpegphoto 属性可能包含一幅 JPEG(二进制)格式的图片。
LDAP 的 objectClass
LDAP 通过属性 objectClass 来控制哪一个属性必须出现或允许出现在一个条目中,它的值决定了该条目必须遵守的模式 规则(可以理解为关系数据库的表结构)。接下来会用到的 objectClass 有
objectClass | 含义 |
---|---|
olcGlobal | 全局配置文件类型, 主要是 cn=config.ldif 的配置项 |
top | 顶层的对象 |
organization | 组织,比如公司名称,顶层的对象 |
organizationalUnit | 重要, 一个目录节点,通常是 group,或者部门这样的含义 |
inetOrgPerson | 重要, 我们真正的用户节点类型,person 类型, 叶子节点 |
groupOfNames | 重要, 分组的 group 类型,标记一个 group 节点 |
olcModuleList | 配置模块的对象 |
LDAP 常用关键字列表
关键字 | 英文全称 | 含义 |
---|---|---|
dc | Domain Component | 域名的部分,其格式是将完整的域名分成几部分,如域名为 example.com 变成 dc=example,dc=com |
uid | User Id | 用户 ID,如“tom” |
ou | Organization Unit | 组织单位,类似于 Linux 文件系统中的子目录,它是一个容器对象,组织单位可以包含其他各种对象(包括其他组织单元) |
cn | Common Name | 公共名称,如 “Thomas Johansson” |
sn | Surname | 姓,如 “Johansson” |
dn | Distinguished Name | 惟一辨别名,类似于 Linux 文件系统中的绝对路径,每个对象都有一个惟一的名称,如“uid= tom,ou=market,dc=example,dc=com”,在一个目录树中 DN 总是惟一的 |
rdn | Relative dn | 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson” |
c | Country | 国家,如“CN”或“US”等。 |
o | Organization | 组织名,如“Example, Inc.” |
我们把:
- dn 当做用户唯一主键
- cn 是 common name,应该等同于用户名,因为用户名必须唯一,通常为邮箱前缀,比如: ryan.miao
- sn 作为姓氏
- uid 作为用户 id
通常用户 id 也是唯一的。所以在使用 ldap 做认证的时候,大概逻辑如下:
- 配置 ldap host, admin, admin pass
- 用户登录时传递 username
- 读取配置的 ldap 信息,查询 cn 或者 uid 等于 username 的数据
- 取出第一个记录, 获得 dn, 根据 dn 和 password 再次去 ldap 服务器认证。即我们必须保证 cn 或 uid 是全局唯一的,
- 认证通常需要进行两次。原因就在于 dn 没办法根据用户名计算出来。
一个 ldap 用户组织可能是这样的:
一个倒桩树组成结构。
二、实战篇
基于 Docker 容器镜像。
1. 安装 Docker、Docker-Compose
# 生成一键安装脚本
$ tee 1key_install_docker.sh <<EOOF
#!/bin/bash
#生成工作目录
mkdir -p /server/{tools,scripts,backup,docker-compose}
##更换阿里云源(针对国内用户,可选!)
mv /etc/yum.repos.d/CentOS-Base.repo{,.backup}
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum update -y
#安装 docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
##镜像加速
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<EOF
{
"registry-mirrors": [
"https://1nj0zren.mirror.aliyuncs.com",
"https://docker.mirrors.ustc.edu.cn",
"https://registry.docker-cn.com"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
systemctl enable docker
## check
docker version
##安装最新 docker-compose
curl -L https://mirrors.aliyun.com/docker-toolbox/linux/compose/$(curl -s https://mirrors.aliyun.com/docker-toolbox/linux/compose/ |egrep '^<a' |awk -F '">|</a>' '{print $2}' |sort -V |tail -1)docker-compose-Linux-x86_64 -o /usr/bin/docker-compose
chmod +x /usr/bin/docker-compose
docker-compose -v
EOOF
# 进行语法检查
$ sh -n 1key_install_docker.sh
# 执行安装
$ sh 1key_install_docker.sh
相关链接:https://developer.aliyun.com/mirror/docker-ce?spm=a2c6h.13651102.0.0.3e221b11yCPzpk
2. 构建 OpenLDAP 服务
$ mkdir -p /server/docker-compose/openldap
$ cd /server/docker-compose/openldap
# 生成服务编排文件
$ tee docker-compose.yml <<EOF
version: '3'
services:
openldap:
image: osixia/openldap:1.3.0
restart: always
container_name: ldap
ports:
- 389:389
- 636:636
volumes:
- /etc/localtime:/etc/localtime:ro
phpldapadmin:
depends_on:
- openldap
image: osixia/phpldapadmin:latest
container_name: phpldapadmin
environment:
PHPLDAPADMIN_LDAP_HOSTS: "openldap"
PHPLDAPADMIN_HTTPS: "false"
ports:
- "8080:80"
EOF
# 语法检查
$ docker-compose config
# 启动服务
$ docker-compose up -d
# 查看实时日志
$ docker-compose logs -f
3. 登入 openLDAP 容器,并执行相关 LDAP 操作
# 查看容器
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e53c92de7493 osixia/openldap:1.3.0 "/container/tool/run" 3 days ago Up 23 minutes 0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp ldap
# 登录容器
$ docker exec -it ldap bash
root@e53c92de7493:/#
# 为方便测试,可更换容器镜像源,并安装常用工具(仅适用国内用户)
root@e53c92de7493:/etc/ldap# tee /etc/apt/sources.list <<EOF
deb http://mirrors.163.com/debian/ buster main non-free contrib
deb http://mirrors.163.com/debian/ buster-updates main non-free contrib
deb http://mirrors.163.com/debian/ buster-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ buster/updates main non-free contrib
deb-src http://mirrors.163.com/debian/ buster main non-free contrib
deb-src http://mirrors.163.com/debian/ buster-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ buster-backports main non-free contrib
deb-src http://mirrors.163.com/debian-security/ buster/updates main non-free contrib
EOF
root@e53c92de7493:/etc/ldap# apt-get update -y
root@e53c92de7493:/etc/ldap# apt-get install tree -y
ldap 的配置文件
安装后的配置目录是: /etc/ldap, 内容包括下列这些文件:
/etc/ldap
├── ldap.conf -> /container/service/slapd/assets/ldap.conf
├── pqchecker
│ └── pqparams.dat
├── sasl2
├── schema
│ ├── collective.ldif
│ ├── collective.schema
│ ├── corba.ldif
│ ├── corba.schema
│ ├── core.ldif
│ ├── core.schema
│ ├── cosine.ldif
│ ├── cosine.schema
│ ├── duaconf.ldif
│ ├── duaconf.schema
│ ├── dyngroup.ldif
│ ├── dyngroup.schema
│ ├── inetorgperson.ldif
│ ├── inetorgperson.schema
│ ├── java.ldif
│ ├── java.schema
│ ├── misc.ldif
│ ├── misc.schema
│ ├── nis.ldif
│ ├── nis.schema
│ ├── openldap.ldif
│ ├── openldap.schema
│ ├── pmi.ldif
│ ├── pmi.schema
│ ├── ppolicy.ldif
│ ├── ppolicy.schema
│ └── README
└── slapd.d
├── changeroot.sh
├── cn=config
├── cn=config.ldif
├── docker-openldap-was-admin-password-set
├── docker-openldap-was-started-with-tls
├── rootdn.ldif
└── tmp.ldif
cn=config.ldif
默认配置文件,位于 /etc/ldap/slapd.d, 文件格式为 LDAP Input Format (LDIF), ldap 目录特定的格式。
root@e53c92de7493:/etc/ldap/slapd.d# cat cn\=config.ldif
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 15165520
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /var/run/slapd/slapd.args
olcLogLevel: none
olcPidFile: /var/run/slapd/slapd.pid
olcToolThreads: 1
structuralObjectClass: olcGlobal
entryUUID: d12ef22e-2606-103a-8748-c9a21a061f1c
creatorsName: cn=config
createTimestamp: 20200509060524Z
olcTLSCipherSuite: SECURE256:+SECURE128:-VERS-TLS-ALL:+VERS-TLS1.2:-RSA:-DHE
-DSS:-CAMELLIA-128-CBC:-CAMELLIA-256-CBC
olcTLSCACertificateFile: /container/service/slapd/assets/certs/ca.crt
olcTLSCertificateFile: /container/service/slapd/assets/certs/ldap.crt
olcTLSCertificateKeyFile: /container/service/slapd/assets/certs/ldap.key
olcTLSDHParamFile: /container/service/slapd/assets/certs/dhparam.pem
olcTLSVerifyClient: demand
entryCSN: 20200509060525.871739Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20200509060525Z
olcDatabase={1}mdb.ldif
db 存储格式,有 mdb、bdb 和 hdb 三种,这里是hdb. 可以直接查看文件,也可以查询:
root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config dn | grep olcDatabase
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
dn: olcDatabase={-1}frontend,cn=config
dn: olcDatabase={0}config,cn=config
dn: olcDatabase={1}mdb,cn=config
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config
核心配置文件,位于 /etc/ldap/slapd.d/cn=config, 可以配置域名 (olcSuffix), 管理员账号 (olcRootDN) 等。
root@e53c92de7493:/etc/ldap/slapd.d# cat cn\=config/olcDatabase\=\{1\}mdb.ldif
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 c057ffa1
dn: olcDatabase={1}mdb
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcLastMod: TRUE
olcDbCheckpoint: 512 30
olcDbMaxSize: 1073741824
structuralObjectClass: olcMdbConfig
entryUUID: d12fb894-2606-103a-8751-c9a21a061f1c
creatorsName: cn=admin,cn=config
createTimestamp: 20200509060524Z
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group
.exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
olcAccess: {1}to * by self write by group.exact="cn=g-admin,ou=Group,dc=demo
,dc=com" write by * none
olcAccess: {2}to attrs=userPassword,shadowLastChange by self write by dn="cn
=admin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: {3}to * by self read by dn="cn=admin,dc=example,dc=org" write by
* none
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: memberOf eq
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbIndex: objectClass eq
olcRootDN: cn=admin,dc=demo,dc=com
olcSuffix: dc=demo,dc=com
olcRootPW:: e1NTSEF9SmFiek9tbjFaSlc0U2hTcFVUaFNBS3BvZ3ppeCtlWXU=
entryCSN: 20200512082802.368305Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20200512082802Z
可以看到很多文件名和字段名都有前缀 “olc” (OpenLDAP Configuration), 理解就好。
创建 olcRootDN 作为管理员账号
看到前面两个配置文件,官方不推荐我们直接修改配置文件,而是通过 ldapmodify 来更新配置。
类似于 update by pk, 这里的 pk 就是 dn 了。
创建 rootdn.ldif
root@e53c92de7493:/etc/ldap/slapd.d# tee rootdn.ldif <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=admin,dc=demo,dc=com
-
replace: olcSuffix
olcSuffix: dc=demo,dc=com
-
replace: olcRootPW
olcRootPW: <pass>
EOF
- 修改 olcRootDN, 设置为我们的 admin: cn=admin,dc=demo,dc=com
- 修改 olcSuffix, 设置为我们的域名 dc=demo,dc=com
- 修改 olcRootPW, 设置我们的 admin 密码, 这个需要加密,所以暂时放一个占位符,等下替换
- changetype 变更类型, replace 表示替换, add 表示增加。
cn=config 是全局配置,必须包含 objectClass: olcGlobal
然后创建 changeroot.sh
root@e53c92de7493:/etc/ldap/slapd.d# cat changeroot.sh
admin_pass=`slappasswd -s admin`
echo "admin pass is: ${admin_pass}"
sed "s!<pass>!${admin_pass}!g" rootdn.ldif > tmp.ldif
echo "备份默认配置"
cp /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif.bak
echo "将要修改的内容:"
cat tmp.ldif
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f tmp.ldif
echo "修改后的变化"
diff /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif /etc/ldap/slapd.d/cn\=config/olcDatabase\=\{1}mdb.ldif.bak
- slappasswd -s admin 获取加密后的密码
- 备份原始文件
- ldapmodify 更新命令, -H指定host,这里ldapi:/// 表示IPC (Unix-domain socket)协议, -f 指定变更的内容。
命令文档: http://man7.org/linux/man-pages/man1/ldapmodify.1.html
使用脚本进行变更,而不是直接命令行交互式变更,这样可以更容易梳理变更逻辑, 而且可以重复使用。
验证
通过 diff,可以看到配置文件已经发生了变更
root@e53c92de7493:/etc/ldap/slapd.d# sh changeroot.sh
修改后的变化
2c2
< # CRC32 b643556d
---
> # CRC32 9b5dd3fc
7a8,9
> olcSuffix: dc=my-domain,dc=com
> olcRootDN: cn=Manager,dc=my-domain,dc=com
14,19c16,18
< olcRootDN: cn=admin,dc=demo,dc=com
< olcSuffix: dc=demo,dc=com
< olcRootPW:: e1NTSEF9Q3puZEw4QzN4aWJNQTlHeEpYV2doNEN3NHJXSm5Fb0s=
< entryCSN: 20190814074323.492640Z#000000#000#000000
< modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
< modifyTimestamp: 20190814074323Z
---
> entryCSN: 20190814074118.406339Z#000000#000#000000
> modifiersName: cn=config
> modifyTimestamp: 20190814074118Z
我们可以通过 search 语法来确定账号密码是否正确:
root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -H ldapi:/// -D "cn=admin,dc=demo,dc=com" -w admia
ldap_bind: Invalid credentials (49)
root@e53c92de7493:/etc/ldap/slapd.d# ldapsearch -H ldapi:/// -D "cn=admin,dc=demo,dc=com" -w admin
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# search result
search: 2
result: 32 No such object
# numResponses: 1
ldapsearch 查询语法:
- -H 指定 host
- -D 指定 admin 的账号,即 rootdn
- -w 指定密码
- -x 启用认证
添加我们的 base 组织结构
有了管理员,还需要配置组织结构 base.ldif。在这之前,我们需要导入一些模板。schema 类似数据库表定义,定义了字段名称和类型。
schema地址:/etc/ldap/schema
默认安装加载了 core.ldif , 我们现在加载几个想要的 schema:
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/cosine.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/nis.ldif
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/ldap/schema/inetorgperson.ldif
然后创建文件 base.ldif
# 为方便调试,讲自定义 LDIF 文件放到用户根目录 ~
root@e53c92de7493:/etc/ldap/slapd.d# cd ~
root@e53c92de7493:~# tee base.ldif <<EOF
dn: dc=demo,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o: ldap测试组织
dc: demo
dn: cn=Manager,dc=demo,dc=com
objectClass: organizationalRole
cn: Manager
description: 组织管理人
dn: ou=People,dc=demo,dc=com
objectClass: organizationalUnit
ou: People
dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group
EOF
使用 ldapadd 命令添加 base:
root@e53c92de7493:~# ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif
使用 ldapsearch 来检查内容
root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "dc=demo,dc=com"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# demo.com
dn: dc=demo,dc=com
objectClass: top
objectClass: dcObject
objectClass: organization
o:: bGRhcOa1i+ivlee7hOe7hw==
dc: demo
# Manager, demo.com
dn: cn=Manager,dc=demo,dc=com
objectClass: organizationalRole
cn: Manager
description:: 57uE57uH566h55CG5Lq6
# People, demo.com
dn: ou=People,dc=demo,dc=com
objectClass: organizationalUnit
ou: People
# Group, demo.com
dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group
# search result
search: 2
result: 0 Success
# numResponses: 5
# numEntries: 4
-x 启用认证 -D bind admin 的 dn -w admin的密码 -b basedn, 查询的基础 dn 可以看到中文被替换成 hash, 后面可以通过其他方式看到
添加人员
ou 并不能当做分组,而仅仅是组织架构的一个单元。ldap的分组都是通过单独的 group 来实现的。
添加人员对应的是树的叶子节点,使用的
oebjectClass: inetOrgPerson
添加组织部门对应的是目录,使用的
objectClass: organizationalUnit
我们要把人员添加到 ou=People,dc=demo,dc=com
下。
创建 adduser.ldif
root@e53c92de7493:~# tee adduser.ldif <<EOF
dn: ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 研发部门
dn: ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 后台组
dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: ryan.miao
departmentNumber: 1
sn: Miao
title: 大牛
mail: ryan.miao@demo.com
uid: 10000
displayName: 中文名
dn: cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: someone
departmentNumber: 1
sn: someone
title: Java工程师
mail: someone@demo.com
uid: 10001
displayName: 某人
dn: ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: 测试组
dn: cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: tester.miao
departmentNumber: 2
sn: Miao
title: 测试工程师
mail: tester@demo.com
uid: 10002
displayName: 测试某人
dn: ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: organizationalUnit
ou: HR
dn: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: fang.huang
departmentNumber: 3
sn: Huang
title: HRBP
mail: fang.huang@demo.com
uid: 10003
displayName: 黄芳
使用 ldapadd 来添加我们的用户:
root@e53c92de7493:~# ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f adduser.ldif
adding new entry "ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com"
adding new entry "ou=HR,ou=People,dc=demo,dc=com"
adding new entry "cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com"
使用 ldapsearch 来查询用户
指定唯一 id 来查询某个用户,比如 cn 唯一,则
root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "dc=demo,dc=com" "cn=ryan.miao"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=ryan.miao
# requesting: ALL
#
# ryan.miao, \E5\90\8E\E5\8F\B0\E7\BB\84, \E7\A0\94\E5\8F\91\E9\83\A8\E9\97\A8,
People, demo.com
dn:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlLGRjP
WRlbW8sZGM9Y29t
objectClass: inetOrgPerson
cn: ryan,miao
cn: ryan.miao
departmentNumber: 1
sn: Miao
title:: 5aSn54mb
mail: ryan.miao@demo.com
uid: 10000
displayName:: 5Lit5paH5ZCN
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
和前面的示例相比,多了一个参数 filter
ldapsearch -x -D “admin的dn” -w “admin的密码” -b “basedn, 最外层的分组” “search filter:”
还可以指定返回的字段
root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b "ou=HR,,dc=demo,dc=com" cn uid displayName
# extended LDIF
#
# LDAPv3
# base <ou=HR,ou=People,dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: cn uid displayName
#
# HR, People, demo.com
dn: ou=HR,ou=People,dc=demo,dc=com
# fang.huang, HR, People, demo.com
dn: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
cn: fang.huang
uid: 10003
displayName:: 6buE6Iqz
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
在配置第三方认证的时候,比如 airflow, 就是通过这样 userfilter 来 search 用户的。
添加用户密码
刚才添加用户太快,忘记添加用户密码了。这就涉及到添加用户的同时指定密码和 admin 修改密码以及用户自己修改密码三个情况了。
添加用户的时候指定密码
一个 hr 肯定太累了,添加一个新的 hr hr-ryan
创建文件 addone.ldif
root@e53c92de7493:~# tee addone.ldif <<EOF
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: hr-ryan
userPassword: 123456
departmentNumber: 3
sn: hr-ryan
title: HRBP
mail: hr-ryan@demo.com
uid: 10004
displayName: 我是猎头
EOF
执行添加
ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f addone.ldif
查询验证
root@e53c92de7493:~# ldapsearch -x -D cn=admin,dc=demo,dc=com -w admin -b dc=demo,dc=com
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-*
# requesting: ALL
#
# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
objectClass: inetOrgPerson
cn: hr-ryan
userPassword:: MTIzNDU2
departmentNumber: 3
sn: hr-ryan
title: HRBP
mail: hr-ryan@demo.com
uid: 10004
displayName:: 5oiR5piv54yO5aS0
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
可以看到,filter 里可以使用通配符。并且,用户密码被加密了。
我们前文说,第三方系统:
- 第一步: 通过 search 拿到 dn,也就是上面这一步。
- 第二步:是验证密码
验证密码是怎么做的呢?直接通过 search 语法连接 ldap,通过则证明密码正确。
root@e53c92de7493:~# ldapsearch -x -D cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com -w 123456
# extended LDIF
#
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# search result
search: 2
result: 32 No such object
# numResponses: 1
修改用户密码
管理员权限最大,可以修改任意密码。使用 ldapmodify
创建文件 updatepass.ldif
root@e53c92de7493:~# tee updatepass.ldif <<EOF
dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: ryanmiao
EOF
执行修改
root@e53c92de7493:~# ldapmodify -a -D "cn=admin,dc=demo,dc=com" -w admin -f updatepass.ldif
modifying entry "cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com"
查询确认
root@e53c92de7493:~# ldapsearch -x -D cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo, ryanmiao -b dc=demo,dc=com "cn=ryan.miao"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=ryan.miao
# requesting: ALL
#
# search result
search: 2
result: 32 No such object
# numResponses: 1
可以确认密码修改成功了,同时也暴露了一个问题,任意一个人都可以 bind 登录,然后查询所有用户的信息。后面我们将关注 acl 权限问题,让每个人只能查询自己的信息,让指定的 group 可以查询所有人的信息。
注意! 我们使用的明文作为密码存储, 这样的传输方式是不推荐的, 可以使用 sha1 来存储。
root@e53c92de7493:~# slappasswd -s ryanmiao
{SSHA}r5yzPeESGLsvX7oxQetVEpel9LhygFef
dn: cn=ryan.miao,ou=后台组,ou=研发部门,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: {SSHA}r5yzPeESGLsvX7oxQetVEpel9LhygFef
root@e53c92de7493:~# slappasswd -h {sha} -s ryanmiao
{SHA}vMV4cx3BhPVf0dRvEur3NOWIDEw=
root@e53c92de7493:~# slappasswd -h {md5} -s ryanmiao
{MD5}J3sqNCJFas5wgycX4lJPsg==
# 或者sha1
userPassword: {SHA}vMV4cx3BhPVf0dRvEur3NOWIDEw=
值得注意的是 sha1 的结果并不是通常我们用的 hex 结果,而是通过 utf8 转换的:
public static String sha1(String str)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
if (null == str || str.length() == 0) {
return null;
}
MessageDigest mdTemp = MessageDigest.getInstance("SHA1");
mdTemp.update(str.getBytes("UTF-8"));
byte[] md = mdTemp.digest();
return "{SHA}" + Utf8.decode(java.util.Base64.getEncoder().encode(md));
}
Springboot 提供了 LdapShaPasswordEncoder, 但标记为 deprecated, 理由是明文的加密算法不够安全。
我们的 ldap 由于属于同步服务,即 ldap 不负责用户信息的维护,只负责查询。需要由用户中心来同步给 ldap 信息。
这就涉及到密码的问题,用户中心没有存储用户明文的,也就是不能直接同步到ldap。好在可以获得用户密码的 sha,
通过 sha 来同步 ldap 的密码,即 ldap 中的密码是一个 sha 的方式存储的。虽然不够安全,容易被撞,但用着也还行。
如果不信任这种算法,那就不用 ldap。可以使用 oauth 的方式认证第三方系统,大部分系统已支持这种认证方案。
前面提到用户已知个人密码的情况下,如何自己修改密码。
ldappasswd -x -p 389 -D "cn=Barbara Jensen,dc=example,dc=org" -w VSzhfbwA -s 123456
我们先不关注这种行为吧,默认所有第三方系统只有登录权限。关于组织架构的维护,即 ldap 组织的更新,我们采用 其他的方案去管理,ldap 只是用来辅助第三方登录的。
即,其他系统想要修改密码之类的,统一到我们的用户中心服务去修改变更,用户中心负责把信息同步给 ldap。
添加组 Group
有人会问,我之前添加人员的时候添加了很多部门的 ou,不就是 group 吗。
是的,理论上应该是 group。但是由于我们丢了一步,没有设置 ou 的 objectClass 为 group。所以,这里单独讲 group 的故事。
ldap 的 group 是一种单独的类型 objectClass: groupOfNames, 有个字段叫做 member, value 就是 entry 的 dn。如此, 实现了 group-user 的映射关系。
我们可以通过 group 来查询 member,然而,并不能通过 user 直接获取到 group。这在配置第三方系统的时候,没办法做group 认证, 比如 airflow 要求输入 group filter, 默认通过 memberof 的属性值来获取 group。所以,理论上 user 应该有个字段叫做 memberof, value 是 group。
大家可能会觉得 dn 已经很明显的分组了好吧,为啥还要这么复杂。事实上,ldap 也提供了 Reverse Group Membership Maintenance. 由系统来维护二者的映射关系。即
- group 添加 member 的时候会自动给对应的 entry 添加 memberof 字段
- 当删除 entry 的时候,也会从 group 里删除 member 字段
这个需要单独配置,默认是不支持的。
添加 memberof 模块
创建 add_module_group.ldif
root@e53c92de7493:~# tee add_module_group.ldif<<EOF
dn: cn=module,cn=config
cn: module
objectClass: olcModuleList
olcModulePath: /usr/lib/ldap
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: memberof.la
EOF
执行添加
root@e53c92de7493:~# ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_module_group.ldif
adding new entry "cn=module,cn=config"
modifying entry "cn=module{0},cn=config"
创建 add_group_objectClass.ldif
root@e53c92de7493:~# tee add_group_objectClass.ldif <<EOF
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
EOF
执行添加
root@e53c92de7493:~# ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_group_objectClass.ldif
adding new entry "olcOverlay=memberof,olcDatabase={1}mdb,cn=config"
添加一个 group
创建 addgroup.ldif
root@e53c92de7493:~# tee addgroup.ldif <<EOF
dn: olcOverlay=memberof,olcDatabase={1}mdb,cn=config
objectClass: olcConfig
objectClass: olcMemberOf
objectClass: olcOverlayConfig
objectClass: top
olcOverlay: memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfNames
olcMemberOfMemberAD: member
olcMemberOfMemberOfAD: memberOf
root@e53c92de7493:~# cat addgroup.ldif
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
EOF
执行添加
root@e53c92de7493:~# ldapmodify -a -D "cn=admin,dc=demo,dc=com" -w admin -f addgroup.ldif
adding new entry "cn=g-admin,ou=Group,dc=demo,dc=com"
查看组
root@e53c92de7493:~# ldapsearch -x -D "cn=admin,dc=demo,dc=com" -w admin -b "ou=Group,c=com"
# extended LDIF
#
# LDAPv3
# base <ou=Group,dc=demo,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#
# Group, demo.com
dn: ou=Group,dc=demo,dc=com
objectClass: organizationalUnit
ou: Group
# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
再来查看 entry 是否添加了 memberof, ldapsearch 当不指定字段的时候,默认返回全部强制字段,memberof 不属于强制,需要单独指明!
root@e53c92de7493:~# ldapsearch -x -D "cn=admin,dc=demo,dc=com" -w admin -b "ou=People,c=com" "(|(cn=ryan.miao)(cn=hr-*))" memberof
# extended LDIF
#
# LDAPv3
# base <ou=People,dc=demo,dc=com> with scope subtree
# filter: (|(cn=ryan.miao)(cn=hr-*))
# requesting: memberof
#
# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com
# ryan.miao, \E5\90\8E\E5\8F\B0\E7\BB\84, \E7\A0\94\E5\8F\91\E9\83\A8\E9\97\A8,
People, demo.com
dn:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlLGRjP
WRlbW8sZGM9Y29t
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
可以看到,这两个人都 link 到了 admin 组。如此实现了我们的组添加和管理。
同时,再次引入了新的查询语法,filter 的正则匹配。
- (|(cn=ryan.miao)(cn=hr-*)) 表示或者满足某个条件,这里就是为了查询这两个人,另外 * 表示通配符
- (&(objectClass=inetOrgPerson)(cn=ryan.miao)) 第三方系统,比如 Python 集成 ldap 的配置,通常会有一个 basedn, 就是我们的域名了,然后 userfilter,这个 filter 就是这个。
我们通常填写 objectClass=inetOrgPerson。然后让我们配置 user_name_attr, 这就是唯一属性,我们说我们的 cn 唯一。
所以,一个 Python 的 ldap 配置,通常是这个样子的。
[ldap]
# set this to ldaps://<your.ldap.server>:<port>
uri = ldap://172.17.0.2:389
user_filter = objectClass=inetOrgPerson
user_name_attr = cn
group_member_attr = memberof
superuser_filter =
data_profiler_filter =
bind_user = cn=admin,dc=demo,dc=com
bind_password = admin
basedn = dc=demo,dc=com
cacert =
search_scope = SUBTREE
添加用户到 group
我们来创建一个 common group, 表示所有人都应该在的一个 group。
root@e53c92de7493:~# tee commongroup.ldif <<EOF
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
EOF
# 执行添加
root@e53c92de7493:~# ldapmodify -a -D "cn=admin,dc=demo,dc=com" -w admin -f commongroup.ldif
adding new entry "cn=g-users,ou=Group,dc=demo,dc=com"
到目前为止,我们添加了 2 个 group:
root@e53c92de7493:~# ldapsearch -D "cn=admin,dc=demo,dc=com" -w admin -b "dc=demo,s sub "objectClass=groupOfNames" dn member
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: objectClass=groupOfNames
# requesting: dn member
#
# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
# g-users, Group, demo.com
dn: cn=g-users,ou=Group,dc=demo,dc=com
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
接下来,我们把剩下的用户加入到 g-users 这个 group, 以后所有人加入的默认 group。
创建 addUserToGroup.ldif
root@e53c92de7493:~# tee addUserToGroup.ldif <<EOF
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
root@e53c92de7493:~# cat addUserToGroup.ldif
dn: cn=g-users,ou=Group,dc=demo,dc=com
changetype: modify
add: member
member: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
member: cn=someone,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
member: cn=tester.miao,ou=测试组,ou=研发部门,ou=People,dc=demo,dc=com
EOF
# 执行添加
root@e53c92de7493:~# ldapmodify -x -D "cn=admin,dc=demo,dc=com" -w admin -f addUserToGroup.ldif
modifying entry "cn=g-users,ou=Group,dc=demo,dc=com"
查看
root@e53c92de7493:~# ldapsearch -D "cn=admin,dc=demo,dc=com" -w admin -b "dc=demo,s sub "objectClass=groupOfNames"
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: objectClass=groupOfNames
# requesting: ALL
#
# g-admin, Group, demo.com
dn: cn=g-admin,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-admin
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
# g-users, Group, demo.com
dn: cn=g-users,ou=Group,dc=demo,dc=com
objectClass: groupOfNames
cn: g-users
member:: Y249cnlhbi5taWFvLG91PeWQjuWPsOe7hCxvdT3noJTlj5Hpg6jpl6gsb3U9UGVvcGxlL
GRjPWRlbW8sZGM9Y29t
member: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
member: cn=fang.huang,ou=HR,ou=People,dc=demo,dc=com
member:: Y249c29tZW9uZSxvdT3lkI7lj7Dnu4Qsb3U956CU5Y+R6YOo6ZeoLG91PVBlb3BsZSxkY
z1kZW1vLGRjPWNvbQ==
member:: Y249dGVzdGVyLm1pYW8sb3U95rWL6K+V57uELG91PeeglOWPkemDqOmXqCxvdT1QZW9wb
GUsZGM9ZGVtbyxkYz1jb20=
# search result
search: 2
result: 0 Success
# numResponses: 3
# numEntries: 2
从 Group 中移除 user
g-admin 是一个管理员分组,我们去掉普通用户
cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
创建 removeUserFromGroup.ldif
root@e53c92de7493:~# tee removeUserFromGroup.ldif <<EOF
dn: cn=g-admin,ou=Group,dc=demo,dc=com
changetype: modify
delete: member
member: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
# 执行删除
root@e53c92de7493:~# ldapmodify -x -D "cn=admin,dc=demo,dc=com" -w admin -f removeUserFromGroup.ldif
modifying entry "cn=g-admin,ou=Group,dc=demo,dc=com"
最终 Group 和 user 的关系
group 可以有多个 user, user 可以归属于多个 group,是多对多的关系。
group 有多个 member 字段, user 有多个 memberof 字段。
ACL权限控制
Access Control List (ACL) 表示权限控制。从前面的测试可以看到,默认是没开启权限的。任何人都可以连接查询和操作。
acl 的设置方式很多,鉴于我们并没有将 ldap 作为主要的数据存储方案,即不做过多的权限设置了,只要关掉匿名访问,只允许 read,允许个人修改个人信息就好了。更多设置方案可以参照官网。
acl 的配置文件
配置文件还是开始提到的,我们可以查看现有的配置:
root@e53c92de7493:~# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config dn
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <cn=config> with scope subtree
# filter: (objectclass=*)
# requesting: dn
#
# config
dn: cn=config
# module{0}, config
dn: cn=module{0},cn=config
# module{1}, config
dn: cn=module{1},cn=config
# schema, config
dn: cn=schema,cn=config
# {0}core, schema, config
dn: cn={0}core,cn=schema,cn=config
# {1}cosine, schema, config
dn: cn={1}cosine,cn=schema,cn=config
# {2}nis, schema, config
dn: cn={2}nis,cn=schema,cn=config
# {3}inetorgperson, schema, config
dn: cn={3}inetorgperson,cn=schema,cn=config
# {4}ppolicy, schema, config
dn: cn={4}ppolicy,cn=schema,cn=config
# {5}dhcp, schema, config
dn: cn={5}dhcp,cn=schema,cn=config
# {6}dnszone, schema, config
dn: cn={6}dnszone,cn=schema,cn=config
# {7}mail, schema, config
dn: cn={7}mail,cn=schema,cn=config
# {8}mmc, schema, config
dn: cn={8}mmc,cn=schema,cn=config
# {9}openssh-lpk, schema, config
dn: cn={9}openssh-lpk,cn=schema,cn=config
# {10}quota, schema, config
dn: cn={10}quota,cn=schema,cn=config
# {11}radius, schema, config
dn: cn={11}radius,cn=schema,cn=config
# {12}samba, schema, config
dn: cn={12}samba,cn=schema,cn=config
# {13}zarafa, schema, config
dn: cn={13}zarafa,cn=schema,cn=config
# {-1}frontend, config
dn: olcDatabase={-1}frontend,cn=config
# {0}config, config
dn: olcDatabase={0}config,cn=config
# {1}mdb, config
dn: olcDatabase={1}mdb,cn=config
# {0}memberof, {1}mdb, config
dn: olcOverlay={0}memberof,olcDatabase={1}mdb,cn=config
# {1}refint, {1}mdb, config
dn: olcOverlay={1}refint,olcDatabase={1}mdb,cn=config
# {2}memberof, {1}mdb, config
dn: olcOverlay={2}memberof,olcDatabase={1}mdb,cn=config
# search result
search: 2
result: 0 Success
# numResponses: 25
# numEntries: 24
acl 就在 dn: olcDatabase={1}mdb,cn=config, 我们可以查看具体的配置:
root@e53c92de7493:~# ldapsearch -Y EXTERNAL -H ldapi:/// -b cn=config 'olcDatabas
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <cn=config> with scope subtree
# filter: olcDatabase={1}mdb
# requesting: ALL
#
# {1}mdb, config
dn: olcDatabase={1}mdb,cn=config
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by dn="cn=a
dmin,dc=example,dc=org" write by anonymous auth by * none
olcAccess: {1}to * by self read by dn="cn=admin,dc=example,dc=org" write by *
none
olcLastMod: TRUE
olcDbCheckpoint: 512 30
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: memberOf eq
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbIndex: objectClass eq
olcDbMaxSize: 1073741824
olcRootDN: cn=admin,dc=demo,dc=com
olcSuffix: dc=demo,dc=com
olcRootPW: {SSHA}JabzOmn1ZJW4ShSpUThSAKpogzix+eYu
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
接下来,使用 modify 添加 acl 即可。
创建 addacl.ldif
root@e53c92de7493:~# tee addacl.ldif <<EOF
dn: olcDatabase={1}mdb,cn=config
changetype: modify
# 只有自己可以修改密码,不允许匿名访问,允许 g-admin 组修改
add: olcAccess
olcAccess: {0}to attrs=userPassword by self write by anonymous auth by group.exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
-
# 自己可以修改自己的信息,g-admin 可以修改任何信息
add: olcAccess
olcAccess: {1}to * by self write by group.exact="cn=g-admin,ou=Group,dc=demo,dc=com" write by * none
EOF
执行
root@e53c92de7493:~# ldapmodify -H ldapi:/// -Y EXTERNAL -f addacl.ldif
SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
modifying entry "olcDatabase={1}mdb,cn=config"
验证权限
添加一个普通用户:
root@e53c92de7493:~# tee addtwo.ldif <<EOF
dn: cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com
changetype: add
objectClass: inetOrgPerson
cn: hr-miao
userPassword: 123456
departmentNumber: 3
sn: hr-miao
title: HRBP
mail: hr-miao@demo.com
uid: 10006
displayName: 我是 HR
EOF
# 执行添加
root@e53c92de7493:~# ldapmodify -a -H ldapi:/// -D "cn=admin,dc=demo,dc=com" -w admin -f addtwo.ldif
adding new entry "cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com"
现在我们有两个用户来比较
- cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com 是普通用户
- cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com 是g-admin用户
分别来查询:
root@e53c92de7493:~# ldapsearch -H ldapi:/// -x -D "cn=hr-ryan,ou=HR,ou=People,dc=demo,w 123456 -b "dc=demo,dc=com" "cn=hr-ryan" dn memberof
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-ryan
# requesting: dn memberof
#
# hr-ryan, HR, People, demo.com
dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com
memberOf: cn=g-admin,ou=Group,dc=demo,dc=com
memberOf: cn=g-users,ou=Group,dc=demo,dc=com
# search result
search: 2
result: 0 Success
# numResponses: 2
# numEntries: 1
root@e53c92de7493:~# ldapsearch -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -b "dc=demo,dc=com" "cn=hr-miao" dn memberof
# extended LDIF
#
# LDAPv3
# base <dc=demo,dc=com> with scope subtree
# filter: cn=hr-miao
# requesting: dn memberof
#
# search result
search: 2
result: 32 No such object
# numResponses: 1
可以看到,g-admin 成员可以查询其他所有, 普通用户只能连接。
比较更新密码能力:
root@e53c92de7493:~# tee updateselfpass.ldif <<EOF
dn: cn=ryan.miao,ou=后台组,ou=研发部门,ou=People,dc=demo,dc=com
changetype: modify
replace: userPassword
userPassword: ryanmiao
EOF
# 执行更改密码
## 修改自己的密码 ok
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -f updateselfpass.ldif
modifying entry "cn=hr-miao,ou=HR,ou=People,dc=demo,dc=com"
## 确认密码被修改
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w 123456 -f updateselfpass.ldif
ldap_bind: Invalid credentials (49)
尝试修改其他人密码
root@e53c92de7493:~# sed -i "1c dn: cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com" updateselfpass.ldif
## 修改 dn 为别人后不能修改密码
root@e53c92de7493:~# ldapmodify -H ldapi:/// -x -D "cn=hr-miao,ou=HR,ou=People,dc=demo,w ryanmiao -f updateselfpass.ldif
modifying entry "cn=hr-ryan,ou=HR,ou=People,dc=demo,dc=com"
ldap_modify: Insufficient access (50)
三、总结 ldap 命令
ldap 主要命令有:
- ldapadd
- ldapmodify
- ldapsearch
我们用到的操作项有
option | 含义 |
---|---|
-H | ldap server 地址, 可以是 ldap://192.168.12.18:389 表示tcp, 可以是 ldap:/// 表示本地的 tcp, 可以是ldapi:/// 本地 unix socket 连接 |
-x | 启用简单认证,通过 -D dn -w 密码的方式认证 |
-f | 指定要修改的文件 |
-a | 使用 ldapmodify 增加一个 entry 的时候等同于 ldapadd |
-b | basedn 根目录, 将在此目录下查询 |
-Y EXTERNAL | 本地执行,修改配置文件,比如 basedn, rootdn,rootpw,acl, module 等信息 |
ldapadd
添加一个 entry. 可以添加 schema 配置
ldapadd -Y EXTERNAL -H ldapi:/// -f /etc/openldap/schema/cosine.ldif
添加额外的 module
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f add_module_group.sh
添加普通 entry
ldapadd -x -D cn=admin,dc=demo,dc=com -w admin -f base.ldif
ldapmodify
修改 entry, 可以更新配置信息
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f tmp.ldif
对应的更新文件语法
dn: 要更新的 entry的dn, 配置为 olcDatabase={2}hdb,cn=config, 用户为用户 dn
changetype: modify
replace: olcRootDN replace替换的字段
olcRootDN: cn=admin,dc=demo,dc=com
-
replace: olcSuffix
olcSuffix: dc=demo,dc=com
-
add: olcRootPW add添加新的字段
olcRootPW: <pass>
ldapsearch
search 查询,主要集中在 filter 的使用上。
ldapsearch -H ldapi:/// -D cn=admin,cn=demo,cn=com -w admin -s sub "filter" attr
-
-s scope 指定查询范围, 有 base one sub children 主要用 sub 表示 base 之下的所有子目录。对应 Python 里的 SUBTREE - filter 语法,正则语法。因为使用的时候传递过来的通常是 username, 需要比较 username 在 ldap 中的字段, 比如
(|(cn=Steve*)(sn=Steve*)(mail=Steve*)(givenName=Steve*)(uid=Steve*))
attr 要返回的字段, 必须返回的字段可以在配置文件里查看。memberof 非必须。
四、通过 phpldapadmin 链接 openldap
确保 phpldapadmin 容器服务已启动
[root@openldap]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0426cb9e1ec4 osixia/phpldapadmin:latest "/container/tool/run" 23 hours ago Up 4 seconds 443/tcp, 0.0.0.0:8080->80/tcp phpldapadmin
e53c92de7493 osixia/openldap:1.3.0 "/container/tool/run" 4 days ago Up 6 hours 0.0.0.0:389->389/tcp, 0.0.0.0:636->636/tcp ldap
通过浏览器访问:
- URL: http://hostIP:8080
- User: cn=admin,dc=demo,dc=com
- Passwd: admin
五、ACL 附录
olcAccess: <access directive>
<access directive> ::= to <what>
[by <who> [<access>] [<control>] ]+
<what> ::= * |
[dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
[filter=<ldapfilter>] [attrs=<attrlist>]
<basic-style> ::= regex | exact
<scope-style> ::= base | one | subtree | children
<attrlist> ::= <attr> [val[.<basic-style>]=<regex>] | <attr> , <attrlist>
<attr> ::= <attrname> | entry | children
<who> ::= * | [anonymous | users | self
| dn[.<basic-style>]=<regex> | dn.<scope-style>=<DN>]
[dnattr=<attrname>]
[group[/<objectclass>[/<attrname>][.<basic-style>]]=<regex>]
[peername[.<basic-style>]=<regex>]
[sockname[.<basic-style>]=<regex>]
[domain[.<basic-style>]=<regex>]
[sockurl[.<basic-style>]=<regex>]
[set=<setspec>]
[aci=<attrname>]
<access> ::= [self]{<level>|<priv>}
<level> ::= none | disclose | auth | compare | search | read | write | manage
<priv> ::= {=|+|-}{m|w|r|s|c|x|d|0}+
<control> ::= [stop | continue | break]
关于 acl 中的 who
Table 6.3: Access Entity Specifiers
Specifier | Entities |
---|---|
* | All, including anonymous and authenticated users |
anonymous | Anonymous (non-authenticated) users |
users | Authenticated users |
self | User associated with target entry |
dn[.]= | Users matching a regular expression |
dn.= | Users within scope of a DN |
关于 dn 的授权
For example, if the directory contained entries named:
0: o=suffix
1: cn=Manager,o=suffix
2: ou=people,o=suffix
3: uid=kdz,ou=people,o=suffix
4: cn=addresses,uid=kdz,ou=people,o=suffix
5: uid=hyc,ou=people,o=suffix
Then:
dn.base="ou=people,o=suffix" match 2;
dn.one="ou=people,o=suffix" match 3, and 5;
dn.subtree="ou=people,o=suffix" match 2, 3, 4, and 5; and
dn.children="ou=people,o=suffix" match 3, 4, and 5.
一个acl示例
# ACL1
access to attrs=userpassword
by self write
by anonymous auth
by group.exact="cn=itpeople,ou=groups,dc=example,dc=com"
write
by * none
# ACL2
access to attrs=carlicense,homepostaladdress,homephone
by self write
by group.exact="cn=hrpeople,ou=groups,dc=example,dc=com"
write
by * none
# ACL3
access to *
by self write
by group.exact="cn=hrpeople,ou=groups,dc=example,dc=com"
write
by users read
by * none
参考
- 官方文档:http://www.openldap.org/doc/admin24/guide.html
- openldap 介绍和使用: https://www.cnblogs.com/woshimrf/p/ldap.html
- 写的贼详细的 ldap 介绍:https://www.cnblogs.com/kevingrace/p/5773974.html
- memberOf 模块:https://www.jianshu.com/p/c877b317f294
- docker ldap:https://github.com/osixia/docker-openldap