
说实话这个题目我本来以为就是改一行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 到底强在哪SCRAMSalted 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以下默认是md514及以上默认是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 psycopg22.8版本开始支持SCRAM。注意psycopg2 2.7.x及以下连不上。其他语言驱动Go的lib/pq在老版本有问题建议用pgxNode.js的pg库8.0.0以上支持。最稳妥的做法是找一个测试环境把所有业务用到的驱动全部升级到当前最新稳定版连接串不变先连一遍SCRAM认证的实例做冒烟测试。2.3 盘点存量账号与业务连接方式改认证方式不只是DBA一个人的事还得看业务方怎么连数据库。需要盘点清楚有哪些账号在用MD5认证。这些账号都从哪些IP段连进来对应pg_hba.conf里的哪些条目。哪些业务的连接串里写了密码密码是存在配置文件里还是环境变量里。有没有中间件、连接池如PgBouncer、Pgpool-II、应用程序内嵌连接池介入如果中间件本身不支持SCRAM服务端改完后面所有人都会连不上。这一步输出一个清单至少有账号名、客户端类型、驱动版本、来源IP、连接池信息、负责人。后面灰度切换的时候这份清单就是操作手册。3. 升级实操分阶段从 MD5 平滑切到 scram-sha-2563.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.xpg_hba改写时漏了网段或顺序不对检查pg_hba匹配规则确认范围写全FATAL: pg_hba.conf rejects connection for host x.x.x.xpg_hba配置错误认证方式不匹配修正pg_hba后reloadSSL 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。写个SQLSELECT 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握手细节排查问题时比单看报错信息高效很多。排查完记得调回正常级别不然日志会膨胀得很快。