很多 PostgreSQL 用户会认为,只要启用了 SSL/TLS,数据库连接就已经足够安全。

但这种认知并不完整。

SSL/TLS 负责保护数据传输过程,而真正决定登录阶段安全性的,是 PostgreSQL 所使用的认证方式。

在这篇文章中,我将带你 深入剖析,为什么 PostgreSQL 的 password 认证方式,实际上是一个 “隐形的安全杀手”,而 md5 和 scram 认证可以提供更强的防护。

01

理论篇:三种认证方式的底层

原理解析

这三种认证方式的核心差异,在于密码在网络通信中是如何传输的,以及如何对抗网络嗅探(Sniffing)、重放攻击(Replay Attack)和拖库攻击。

1

password 认证:在互联网上“裸奔”

工作原理

当客户端尝试连接数据库时,服务端要求输入密码。

客户端接收到用户输入的密码后,不加任何处理,直接以纯明文的形式(Plaintext)封装在 TCP 数据包中发给服务端。

安全致命点

任何位于同一局域网、路由器节点或通信链路上的攻击者,只要使用网络抓包工具(如 tcpdump、Wireshark),就能像看普通文本一样直接读取你的数据库密码。

2

md5 认证:经典的“挑战-响应”机制

为了解决明文传输的问题,PostgreSQL 引入了基于 MD5 的挑战-响应(Challenge-Response)机制。

工作原理

a) 挑战

客户端发起连接,服务端随机生成一个 4字节的盐(Salt) 发给客户端。

b)计算

客户端使用用户输入的密码、用户名以及服务端发来的盐,进行双重哈希计算:Hash = MD5( MD5(password + username) + salt )。

c)响应

客户端将计算出的最终 Hash 值发给服务端,服务端用相同规则比对。

为什么安全

密码的明文从未在网络上出现过。

攻击者抓包只能看到服务端的“盐”和客户端的“Hash响应”。

由于每次连接“盐”都是随机变化的,攻击者即使截获了这次的 Hash 值,也无法在下次连接时用来重放登录。

3

scram-sha-256 认证:现代密码学的终极

防线

虽然 MD5 能防网络嗅探,但 MD5 算法本身已不安全,且存在“哈希传递攻击(Pass-the-Hash)”的弱点(即:如果系统表 pg_authid 被盗,攻击者可以直接用里面存储的 MD5 值冒充客户端登录)。

因此,PG 10 引入了 SCRAM(Salted Challenge Response Authentication Mechanism,RFC 7677)。

工作原理

它是一种复杂的双向认证机制。

客户端和服务端会互相交换自己生成的随机数(Nonce),并通过 PBKDF2-SHA-256 算法进行成千上万次的迭代运算,最终生成复杂的 ClientProof(客户端证明)和 ServerSignature(服务端签名)

为什么最安全

  • 极强的防嗅探能力

网络上传输的仅仅是基于随机数和迭代计算的动态证明。

  • 防伪造服务器

不仅服务端验证客户端,客户端也能验证服务端(防止中间人伪造数据库节点)。

  • 防拖库传递

即使数据库密码表泄露,攻击者也无法反推明文,更无法直接使用泄露的密文进行登录。

02

实战篇:全流程 tcpdump 抓包与

协议解析

为了让理论落地,我们设计了以下完整的实验流程。

我们将通过 tcpdump 深入 PostgreSQL 的通讯协议层(Wire Protocol),亲眼见证数据包里的秘密。

1

实验环境准备

我们需要开启两个终端窗口:终端 A 用于执行抓包,终端 B 用于模拟客户端登录。

  • 测试用户

dbuser

  • 测试密码

Secret@123

  • 抓包命令

sudo tcpdump -i any -A -s 0 port 5432

  • -i any

监听所有网卡接口。

  • -A

核心参数,将数据包的 Payload 以 ASCII 明文形式打印,便于观察。

  • -s 0

捕获完整的数据包,防止报文截断。

注意

为了确保抓包能看到认证协议,实验全程使用 PGSSLMODE=disable 环境变量强制关闭 SSL 链路加密,模拟纯明文通道下的认证过程。

2

实验 1:令人触目惊心的 password 抓包

Step 1: 修改配置并重载

在数据库服务器上,编辑 pg_hba.conf,将 IPv4 本地连接的认证方式改为 password:

# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    all             dbuser          0.0.0.0/0               passwor

执行重载命令生效:SELECT pg_reload_conf();

Step 2: 开启抓包

在 终端 A 执行:

sudo tcpdump -i any -A -s 0 port 5432

Step 3: 模拟客户端登录

在 终端 B 发起连接并输入密码 Secret@123:

PGSSLMODE=disable psql -U dbuser -h 127.0.0.1 -W

Step 4: 报文解析

回到终端 A,你会看到长串的 TCP 交互。

我们在其中精准定位到客户端发送密码的数据包:

图片

深度剖析

注意最后一行 ....p...Secret@123.。在 PostgreSQL 协议中:

  • 字母 p 代表这是一个 PasswordMessage。

  • 紧接着的 4 个字节代表消息长度。

  • 后面跟着的,就是毫无遮掩的纯明文密码 Secret@123,最后以 \0 结尾。

  • 结论

在没有配置 SSL 的情况下使用 password 认证,任何能接触到网络流量的人都能瞬间盗取你的数据库最高权限。

3

实验 2:md5 是如何隐藏密码的?

Step 1: 修改配置与密码加密方式

修改 pg_hba.conf 的 METHOD 为 md5 并重载配置。

# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    all             dbuser          0.0.0.0/0               md5

同时,我们需要确保数据库以 md5 格式存储该用户的密码(PostgreSQL 14+ 默认是 scram,需回退演示):

ALTER SYSTEM SET password_encryption = 'md5';
SELECT pg_reload_conf();
\password dbuser  -- 重新输入 Secret@123,使其以 md5 哈希存入系统表

Step 2: 开启抓包并登录

保持终端 A 的 tcpdump 运行,终端 B 再次执行登录:

PGSSLMODE=disable psql -U dbuser -h 127.0.0.1 -W

Step 3: 报文解析

这一次,我们在抓包结果中找到了两次关键交互(挑战与响应):

以下是一份真实的、原汁原味的 Linux 生产环境抓包日志。

我们将逐个数据包拆解 MD5 是如何隐藏密码的。

数据包 1:服务端发起 MD5 挑战(下发随机 Salt)

18:51:15.869475 IP localhost.postgres > localhost.61332: Flags [P.], seq 1:14, ack 81, win 128, options [nop,nop,TS val 159740625 ecr 159740623], length 13
E..A..@.@.f(.........8...R.o.s.......5.....
        .r.     .r.R........iJ..

分析

1)TCP 干扰项

开头的 .r. .r. 是底层 TCP 时间戳选项(TCP Timestamp Options)被强制转换为 ASCII 的结果,与数据库无关。

2)PG 协议载荷

真正的核心是最后的 R........iJ..(总长 13 字节)。

  • R

代表消息类型为 AuthenticationRequest。

  • 紧接着不可见的 4 字节代表认证类型,值为 5(MD5 认证)。

  • iJ..

这就是服务端这次随机生成的 4 字节盐(Salt)。

数据包 2:客户端发送 MD5 响应(Hash 值)

18:51:15.870407 IP localhost.61332 > localhost.postgres: Flags [P.], seq 81:122, ack 14, win 128, options [nop,nop,TS val 159740626 ecr 159740625], length 41
E..]')@.@..p...........8.s...R.|.....Q.....
        .r.     .r.p...(md585eba7f665f54bf44e628307c6d2c3f1.

逐字节拆解

这是 MD5 认证最核心的一步,PG 负载内容为 p...(md585eba7f665f54bf44e628307c6d2c3f1.:

1)p

代表消息类型 PasswordMessage。

2) ...(

表示消息长度。

有趣的是,括号 ( 在 ASCII 码表中的十进制正是 40。

加上开头的 p,总长度恰好是 41 字节。

3) md585eba7f66...

这是客户端使用公式 MD5(MD5(密码+用户名) + "iJ..") 计算出的响应值。

结论

我们在这个包里找不到任何明文密码

黑客即便截获了这一串 md585eba... 也无法反推密码,且因为下一次登录服务端会下发全新的 Salt,这个旧哈希值无法用于重放攻击。

数据包 3:认证成功与参数下发(意外的安全启示)

18:51:15.872288 IP localhost.postgres > localhost.61332: Flags [P.], seq 14:467, ack 122, win 128, options [nop,nop,TS val 159740628 ecr 159740626], length 453
E.....@.@.do.........8...R.|.s.............
        .r.     .r.R........S....in_hot_standby.off.S....integer_datetimes.on.S....TimeZone.Asia/Shanghai.S....IntervalStyle.postgres.S... search_path."$user", public.S....is_superuser.off.S....application_name.psql.S...&default_transaction_read_only.off.S....scram_iterations.4096.S....DateStyle.ISO, MDY.S...#standard_conforming_strings.on.S...!session_authorization.dbuser.S....client_encoding.UTF8.S....server_version.18.3.S....server_encoding.UTF8.K.......&.r..Z....I

逐字节拆解

你可能会对这段突然冒出的大量“明文”感到疑惑。

这恰恰证明了MD5 认证已经通过!

1) R........

这里的 R 代表 AuthenticationOk(认证成功)。

2)大段的 S....

代表 ParameterStatus。

服务端开始向客户端下发明文会话参数,如 TimeZone.Asia/Shanghai、server_version.18.3、client_encoding.UTF8 等。

3)最后的 Z....I

代表 ReadyForQuery,状态为 I(Idle)。

至此,连接建立完成,可以敲 SQL 了。

3

实验 3:scram-sha-256 的硬核交互过程

虽然 MD5 防住了抓包,但 MD5 算法本身容易被碰撞破解,且如果黑客拿到了数据库的 pg_authid 系统表,可以直接用里面的 MD5 值冒充客户端登录。

我们来看 SCRAM 是怎么解决的。

Step 1: 修改配置与密码加密方式

修改 pg_hba.conf 的 METHOD 为 scram-sha-256 并重载。

# TYPE  DATABASE        USER            ADDRESS                 METHOD
host    all             dbuser          0.0.0.0/0               scram-sha-256

同时,将会话加密方式切换为 scram 并重置密码:

ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
\password dbuser  -- 重新输入 Secret@123,以高强度 SCRAM 格式存储

Step 2: 开启抓包并登录

执行抓包和 psql 登录命令。

PGSSLMODE=disable psql -U dbuser -h 127.0.0.1 -W、

Step 3: 报文解析

SCRAM 的认证过程基于 SASL(简单认证与安全层)框架,交互极度复杂,我们在 tcpdump 中会看到三次握手级的认证报文:

报文 1:Client First Message (客户端发起 SASL 认证,包含客户端生成的随机数 Nonce)

18:54:07.123827 IP localhost.13366 > localhost.postgres: Flags [P.], seq 81:136, ack 25, win 128, options [nop,nop,TS val 159911880 ecr 159911879], length 55
E..k..@.@.".........46.8 h....K......_.....
        ...     ...p...6SCRAM-SHA-256.... n,,n=,r=3ewP3o9XZ8s/WTY4RXKJKxNY

报文 2:Server First Message (服务端返回自己的随机数、Salt、以及 PBKDF2 迭代次数)

18:54:07.123967 IP localhost.postgres > localhost.13366: Flags [P.], seq 25:118, ack 136, win 128, options [nop,nop,TS val 159911880 ecr 159911880], length 93
E...2!@.@.
D.........846..K. h.............
        ...     ...R...\....r=3ewP3o9XZ8s/WTY4RXKJKxNYaR5rBiV4x/WhGbfOU81L1qjY,s=aYJIjMymmcu7HyWPrtzJUg==,i=4096

深度剖析

可以看到服务端带回了 s= (Base64编码的盐) 和 i=4096 (告诉客户端:你需要把密码进行 4096 次 SHA-256 迭代计算!),以此极大增加暴力破解的算力成本。

报文 3:Client Final Message (客户端发送复杂的计算证明)

18:54:07.129549 IP localhost.13366 > localhost.postgres: Flags [P.], seq 136:245, ack 118, win 128, options [nop,nop,TS val 159911885 ecr 159911880], length 109
E.....@.@."k........46.8 h....K{...........
        ...     ...p...lc=biws,r=3ewP3o9XZ8s/WTY4RXKJKxNYaR5rBiV4x/WhGbfOU81L1qjY,p=c10/1yrKOH3VPAfYzTJ5IG069ZTxVBx+kglex3qsNGY=

深度剖析

客户端最终发回的 p= 是 ClientProof。它是通过 ClientKey 和 ServerSignature 进行异或运算得出的。

这个证明不仅向服务端证实了“我知道密码”,同时也验证了“当前连接的服务端是真的”,防住了中间人攻击。

结论

全程没有密码影子,黑客即便抓包拿到了所有随机数和 Proof,面对 SHA-256 和 4096 次迭代,也根本无法逆向出密码。

03

结论

通过上述完整的环境配置、tcpdump 的 -A 抓包实验以及协议层解析,我们用实打实的数据包证明了结论:

图片

避坑指南

即便你使用了 scram-sha-256,它保护的也仅仅是认证阶段的密码安全。

一旦认证通过,你执行的 SQL 语句和业务数据依然是明文传输的(如前文 tcpdump 演示,SQL 语句也会被一览无余)。

因此,在生产环境中,配置 SCRAM-SHA-256 认证 + 强制开启 SSL/TLS 加密(将 pg_hba.conf 中的 host 改为 hostssl),才是保障数据库链路安全的终极黄金法则。

写在最后

你在生产环境中是否曾遇到过认证安全问题?

你使用的是哪种认证方式?

是否曾经因认证漏洞造成性能或安全隐患?

欢迎在评论区分享你的经验,探讨更多 PostgreSQL 安全防护 的最佳实践!

如果想要更好的阅读体验,请查看原文链接:https://mp.weixin.qq.com/s/2K6G4aYPjyqafRIAlZWgQA

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐