密码认证(md5 / scram-sha-256)
密码认证是 PostgreSQL 使用最广泛的认证方式:客户端证明「我知道这个角色设置的口令」,服务器依据 pg_hba.conf 中指定的密码认证方法(scram-sha-256、md5 或 password)选择校验协议。上一页 pg_hba.conf 详解 讲清楚了「记录怎么匹配、方法怎么选」,本页把三种密码方法拆开比较,重点讲清楚为什么 SCRAM-SHA-256 从 PG14 起成为默认、md5 为什么必须进迁移计划、以及围绕密码的存储、策略、过期与迁移如何落地。
先把结论放在最前面:现代生产环境一律使用 scram-sha-256 作为 pg_hba 认证方法与 password_encryption 的取值。md5 是历史包袱(PG10 之前的主流方案),password 是明文方案,两者都有明确的、可阐述的缺陷。理解这三种方法在协议层面的差异,不仅能帮助做出正确的选型,也能在「客户端连不上」「密码迁移失败」这类故障时知道往哪个方向查。
密码认证涉及三样东西:协议(网络上怎么验证)、存储(服务端怎么保存口令的秘密)、策略(口令的强度与生命周期)。这三层经常被混为一谈——比如「存储已经加密了所以网络传输安全」就是典型误解。本页会按这三层分别展开,最后给出一份可执行的 md5 → SCRAM 迁移方案。
前置知识
建议先阅读:pg_hba.conf 详解
基础概念
三种密码认证方法速览
| 方法 | 引入版本 | 网络传输 | 服务端存储 | 安全性 | 定位 |
|---|---|---|---|---|---|
password | 极早 | 明文发送密码 | SCRAM / md5 / 明文哈希 | 极低(无 TLS 时) | 仅限加密管道内的兼容方案 |
md5 | 极早 | 挑战-响应(不传密码) | md5<hash> | 低(pass-the-hash、彩虹表) | 兼容旧客户端 |
scram-sha-256 | PG10 | 挑战-响应 + TLS 通道绑定 | SCRAM verifier | 高 | 现代默认 |
三个方法的关键差异不在「密码存得安不安全」,而在协议层:
password把密码直接发给服务器,安全性完全依赖传输层加密。没有 SSL 时,任何嗅探者都能拿到密码原文。md5不传密码,传的是「用服务端随机 salt 加工过的哈希」。但它存储的哈希值恰好就是应答挑战所需的长期秘密——拿到服务端存储就能直接通过认证(pass-the-hash),而且 md5 算法本身已被认为不安全。scram-sha-256是 RFC 5802 / RFC 7677 定义的标准协议。存储的 verifier 无法被用来直接应答挑战,还支持 TLS 通道绑定以对抗中间人,是目前唯一「服务端泄露 ≠ 密码泄露」的密码方法。
挑战-响应机制
md5 与 scram-sha-256 共享同一个设计思想:服务器出题,客户端答题,密码本身不过网络。
以 md5 为例:客户端发起连接后,服务器生成一个 4 字节随机 salt 发给客户端;客户端计算 md5(md5(password + username) + salt) 作为应答发回;服务器用自己存储的 md5(password + username) 加上同一个 salt 做同样的运算,比对结果。整个过程中网络上只出现过 salt 和应答值,密码原文与长期哈希都不出网。
SCRAM 的流程更复杂(多了客户端随机数、双方 nonce、三步握手),但思想一致,且在三个方面做得更好:服务端存储的 verifier 数学上推不出可用于应答的密钥;支持与 TLS 通道绑定防止中间人把握手「搬运」到别的连接;协议本身可协商(有标准的扩展机制)。
一次密码认证的完整时序
以 scram-sha-256 为例,把「密码认证」放回连接时序里看:
客户端 PostgreSQL
│ ──① 启动消息(user/database)──────────────► │
│ │ ② pg_hba 匹配 → 命中 scram-sha-256
│ ◄─③ AuthenticationSASL(宣告机制)─────────── │
│ ──④ client-first-message(nonce)──────────► │
│ ◄─⑤ server-first-message(salt + 迭代次数)── │ salt/迭代次数取自 pg_authid 的 verifier
│ ──⑥ client-final-message(证明)───────────► │ 客户端用「密码+salt」本地推导
│ ◄─⑦ server-final-message(服务器证明)─────── │ 双向确认,认证完成时序里藏着几个排错路标:②失败报 no pg_hba.conf entry;③之后客户端若突然断开,多半是驱动不支持 SCRAM(协议宣告后没有后续消息);⑥失败报 password authentication failed——注意这时无法区分「密码错」与「verifier 是 md5 格式(协议不匹配)」,两种情况在客户端视角一样,要到服务端日志或 pg_authid 里找差异。md5 认证的时序结构相同,只是消息更简单(一轮 salt 应答)。
SCRAM-SHA-256 验证器(verifier)
服务端不存密码本身,也不存「可以反推密码」的东西,而是存一个 SCRAM verifier。它的格式长这样:
SCRAM-SHA-256$4096:<base64 编码的 salt>$<base64 的 StoredKey>:<base64 的 ServerKey>构造过程(由服务端在设置密码时完成):
SaltedPassword = PBKDF2-HMAC-SHA-256(明文密码, salt, 迭代次数)
ClientKey = HMAC-SHA-256(SaltedPassword, "Client Key")
StoredKey = SHA-256(ClientKey)
ServerKey = HMAC-SHA-256(SaltedPassword, "Server Key")关键在于:StoredKey 是 ClientKey 的单向哈希。认证时客户端需要给出 ClientKey(或等价证明),而服务端存储的 StoredKey 无法反推出 ClientKey——所以即使 pg_authid 整张表泄露,攻击者也无法拿着存储值伪装客户端(md5 方案做不到这一点)。ServerKey 则用于服务器向客户端证明「我确实存有你密码的 verifier」,防的是假服务器钓鱼。
迭代次数是可调的强度旋钮
verifier 中的 4096 是 PBKDF2 迭代次数。PG16 起提供 scram_iterations 参数(默认 4096),调大它能显著增加暴力破解单条密码的成本(代价是认证时的 CPU 开销也变大)。这是密码存储强度上少数可以按需拧紧的旋钮。
密码存在哪里
密码存储在系统表 pg_authid 的 rolpassword 列(集簇级系统表,仅超级用户可读)。pg_shadow 是它的一个视图,只包含可登录(rolcanlogin)的角色,字段一致:
-- 需要超级用户
SELECT rolname, rolpassword FROM pg_authid WHERE rolpassword IS NOT NULL LIMIT 3; rolname | rolpassword
-----------+--------------------------------------------------------------------------------------------
alice | SCRAM-SHA-256$4096:zL3k9...$Zk9...:Qm2...
bob | md5d7a1c30f8b9e2f0c4a6d8e1b2c3d4e5f开头是 SCRAM-SHA-256$ 还是 md5,一眼即可判断该角色存储的是哪种 verifier——这是迁移前盘点的主要手段。
语法与用法
pg_hba 记录
密码认证的三种方法都写在 HBA 记录的认证方法段:
# TYPE DATABASE USER ADDRESS METHOD
hostssl app app_rw 10.0.1.0/24 scram-sha-256
host legacy_db old_app 10.0.2.0/24 md5
hostssl app etl 10.0.9.0/24 password方法与存储格式的配合关系值得注意(PG11+ 行为):
| HBA 方法 | 存储 SCRAM verifier 时 | 存储 md5 verifier 时 |
|---|---|---|
scram-sha-256 | 正常 SCRAM 认证 | 认证失败(协议不匹配) |
md5 | 自动升级为 SCRAM 认证(向后兼容设计) | 正常 md5 认证 |
password | 按所选协议把密码发给服务器后按存储校验 | 同左 |
也就是说:HBA 里写 md5 而存储已是 SCRAM 时,实际执行的是 SCRAM 认证(md5 在这里退化为「任意密码挑战-响应方法」)。这让迁移可以分两步走:先改存储格式,再改 HBA 方法。
设置与修改密码
-- 设置密码(存储格式由 password_encryption 决定)
ALTER ROLE app_rw PASSWORD 'S7rong#pass';
-- 显式指定过期时间(详见后文 VALID UNTIL)
ALTER ROLE app_rw PASSWORD 'S7rong#pass' VALID UNTIL '2027-01-01 00:00+08';
-- 删除密码:该角色将无法通过任何密码方法认证
ALTER ROLE app_rw PASSWORD NULL;password_encryption:决定存储格式
password_encryption 决定 CREATE ROLE ... PASSWORD / ALTER ROLE ... PASSWORD 时用什么格式存储:
-- 查看当前值(PG14 起默认 scram-sha-256;更早版本默认 md5)
SHOW password_encryption;
-- 切换(可在会话级临时切换,也可 ALTER SYSTEM 持久化)
SET password_encryption = 'scram-sha-256';
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();注意它只影响之后设置的密码,不会重写已有密码——这就是为什么密码迁移必须逐角色重置。
检查存储格式
-- 全集簇盘点:SCRAM / md5 / 明文各多少账号
SELECT CASE
WHEN rolpassword LIKE 'SCRAM-SHA-256$%' THEN 'scram-sha-256'
WHEN rolpassword LIKE 'md5%' THEN 'md5'
WHEN rolpassword IS NULL THEN '(no password)'
ELSE rolpassword -- 未加密的明文存储(password_encryption 旧取值)
END AS format,
count(*)
FROM pg_authid
WHERE rolcanlogin
GROUP BY 1
ORDER BY 1;客户端连接
libpq 系客户端(psql、各语言 libpq 绑定)会依据服务端宣告的认证要求自动协商,无需任何特殊配置:
# psql 会自动完成 SCRAM 握手
psql "host=pg1 dbname=app user=app_rw sslmode=require"
# psql \password 为自己设置新密码(不在命令行明文暴露)
psql -c "\password"对安全性有更高要求时,可要求通道绑定:
# channel_binding=require:强制 SCRAM 使用 TLS 通道绑定,防中间人搬运握手
psql "host=pg1 dbname=app user=app_rw sslmode=require channel_binding=require"详细说明
md5 的三宗罪
其一:pass-the-hash。 md5 存储值 md5(password + username) 正是应答挑战所需的长期秘密。攻击者只要读到 pg_authid.rolpassword(SQL 注入读表、备份文件泄露、内存 dump 都可能),就能离线计算应答直接登录,完全不需要破解密码。SCRAM 的 verifier 不具备这种「拿来即用」的性质。
其二:算法弱。 md5 哈希计算极快,配合 GPU 的暴力破解/彩虹表吞吐量巨大。username 作为固定 salt 缓解了「跨库撞库」,但对同一 (user, password) 组合的彩虹表攻击无效。
其三:无通道绑定。 md5 挑战-响应与 TLS 通道无关,中间人可以把 A 客户端的合法握手搬到自己的连接上(credential forwarding),也可以直接降级攻击。SCRAM + channel_binding=require 堵死了这条路。
password 方法的适用边界
password 方法把明文密码交给服务器,唯一的正确用法是包在加密管道里(hostssl 记录 + 客户端 sslmode=verify-ca 以上)。它的存在价值是兼容性:某些外部认证后端(如部分 PAM 场景)需要拿到明文密码。除此外没有任何理由选它——同样的管道里 scram-sha-256 更安全,成本为零。
没有 SSL 的 password 方法等于裸奔
host all all 0.0.0.0/0 password 这样的记录意味着全网段内任何嗅探点(交换机、ARP 代理、云平台虚拟化层、办公网 Wi-Fi)都能直接读到密码。审计发现此组合应立即整改。
SCRAM 握手流程一瞥
理解握手有助于排错(比如 channel_binding 失败、驱动兼容问题)。SCRAM-SHA-256 的完整交互分三步:
C: client-first-message → n=,r=客户端nonce
S: server-first-message → r=拼接后的nonce,s=salt,i=迭代次数
C: client-final-message → 不含密码的证明(基于 ClientKey 计算)
S: server-final-message → 服务器的签名证明(基于 ServerKey)密码的参与发生在客户端内部:用「密码 + salt + 迭代次数」算出 SaltedPassword,再导出 ClientKey 参与证明计算。整个流程中明文密码与服务端 verifier 都不出现在网络上。
pg_authid 与 pg_shadow
两处存储是同一份数据:pg_shadow 是 pg_authid 在每个数据库里对可登录角色的投影视图。安全属性相同——都需要超级用户才能读。这带来两条实践要求:
- 给应用账号授予
pg_read_all_stats之类的监控角色时,确认它没有读pg_authid的能力(该内置角色不包含读系统表密码列的权限,保持默认即可)。 - 备份与日志的权限域要覆盖到:
pg_dumpall的输出包含ALTER ROLE ... PASSWORD语句(以加密格式内嵌),备份文件的访问控制与数据库同等对待。
password_encryption 与 scram_iterations
| 参数 | 作用域 | 默认值 | 说明 |
|---|---|---|---|
password_encryption | user(会话级可改) | scram-sha-256(PG14+;PG13 及以前为 md5) | 新设密码的存储格式 |
scram_iterations | user(PG16+) | 4096 | 生成 verifier 的 PBKDF2 迭代次数 |
两者都是「影响下一次密码写入」的参数。高安全场景可以把 scram_iterations 提到 10000~30000 量级——每次认证的服务端开销会从亚毫秒上升到几毫秒,对低频认证的系统可接受;但对每秒上百次连接(连接池穿透到数据库的场景)要压测后再定。
老密码如何升级
SCRAM verifier 的构造需要明文密码(或至少能推导出它的输入),而 md5 存储值不可逆,所以不存在原地升级:把 password_encryption 改成 scram-sha-256 之后,老账号必须重新设置一次密码才会生成 SCRAM verifier。可行路径有三条:
- 通知用户自己改:
ALTER ROLE xxx PASSWORD NULL强制失效,用户下次连接时由管理员重置临时密码(体验最差,适合非核心账号)。 - 管理员批量重置:与账号负责人约定新密码后批量
ALTER ROLE ... PASSWORD(适合服务账号,需要改密配合应用配置)。 - 统一维护窗口重置:停机或只读窗口内全量重置并同步下发(适合账号数量可控的中小环境)。
无论哪条路,都要先解决客户端兼容性(见下节),否则改完存储格式,旧客户端会直接连不上。
客户端兼容性
SCRAM 要求客户端协议支持(PG10 引入)。兼容性盘点清单:
| 客户端 | 最低要求 |
|---|---|
| libpq(psql 等) | libpq ≥ 10(2017 年) |
| JDBC(pgjdbc) | 42.2.x 起完整支持 SCRAM |
| Npgsql / psycopg / node-postgres 等现代驱动 | 均已支持,旧版本注意升级 |
| 第三方连接池(如 PgBouncer) | 1.8 起支持 SCRAM |
一个细节:HBA 里写 md5 而存储是 SCRAM 时,不支持 SCRAM 的老客户端依然连不上(因为实际执行的是 SCRAM 协议)。不要指望「HBA 写 md5」给老客户端留后门——存储格式决定协议。
channel_binding 与防中间人
libpq 参数 channel_binding 有三个取值:disable、prefer(默认)、require。它控制 SCRAM 握手是否与 TLS 通道绑定:
- 绑定的原理是把 TLS 层的证书信息混入 SCRAM 的证明计算中,使握手结果只能在这条 TLS 连接上成立,攻击者无法把合法握手转发到自己的连接。
require时若连接没有使用 TLS(或服务端不支持),认证直接失败——因为没有通道可绑。所以channel_binding=require必须与sslmode=require以上搭配使用。- 应用侧两个参数建议同时写进连接串:
sslmode=verify-full&channel_binding=require,构成「传输加密 + 身份校验 + 防搬运」的完整组合。
密码策略:passwordcheck 扩展
contrib 自带的 passwordcheck 扩展在 CREATE ROLE / ALTER ROLE 时拦截弱密码:
# postgresql.conf:随实例预加载(需重启)
shared_preload_libraries = 'passwordcheck'CREATE EXTENSION passwordcheck;-- 太短的密码会被直接拒绝
ALTER ROLE app_rw PASSWORD '123456';ERROR: password is too short它的规则朴素而有效:长度至少 8 位、必须同时包含字母和数字、不得与用户名相同。没有黑名单与复杂度正则——这些高级需求要么改源码宏,要么用事件触发器(event trigger)自行实现,要么在应用层做。对多数团队,先保证「8 位 + 字母数字」这条底线全量生效,比追求花哨策略更有价值。
passwordcheck 只能防「新建时的弱密码」
它管不了已存在的弱密码,也管不了通过复制、恢复等方式进来的账号。上线后应跑一次存量密码审计(结合后文的存储格式盘点),并对从未改过密码的账号强制轮换。
VALID UNTIL:密码过期与强制改密
VALID UNTIL 给角色设置密码有效期,到期后密码认证将失败(注意:它只作用于密码类认证,cert、peer 等非密码认证不受影响):
-- 90 天后密码过期
ALTER ROLE app_rw PASSWORD 'S7rong#pass' VALID UNTIL 'now + 90 days';
-- 查看过期时间
SELECT rolname, rolvaliduntil FROM pg_authid WHERE rolvaliduntil IS NOT NULL;
-- 取消过期限制
ALTER ROLE app_rw VALID UNTIL 'infinity';PostgreSQL 没有内置「过期后允许登录但强制改密」的机制——过期即拒绝认证。「强制改密」只能用流程实现:管理员提前把密码改为一个只有用户可取的临时值并设置近期的 VALID UNTIL;用户在期限内登录并用 ALTER ROLE ... PASSWORD(或 psql 的 \password)自行改密;确认改密后管理员把 VALID UNTIL 恢复为 infinity。
服务账号慎用 VALID UNTIL
应用连接串里的密码不会自己更新,服务账号密码过期 = 生产故障。VALID UNTIL 适用于人类账号,服务账号的轮换应走「新建新密码角色 → 应用灰度切换 → 删除旧角色」的双账号流程。
auth_delay:减缓暴力破解
contrib 自带的 auth_delay 在认证失败后引入一段人为延迟,提高在线暴力破解的时间成本:
# postgresql.conf
shared_preload_libraries = 'auth_delay'
auth_delay.milliseconds = 500要点:它只延迟失败响应(认证成功不受影响),不改协议,属于「补充缓解」。真正的防线仍是强密码 + 网段白名单。另外注意它与连接池的交互:配置错误的连接池高频重试时,延迟会被放大为连接堆积。
.pgpass 与 PGPASSWORD
脚本与自动化场景总要有地方放密码,PostgreSQL 官方给出了两个途径,安全性差别很大:
.pgpass 文件(推荐)。默认路径 ~/.postgresql/.pgpass(Windows 为 %APPDATA%\postgresql\pgpass.conf,也可用 PGPASSFILE 指定),每行一条:
# 格式:hostname:port:database:username:password(* 为通配)
pg1.example.com:5432:app:app_rw:S7rong#pass
localhost:5432:*:dbmon:M0n1tor#passchmod 0600 ~/.pgpass # 权限必须收紧,libpq 会拒绝宽松权限的文件PGPASSWORD 环境变量(不推荐)。它立即生效、写起来方便,但进程环境对同机其他用户可见(/proc/<pid>/environ、ps eww 等),且容易随 shell 历史与脚本一起泄漏。仅在一次性调试、且能接受泄露后果时使用。
两者的优先级:PGPASSWORD > .pgpass 文件 > 交互提示。
两类报错的定位路径
密码认证出问题时,客户端报错只有两种,但排查路径完全不同:
| 客户端报错 | 服务端日志常见记录 | 定位动作 |
|---|---|---|
no pg_hba.conf entry for host ... | 同文案的 FATAL | 根本是没走到密码环节:用 pg_hba_file_rules 查规则的 error 列、核对来源地址段与记录顺序(见 pg_hba.conf 详解 的排查五步) |
password authentication failed for user "xxx" | 同文案的 FATAL(含 detail) | 密码环节被拒,三查:① pg_authid.rolpassword 的前缀是什么格式;② 密码本身对不对(psql 直连重试排除驱动变量);③ 驱动是否支持 SCRAM(老驱动 + scram-sha-256 HBA 的组合也会落到这个错) |
| 连接挂起后超时 | 无对应记录或半截日志 | 交互式方法等待输入、网络半开或 authentication_timeout 打断,按「方法 + 网络」分别定位 |
服务端日志永远比客户端报错信息丰富:pg_hba.conf 的匹配结论、认证方法的协商细节都在 FATAL 的前后行里。给密码认证类故障定一条排查纪律——先看服务端日志,再猜配置;同时开启 log_connections 让成功连接也留痕,失败(被拒)与成功(authorized)的时间差往往是定位「改了什么之后开始失败」的关键线索。
顺带认识客户端统一报错的设计意图:「密码错误」与「角色不存在」对外是同一个错,防止探测者枚举有效用户名;而服务端日志会写明 Role "xxx" does not exist。枚举防护靠客户端的模糊报错,运维定位靠服务端日志的精确信息——两层各自成立,不要试图把客户端报错「精确化」。
把这两类报错的处置顺序写进值班手册第一页:遇到第一类先跑 pg_hba_file_rules 预检,遇到第二类先看 pg_authid.rolpassword 格式——顺序对了,定位时间从一小时缩到五分钟。
实战示例
示例一:从 md5 迁移到 scram-sha-256
按「盘点 → 升级客户端 → 切换默认 → 重置密码 → 切换 HBA → 验证」六步执行。
第 1 步:盘点存储格式与过期账号。
SELECT CASE
WHEN rolpassword LIKE 'SCRAM-SHA-256$%' THEN 'scram'
WHEN rolpassword LIKE 'md5%' THEN 'md5'
ELSE 'other'
END AS fmt, count(*)
FROM pg_authid WHERE rolcanlogin GROUP BY 1;
-- 列出待迁移的 md5 账号清单
SELECT rolname FROM pg_authid
WHERE rolcanlogin AND rolpassword LIKE 'md5%';第 2 步:确认所有客户端支持 SCRAM。
# 确认 psql/libpq 版本(>= 10)
psql --version
# 确认应用驱动版本(以 JDBC 为例,>= 42.2.x)
grep -i postgresql app/pom.xml第 3 步:切换存储默认值。
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
SHOW password_encryption;第 4 步:逐批重置密码(生成 SCRAM verifier)。
-- 与账号负责人约定后执行;服务账号要同步改应用配置
ALTER ROLE app_rw PASSWORD 'NewStrong#2026';
ALTER ROLE dbmon PASSWORD 'NewMon#2026';
-- 验证存储格式已变
SELECT rolname, left(rolpassword, 15) AS fmt
FROM pg_authid WHERE rolname IN ('app_rw', 'dbmon'); rolname | fmt
---------+------------------
app_rw | SCRAM-SHA-256$40
dbmon | SCRAM-SHA-256$40第 5 步:修改 pg_hba.conf 并重载。
# 把
hostssl app app_rw 10.0.1.0/24 md5
# 改为
hostssl app app_rw 10.0.1.0/24 scram-sha-256psql -c "SELECT pg_reload_conf();"第 6 步:验证与观察。
# 正向验证
psql "host=pg1 dbname=app user=app_rw sslmode=require" -c "SELECT current_user;"
# 观察认证失败(旧客户端残留会在日志里显形)
tail -f $PGDATA/log/postgresql-*.log | grep -i 'authentication failed'迁移期的灰度技巧:HBA 方法先保持 md5(存储 SCRAM 时自动按 SCRAM 执行),待所有客户端验证通过后再把 HBA 显式改为 scram-sha-256,可把风险拆成两半。
示例二:找出「该过期而没过期」的账号
配合安全基线,定期找出密码永不过期的人类账号:
SELECT rolname, rolvaliduntil
FROM pg_authid
WHERE rolcanlogin
AND rolvaliduntil IS NULL -- 永不过期
AND rolname NOT LIKE 'pg_%' -- 排除内置角色
AND rolname NOT IN ('dbmon','app_rw','app_ro','etl') -- 排除服务账号白名单
ORDER BY rolname; rolname | rolvaliduntil
----------+---------------
alice |
bob |
(2 rows)-- 对盘点出的人类账号设置轮换周期
ALTER ROLE alice VALID UNTIL '2026-12-31 18:00+08';
ALTER ROLE bob VALID UNTIL '2026-12-31 18:00+08';示例三:passwordcheck + auth_delay 组合加固
# postgresql.conf(两扩展均需预加载,改后重启)
shared_preload_libraries = 'passwordcheck,auth_delay'
auth_delay.milliseconds = 500-- 重启后创建扩展(passwordcheck 在每个库 CREATE EXTENSION 才对该库生效)
CREATE EXTENSION passwordcheck;验证效果:
-- 弱密码被拒
ALTER ROLE test1 PASSWORD 'short1c';ERROR: password is too short# 故意输错密码,观察响应被延迟约 500ms
time psql "host=pg1 dbname=app user=app_rw sslmode=require" -c "SELECT 1;"示例四:SCRAM 迁移的验收脚本
把迁移完成的判定做成一个可重复执行的脚本,作为变更单的验收附件:
#!/usr/bin/env bash
# verify-scram-migration.sh —— 迁移验收三查
set -euo pipefail
PGHOST=${PGHOST:-pg1}; PGDATABASE=${PGDATABASE:-postgres}; PGUSER=postgres
export PGHOST PGDATABASE PGUSER
echo "== 查 1:存储格式盘点(目标:md5 计数为 0)=="
psql -Atc "
SELECT CASE WHEN rolpassword LIKE 'SCRAM-SHA-256\$%' THEN 'scram'
WHEN rolpassword LIKE 'md5%' THEN 'md5'
ELSE 'other' END AS fmt, count(*)
FROM pg_authid WHERE rolcanlogin GROUP BY 1;"
echo "== 查 2:HBA 中残留的 md5/password 记录(目标:无输出)=="
psql -c "
SELECT rule_number, auth_method, error
FROM pg_hba_file_rules
WHERE auth_method IN ('md5','password');"
echo "== 查 3:明文密码认证的连接探测(目标:被拒)=="
psql "host=$PGHOST dbname=app user=app_rw sslmode=require" -c "SELECT current_user;" \
&& echo "scram 连接 OK"== 查 1:存储格式盘点(目标:md5 计数为 0)==
scram 37
md5 0
== 查 2:HBA 中残留的 md5/password 记录(目标:无输出)==
rule_number | auth_method | error
-------------+-------------+-------
(0 rows)
== 查 3:明文密码认证的连接探测(目标:被拒)==
app_rw
(1 row)
scram 连接 OK三查全绿即可在变更单上签「迁移完成」。把脚本放进 CI 定期执行,还能顺带发现「有人新加了 md5 记录」「有人重置回了 md5 verifier」这类回退——迁移不是一次性动作,是持续维持的状态。
注意事项
password_encryption 只管新密码。 切换默认值后,所有存量密码保持原格式。把「切换参数」误当成「完成迁移」是这一步最常见的翻车点——以 pg_authid 的盘点结果为准,而不是以参数为准。
HBA 方法与存储格式不匹配会直接认证失败。 记录写 scram-sha-256 而账号还是 md5 verifier 时,报错是 password authentication failed(不是 no pg_hba.conf entry),容易误导排查方向。切换 HBA 前先确认目标账号的存储格式。
VALID UNTIL 只影响密码认证。 给账号设置了过期时间,不代表 cert / peer 认证也会失效。反过来说,想通过过期「禁用」某个可能走证书认证的账号是不成立的,禁用要用 ALTER ROLE ... NOLOGIN。
明文密码会进入 SQL 日志与 pg_stat_activity 吗? ALTER ROLE ... PASSWORD 'xxx' 的语句文本可能被 log_statement = all、pgaudit 等记录。重置密码时优先让用户执行 \password,或由管理员执行后立即清理相关日志;不要在脚本里硬编码密码参数。
md5 存储泄露即可登录。 强调过的事实再重复一次:md5 verifier 泄露 = 密码作废。备份文件、pg_dumpall 输出、云快照的访问控制必须与库本身同级别,并且泄密事件中 md5 账号要按「密码已泄露」处置(全量重置)。
.pgpass 权限错误会静默跳过。 权限宽松时 libpq 不读取该文件并回退到交互提示,自动化脚本表现为「卡住等待输入」。chmod 0600 后用 PGPASSFILE 显式指定路径可以避免找错文件。
auth_delay 不区分攻击者与配置错误。 配置错误的连接池以每秒数十次的频率认证失败时,500ms 延迟会显著放大故障。启用前先修复「高频认证失败」的根因,监控 FATAL: password authentication failed 的频率。
内置角色与复制账号也要盘点。 迁移盘点时容易漏掉 repl 复制账号与监控账号——它们也走密码认证,也要换成 SCRAM verifier,否则改 HBA 方法时最先挂的是它们。
没有内置的失败锁定机制。 连续输错密码不会触发账号锁定——PostgreSQL 原生没有这个能力。防爆破的组合拳是:HBA 网段白名单收窄来源、auth_delay 拖慢高频尝试、主机层 fail2ban 类工具按日志封禁;把认证失败频率做成告警指标(见最佳实践),比事后翻日志有效得多。
scram_iterations 只作用于新写入的 verifier。 调高迭代次数增强抗爆破能力,但存量 verifier 不会重算,ALTER ROLE ... PASSWORD 重置后才用新值——做「全局迭代次数升级」按重置清单滚动推进,别指望一次 reload。
服务账号的密码轮换要和应用发布对齐。 .pgpass/环境变量里的密码与应用配置里的密码可能不同步:先改库侧会让老应用断线,先改应用侧会让新凭据失效。轮换脚本按「双写窗口 → 改库 → 改应用」编排,并把 VALID UNTIL 设在轮换窗口内,到期强制触发轮换。
认证失败告警要按「账号 + 来源」分组。 连接池配错密码的高频失败与真实爆破在日志里长得一样,但处置路径完全不同——前者找配置负责人,后者走安全响应。告警规则按账号与来源 IP 分组,才能第一眼分辨这两种情况。
最佳实践
password_encryption与 pg_hba 方法统一为scram-sha-256。 两处保持一致可以彻底杜绝「协议不匹配」类故障,也是 PG14+ 的开箱默认,不必人为降级。- 禁止在
host(非 SSL)记录上使用password方法。 若确需password(极少数 PAM 兼容场景),必须放在hostssl记录内,让明文只在加密管道里流动。 - md5 只作为过渡期兼容层并设定下线时间。 把「HBA 中的 md5 记录数」做成巡检指标,目标值是 0。
- 用存储格式盘点驱动迁移,而不是凭感觉。 迁移完成的定义是:
pg_authid中可登录角色的rolpassword全部以SCRAM-SHA-256$开头,且 HBA 无 md5 记录。 - 高价值账号启用
channel_binding=require。 与sslmode=verify-full搭配,防止合法握手被中间人搬运;启用前确认连接确实走 TLS。 - 人类账号用
VALID UNTIL建立轮换节奏,服务账号用双账号轮换。 永远不要让服务账号密码过期——应用不会替你改密码。 - 启用
passwordcheck设立密码底线。 8 位 + 字母数字是极低成本即可全量生效的底线策略;更高要求的策略放应用层或事件触发器。 - 对外暴露的实例启用
auth_delay并监控认证失败频率。 延迟只是缓释,认证失败突增本身就是入侵尝试信号,应接入告警。 - 密码的传递用
.pgpass或密钥管理服务,禁用PGPASSWORD。 确需环境变量的场景限定生命周期(临时 shell 内 export,脚本结束即清),并禁入代码仓库。 - 备份与
pg_dumpall输出视同密码库管理。 加密存储、限制访问、审计下载;md5 时代的备份尤其按「内含可用凭据」处理。 - 把「存储格式 + 过期状态」纳入周期审计。 一条 SQL 即可产出审计数据(见示例二),每月执行并归档,异常账号当周处置。
- 重置密码的动作避免进入日志。 优先
\password或独立维护会话;开启语句日志的库,为ALTER ROLE ... PASSWORD配置脱敏(pgaudit 的ROLE类日志需人工审查保留策略)。
下一节
继续学习:LDAP / Kerberos 认证
参考链接
- PostgreSQL 官方文档:Authentication Methods
- PostgreSQL 官方文档:SCRAM-SHA-256 Authentication
- PostgreSQL 官方文档:The pg_hba.conf File
- PostgreSQL 官方文档:pg_authid 系统表
- PostgreSQL 官方文档:pg_shadow 视图
- PostgreSQL 官方文档:Connection and Authentication 配置参数(password_encryption)
- PostgreSQL 官方文档:ALTER ROLE
- PostgreSQL 官方文档:passwordcheck 模块
- PostgreSQL 官方文档:auth_delay 模块
- PostgreSQL 官方文档:The Password File(.pgpass)
- PostgreSQL 官方文档:Environment Variables(PGPASSWORD 等)
- PostgreSQL 官方文档:Authentication Problems