☰
PostgreSQL密码认证升级:从MD5迁移到scram-sha-256的完整实践指南
2026/10/7 3:47:31 网站建设 项目流程

说实话,这个题目我本来以为就是改一行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有一个根本性的不同:它采用了挑战-响应机制,全程不传输密码本身,也不传输密码的哈希值。

整个过程大致是这样:

  1. 客户端向服务器发起认证请求。
  2. 服务器返回一个随机挑战值(包括盐值、迭代次数等信息)。
  3. 客户端利用密码、盐值和迭代次数,计算出响应值发回给服务器。
  4. 服务器端用自己存的验证密钥做同样的计算,比对结果。

这带来三个直接好处:

  • 因为盐值每次都是随机的,同一个密码在不同会话、不同用户下生成的验证值完全不同。
  • 密码和验证密钥不会在网络中明文传输,即便抓包也拿不到可重放的东西。
  • 服务端存储的也不是密码本身,而是经过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 快速回滚怎么做

回滚并不复杂,核心就是两件事:

  1. 把pg_hba.conf里对应条目改回md5。
  2. 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握手细节,排查问题时比单看报错信息高效很多。排查完记得调回正常级别,不然日志会膨胀得很快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询