RisingWave LDAP 认证端到端测试全解析:Simple Bind / Search and Bind、双向 TLS 与 URL 格式配置指南
RisingWave LDAP 认证端到端测试全解析:Simple Bind / Search and Bind、双向 TLS 与 URL 格式配置指南
RisingWave 通过 PostgreSQL 协议的 hba.conf 机制支持将数据库用户认证委托给 LDAP 服务器,覆盖 Simple Bind、Search and Bind 两种认证模式。本文以仓库内 ci/ldap-test/ 的端到端测试工程为主线,结合 src/utils/pgwire/src/ldap_auth.rs 的源码实现与 ci/ldap-test/、src/config/、e2e_test/ldap/ 下的真实配置,完整讲解 LDAP 认证的原理、TLS/双向 TLS 配置、CI 测试编排以及本地复现步骤,读者可据此在自己的 RisingWave 环境中搭建 LDAP 认证。
一、LDAP 认证测试工程概览
ci/ldap-test/README.md 所描述的是一个专为 CI 设计的 LDAP 认证端到端测试工程。它的目标非常明确:验证 RisingWave 能否通过两种认证模式对接 LDAP 服务,并覆盖“是否携带客户端证书”“参数格式是分离字段还是 URL”等全部组合。
两种核心认证模式分别是:
- Simple Bind(简单绑定):使用
ldapprefix与ldapsuffix直接拼接出用户的完整 DN,然后用该 DN 与用户提交的密码向 LDAP 发起绑定请求; - Search and Bind(搜索后绑定):先使用
ldapbasedn、ldapsearchattribute(或ldapsearchfilter)在 LDAP 中搜索出用户条目,拿到用户完整 DN 后,再以ldapbinddn/ldapbindpasswd的凭据完成绑定校验。
每种模式又分别测试了“携带客户端证书(双向 TLS)”与“不携带客户端证书”两种链路,并且对参数提供方式同时覆盖了传统分离字段格式与 RFC 4516 URL 格式。
注意:该测试工程仅面向 CI 运行,LDAP 服务器由
ci/docker-compose.yml中的ldap-server服务托管,不依赖外部 LDAP 环境。
相关文件结构
ci/
├── docker-compose.yml # 包含 ldap-server 服务定义
└── ldap-test/
├── setup-ldap-certs.sh # 生成 TLS 证书的脚本
├── ldif/
│ └── 01-users.ldif # LDAP 测试用户定义
├── certs/ # 生成的 TLS 证书(gitignore,自动生成)
└── README.md # 本文讲解的说明文档
src/config/
├── ci-ldap-simple-bind.toml # Simple Bind 的 RisingWave 配置
├── ci-ldap-search-bind.toml # Search and Bind 配置
├── ci-ldap-simple-bind-url.toml # Simple Bind(URL 格式)
├── ci-ldap-search-bind-url.toml # Search and Bind(URL 格式)
└── ci-ldap-search-bind-filter.toml # Search and Bind(自定义搜索过滤器)
e2e_test/ldap/
└── ldap_auth.slt # LDAP 认证测试套件(所有场景复用)
ci/scripts/
└── e2e-ldap-test.sh # 端到端测试脚本
二、测试用户与 LDAP 目录结构
测试工程在 LDAP 中只创建一个测试用户(定义见 ci/ldap-test/ldif/01-users.ldif):
- 用户名:
testuser1,密码:testpass1 - 所在组织单元:
ou=people,dc=example,dc=com
LDIF 文件中的完整定义如下:
dn: ou=people,dc=example,dc=com
objectClass: organizationalUnit
ou: people
dn: uid=testuser1,ou=people,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: testuser1
cn: Test User 1
sn: User1
givenName: Test
mail: testuser1@example.com
userPassword: {SSHA}testpass1
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/testuser1
loginShell: /bin/bash
值得注意的一个细节:LDIF 中 userPassword 字段写的是 {SSHA}testpass1——SSHA 散列是生成 LDIF 时临时填写的占位值。由于散列值与真实密码无法直接对齐,CI 脚本会在测试启动前通过 ldappasswd 命令把该用户的密码重置为明文 testpass1,确保绑定校验可用。这也是测试脚本中专门存在“设置测试用户密码”步骤的原因。
三、整体工作机制
1. 证书生成(容器内完成)
TLS 证书并非在宿主机上预先准备,而是在 ldap-server 容器启动期间由入口脚本生成,流程如下(对应 ci/docker-compose.yml 中 ldap-server 服务的 entrypoint):
- 容器入口以
CERT_DIR=/container/service/slapd/assets/certs调用 ci/ldap-test/setup-ldap-certs.sh; - 脚本在 OpenLDAP 期望的路径上直接生成 CA、服务端、客户端证书;
- 同一目录通过 bind mount 挂载到宿主机的
./ldap-test/certs/,供测试脚本读取; - OpenLDAP 使用生成的证书启动,提供 STARTTLS(389 端口)与 LDAPS(636 端口)两种加密通道。
容器环境的对应配置(ci/docker-compose.yml):
ldap-server:
image: osixia/openldap:1.5.0
hostname: ldap-server
environment:
LDAP_ORGANISATION: "Example Corp"
LDAP_DOMAIN: "example.com"
LDAP_BASE_DN: "dc=example,dc=com"
LDAP_ADMIN_PASSWORD: "admin123"
LDAP_TLS: "true"
LDAP_TLS_CRT_FILENAME: "server.crt"
LDAP_TLS_KEY_FILENAME: "server.key"
LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
LDAP_TLS_DH_PARAM_FILENAME: "dhparam.pem"
LDAP_TLS_ENFORCE: "false"
LDAP_TLS_VERIFY_CLIENT: "try"
ports:
- "389:389"
- "636:636"
volumes:
- ./ldap-test/ldif:/workspace/ldif
- ./ldap-test/certs:/container/service/slapd/assets/certs
- ./ldap-test/setup-ldap-certs.sh:/workspace/setup-ldap-certs.sh
LDAP_TLS_VERIFY_CLIENT: "try" 表示 OpenLDAP 会请求客户端证书但并非强制拒绝——这为“有/无客户端证书”两种测试场景提供了空间。
2. 测试执行流程
测试脚本 ci/scripts/e2e-ldap-test.sh 只做最小化准备,具体步骤:
- 通过
apt-get install ldap-utils安装 LDAP 命令行工具; - 等待 STARTTLS 就绪:使用
openssl s_client -connect ldap-server:389 -starttls ldap探测,最多重试 10 次,每次间隔 2 秒; - 用
ldapsearch验证testuser1已存在于 LDAP; - 用
ldappasswd将测试用户密码设置为testpass1; - 导出 RisingWave 所需的 TLS 环境变量:
LDAPTLS_CACERT:CA 证书路径;LDAPTLS_REQCERT=demand:严格校验模式;LDAPTLS_CERT/LDAPTLS_KEY:客户端证书(仅双向 TLS 场景);
- 依次运行 7 个测试场景。
3. 测试场景清单
脚本实际执行了 7 个场景(README 中列出的 6 个场景 + 一个自定义搜索过滤器场景):
| # | 场景 | 配置文件名 | 是否带客户端证书 |
|---|---|---|---|
| 1 | Simple Bind(无客户端证书) | ci-ldap-simple-bind.toml |
否 |
| 2 | Simple Bind(带客户端证书) | ci-ldap-simple-bind.toml |
是 |
| 3 | Search and Bind(无客户端证书) | ci-ldap-search-bind.toml |
否 |
| 4 | Search and Bind(带客户端证书) | ci-ldap-search-bind.toml |
是 |
| 5 | Simple Bind URL 格式(无证书) | ci-ldap-simple-bind-url.toml |
否 |
| 6 | Search and Bind URL 格式(无证书) | ci-ldap-search-bind-url.toml |
否 |
| 7 | Search and Bind 自定义搜索过滤器(无证书) | ci-ldap-search-bind-filter.toml |
否 |
每个场景的执行模式高度一致:
- 用
risedev ci-start <profile>以对应配置启动 RisingWave; - 用
sqllogictest -p 4566 -d dev './e2e_test/ldap/ldap_auth.slt'运行同一套 SQL 逻辑测试; - 覆盖失败场景(错误密码、不存在的用户、LDAP 注入尝试);
- 用
risedev ci-kill停止集群后进入下一个场景。
四、测试用例:ldap_auth.slt
所有场景复用同一套测试套件 e2e_test/ldap/ldap_auth.slt,它通过 system 命令调用 psql 完成真实认证验证:
-- 在 RisingWave 中创建用户
statement ok
CREATE USER testuser1;
statement ok
GRANT ALL PRIVILEGES ON SCHEMA public TO testuser1;
-- 成功认证
system ok
PGPASSWORD=testpass1 psql -U testuser1 -c "SELECT 1 AS test;"
-- 验证用户可执行 DDL / DML / 查询
system ok
PGPASSWORD=testpass1 psql -U testuser1 -c "CREATE TABLE test_table (id INT, name VARCHAR);"
system ok
PGPASSWORD=testpass1 psql -U testuser1 -c "INSERT INTO test_table VALUES (1, 'test1'), (2, 'test2');FLUSH;"
system ok
PGPASSWORD=testpass1 psql -U testuser1 -c "SELECT * FROM test_table;" | grep -q "test1"
system ok
PGPASSWORD=testpass1 psql -U testuser1 -c "SELECT count(*) FROM test_table;" | grep -q "2"
-- 错误密码:应返回 invalidCredentials
system ok
PGPASSWORD=wrongpassword psql -U testuser1 -c "SELECT 1;" 2>&1 | grep -q "invalidCredentials"
-- 不存在的用户:应报 does not exist
system ok
PGPASSWORD=anypass psql -U non-exist-user -c "SELECT 1;" 2>&1 | grep -q "does not exist"
-- LDAP 注入尝试:用户名含通配符 * 也应被拒绝
system ok
PGPASSWORD=anypass psql -U "test*user" -c "SELECT 1;" 2>&1 | grep -q "does not exist"
-- 清理
statement ok
DROP TABLE test_table;
statement ok
DROP USER testuser1;
这套测试的验证点很有代表性:正向验证成功登录与增删改查;反向验证错误密码返回 invalidCredentials、不存在用户返回 does not exist;特别地,用 test*user 这样的用户名模拟 LDAP 注入攻击——该用户名不会被当成通配符匹配到 testuser1,因此同样必须报错。这个用例直接对应源码中对 DN 和过滤器做转义处理的实现(详见后文源码分析)。
五、两种认证模式的配置参数详解
RisingWave 通过 frontend.hba_config.entries 配置认证规则,这与 PostgreSQL 的 pg_hba.conf 语义一致。所有 LDAP 相关参数都放在条目的 auth_options 子表中,由 src/utils/pgwire/src/ldap_auth.rs 解析使用。
1. Simple Bind(分离字段格式)
配置文件 src/config/ci-ldap-simple-bind.toml 的核心片段:
[[frontend.hba_config.entries]]
connection_type = "Host"
databases = ["all"]
users = ["testuser1"]
addresses = ["all"]
auth_method = "ldap"
[frontend.hba_config.entries.auth_options]
ldapserver = "ldap-server"
ldapscheme = "ldap"
ldapport = "389"
ldaptls = "true"
# Simple bind 配置
ldapprefix = "uid="
ldapsuffix = ",ou=people,dc=example,dc=com"
Simple Bind 的原理:用户登录时提交用户名 testuser1,RisingWave 将其拼成完整 DN:
uid=testuser1,ou=people,dc=example,dc=com
然后以该 DN + 用户密码发起 LDAP Bind。这里 ldapscheme = "ldap" 配合 ldaptls = "true",表示使用 389 端口上的 STARTTLS 加密。ldapprefix 与 ldapsuffix 是可独立配置的字符串,实际部署中可据此适配任意 LDAP 目录结构(如 cn= 前缀或不同 OU)。
2. Search and Bind(分离字段格式)
配置文件 src/config/ci-ldap-search-bind.toml 的核心片段:
[[frontend.hba_config.entries]]
connection_type = "Host"
databases = ["all"]
users = ["testuser1"]
addresses = ["all"]
auth_method = "ldap"
[frontend.hba_config.entries.auth_options]
ldapserver = "ldap-server"
ldapscheme = "ldaps"
ldapport = "636"
# Search and bind 配置
ldapbasedn = "ou=people,dc=example,dc=com"
ldapsearchattribute = "uid"
# 非匿名搜索必需
ldapbinddn = "cn=admin,dc=example,dc=com"
ldapbindpasswd = "admin123"
Search and Bind 的原理分两步:
- 搜索:RisingWave 以
ldapbinddn/ldapbindpasswd的凭据绑定 LDAP(非匿名搜索场景必需,对应代码中"Required for non-anonymous search"注释),在ldapbasedn指定的子树下按ldapsearchattribute = "uid"搜索用户testuser1,得到完整 DN; - 绑定:用搜索到的完整 DN + 用户密码再次绑定 LDAP,完成密码校验。
这里 ldapscheme = "ldaps"、ldapport = "636",走的是独立 TLS 端口而非 STARTTLS。
3. Search and Bind 自定义搜索过滤器
配置文件 src/config/ci-ldap-search-bind-filter.toml 展示了用 ldapsearchfilter 替代 ldapsearchattribute 的用法,适合目录结构复杂、需要自定义匹配规则的环境:
[frontend.hba_config.entries.auth_options]
ldapserver = "ldap-server"
ldapscheme = "ldaps"
ldapport = "636"
ldapbasedn = "ou=people,dc=example,dc=com"
# 自定义过滤器,$username 为占位符
ldapsearchfilter = "(&(objectClass=person)(uid=$username))"
ldapbinddn = "cn=admin,dc=example,dc=com"
ldapbindpasswd = "admin123"
过滤器中的 $username 是 RisingWave 提供的占位符,会被替换为实际登录用户名。
4. LDAP 认证参数速查表
| 参数 | 适用模式 | 说明 |
|---|---|---|
ldapserver |
两者 | LDAP 服务器主机名 |
ldapport |
两者 | 端口(389 为明文/STARTTLS,636 为 LDAPS) |
ldapscheme |
两者 | ldap 或 ldaps |
ldaptls |
两者 | true 时在 ldap:// 上启用 STARTTLS |
ldapprefix / ldapsuffix |
Simple Bind | 拼接用户 DN 的前缀与后缀 |
ldapbasedn |
Search and Bind | 搜索起始 Base DN |
ldapsearchattribute |
Search and Bind | 用于匹配用户名的属性(默认通常为 uid) |
ldapsearchfilter |
Search and Bind | 自定义搜索过滤器,$username 为占位符 |
ldapbinddn / ldapbindpasswd |
Search and Bind | 执行搜索所用凭据(非匿名目录必需) |
ldapurl |
两者 | RFC 4516 URL 格式的完整连接描述 |
六、URL 格式(RFC 4516)配置
除分离字段外,RisingWave 还支持通过 ldapurl 一次性描述 LDAP 连接,格式为:
ldap[s]://host:port/basedn?attributes?scope?filter
各段含义(来自 src/config/ci-ldap-search-bind-url.toml 的注释说明):
basedn:搜索 Base DN,如ou=people,dc=example,dc=com;attributes:搜索属性,通常为uid(RisingWave 使用第一个属性);scope:搜索范围(base、one、sub),RisingWave 当前不使用该段;filter:附加 LDAP 过滤器,可选。
Simple Bind + URL
src/config/ci-ldap-simple-bind-url.toml:
[frontend.hba_config.entries.auth_options]
# 格式:ldap[s]://host:port/
# 注意:ldaptls 参数可与 ldap:// scheme 搭配实现 STARTTLS
ldapurl = "ldap://ldap-server:389/"
ldaptls = "true"
ldapprefix = "uid="
ldapsuffix = ",ou=people,dc=example,dc=com"
Simple Bind 的 URL 只需要主机与端口部分,前缀/后缀仍由 ldapprefix / ldapsuffix 提供。
Search and Bind + URL
src/config/ci-ldap-search-bind-url.toml:
[frontend.hba_config.entries.auth_options]
ldapurl = "ldaps://ldap-server:636/ou=people,dc=example,dc=com?uid"
# 非匿名搜索所需的绑定凭据
ldapbinddn = "cn=admin,dc=example,dc=com"
ldapbindpasswd = "admin123"
这里 ldaps:// scheme 直接表明使用 636 端口上的独立 TLS 通道;URL 中 ?uid 即 ldapsearchattribute 的等价物。需要注意,即便使用 URL 格式,ldapbinddn / ldapbindpasswd 仍需要单独配置(它们不属于 RFC 4516 URL 的组成部分)。
七、RisingWave 的 LDAP TLS 配置(环境变量)
RisingWave 从环境变量读取 LDAP 的 TLS 配置,这些变量必须在启动 RisingWave 之前设置:
| 环境变量 | 作用 | 取值/说明 |
|---|---|---|
LDAPTLS_CACERT |
CA 证书文件路径 | 自签名证书场景必需 |
LDAPTLS_CERT |
客户端证书路径 | 可选,用于双向 TLS |
LDAPTLS_KEY |
客户端私钥路径 | 可选,用于双向 TLS |
LDAPTLS_REQCERT |
证书校验策略 | never、allow、try、demand |
典型用法(README 给出的示例):
export LDAPTLS_CACERT="$(pwd)/ci/ldap-test/certs/ca.crt"
export LDAPTLS_REQCERT="demand"
risedev ci-start ci-ldap-simple-bind
关键注意事项:如果未设置 LDAPTLS_CACERT,RisingWave 会回退到系统原生证书库(对应源码 rustls_native_certs::load_native_certs() 分支)。而测试环境使用的自签名 CA 并不在系统证书库中,此时认证会以 invalid peer certificate 错误失败。这一行为在源码 src/utils/pgwire/src/ldap_auth.rs 中有明确实现:
- 设置了
LDAPTLS_CACERT时,用 PEM 解析并加载 CA 证书到rustls::RootCertStore; - 未设置时,加载系统原生证书;
LDAPTLS_REQCERT解析为never/allow/try/demand四种策略,默认值为demand(严格校验);LDAPTLS_CERT/LDAPTLS_KEY存在时构建客户端身份,实现双向 TLS。
八、本地开发与复现指南
1. 前置条件:添加主机名映射
证书 CN 与 SAN 中包含 ldap-server,为了让本机与 CI 都能解析该主机名,需要在 /etc/hosts 中添加:
sudo bash -c 'echo "127.0.0.1 ldap-server" >> /etc/hosts'
2. 启动 LDAP 服务器
在仓库根目录执行:
docker-compose -f ci/docker-compose.yml up -d ldap-server
该命令会自动完成三步:在容器内生成 TLS 证书 → 启动带 STARTTLS 的 OpenLDAP → 从 ldif/01-users.ldif 加载测试用户。
3. 设置测试用户密码
ldappasswd -x -H ldap://ldap-server:389 \
-D "cn=admin,dc=example,dc=com" -w "admin123" \
-s "testpass1" "uid=testuser1,ou=people,dc=example,dc=com"
4. 验证 LDAP 连接
export LDAPTLS_CACERT="$(pwd)/ci/ldap-test/certs/ca.crt"
export LDAPTLS_REQCERT="demand"
# -ZZ 强制 STARTTLS
ldapwhoami -x -H ldap://ldap-server:389 -ZZ \
-D "cn=admin,dc=example,dc=com" -w "admin123"
macOS 提示:系统自带的 LDAP 客户端对 STARTTLS 支持不佳,建议通过 Homebrew 安装 OpenLDAP:
brew install openldap
# 使用:/opt/homebrew/opt/openldap/bin/ldapwhoami
5. 清理
docker-compose -f ci/docker-compose.yml down
rm -rf ci/ldap-test/certs/
九、证书生成实现细节
ci/ldap-test/setup-ldap-certs.sh 生成的证书规格如下:
CA 证书(ca.crt / ca.key)
- 自签名根 CA,subject 为
CN=Example Root CA; - RSA 4096 位密钥,SHA256 签名,有效期 365 天。
服务端证书(server.crt / server.key)
- CN:
ldap-server; - SAN:
DNS:ldap-server、DNS:localhost、DNS:ldap.example.com、IP:127.0.0.1、IP:::1; - Key Usage:
digitalSignature, keyEncipherment; - Extended Key Usage:
serverAuth; - 同样 RSA 4096 + SHA256。
客户端证书(client.crt / client.key)
- CN:
RisingWave Client; - Key Usage:
digitalSignature, keyEncipherment; - Extended Key Usage:
clientAuth; - RSA 4096 + SHA256。
DH 参数(dhparam.pem):2048 位。脚本中该文件为预生成并签入仓库(脚本内嵌了 PEM 内容),因为 openssl dhparam 生成 2048 位参数非常耗时,尤其不适合 CI——脚本中相关生成命令以注释形式保留备查。脚本最后还会遍历验证所有 .crt 文件,确认 subject、issuer 与有效期无误后才允许 OpenLDAP 启动。
证书权限也有讲究:*.crt 设为 644,*.key 设为 600,兼顾容器内 OpenLDAP 进程与宿主机测试脚本的可读性与私钥保护。
十、源码级实现佐证
从源码结构可以确认整套 LDAP 认证链路的落点:
-
参数解析:
ldapserver、ldapport、ldapscheme、ldapbasedn、ldapsearchfilter、ldapsearchattribute、ldapbinddn、ldapbindpasswd、ldapprefix、ldapsuffix、ldapurl、ldaptls这组键名定义在 src/utils/pgwire/src/ldap_auth.rs 开头的常量中,与 hba 配置结构体 src/common/src/config/hba.rs 对应。 -
LDAP 客户端:RisingWave 使用
ldap3crate 的异步客户端(LdapConnAsync)建立连接,搜索时使用Scope与SearchEntry。 -
注入防护:源码引用了
dn_escape与ldap_escape两个转义函数——分别用于对拼接入 DN 的用户名与搜索过滤器进行转义,这正是ldap_auth.slt中test*user注入用例能够被安全拦截的实现基础:用户名中的*等特殊字符会被转义,无法形成通配符攻击。 -
TLS 栈:TLS 基于
rustls(rustls::ClientConfig+rustls::RootCertStore),CA 证书通过CertificateDer::pem_slice_iter解析 PEM;客户端证书与私钥经rustls_pki_types的CertificateDer/PrivateKeyDer加载。LDAPTLS_REQCERT的四种取值映射到ReqCertPolicy枚举,未识别值回退为demand。
十一、常见问题排查速查
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
invalid peer certificate |
未设置 LDAPTLS_CACERT,RisingWave 回退到系统证书库,不信任自签名 CA |
启动前 export LDAPTLS_CACERT=... 指向 CA 证书 |
认证报 invalidCredentials |
用户密码未通过 ldappasswd 重置为明文 |
按上文命令重置密码后再启动测试 |
本地连不上 ldap-server |
缺少 /etc/hosts 映射 |
添加 127.0.0.1 ldap-server |
| STARTTLS 握手失败 | OpenLDAP 尚未就绪 | 参考脚本中最多 10 次、每次 2 秒的重试策略 |
| 非匿名目录搜索失败 | 缺少绑定凭据 | 配置 ldapbinddn / ldapbindpasswd |
小结
RisingWave 的 LDAP 认证以 PostgreSQL 兼容的 hba 配置为入口,通过 src/utils/pgwire/src/ldap_auth.rs 实现两种认证模式、URL 格式解析与基于 rustls 的(双向)TLS 链路;ci/ldap-test/ 工程则用一套可复现的 OpenLDAP 容器 + 7 个场景 × 同一份 .slt 套件,系统验证了这些能力在真实 CI 环境中的行为,包括失败路径与注入防护。读者既可以按“本地开发与复现指南”一节在仓库内完整跑通验证,也可以借鉴 src/config/ 下的 5 个配置模板,将 LDAP 认证接入自己的 RisingWave 部署。