说实话,这个题目我本来以为就是改一行pg_hba.conf的事,结果真做起来才发现,水比想象中深得多。上个月帮客户做PostgreSQL安全加固,从老旧的MD5认证迁移到scram-sha-256,前后折腾了小一周。迁移过程中踩了不少坑,也把很多平时文档里看不到的细节摸清楚了。这篇就把整个迁移思路、操作步骤、报错排查都整理出来,给后面要做的朋友省点时间。
1. 为什么要从 MD5 迁到 scram-sha-256:安全背景与断代史
1.1 MD5 曾经的地位与今日的软肋
很多从PostgreSQL 9.x、10.x时代过来的同学都知道,早期版本默认的密码认证方式就是MD5。那时候MySQL也大量用mysql_native_password,大家习惯了一种共识:反正密码在数据库里存的是哈希,不是明文,应该挺安全的吧?
这里就有个误区。MD5作为一个哈希算法,当年设计出来确实是为了完整性校验,比如校验文件下载有没有损坏。但用在密码哈希场景下,问题是一连串的:
- MD5运算速度太快,现代GPU可以每秒算几十亿次,暴力破解的成本极低。
- 无盐值或盐值可预测,同一密码在不同用户下生成的哈希完全相同,攻击者拿到一批哈希后可以批量碰撞。
- 早在2004年,王小云团队就发表了MD5碰撞攻击方法。2008年US-CERT直接宣布MD5“密码学上已被破解”。
- 部署在公网、半公网的PostgreSQL,如果还守着MD5认证,等于把密码哈希赤裸裸地放在攻击者能拿到的地方。
这里不讨论情报机构级别的威胁,单说现实场景:内网扫描脚本一旦扫到pg_hba.conf里写着md5,社工、爆破、彩虹表一拥而上,安全评估报告里必然飘红。
1.2 scram-sha-256 到底强在哪
SCRAM(Salted Challenge Response Authentication Mechanism)是RFC 5802定义的认证机制,PostgreSQL从10版本开始支持,从14版本开始作为默认值。scram-sha-256就是基于SHA-256的SCRAM实现。
和MD5这种“客户端把密码哈希直接发给服务器”的静态方式相比,SCRAM有一个根本性的不同:它采用了挑战-响应机制,全程不传输密码本身,也不传输密码的哈希值。
整个过程大致是这样:
- 客户端向服务器发起认证请求。
- 服务器返回一个随机挑战值(包括盐值、迭代次数等信息)。
- 客户端利用密码、盐值和迭代次数,计算出响应值发回给服务器。
- 服务器端用自己存的验证密钥做同样的计算,比对结果。
这带来三个直接好处:
- 因为盐值每次都是随机的,同一个密码在不同会话、不同用户下生成的验证值完全不同。
- 密码和验证密钥不会在网络中明文传输,即便抓包也拿不到可重放的东西。
- 服务端存储的也不是密码本身,而是经过PBKDF2(默认迭代次数4096次)派生出的验证密钥,破解一个哈希的成本远高于MD5。
一句话概括:MD5像是一把锁,钥匙的齿形直接暴露在钥匙孔外;SCRAM则像是每次开门都需要对暗号,而且暗号每次都不一样。
2. 升级前的现场盘点:先摸清家底再动手
2.1 先确认 PostgreSQL 服务端版本
这个步骤很多人会跳过,但我强烈建议先做。不同版本的PostgreSQL对SCRAM的支持细节不一样,后面配置写法、默认值都有差异。
postgres=# SHOW server_version;如果你的版本还在9.x、10.x甚至更老,情况会比较尴尬。PostgreSQL 10才正式引入SCRAM-SHA-256的支持,9.x根本不能用。这时候建议先规划版本升级,再做认证迁移。如果只是从旧版本升级到14或更高版本(比如 pg_upgrade ),默认认证方式会自动变为scram-sha-256,但存量用户的密码哈希不一定跟随迁移,这是后面一个大坑,我会在第3节详细说。
版本确认时还顺带看两个参数:
SHOW password_encryption; SHOW dynamic_shared_memory_type;password_encryption在14以下默认是md5,14及以上默认是scram-sha-256。这个参数直接决定你之后执行ALTER ROLE ... PASSWORD时新密码以什么方式存储,所以迁移时一定要改它。
2.2 客户端和驱动兼容性摸底
这是很多迁移翻车的重灾区。服务端把认证改成scram-sha-256,客户端驱动不支持,业务立马报错。
需要重点确认的:
- psql客户端:PostgreSQL 10以上自带的
psql都支持SCRAM,老版本不在考虑范围内。 - libpq版本:PostgreSQL 10对应的libpq开始支持SCRAM。
- JDBC驱动:PostgreSQL JDBC 42.2.0及以上版本支持SCRAM。如果你的老项目还在用
postgresql-9.4.1212之类的古董,升级认证之前必须同步升级驱动。 - Python psycopg2:2.8版本开始支持SCRAM。注意psycopg2 2.7.x及以下连不上。
- 其他语言驱动:Go的
lib/pq在老版本有问题,建议用pgx;Node.js的pg库8.0.0以上支持。
最稳妥的做法是:找一个测试环境,把所有业务用到的驱动全部升级到当前最新稳定版,连接串不变,先连一遍SCRAM认证的实例做冒烟测试。
2.3 盘点存量账号与业务连接方式
改认证方式不只是DBA一个人的事,还得看业务方怎么连数据库。需要盘点清楚:
- 有哪些账号在用MD5认证。
- 这些账号都从哪些IP段连进来(对应
pg_hba.conf里的哪些条目)。 - 哪些业务的连接串里写了密码,密码是存在配置文件里还是环境变量里。
- 有没有中间件、连接池(如PgBouncer、Pgpool-II、应用程序内嵌连接池)介入,如果中间件本身不支持SCRAM,服务端改完后面所有人都会连不上。
这一步输出一个清单,至少有:账号名、客户端类型、驱动版本、来源IP、连接池信息、负责人。后面灰度切换的时候,这份清单就是操作手册。
3. 升级实操:分阶段从 MD5 平滑切到 scram-sha-256
3.1 第一步:修改 password_encryption
修改postgresql.conf里这个参数:
password_encryption = scram-sha-256修改完需要重启实例,或者至少SELECT pg_reload_conf();,让参数生效。
这一步做完后,新创建的账号或者重置密码的账号,都会以SCRAM方式存储。但存量用户的密码哈希仍然是MD5格式,所以不能让所有人都立刻切到scram认证,否则他们连不上。这就需要一个过渡策略。
3.2 第二步:在 pg_hba.conf 中分范围切换
pg_hba.conf是PostgreSQL的访问控制核心,认证方式在这里配。假设原来有这些类型的条目:
host all all 192.168.1.0/24 md5 host all all 10.0.0.0/8 md5 local all all md5推荐做法是按范围分批改,不要一次性全改。比如先在低风险网段试点:
host all all 192.168.100.0/24 scram-sha-256 host all all 192.168.1.0/24 md5 host all all 10.0.0.0/8 md5 local all all md5注意pg_hba.conf的匹配规则是第一条匹配即生效,所以把新规则写在前面,后面的老规则还能兜底。等试点网段验证没问题了,再逐步把其他网段切换过来。
改完配置记得执行:
SELECT pg_reload_conf();不用重启实例,这个很关键,生产环境重启代价太大。
3.3 第三步:批量重置用户密码(最核心的一步)
这一步是很多人忽略的。pg_hba.conf改成scram-sha-256之后,如果某个用户当前的密码哈希在pg_authid里存的仍然是MD5格式,认证会直接失败,报错类似:
FATAL: password authentication failed for user "app_user" DETAIL: Role does not exist or password is incorrect.注意这个报错非常具有迷惑性,看起来像是密码错了,但真实原因其实是服务端存的是MD5格式,而客户端按照SCRAM流程发送的响应无法用旧哈希验证。
解决办法很简单:在切认证方式之前或之后,针对存量用户重新设置一次密码,PostgreSQL会用当前的password_encryption参数来决定存储格式。
ALTER ROLE app_user WITH PASSWORD 'new_secure_password';如果不想改业务密码,也可以设置成原密码,本质上就是让服务端以新格式重新存储一遍。这一步可以用DO块批量处理,但生产环境我更建议逐个账号来,每个账号重置后立刻验证,避免一个脚本把几十个账号搞乱了难排查。
重置之后可以确认存储格式:
SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'app_user';看到rolpassword前缀是SCRAM-SHA-256$就对了,如果是md5开头说明还没重置成功。
3.4 第四步:全量验证连接
每切换一个网段、改完一批账号后,要做完整的连接验证。我习惯写一个脚本,逐项测试:
- 用psql从对应网段连一次。
- 跑几个常规查询确认权限没变。
- 用应用侧真实驱动走一遍(比如JDBC的测试程序、Python的psycopg2脚本)。
- 观察服务端日志有没有认证失败记录。
tail -f /var/log/postgresql/postgresql.log | grep "scram"日志里出现scram-sha-256 authentication succeeded是理想状态。出现password authentication failed就要去排查,大概率是密码还没重置或者驱动版本不对。
4. 升级过程中最容易踩的坑与排查实录
4.1 坑一:pg_hba 改了,但密码还是 MD5 存储
这个是最常见的坑,症状就像上面说的,报password authentication failed,但密码明明是对的。
排查思路:
SELECT rolname, left(rolpassword, 20) AS pwd_prefix FROM pg_authid;如果看到md5前缀,说明该用户密码从未重置过。必须执行ALTER ROLE重置。要特别提醒的是,PostgreSQL升级(比如10升到14)时,即使pg_hba.conf写的是scram-sha-256,存量用户的rolpassword也可能还是MD5格式,不会自动转换。
4.2 坑二:客户端驱动版本太老
症状更直接:客户端报错SCRAM authentication is not supported。
这个错误说明客户端在收到服务端的SCRAM挑战之后,压根不知道怎么回应。老版本libpq、psycopg2 2.7.x、JDBC 42.2.0以下都会这样。
排查重点不是数据库端,而是所有会连数据库的进程:应用服务器、定时任务、数据同步工具、监控采集器。建议提前在测试环境把所有可能的客户端版本都列出来,逐一验证。
4.3 坑三:连接池的认证缓存
PgBouncer这层中间件可能会让问题更隐蔽。连接池在建立连接时可以正常认证,但池子里缓存的连接在服务端切换认证前后状态可能不一致。老的连接池版本压根不认识SCRAM,或者升级了但池里仍然保留着旧认证协议建立的连接。
踩过的具体场景是:PgBouncer升级到了支持SCRAM的版本,也配了正确的auth_query,但连接池重启前,已有连接仍然走的是旧链路。实际处理时最好在切换认证方式的同时,让连接池做一次平滑重启或清空连接池,确保所有新连接都走新认证流程。
4.4 常见报错速查表
| 报错信息 | 可能原因 | 解决方案 |
|---|---|---|
password authentication failed for user "xxx" | 密码哈希仍是MD5格式,或密码确实不对 | 执行ALTER ROLE重置密码;确认密码 |
SCRAM authentication is not supported | 客户端/驱动版本太老 | 升级驱动到支持SCRAM的版本 |
no pg_hba.conf entry for host "x.x.x.x" | pg_hba改写时漏了网段或顺序不对 | 检查pg_hba匹配规则,确认范围写全 |
FATAL: pg_hba.conf rejects connection for host "x.x.x.x" | pg_hba配置错误,认证方式不匹配 | 修正pg_hba后reload |
SSL connection has been established but... | SSL和认证方式没有必然关系,但注意两者叠加时的兼容性 | 确认二者都配置正确 |
could not translate host name ... | 连接串解析问题,跟认证无关,但容易在切换时同时出现 | 检查DNS和连接串格式 |
4.5 几个可以保命的操作习惯
- 改
pg_hba.conf之前先备份:cp pg_hba.conf pg_hba.conf.bak_20240301。 - 每次改完立刻
pg_reload_conf(),但不要反复加载,配置没改对再加载也会报错。 - 保持一个本地
local或host 127.0.0.1的trust条目作为逃生通道,万一其他方式全废了,至少能本机登录上去修。 - 密码重置建议用带引号的字符串,避免特殊字符被shell或者SQL解析掉。
5. 灰度切换与快速回滚方案
5.1 怎么设计灰度策略
生产环境不建议一步到位,更不建议周末深夜一把梭。我这次迁移用的是两阶段灰度:
第一阶段,选一个业务量小的应用模块,下游数据库用一个独立账号,从应用连接池到数据库这段链路全部切到SCRAM。验证周期至少一天,观察连接成功率、慢查询变化、错误日志。
第二阶段,把连接该库的其他业务分成三批,每批间隔半天或一天,依次切换。顺序按照影响范围从低到高:先数据统计类任务,再后台管理类,最后前台高并发业务。每一批切换前都执行slect count(*)冒烟测试,确保连接正常。
灰度过程中有一个很关键的监控项:PostgreSQL日志中的认证失败数量。切换当天盯着grep "FATAL"、grep "password authentication failed",一旦数量异常升高,立刻定位是哪个网段哪个账号。
5.2 快速回滚怎么做
回滚并不复杂,核心就是两件事:
- 把
pg_hba.conf里对应条目改回md5。 pg_reload_conf()。
因为密码的存储格式是SCRAM-SHA-256也不会影响MD5认证?这里有个关键点要提前说清楚:MD5认证只能验证MD5格式存储的密码,如果用户密码已经重置成SCRAM格式,pg_hba改回md5也无法通过认证。
所以稳妥的回滚方案是:存量用户的密码哈希在迁移过程中保留MD5格式不改,或者再准备一份旧的MD5密码备份。更实际的做法是,在迁移前把pg_authid中rolpassword字段备份出来,回滚时恢复备份。
这一点我觉得很容易被忽略,很多文章只告诉你怎么升,没告诉你怎么退,真出问题了你连退路都没有。所以迁移前一定记得备份。
6. 迁移后的收尾与固化措施
切换完认证方式不等于事情结束了。我这次迁移完成后还做了几件收尾的事,如果你也在做类似工作,可以参考:
- 检查所有账号的
rolpassword前缀,确保没有漏网之鱼还在用MD5。写个SQL:
SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';- 把
password_encryption参数固化到配置文件里,而不是只依赖运行时SET。 - 更新数据库巡检脚本,把认证方式的检查项加进去。
- 通知业务方把数据库连接串里的密码轮换一遍,因为密码已经在迁移中以新格式存储,理论上旧密码失效,但为了合规,还是得轮换。
还有一点:如果这个库将来还要升级版本,提前和升级方案一起做测试,别等到升级那天才发现认证方式和新版本不兼容。
实操总结与最后提醒
做了这一轮MD5到scram-sha-256的迁移,最大的感受是:改认证方式本身不难,难的是把受影响的所有链路摸清楚。数据库就像路网中间的大转盘,不只是车(应用)直接开上来,还有很多小路(连接池、监控、定时任务)也在汇入。只看到了主干道的车流,忽略了小路,结果就是主干道全通了,小路全堵了。
以我个人经验,几个最值得注意的点再强调一遍:
- 先改
password_encryption,再改pg_hba.conf,最后挨个重置密码。 - 所有客户端驱动必须提前验证SCRAM兼容性。
- 改完不代表能连上,日志认证失败是最好的检验指标。
- 别忘记备份
pg_authid,这是回滚的最后保障。
最后说一个小技巧:如果迁移窗口允许,可以先把log_min_error_statement暂时调到debug5级别观察SCRAM握手细节,排查问题时比单看报错信息高效很多。排查完记得调回正常级别,不然日志会膨胀得很快。