LDAP在企业开发中常用的一种协议,实现企业账户统一管理的方案。常用的LDAP开源方案主要是openldap, Debian系列和CentOS系统都会自带,但需要注意配置文件不同。在基于LDAP开发相关功能时,由于LDAP协议属于明文协议,业务对安全性要求较高的场合需要搭配TLS完成对数据加密传输。LDAP默认支持TLS协议,开放的端口是636。

0x1 基础配置

openldap服务配置TLS:

  • slapd手动运行

    1
    2
    #openldap v2.4.44版本自带TLS,无需配置, slapd需要指定 "ldaps:///"
    /usr/sbin/slapd -u ldap -h ldap:/// ldaps:///
  • service服务运行 (CentOS环境)

    1
    2
    #vim /etc/sysconfig/slapd
    service slapd start
  • 验证

    1
    2
    3
    ps uax|grep slapd

    lsof -i:636

openldap docker服务配置(简易):

  • 环境变量设置
    1
    2
    3
    4
    5
    6
    # 替换成自己的证书或使用自带证书
    LDAP_TLS: true
    LDAP_TLS_CRT_FILENAME: ldap.crt
    LDAP_TLS_KEY_FILENAME: ldap.key
    LDAP_TLS_DH_PARAM_FILENAME: dhparam.pem
    LDAP_TLS_CA_CRT_FILENAME: ca.crt

0x2 开发使用

经过以上简单配置,ldap已经支持TLS传输数据了。开发语言中实现ldap客户端的方式不同,需要注意开发语言官方文档

  • PHP客户端
1
2
3
4
5
6
7
8
# 系统需要安装cryus-sasl库
yum install cyrus-sasl cyrus-sasl-devel cyrus-sasl-ldap

# 安装ldap扩展(phpbrew示例)
phpbrew ext install ldap -- --with-ldap=/usr/local/Cellar/openldap/2.4.47 --with-ldap-sasl=/usr/local/opt/cyrus-sasl

# 查看配置信息 (注意`SASL Support`启用状态)
php --ri ldap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#本地代码运行环境配置ldap.conf (php的ldap依赖这个配置文件)
echo "TLS_REQCERT never" >> /etc/openldap/ldap.conf

# 示例代码一
<?php
$host = "127.0.0.1";
$port = 389; #注意端口

$username = 'cn=username,ou=people,dc=test,dc=com'
$password = '123456'

$conn = ldap_connect($host, $port);
#调试信息
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);

# tls开启 (使用的是ldap_start_tls函数)
ldap_start_tls($conn);

# 认证
$auth = ldap_bind($conn, $username, $password);
echo $auth ? "ok" : "fail";

# 示例代码二 (不推荐)
<?php
$host = "ldaps://127.0.0.1"; #必须带ldaps协议头部,标识开启TLS
$port = 636;

$username = 'cn=username,ou=people,dc=test,dc=com'
$password = '123456'

$conn = ldap_connect($host, $port);
#调试信息
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option(NULL, LDAP_OPT_DEBUG_LEVEL, 7);

# 认证
$auth = ldap_bind($conn, $username, $password);
echo $auth ? "ok" : "fail";
  • Python客户端
1
2
3
4
5
6
7
8
import ldap
ldap = "ldap://127.0.0.1:389"
ldaps = "ldaps://127.0.0.1:636"
ldapconn = ldap.initialize(ldaps)

username = 'cn=username,ou=people,dc=test,dc=com'
password = '123456'
ldapconn.simple_bind_s(username, password)
  • Golang客户端
1
2
3
4
5
6
7
8
9
10
11
12
//...省略
l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389))
if err != nil {
log.Fatal(err)
}
defer l.Close()

// 注意InsecureSkipVerify
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
log.Fatal(err)
}

0x3 TLS分析

上述的配置基本实现了ldap通信数据加密需求,如果留意代码中注释的话,会对文章开头TLS端口636与代码中实际使用却不同。这个需要指出的是客户端语言实现不同导致的。

以PHP语言为例分析LDAP扩展中实现通信逻辑:

  • 使用636端口测试
    1
    2
    3
    # 使用文中的示例代码,端口修改成636
    输出结果
    `PHP Warning: ldap_start_tls(): Unable to start TLS: `
  • 关闭636端口测试
    1
    测试无影响,数据加密传输

在PHP文档ldap_start_tls函数评论中提到,ldaps和startTLS不是一回事,ldaps服务开放636端口,389端口支持加密和非加密数据传输; 并且ldaps不推荐使用。

PHP扩展ldap本质是调用openldap库中的函数实现,重点分析openldap中ldap_connectldap_start_tls两个函数实现。

笔者以openldap-2.4.47版本分析startTLS和ldaps实现及区别

openldap库目录结构:

1
2
3
4
5
6
7
8
openldap-2.4.47
- libraries/libldap/* #PHP使用的ldap扩展底层依赖库
- init.c #初始化操作
- open.c. #连接操作
- tls2.c. #TLS相关
- options.c #配置相关
- url.c. #URL处理相关
- request.c #网络通信相关

ldap_connect函数主要实现功能

1
2
3
4
5
6
7
8
//ldap初始化
int ldap_initialize( LDAP **ldp, LDAP_CONST char *url )

// 设置ldp参数,如果启用sasl相关则初始化sasl相关设置, 加载系统配置文件
int ldap_create( LDAP **ldp )

// 解析URL,设置连接信息
int ldap_set_option(LDAP *ld, int option, LDAP_CONST void *invalue)

ldap_start_tls函数主要实现功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//ldap_start_tls函数调用底层ldap_start_tls_s函数
int ldap_start_tls_s(LDAP *ld, LDAPControl **serverctrls, LDAPControl **clientctrls)
{
//...省略

// 默认连接检查是否开启TLS
if ( ldap_tls_inplace( ld ) ) {
return LDAP_LOCAL_ERROR;
}
// 建立连接
rc = ldap_extended_operation_s( ld, LDAP_EXOP_START_TLS,
NULL, serverctrls, clientctrls, &rspoid, &rspdata );
//...省略
if ( rc == LDAP_SUCCESS ) {
// 协商TLS通信会话
rc = ldap_int_tls_start( ld, ld->ld_defconn, NULL );
}

return rc;

//...省略
}

ldap使用流程对比:

1
2
ldap_connect -> ldap_bind
ldap_connect -> ldap_start_tls -> ldap_bind

ldap_start_tls在bind认证前开启TLS验证,ldaps是在ldap_bind操作时再初始化TLS会话,身份认证操作。两种操作在底层实现相差不大,主要是服务端389端口支持加密和非加密两种功能。

slapd服务端连接判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Connection * connection_init(
ber_socket_t s,
Listener *listener,
const char* dnsname,
const char* peername,
int flags,
slap_ssf_t ssf,
struct berval *authid
LDAP_PF_LOCAL_SENDMSG_ARG(struct berval *peerbv))
{
//...省略


#ifdef HAVE_TLS
// TLS连接判断
if ( flags & CONN_IS_TLS ) {
c->c_is_tls = 1;
c->c_needs_tls_accept = 1;
} else {
c->c_is_tls = 0;
c->c_needs_tls_accept = 0;
}
#endif

slap_sasl_open( c, 0 );
slap_sasl_external( c, ssf, authid );

slapd_add_internal( s, 1 );

backend_connection_init(c);

//...省略

有一点需要注意的是客户端使用ldap_start_tls函数时,会自动加载本地目录/etc/openldap/ldap.conf文件, 具体参考PHP测试说明。其中ldap.conf配置内容参考man ldap.conf命令说明及官方文档

0x4 参考

  1. openldap TLS文档
  2. openldap docker
  3. php TLS使用
  4. golang-ldap库

评论