LDAP / Kerberos 认证
当账号规模从「十几个人」增长到「几百上千个应用与同事」,在数据库里逐个维护 CREATE ROLE ... PASSWORD 就不再现实:入职离职要同步、密码要轮换、审计要统一。此时 PostgreSQL 的答案是把「证明身份」这件事外包给企业已有的基础设施——LDAP(把密码校验交给目录服务,数据库不自存密码)与 Kerberos/GSSAPI(把凭据交给域控制器,用户凭票证免密登录)。前几页讲的 scram-sha-256 是「数据库自己管密码」,本页讲的是「数据库借用别人的账号体系」。
两种技术的定位完全不同,值得先说清楚:LDAP 认证的运行时形态仍然是「客户端发一个密码、服务器问 LDAP 这个密码对不对」——体验与密码认证一致,只是校验发生在外部;Kerberos 认证则根本不传密码,客户端凭 Kerberos 票证(ticket)证明身份,配合 GSSAPI 加密还可以让整个会话免用 TLS。企业里最常见的组合是:LDAP 做账号目录与集中管理,Kerberos 做单点登录(SSO)——两者通常由同一套域基础设施提供(如 Active Directory 同时是 LDAP 服务器和 KDC)。
本页先拆解 LDAP 的两种模式与全部选项,再讲 Kerberos/GSSAPI 的服务端与客户端配置,最后给出 Active Directory 场景的实战与排错路径。示例以 Linux 服务器 + OpenLDAP / Active Directory 为背景,Windows SSPI 认证(Windows 客户端的 GSSAPI 等价物)只做简要说明。
前置知识
基础概念
两种技术在体系中的位置
| 维度 | LDAP 认证 | Kerberos / GSSAPI 认证 |
|---|---|---|
| 认证依据 | 用户名 + 密码(由 LDAP 服务器校验) | Kerberos 票证(由 KDC 签发) |
| 网络上是否传密码 | 密码经 PostgreSQL → LDAP 服务器传输 | 不传密码 |
| 需要的基础设施 | LDAP 服务器(OpenLDAP / AD 等) | KDC(域控制器)+ keytab |
| 数据库角色名来源 | 登录时的数据库角色名(或经映射) | Kerberos 主体(user@REALM),经映射/裁剪 |
| 单点登录 | 否(每次仍输密码) | 是(kinit 一次,全程免密) |
| 典型用途 | 集中账号管理、密码策略统一 | 企业域内 SSO、自动化作业的免密认证 |
LDAP 认证的本质:外包密码校验
LDAP(Lightweight Directory Access Protocol)是一棵「目录树」,每个用户是树上的一个条目(entry),有 DN(Distinguished Name,可分辨名称)与密码属性。PostgreSQL 拿到客户端提供的密码后,以用户的 DN + 该密码向 LDAP 服务器发起一次绑定(bind)操作——bind 成功即密码正确,bind 失败即认证失败。数据库自己只存角色名,不存密码,密码策略(复杂度、锁定、轮换)全部由 LDAP 侧统一管理。
┌────────┐ ① 用户名+密码 ┌────────────┐ ② 以用户 DN bind ┌─────────────┐
│ psql │ ───────────────► │ PostgreSQL │ ─────────────────► │ LDAP 服务器 │
│ │ ◄─────────────── │ (认证转发) │ ◄──── bind 成功? ── │ (目录树) │
└────────┘ ③ 认证结果 └────────────┘ ④ bind 结果 └─────────────┘Kerberos / GSSAPI 认证的本质:凭票证入场
Kerberos 体系里有三个角色:客户端、服务方(PostgreSQL)、KDC(Key Distribution Center,域控制器)。用户先 kinit 向 KDC 证明身份(这一步用密码,但密码不出本机),拿到 TGT(Ticket Granting Ticket);此后访问 PostgreSQL 时,客户端用 TGT 换取「postgres 服务的票证」,服务器用自己 keytab 文件中的密钥验证票证,全程不需要密码参与。GSSAPI 是 Kerberos 的标准化编程接口,PostgreSQL 的 gss 认证方法与 GSSAPI 加密都构建在其上。
服务方的身份由服务主体名(service principal)标识,PostgreSQL 的惯例形态是:
postgres/pg1.example.com@EXAMPLE.COM
└─服务名─┘└────实例主机名────┘└────域────┘对应的服务端密钥存放在 keytab 文件中(路径由 krb_server_keyfile 参数指定),它就是「服务器的密码」,权限必须像密码一样管理。
Kerberos 票证交换的完整时序
把「kinit → 连库」的全过程拆开,每一步失败对应一类排错方向:
客户端 KDC(域控制器) PostgreSQL(持 keytab)
│ ─① AS-REQ:报身份+密码 ─────────────► │
│ (密码只在本机参与推导,不出网) │
│ ◄─② AS-REP:发放 TGT ─────────────── │
│ │
│ ─③ TGS-REQ:用 TGT 换服务票证 ───────► │
│ 服务主体 = postgres/pg1.example.com
│ ◄─④ TGS-REP:发放服务票证 ──────────── │
│ │
│ ─⑤ AP-REQ:出示服务票证 ──────────────────────────────────────► │
│ │ ⑥ 用 keytab 密钥解票证 → 身份确认
│ ◄═════ ⑦ GSS 会话建立(可选全程加密)═══════════════════════════ │时序里的关键点:
- 密码只在第①步参与(且不出本机),第⑤步之后连 KDC 都不再介入——运行时可用性只依赖 PostgreSQL 与客户端,不依赖 KDC 在线(但票证续期需要 KDC);
- 服务器只凭 keytab 就能验证票证(第⑥步),keytab 与 KDC 数据库中该主体的密钥必须同版本(kvno 一致),这是 ②③④ 不报错、⑥ 却失败这类「诡异故障」的根源;
- TGT 有有效期(默认常见 10 小时量级,由 KDC 策略定),过期后要重新
kinit——交互用户有续期机制,无人值守进程没有,这是「cron 里 Kerberos 不可用」的根本原因。
为什么两个经常一起出现
Active Directory 同时扮演 LDAP 服务器与 KDC:账号在 AD 里集中维护(LDAP 能查),域内登录凭 TGT 通行(Kerberos 能验)。PostgreSQL 侧的常见架构是「HBA 里同时保留 ldap 记录(给非域设备与外部工具)与 hostgssenc + gss 记录(给域内工作站)」,两套互为补充。
语法与用法
ldap 方法的完整选项表
ldap 方法写在 pg_hba 记录的认证方法段,后跟选项。全部选项如下:
| 选项 | 默认 | 说明 |
|---|---|---|
ldapserver= | 无(必填) | LDAP 服务器主机名或 IP,可写多个(空格分隔) |
ldapport= | 389 | LDAP 服务端口 |
ldapscheme= | ldap | 协议方案:ldap 或 ldaps(LDAPS 通常配 636 端口) |
ldaptls= | 0 | 设为 1 时在普通 LDAP 端口上做 StartTLS 升级加密 |
ldapurl= | 无 | 用一个 LDAP URL 一次性表达 server/port/scheme/basedn/属性/过滤(见下) |
ldapbasedn= | 无 | search+bind 模式的搜索起点 DN |
ldapbinddn= | 无 | 执行搜索所用的固定账号 DN(目录一般不允许匿名搜索时必须提供) |
ldapbindpasswd= | 无 | 上述搜索账号的密码 |
ldapsearchattribute= | uid | 用登录用户名匹配的属性(AD 常设为 sAMAccountName) |
ldapsearchfilter= | 无 | 自定义 LDAP 过滤表达式,%u 会被替换为登录用户名 |
ldapprefix= | 无 | simple bind 模式:拼在用户名前的 DN 前缀 |
ldapsuffix= | 无 | simple bind 模式:拼在用户名后的 DN 后缀 |
ldapsearchattribute 与 ldapsearchfilter 二选一(都写会报错);ldapbasedn 与 ldapprefix/ldapsuffix 是两种互斥模式的开关,混合指定同样会报错。
ldapurl 的格式是标准的 LDAP URL:scheme://host:port/basedn?attribute?scope?filter,例如:
ldap://ldap.example.net/dc=example,dc=net?uid?sub它等价于分别写 ldapserver=ldap.example.net、ldapbasedn=dc=example,dc=net、ldapsearchattribute=uid。用 URL 时,与其重复的选项不能再写。
模式一:search+bind(推荐,AD 场景几乎必须)
# TYPE DATABASE USER ADDRESS METHOD OPTIONS
host all all 10.0.0.0/8 ldap ldapserver=ldap.example.net ldapbasedn="dc=example,dc=net" ldapsearchattribute=uid执行流程:
- 服务器以
ldapbinddn+ldapbindpasswd绑定目录(未提供时尝试匿名绑定); - 以
ldapbasedn为起点、按ldapsearchattribute=登录用户名(或ldapsearchfilter)搜索用户的 DN; - 以搜到的 DN + 客户端提供的密码再次绑定,成功即认证通过。
优点是用户 DN 可以不必符合固定模板(姓名里有空格、OU 结构复杂都能处理),这正是 Active Directory 的常态——AD 里账号的 DN 是 CN=张三,OU=IT,... 而登录名在 sAMAccountName 属性里,无法简单拼接,必须搜索。
模式二:simple bind(DN 结构规整时最省事)
host all all 10.0.0.0/8 ldap ldapserver=ldap.example.net ldapprefix="uid=" ldapsuffix=",ou=people,dc=example,dc=net"执行流程只有一步:把用户名夹进 ldapprefix 与 ldapsuffix 之间拼出 DN(uid=alice,ou=people,dc=example,dc=net),直接用这个 DN + 密码绑定。没有搜索环节,速度快、配置少,代价是要求所有用户的 DN 都符合同一模板。
传输加密:ldaps:// 与 StartTLS
LDAP 认证中密码会从 PostgreSQL 发往 LDAP 服务器,管道必须加密,两种方式:
# 方式一:LDAPS —— LDAP over TLS,独立端口 636
host all all 10.0.0.0/8 ldap ldapserver=ldap.example.net ldapscheme=ldaps ldapport=636 ldapbasedn="dc=example,dc=net" ldapbinddn="cn=svc_pg,ou=service,dc=example,dc=net" ldapbindpasswd="S3cret!" ldapsearchattribute=uid
# 方式二:StartTLS —— 在普通 389 端口上升级为 TLS
host all all 10.0.0.0/8 ldap ldapserver=ldap.example.net ldaptls=1 ldapbasedn="dc=example,dc=net" ldapsearchattribute=uid注意 ldaptls 与 ldapscheme=ldaps 不能同时使用(一个是升级式、一个是原生式)。TLS 证书校验所用的信任配置由系统的 LDAP 客户端库决定(PostgreSQL 链接的是 libldap),即 /etc/openldap/ldap.conf(RHEL 系)或 /etc/ldap/ldap.conf(Debian 系)中的 TLS_CACERT / TLS_CACERTDIR——排错时这是个高频盲区。
ldapsearchfilter 表达式
当登录名与属性的关系更复杂时,用 ldapsearchfilter 写标准 LDAP 过滤表达式,%u 替换为登录用户名:
# 等价于 ldapsearchattribute=uid
ldapsearchfilter="(uid=%u)"
# 只允许 active 组的成员登录
ldapsearchfilter="(&(uid=%u)(memberOf=cn=db_users,ou=groups,dc=example,dc=net))"
# AD:按账号名且必须未禁用
ldapsearchfilter="(&(sAMAccountName=%u)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"过滤器把「能不能连数据库」与「目录里的分组/状态」绑在一起,是 LDAP 方案里实现最小授权的常用手段——比在 pg_hba 里逐个枚举用户名可持续得多。
gss 方法与服务端配置
gss 认证方法只关心「票证是否有效、主体是谁」,因此它的记录没有地址以外的参数,选项只有三个:
| 选项 | 默认 | 说明 |
|---|---|---|
include_realm= | 1 | 是否把 @REALM 保留在用户名中;0 时去掉 realm 再匹配/映射 |
map= | 无 | 引用 pg_ident.conf 映射,把主体名翻译成数据库角色 |
krb_realm= | 无 | 限定只接受来自该 realm 的主体 |
# postgresql.conf:keytab 与大小写策略
krb_server_keyfile = '/etc/postgresql/krb5.keytab'
krb_caseins_users = off# pg_hba.conf:域内用户走 GSS 认证,且强制 GSS 加密通道
hostgssenc all +staff 10.0.0.0/8 gss map=krbmap客户端配置
# 1) 获得 Kerberos 票证(密码不出本机)
kinit alice@EXAMPLE.COM
# 2) psql 自动使用 GSSAPI;连接串无需用户名密码
psql "host=pg1.example.com dbname=app gssencmode=require"
# 3) gssencmode:disable / prefer(默认) / require
# require 强制使用 GSS 加密通道(配合 hostgssenc 记录)libpq 环境变量 PGGSSENCMODE 可以为整台机器设置默认的加密模式;Windows 域内客户端不跑 kinit,走的是 SSPI(sspi 认证方法),语义与 gss 一致、自动使用当前域登录凭据。
客户端 krb5.conf 的最小内容
非域管理的 Linux 客户端需要一份正确的 /etc/krb5.conf 才能找到 KDC。最小可用形态:
[libdefaults]
default_realm = EXAMPLE.COM
[realms]
EXAMPLE.COM = {
kdc = kdc1.example.com
kdc = kdc2.example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM三段各自的作用:
[libdefaults]的default_realm是兜底:kinit alice不带@REALM时按它补全;[realms]登记每个 realm 的 KDC 地址,写多个 kdc 就是天然高可用,客户端按序尝试;[domain_realm]把主机名映射到 realm——pg1.example.com属于EXAMPLE.COM,客户端据此为服务票证选择正确的 realm,映射缺失时表现为「找不到正确的主机 realm」类错误。
数据库服务器自己也是 Kerberos 客户端(它要理解 realm),这份文件的正确性对服务器侧同样重要。kinit / klist 的行为异常时,先核对这三段再怀疑 PG 配置。
详细说明
search+bind 的搜索账号设计
search+bind 模式需要一个「先替 PostgreSQL 翻目录」的固定账号(ldapbinddn / ldapbindpasswd)。设计要点:
- 最小权限:该账号只需要「在 basedn 范围内读用户条目」的权限,不要用管理员账号充当。
- 密码管理:
ldapbindpasswd以明文写在 pg_hba.conf 中。把这类记录放进单独的 include 文件(配合 pg_hba.conf 详解 一页的include机制),将文件属主设为 postgres 且权限0600;同时把该密码纳入正常轮换流程——它泄露的后果是「可枚举目录」,不是「可登录数据库」,但仍要管。 - 密码过期陷阱:AD 环境里服务账号默认也会密码过期,
ldapbindpasswd过期后所有 LDAP 认证用户集体无法登录。服务账号要设置「密码永不过期」。
simple bind 的适用判断
simple bind 把「用户名」直接嵌入 DN,等于假设目录结构可以机械生成。判断标准很简单:随机抽三个用户的 DN,模板是否一致。OpenLDAP 的 uid=alice,ou=people,dc=... 结构适合 simple bind;一旦出现 CN=Zhang San(姓名带空格、重名)就要立刻切到 search+bind。还有一个安全细节:simple bind 会在 LDAP 服务器日志里留下完整 DN,而 search+bind 的匿名/低权搜索会在日志里留下「搜索条件」,两者对审计敏感度不同,按需选择。
目录侧的准备工作(OpenLDAP 条目与组)
search+bind 模式要求目录里提前备好三类条目。一份可直接导入的 LDIF 示例:
# ① 用户条目:uid 属性是 ldapsearchattribute 的匹配目标
dn: uid=alice,ou=people,dc=example,dc=net
objectClass: inetOrgPerson
uid: alice
cn: Alice Zhang
sn: Zhang
userPassword: {SSHA}h2VZz3...(由 slappasswd 生成)
# ② 组条目:db_users 组,成员用 member 属性
dn: cn=db_users,ou=groups,dc=example,dc=net
objectClass: groupOfNames
cn: db_users
member: uid=alice,ou=people,dc=example,dc=net
# ③ 服务账号:专供 PostgreSQL 搜索目录用
dn: cn=svc_pg,ou=service,dc=example,dc=net
objectClass: organizationalRole
objectClass: simpleSecurityObject
cn: svc_pg
userPassword: {SSHA}k9Qm2...# 导入(管理员身份执行)
ldapadd -x -D "cn=admin,dc=example,dc=net" -W -f entries.ldif
# 密码哈希的生成工具
slappasswd三个注意点:
memberOf是反向属性(组里加人后自动出现在成员条目上)。AD 原生维护它;OpenLDAP 需要 memberof overlay(slapo-memberof)启用,否则(&(uid=%u)(memberOf=...))过滤器永远匹配不到;- 服务账号的搜索权限应通过 ACL 收敛到「读
ou=people子树的 uid/cn 等最小属性集」,不给全目录读权; - 用户密码用
{SSHA}等哈希存储是底线——目录里出现明文userPassword,LDAP 认证做得再好也是白搭。
传输加密的三个易错点
ldaps 与 StartTLS 选一种,端口要对。 ldapscheme=ldaps 走 636,ldaptls=1 走 389;配错组合的典型症状是连接挂起或立即断开,服务器日志显示握手失败。
CA 信任在系统 LDAP 库配置里。 证书报错(TLS handshake failure、certificate verify failed)时,先查 /etc/openldap/ldap.conf(或 /etc/ldap/ldap.conf)的 TLS_CACERT 指向的 CA 文件是否存在、是否覆盖了 LDAP 服务器的证书链——PostgreSQL 自己的 ssl_ca_file 与此无关。
加密验证别靠猜。 在 LDAP 服务器上抓包(或用 ldapsearch -ZZ 走同样的 StartTLS 路径)确认管道确实加密。LDAP 认证里明文密码过内网的场景比想象中常见。
LDAP 与 pg_ident.conf 的配合
ldap 方法本身不支持 map=(身份就是数据库角色名,密码只是校验手段),但 LDAP 目录里丰富的分组信息可以通过 ldapsearchfilter 间接利用——「只允许某组成员认证」在过滤器层完成。若需要「目录账号 → 数据库角色」的改名式映射(如 jzhang → zhang_j),要么在目录里给用户加第二个属性/条目,要么让用户用目标数据库角色名登录(psql -U zhang_j),由过滤器按该名字搜索。这是 LDAP 方案与 peer / ident 认证 一页映射机制的一个显著差异。
keytab 与服务主体的生成
Kerberos 服务端的准备工作在 KDC(域控制器)侧完成:
# MIT Kerberos(kadmin)示例:创建服务主体并导出 keytab
kadmin -q "addprinc -randkey postgres/pg1.example.com@EXAMPLE.COM"
kadmin -q "ktadd -k /etc/postgresql/krb5.keytab postgres/pg1.example.com@EXAMPLE.COM"# 服务器上的收尾:属主与权限
chown postgres:postgres /etc/postgresql/krb5.keytab
chmod 0600 /etc/postgresql/krb5.keytab要点:
- 主体名必须与客户端访问的主机名一致。客户端连
pg1.example.com,主体就得是postgres/pg1.example.com@REALM;客户端用 IP 或别名连接时,票证申请的主机名与 keytab 对不上,认证直接失败——这是 Kerberos 排错第一大户。libpq 侧服务主体名按postgres/主机名@REALM构造,个别驱动暴露自定义服务名的参数,细节以所用驱动文档为准,不要凭记忆硬编码。 - keytab 变更要注意 kvno:KDC 侧重新生成 keytab 后,新文件的密钥版本号(kvno)必须与 KDC 记录一致,否则报
Key version number for principal in key table is incorrect。
include_realm 与用户名映射
GSS 认证通过后,服务器拿到的身份是完整主体 alice@EXAMPLE.COM。默认 include_realm=1,意味着数据库里得有一个叫 alice@EXAMPLE.COM 的角色——很少有人在库里这么建角色。两种常规处理:
# 方式一:去掉 realm(用户名在多 realm 时有歧义风险)
hostgssenc all all 10.0.0.0/8 gss include_realm=0
# 方式二:映射(推荐,可同时做白名单)
# pg_ident.conf
krbmap /^(.*)@EXAMPLE\.COM$ \1
# pg_hba.conf
hostgssenc all all 10.0.0.0/8 gss map=krbmap映射写法的额外收益是显式白名单:只有正则匹配且映射成功的主体能进来,其他 realm 的主体自然被拒。多 realm(如 EXAMPLE.COM 与 CORP.EXAMPLE.COM 并存)的环境必须用映射,include_realm=0 会把不同 realm 的同名用户混为一谈。
GSS 认证与 GSS 加密是两件事
初学者最容易混淆的两层:gss 认证方法解决「你是谁」,GSS 加密(gssencmode、hostgssenc)解决「传输是否加密」。四种组合都合法:
| pg_hba 记录 | 客户端 gssencmode | 结果 |
|---|---|---|
host + gss | prefer | GSS 认证,传输是否加密取决于协商 |
host + gss | disable | 仅认证用 GSSAPI,数据明文 |
hostgssenc + gss | require | 认证与传输都用 GSSAPI(无 TLS 依赖) |
hostssl + scram-sha-256 | sslmode=require | 传输用 TLS,认证用 SCRAM |
企业域内若全量 Kerberos 化,hostgssenc + gss 可以完全替代 TLS 证书体系;混合环境则按网段/账号拆分记录。
认证链路的性能与可用性
外部认证把「登录路径」从一次本地校验变成了一串网络往返,量级要心里有数:
- search+bind 一次登录 = 3 次目录往返(搜索账号 bind + 搜索 + 用户 bind),加内网 RTT 通常仍在毫秒级;对「高频新建连接」的应用要叠连接池,别让每次请求都走完整认证;
- 目录不可达 = 认证不可用。
ldapserver支持写多台(空格分隔),客户端库按序尝试;即便如此,目录服务的健康监控要和数据库本身同级,LDAP 抖动会直接表现为「数据库突然连不上」; - KDC 不可用只影响「新票证」。已有票证的会话照常工作,续期与首次
kinit会失败——这给了故障排查一个区分手段:全员第一次连接失败 vs 只有续期失败,指向不同的组件; - 应急降级路径必须预置:保留一两个
scram-sha-256本地管理账号(不入目录、不依赖任何外部服务),目录体系故障时管理员仍能进库处置。这条要写进应急预案并定期演练。
排错:LDAP 侧
按症状定位:
| 症状 | 优先检查 |
|---|---|
| 连接长时间挂起后超时 | ldapserver 可达性(nc -vz ldap.example.net 389/636)、防火墙、authentication_timeout |
| 认证立即失败,日志有 bind 相关错误 | ldapbinddn/ldapbindpasswd 是否正确、是否密码过期 |
| 特定用户失败,其他正常 | ldapsearchattribute / ldapsearchfilter 是否匹配到该用户、用户是否被禁用 |
| 全部失败且日志提示 TLS | StartTLS/LDAPS 混淆、系统 LDAP 库的 CA 配置 |
| 日志提示找到 0 个或多个条目 | ldapbasedn 写错层级;目录中存在重名条目 |
# 用系统工具独立验证,把「PG 配置问题」与「目录问题」分开
ldapsearch -x -H ldap://ldap.example.net -b "dc=example,dc=net" \
-D "cn=svc_pg,ou=service,dc=example,dc=net" -W "(uid=alice)"排错:Kerberos 侧
| 症状 | 优先检查 |
|---|---|
kinit 成功但连库失败 | 主体名与连接主机名是否一致;keytab 里是否恰好有该主体 |
| kvno 类错误 | keytab 与 KDC 的密钥版本不一致,重新 ktadd |
| 时钟偏差错误 | 服务器与 KDC 的时间同步(默认容忍 5 分钟) |
| realm 不匹配 | krb5.conf 的 [realms]/[domain_realm] 段;krb_caseins_users |
| keytab 读取失败 | 文件属主/权限是否允许 postgres 用户读 |
# 服务器侧验证 keytab 内容与 kvno
klist -k /etc/postgresql/krb5.keytab
kvno postgres/pg1.example.com@EXAMPLE.COM
# 客户端侧确认票证
klist实战示例
示例一:LDAP search+bind 完整配置(OpenLDAP)
目标:目录树 dc=example,dc=net 下有 ou=people 与 ou=groups;服务账号 cn=svc_pg,ou=service,dc=example,dc=net 负责搜索;只有 db_users 组成员可以连库。
# ---------- pg_hba.conf ----------
# 包含敏感 bind 密码的记录集中在一个独立文件,权限 0600
include_if_exists /etc/postgresql/pg_hba_ldap.conf
# ---------- /etc/postgresql/pg_hba_ldap.conf ----------
hostssl all all 10.0.0.0/8 ldap
ldapserver=ldap.example.net
ldapbasedn="dc=example,dc=net"
ldapbinddn="cn=svc_pg,ou=service,dc=example,dc=net"
ldapbindpasswd="S3cret!"
ldapsearchfilter="(&(uid=%u)(memberOf=cn=db_users,ou=groups,dc=example,dc=net))"# 1) 收紧敏感文件权限
chown postgres:postgres /etc/postgresql/pg_hba_ldap.conf
chmod 0600 /etc/postgresql/pg_hba_ldap.conf
# 2) 先用 ldapsearch 验证过滤器和 bind 账号(不等价于成功,但能排除目录问题)
ldapsearch -x -H ldaps://ldap.example.net \
-D "cn=svc_pg,ou=service,dc=example,dc=net" -w 'S3cret!' \
-b "dc=example,dc=net" \
"(&(uid=alice)(memberOf=cn=db_users,ou=groups,dc=example,dc=net))" dn
# 3) 预检并重载
psql -c "SELECT rule_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;"
psql -c "SELECT pg_reload_conf();"
# 4) 验证
psql "host=pg1 dbname=app user=alice sslmode=require" -c "SELECT current_user;"Password for user alice: (输入的是 LDAP 密码,不是数据库密码)
current_user
--------------
alice
(1 row)前置条件:数据库侧要有同名角色(目录里的账号不会自动变成数据库角色):
CREATE ROLE alice LOGIN IN ROLE app_rw;示例二:Kerberos SSO(hostgssenc + gssencmode=require)
目标:域内工作站用户 kinit 后免密直连,非域用户走 SCRAM + TLS。
# postgresql.conf
krb_server_keyfile = '/etc/postgresql/krb5.keytab'# pg_hba.conf
# 域内网段:GSS 认证 + GSS 加密,主体经映射转成数据库角色
hostgssenc app all 10.0.0.0/8 gss map=krbmap
# 其余网段:常规密码方案
hostssl app all 0.0.0.0/0 scram-sha-256# pg_ident.conf
krbmap /^(.*)@EXAMPLE\.COM$ \1-- 数据库角色按主体裁剪后的名字创建
CREATE ROLE alice LOGIN;# 客户端全流程
kinit alice@EXAMPLE.COM
psql "host=pg1.example.com dbname=app gssencmode=require" -c "\conninfo"You are connected to database "app" as user "alice" on host "pg1.example.com" ...示例三:Active Directory 场景的检查单
AD 与 OpenLDAP 的差异集中在属性与账号形态上,逐项核对:
# 1) AD 的登录名属性是 sAMAccountName,不是 uid
# ldapsearchattribute=sAMAccountName 或 ldapsearchfilter="(sAMAccountName=%u)"
# 2) AD 用户 DN 用的是显示名 CN,无法拼接 —— 必须用 search+bind 模式
# 3) 只放行未禁用的账号(位标志 0x2 = ACCOUNTDISABLE)
# ldapsearchfilter="(&(sAMAccountName=%u)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"
# 4) 全局编目端口 3268/3269 用于跨域搜索;单域用 389/636 即可
# 5) 验证目录可查(-ZZ 强制 StartTLS 成功才继续)
ldapsearch -x -ZZ -H ldap://dc1.corp.example.com \
-D "cn=svc_pg,ou=service,dc=corp,dc=example,dc=com" -W \
-b "dc=corp,dc=example,dc=com" "(sAMAccountName=alice)" dn示例四:域内工作站的双轨接入(Kerberos 为主、LDAP 兜底)
目标:域内工作站(10.0.0.0/8)凭 Kerberos SSO 免密进库;域外工具(监控、外部供应商)走 LDAP 校验;两者都无法满足的来源一律拒绝。
# postgresql.conf
krb_server_keyfile = '/etc/postgresql/krb5.keytab'# pg_hba.conf —— 双轨布局
# 轨道一:域内网段,GSS 认证 + GSS 加密,主体经映射裁剪
hostgssenc all all 10.0.0.0/8 gss map=krbmap
# 轨道二:其余来源,LDAP 校验 + TLS 管道(仅允许指定网段的服务设备)
hostssl all all 10.20.0.0/16 ldap ldapserver=ldap.example.net
ldapbasedn="dc=example,dc=net" ldapbinddn="cn=svc_pg,ou=service,dc=example,dc=net"
ldapbindpasswd="S3cret!" ldapsearchfilter="(&(uid=%u)(memberOf=cn=db_users,ou=groups,dc=example,dc=net))"
# 兜底
hostnossl all all 0.0.0.0/0 reject
host all all 0.0.0.0/0 reject# pg_ident.conf
krbmap /^(.*)@EXAMPLE\.COM$ \1-- 数据库侧:角色提前就位(目录账号不会自动出现)
CREATE ROLE alice LOGIN; -- 供 Kerberos 主体 alice@EXAMPLE.COM 使用
CREATE ROLE svc_monitor LOGIN; -- 供域外监控设备走 LDAP 使用# 验证:域内走票证(不输密码)
kinit alice@EXAMPLE.COM
psql "host=pg1.example.com dbname=app gssencmode=require" -c "\conninfo"
# 验证:域外走 LDAP(输目录密码)
psql "host=pg1.example.com dbname=app user=svc_monitor sslmode=verify-full"
# 验证:兜底拒绝(明文连接应报 no encryption)
psql "host=pg1.example.com dbname=app user=alice sslmode=disable"# 巡检两种流量的分布,确认双轨各有量、无异常漂移
psql -c "SELECT a.usename, count(*)
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.client_addr IS NOT NULL
GROUP BY 1 ORDER BY 2 DESC;"双轨架构的验收清单:① 域内用户 klist 有 TGT 且连接免密;② 域外设备输目录密码可连、禁用目录账号后立即被拒;③ 两个轨道的日志特征可区分(GSS 认证与 ldap 认证在连接日志中来源不同);④ 兜底记录在位,第三方网段无法绕过轨道直连。
注意事项
LDAP 账号不会自动出现在数据库里。 认证通过只代表「这个人在目录里存在且密码正确」,数据库侧仍需提前建好同名角色(或提前建好组角色并授予成员资格)。账号生命周期管理要设计成「目录新增 → 自动/人工建角色」的流程,否则会出现「能认证但 role does not exist」的报错。
ldapbindpasswd 是明文落盘的。 用 include 拆分 + 0600 权限控制读面,纳入轮换计划;不能用 include 时至少确保 pg_hba.conf 自身权限收紧。任何能读这个文件的人都能枚举你的目录。
LDAPS/StartTLS 的证书信任在系统 LDAP 库侧。 PostgreSQL 的 ssl_ca_file 管「客户端连 PostgreSQL」的证书,不管「PostgreSQL 连 LDAP」的证书——两套信任链别搞混。
服务主体名对主机名敏感。 客户端用什么主机名连接,票证就请求什么主机的服务;用 IP 或 CNAME 连接会直接失败。把「连接规范」(统一用 FQDN)写进团队文档可以省掉大量 Kerberos 工单。
keytab 是服务器的密码。 0600 + postgres 属主是底线;keytab 文件进备份、进 Git 都是事故。轮换 keytab(KDC 侧重新 ktadd)会使旧密钥失效,注意滚动窗口内备库与主库的 keytab 要同步更新。
include_realm=0 在多 realm 环境有歧义。 两个 realm 里都有 alice 时,include_realm=0 会把她们映射到同一个数据库角色。多 realm 必须用 map + 正则显式区分。
authentication_timeout 是 LDAP 故障的显形机制。 目录服务器无响应时,客户端连接会在该超时(默认 1 分钟)后被切断。LDAP 高可用(多台 ldapserver、健康检查)要纳入方案,否则 LDAP 抖动等于数据库拒绝服务。
SSPI 只在 Windows 有意义。 Linux 服务器上配 sspi 记录永远不会匹配成功;Windows 客户端 + Linux 服务器的组合走的是 GSSAPI。
Kerberos 票据缓存不适用于无人值守进程。 cron、CI runner、守护进程没有交互式 kinit,TGT 过期后认证直接失败。这类场景要么用 keytab 方式取票(kinit -k -t service.keytab 主体名 的非交互形态),要么干脆改走证书认证——「Kerberos 只给人用、证书给机器用」是最省心的分界。
LDAP 认证的角色可能同时持有本地密码。 LDAP 方法只要求「目录密码正确」,并不检查数据库侧有没有设密码;如果给 LDAP 账号也设了 SCRAM 密码,它就多了一条不经过目录的认证入口(目录禁用后仍能登录)。做「账号生命周期以目录为准」的方案时,确认这些角色 PASSWORD NULL。
最佳实践
- 优先 search+bind 模式。 DN 模板假设迟早被打破(改名、重名、组织调整),搜索模式的维护成本长期更低;AD 环境没有别的选择。
- LDAP 管道必须加密。
ldapscheme=ldaps(636)或ldaptls=1(StartTLS)二选一,并用 ldapsearch 验证加密确实生效。 - 用
ldapsearchfilter实现目录侧授权。 「组内成员 + 未禁用账号」的过滤器让授权与目录生命周期自动同步,避免在 pg_hba 里手写用户名单。 - bind 账号最小权限 + 永不过期 + include 隔离。 三件事一起做:只读权限、
0600的独立 include 文件、按企业流程轮换。 - Kerberos 主体名、keytab、连接主机名三处一致。 用 FQDN 连接作为团队规范;keytab
0600且属主 postgres;轮换时主备同步。 - GSS 主体一律走 map 映射。 正则裁剪 realm 的同时获得白名单语义,多 realm 环境强制要求。
- 域内用户 GSS +
hostgssenc,域外用户 SCRAM +hostssl,两套并存。 不要试图用一套认证覆盖所有来源;按网段拆分记录,各走各的强认证。 - 把 LDAP/KDC 列为数据库可用性的依赖项。 监控目录服务可用性;LDAP 查询延迟要纳入告警,目录故障要有「降级到本地账号」的应急预案(保留少量
scram-sha-256管理账号)。 - 变更先 ldapsearch / klist 旁路验证。 目录问题与 PG 配置问题用系统工具分离定位,不要在数据库层盲改。
- 审计对齐:LDAP 侧日志 + PostgreSQL 侧日志关联。 bind 失败与认证失败在两侧都有记录,建立「同一时间段联合检索」的排障流程,取证时两侧证据链都拿得出。
- 密码之外的账号同步不要依赖 LDAP 认证。 目录里的账号删除/禁用应触发数据库侧
ALTER ROLE ... NOLOGIN(通过自动化同步),否则已离职账号仍持有可用角色(只是改不了密码)。 - 文档化 realm、basedn、bind 账号与服务主体命名规范。 这些跨系统约定散落在 pg_hba 注释里迟早丢失,集中成一份「认证架构说明」并在 HBA 文件头引用。
下一节
继续学习:证书认证
参考链接
- PostgreSQL 官方文档:Authentication Methods(LDAP / GSSAPI / SSPI)
- PostgreSQL 官方文档:GSSAPI Authentication
- PostgreSQL 官方文档:SSPI Authentication
- PostgreSQL 官方文档:User Name Maps
- PostgreSQL 官方文档:The pg_hba.conf File
- PostgreSQL 官方文档:Connection and Authentication 配置参数(krb_server_keyfile 等)
- PostgreSQL 官方文档:Database Connection Control Functions(gssencmode 等)
- PostgreSQL 官方文档:Environment Variables
- PostgreSQL 官方文档:Authentication Problems