你的 PostgreSQL 开启了 SSL,日志里也没有报错——但你的查询和密码可能仍在网络上明文传输。这不是夸张,而是大多数部署的真实状态。默认的 host 条目允许非加密连接静默通过,客户端默认的 sslmode=prefer 会无声地降级回明文。配置文件写了 ssl = on,不代表连接真的走了加密隧道。
把这件事做对,需要一条完整的链路:证书 → 服务端配置 → 强制拒绝明文 → 客户端验证 → 持续监控。缺任何一环,加密就是摆设。
明文连接暴露了什么
PostgreSQL 默认不加密网络传输。用户名、密码、每一条 SQL、每一行返回数据——全部裸奔在 TCP 上。一个同网段的嗅探器就够了,不需要任何高级攻击技术。
"我们在内网/VPC 里,不需要加密"是最常见的误判。内网不能防御横向移动:攻击者一旦进入边界内部,明文数据库连接就是最肥的猎物。
SSL/TLS 在数据交换之前建立加密隧道。传输内容变成只有持有会话密钥的双方才能解读的噪声。PostgreSQL 文档和社区习惯仍称 SSL,实际指现代 TLS——别被名字迷惑。
加密隧道的建立过程
客户端发起 SSL 连接时,数据库流量交换之前,以下步骤先完成:
- 客户端打开 TCP 连接,请求 SSL。
- 服务端发送证书——包含公钥和身份签名的文件。
- 客户端验证:证书是否由受信 CA 签发?证书中的主机名是否与连接目标匹配?
- 双方协商加密套件,推导共享会话密钥。
- 之后所有流量——认证、查询、结果——都在隧道内传输。
第 3 步是关键。不验证证书,你可能把加密数据直接送到了攻击者的机器上。证书是"你在和正确的服务器对话"的证明。
生成证书:先造钥匙再开门
PostgreSQL 不自带证书生成能力,用 OpenSSL。关键顺序:证书文件必须在编辑 postgresql.conf 之前就位。ssl = on 生效时,PostgreSQL 立刻查找证书和密钥文件,找不到则拒绝启动。先造钥匙,再开门。
开发和内部工具用自签名证书完全够用——你控制两端,不需要第三方背书:
# Step 1 — 生成私钥
openssl genrsa -out server.key 4096
# Step 2 — 生成自签名证书,有效期 365 天
# CN 必须匹配客户端连接时使用的主机名
openssl req -new -x509 -days 365 -key server.key -out server.crt \
-subj "/CN=db-hostname"
# Step 3 — 自签名证书本身就是 CA
# 复制为 root.crt,供客户端验证使用
cp server.crt root.crt
# Step 4 — 锁定权限(PostgreSQL 权限不对会拒绝启动)
chmod 0600 server.key
chown postgres:postgres server.key server.crt root.crt
把 server.key、server.crt、root.crt 三个文件放入 PostgreSQL 数据目录。不确定路径?执行:
psql -c "SHOW data_directory;"
为什么把 server.crt 复制为 root.crt?客户端 sslmode=verify-full 需要一个 CA 证书来验证服务端。自签名证书的签发者就是自己,所以同一个文件既当服务端证书又当客户端信任根。
生产环境应使用正规 CA 签发的证书:生成私钥和 CSR,提交给内部或公共 CA,拿回签发证书作为 server.crt。这样客户端无需持有服务端证书本身,只需信任 CA 根。
postgresql.conf:开启加密通道
证书就位后,编辑 postgresql.conf:
# postgresql.conf
ssl = on
ssl_cert_file = 'server.crt' # 相对于数据目录
ssl_key_file = 'server.key'
ssl_ca_file = 'root.crt' # 客户端 verify-full 必需
ssl_min_protocol_version = 'TLSv1.2' # 拒绝不安全的老版本协议
ssl = on 让加密连接可用,但不强制。强制是 pg_hba.conf 的事。
ssl_ca_file 经常被示例遗漏,但客户端要 verify-full 就必须有它。没有 CA 文件,服务端无法配合完成完整验证链。
一个词决定安全还是摆设
pg_hba.conf 的连接类型列有三个关键值:
| 值 | 行为 | 适用场景 |
|---|---|---|
host |
同时接受 SSL 和明文 | 仅本地开发 |
hostssl |
只接受 SSL,明文直接拒绝 | 生产环境 |
hostnossl |
只接受明文,拒绝 SSL | 极少特殊场景 |
大多数部署默认用 host。不请求 SSL 的客户端照样进来,日志里记录的是"连接成功",没有任何标记说明它未加密。一切看起来正常。
把 host 改成 hostssl,是关闭缺口的那一步——没有降级,没有回退,没有选项:
# pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
hostssl all all 0.0.0.0/0 scram-sha-256
改完两个配置文件后,重启 PostgreSQL,不是 reload。ssl = on 需要完整重启才生效。
客户端 sslmode:六个级别,只有两个安全
客户端通过连接字符串的 sslmode 控制行为。六个模式中,大多数给了"安全感"而非真实安全:
| 模式 | 行为 | 安全? |
|---|---|---|
disable |
不用 SSL | 否 |
prefer |
尝试 SSL,失败则静默降级明文 | 否——这是 libpq 和多数 ORM 的默认值 |
require |
要求 SSL,但不验证证书 | 部分——中间人可伪造证书 |
verify-ca |
SSL + 验证证书链 | 好,但不检查主机名 |
verify-full |
SSL + 验证证书 + 验证主机名 | 生产环境应使用 |
prefer 是最危险的默认值。Django、SQLAlchemy、ActiveRecord 几乎都不显式覆盖它。攻击者截断 SSL 协商,应用静默回退明文,日志里没有任何异常。
require 加密了流量但不验证证书——中间人可以提交自己的证书,你的应用照收不误。
生产连接字符串示例:
# psql 命令行
psql "host=db.example.com dbname=mydb user=appuser \
sslmode=verify-full sslrootcert=/path/to/root.crt"
# URL 格式(应用配置和 ORM 常用)
postgresql://appuser@db.example.com/mydb?sslmode=verify-full&sslrootcert=/path/to/root.crt
服务端 hostssl + 客户端 verify-full,这才是"SSL 已配置"应有的含义。
验证:别信配置,查运行状态
每次 SSL 相关变更后,直接验证。PostgreSQL 提供 pg_stat_ssl 视图,展示每条连接的加密状态:
-- 当前会话是否加密?
SELECT ssl, version, cipher, bits
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();
-- 是否有远程连接正在明文传输?
SELECT pid, usename, application_name, client_addr, ssl
FROM pg_stat_ssl
JOIN pg_stat_activity USING (pid)
WHERE ssl = false
AND client_addr IS NOT NULL;
第二条查询返回任何行,就意味着那些连接此刻在裸奔。把它放进监控栈,定时执行,对非本地地址的结果告警。几分钟设置,换来持续证据而非一次性配置审查。
证书过期检查——别等它炸了才发现:
openssl x509 -in server.crt -noout -dates
常见失误清单
pg_hba.conf用host而非hostssl:证书正确、配置正确,仍然接受明文。最常见缺口,值得反复强调。- 应用留
sslmode=prefer:默认值,几乎没人显式覆盖。攻击者剥离 SSL 协商,应用静默回退明文,零日志痕迹。 sslmode=require就收工:加密了但不验证证书,中间人伪造证书照样通过。比prefer好,但不是verify-full。- 忘记证书过期:证书到期后 PostgreSQL 拒绝所有 SSL 连接,在最不巧的日子炸掉。要么自动化轮换,要么提前 30 天设日历提醒——"到时候再说"一定会忘。
- 只配主库不配副本:主库收紧了 SSL,几个月后加 standby,
pg_hba.conf没同步。读流量走副本,副本是敞开的。副本安静、不出事,配置漂移就这么积累。
SSL 不覆盖的范围
SSL 保护应用与数据库之间的传输通道。它对静态数据没有立场——加密存储、加密表空间、操作系统级加密是独立问题。它也不替代网络控制:私有子网、5432 端口防火墙规则、最小暴露面仍是基础。SSL 在这个结构里是一层,不是替代品。
配置顺序与自检
正确的完整顺序:
- 生成证书文件,放入数据目录,锁定权限。
postgresql.conf设置ssl = on及相关路径。pg_hba.conf远程连接改hostssl。- 重启 PostgreSQL。
- 客户端连接字符串设
sslmode=verify-full并指定sslrootcert。 - 执行
pg_stat_ssl查询确认。
按这个顺序做完,再跑验证查询。那才是"SSL 已配置"的实际含义。