Skip to content

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 等价物)只做简要说明。

前置知识

建议先阅读:密码认证(md5 / scram-sha-256)

基础概念 ​

两种技术在体系中的位置 ​

维度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 侧统一管理。

text
┌────────┐  ① 用户名+密码   ┌────────────┐  ② 以用户 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 的惯例形态是:

text
postgres/pg1.example.com@EXAMPLE.COM
└─服务名─┘└────实例主机名────┘└────域────┘

对应的服务端密钥存放在 keytab 文件中(路径由 krb_server_keyfile 参数指定),它就是「服务器的密码」,权限必须像密码一样管理。

Kerberos 票证交换的完整时序 ​

把「kinit → 连库」的全过程拆开,每一步失败对应一类排错方向:

text
客户端                                   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=389LDAP 服务端口
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,例如:

text
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 场景几乎必须) ​

text
# 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

执行流程:

  1. 服务器以 ldapbinddn + ldapbindpasswd 绑定目录(未提供时尝试匿名绑定);
  2. 以 ldapbasedn 为起点、按 ldapsearchattribute=登录用户名(或 ldapsearchfilter)搜索用户的 DN;
  3. 以搜到的 DN + 客户端提供的密码再次绑定,成功即认证通过。

优点是用户 DN 可以不必符合固定模板(姓名里有空格、OU 结构复杂都能处理),这正是 Active Directory 的常态——AD 里账号的 DN 是 CN=张三,OU=IT,... 而登录名在 sAMAccountName 属性里,无法简单拼接,必须搜索。

模式二:simple bind(DN 结构规整时最省事) ​

text
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 服务器,管道必须加密,两种方式:

text
# 方式一: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 替换为登录用户名:

text
# 等价于 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 的主体
conf
# postgresql.conf:keytab 与大小写策略
krb_server_keyfile = '/etc/postgresql/krb5.keytab'
krb_caseins_users = off
text
# pg_hba.conf:域内用户走 GSS 认证,且强制 GSS 加密通道
hostgssenc  all   +staff   10.0.0.0/8   gss map=krbmap

客户端配置 ​

bash
# 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。最小可用形态:

ini
[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 示例:

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...
bash
# 导入(管理员身份执行)
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(域控制器)侧完成:

bash
# 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"
bash
# 服务器上的收尾:属主与权限
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 的角色——很少有人在库里这么建角色。两种常规处理:

text
# 方式一:去掉 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 + gsspreferGSS 认证,传输是否加密取决于协商
host + gssdisable仅认证用 GSSAPI,数据明文
hostgssenc + gssrequire认证与传输都用 GSSAPI(无 TLS 依赖)
hostssl + scram-sha-256sslmode=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 是否匹配到该用户、用户是否被禁用
全部失败且日志提示 TLSStartTLS/LDAPS 混淆、系统 LDAP 库的 CA 配置
日志提示找到 0 个或多个条目ldapbasedn 写错层级;目录中存在重名条目
bash
# 用系统工具独立验证,把「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 用户读
bash
# 服务器侧验证 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 组成员可以连库。

text
# ---------- 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))"
bash
# 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;"
text
Password for user alice:  (输入的是 LDAP 密码,不是数据库密码)
 current_user
--------------
 alice
(1 row)

前置条件:数据库侧要有同名角色(目录里的账号不会自动变成数据库角色):

sql
CREATE ROLE alice LOGIN IN ROLE app_rw;

示例二:Kerberos SSO(hostgssenc + gssencmode=require) ​

目标:域内工作站用户 kinit 后免密直连,非域用户走 SCRAM + TLS。

conf
# postgresql.conf
krb_server_keyfile = '/etc/postgresql/krb5.keytab'
text
# 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
text
# pg_ident.conf
krbmap  /^(.*)@EXAMPLE\.COM$    \1
sql
-- 数据库角色按主体裁剪后的名字创建
CREATE ROLE alice LOGIN;
bash
# 客户端全流程
kinit alice@EXAMPLE.COM
psql "host=pg1.example.com dbname=app gssencmode=require" -c "\conninfo"
text
You are connected to database "app" as user "alice" on host "pg1.example.com" ...

示例三:Active Directory 场景的检查单 ​

AD 与 OpenLDAP 的差异集中在属性与账号形态上,逐项核对:

bash
# 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 校验;两者都无法满足的来源一律拒绝。

conf
# postgresql.conf
krb_server_keyfile = '/etc/postgresql/krb5.keytab'
text
# 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
text
# pg_ident.conf
krbmap   /^(.*)@EXAMPLE\.COM$    \1
sql
-- 数据库侧:角色提前就位(目录账号不会自动出现)
CREATE ROLE alice LOGIN;            -- 供 Kerberos 主体 alice@EXAMPLE.COM 使用
CREATE ROLE svc_monitor LOGIN;      -- 供域外监控设备走 LDAP 使用
bash
# 验证:域内走票证(不输密码)
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"
text
# 巡检两种流量的分布,确认双轨各有量、无异常漂移
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。

最佳实践 ​

  1. 优先 search+bind 模式。 DN 模板假设迟早被打破(改名、重名、组织调整),搜索模式的维护成本长期更低;AD 环境没有别的选择。
  2. LDAP 管道必须加密。 ldapscheme=ldaps(636)或 ldaptls=1(StartTLS)二选一,并用 ldapsearch 验证加密确实生效。
  3. 用 ldapsearchfilter 实现目录侧授权。 「组内成员 + 未禁用账号」的过滤器让授权与目录生命周期自动同步,避免在 pg_hba 里手写用户名单。
  4. bind 账号最小权限 + 永不过期 + include 隔离。 三件事一起做:只读权限、0600 的独立 include 文件、按企业流程轮换。
  5. Kerberos 主体名、keytab、连接主机名三处一致。 用 FQDN 连接作为团队规范;keytab 0600 且属主 postgres;轮换时主备同步。
  6. GSS 主体一律走 map 映射。 正则裁剪 realm 的同时获得白名单语义,多 realm 环境强制要求。
  7. 域内用户 GSS + hostgssenc,域外用户 SCRAM + hostssl,两套并存。 不要试图用一套认证覆盖所有来源;按网段拆分记录,各走各的强认证。
  8. 把 LDAP/KDC 列为数据库可用性的依赖项。 监控目录服务可用性;LDAP 查询延迟要纳入告警,目录故障要有「降级到本地账号」的应急预案(保留少量 scram-sha-256 管理账号)。
  9. 变更先 ldapsearch / klist 旁路验证。 目录问题与 PG 配置问题用系统工具分离定位,不要在数据库层盲改。
  10. 审计对齐:LDAP 侧日志 + PostgreSQL 侧日志关联。 bind 失败与认证失败在两侧都有记录,建立「同一时间段联合检索」的排障流程,取证时两侧证据链都拿得出。
  11. 密码之外的账号同步不要依赖 LDAP 认证。 目录里的账号删除/禁用应触发数据库侧 ALTER ROLE ... NOLOGIN(通过自动化同步),否则已离职账号仍持有可用角色(只是改不了密码)。
  12. 文档化 realm、basedn、bind 账号与服务主体命名规范。 这些跨系统约定散落在 pg_hba 注释里迟早丢失,集中成一份「认证架构说明」并在 HBA 文件头引用。

下一节

继续学习:证书认证

参考链接 ​