RisingWave LDAP 认证端到端测试全解析:Simple Bind / Search and Bind、双向 TLS 与 URL 格式配置指南

原创2026-09-24 22:18:511,729 阅读
文章标签:数据库流处理后端数据工程

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”等全部组合。

两种核心认证模式分别是:

  1. Simple Bind(简单绑定):使用 ldapprefix 与 ldapsuffix 直接拼接出用户的完整 DN,然后用该 DN 与用户提交的密码向 LDAP 发起绑定请求;
  2. 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):

  1. 容器入口以 CERT_DIR=/container/service/slapd/assets/certs 调用 ci/ldap-test/setup-ldap-certs.sh;
  2. 脚本在 OpenLDAP 期望的路径上直接生成 CA、服务端、客户端证书;
  3. 同一目录通过 bind mount 挂载到宿主机的 ./ldap-test/certs/,供测试脚本读取;
  4. 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 只做最小化准备,具体步骤:

  1. 通过 apt-get install ldap-utils 安装 LDAP 命令行工具;
  2. 等待 STARTTLS 就绪:使用 openssl s_client -connect ldap-server:389 -starttls ldap 探测,最多重试 10 次,每次间隔 2 秒;
  3. 用 ldapsearch 验证 testuser1 已存在于 LDAP;
  4. 用 ldappasswd 将测试用户密码设置为 testpass1;
  5. 导出 RisingWave 所需的 TLS 环境变量:
    • LDAPTLS_CACERT:CA 证书路径;
    • LDAPTLS_REQCERT=demand:严格校验模式;
    • LDAPTLS_CERT / LDAPTLS_KEY:客户端证书(仅双向 TLS 场景);
  6. 依次运行 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 的原理分两步:

  1. 搜索:RisingWave 以 ldapbinddn / ldapbindpasswd 的凭据绑定 LDAP(非匿名搜索场景必需,对应代码中"Required for non-anonymous search"注释),在 ldapbasedn 指定的子树下按 ldapsearchattribute = "uid" 搜索用户 testuser1,得到完整 DN;
  2. 绑定:用搜索到的完整 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 认证链路的落点:

  1. 参数解析: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 对应。

  2. LDAP 客户端:RisingWave 使用 ldap3 crate 的异步客户端(LdapConnAsync)建立连接,搜索时使用 Scope 与 SearchEntry。

  3. 注入防护:源码引用了 dn_escape 与 ldap_escape 两个转义函数——分别用于对拼接入 DN 的用户名与搜索过滤器进行转义,这正是 ldap_auth.slt 中 test*user 注入用例能够被安全拦截的实现基础:用户名中的 * 等特殊字符会被转义,无法形成通配符攻击。

  4. 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 部署。

登录后查看全文
risingwave