强制 SSL 连接
上一页把 SSL 配置讲全了,但有一个隐藏的前提没有挑明:「服务器支持 SSL」与「所有连接都被加密」是两回事。默认的 ssl = on 只是「可以加密」,客户端用 sslmode=disable 照样明文连进来;pg_hba 里一条 host 记录同样兼容明文。这一页解决的就是收口问题——把加密从「可选能力」变成「唯一路径」,让任何一条非加密连接都无法建立。
强制加密没有单点开关,而是四层手段的组合拳:pg_hba 记录层面(hostssl 只放行加密连接)、客户端层面(sslmode=require/verify-ca/verify-full 拒绝降级)、配置卫生层面(移除或收紧明文可达的 host 记录)、平台层面(云数据库参数、代理层强制)。四层各管一段,任何一层单独存在都有绕过方式,叠加起来才构成真正的强制。
另一个必须在此页说透的概念是「加密 ≠ 验证」。客户端的 require 档只保证密文传输,不验证服务端身份——中间人可以在正中间做「双向代理」,两段都加密而内容尽收。真正强制的是 verify-full 档 + 正确的 CA 分发,这也是迁移方案的终点档位。我们按「手段对比 → 服务端收口 → 客户端收口 → 验证监控 → 分阶段迁移」的顺序展开。
前置知识
基础概念
四种强制手段对比
| 手段 | 作用层 | 强度 | 局限 |
|---|---|---|---|
① hostssl 记录 | 服务端 HBA | 高:非 SSL 连接在该记录上不可见 | 只约束命中该记录的连接;漏网 host 记录仍可明文 |
② 客户端 sslmode | 客户端 | 高:require 以上拒绝明文 | 逐个客户端改配置,漏一台就漏一路 |
| ③ 移除/收紧明文记录 | 服务端 HBA | 高:消除明文路径 | 需要先盘点存量明文流量 |
| ④ 平台强制(云参数/代理) | 平台 | 中高:一刀切 | 能力随平台而定,配置在库外 |
四层是「与」的关系:服务端 hostssl 决定「能不能明文连」,客户端 sslmode 决定「愿不愿明文连」,两者任一达标即可阻断明文——但单边强制的另一半是兜底,双端都收口才没有「谁忘了改配置就退化到明文」的缝隙。
host / hostssl / hostnossl 的准确语义
| 记录类型 | 匹配的连接 | 常见误解 |
|---|---|---|
host | TCP 连接,无论是否 SSL | 「host = 明文」是误解;它也匹配 SSL 连接 |
hostssl | 仅 SSL/TLS 连接 | 强制加密的正确载体 |
hostnossl | 仅非 SSL 连接 | 显式给明文连接单独立规则(通常是更严的规则) |
由语义直接推出两条收口技巧:
- 把业务规则从
host改写为hostssl,等于「这条规则只对加密连接生效」; - 再用一条
hostnossl ... reject兜底,把所有明文连接显式拒绝——虽然「无匹配即拒绝」也会拒掉它们,但显式reject让拒绝原因在日志与配置评审中一目了然。
证书校验的三层递进
客户端 sslmode 的后三档是递进关系:
require → 加密,但不验证服务端证书(中间人可双向代理)
verify-ca → require + 验证「证书是可信 CA 签的」(中间人需持可信 CA 证书)
verify-full → verify-ca + 验证「证书里的主机名 == 连接的主机名」(彻底防中间人)verify-full 的主机名校验规则:优先比对证书 SAN(subjectAltName)里的 DNS 条目,无 SAN 时退回 CN;支持最左标签通配符(*.example.com 匹配 pg1.example.com 但不匹配 a.b.example.com)。这也是 证书生成与管理 一页强调「签发时必须写 SAN」的原因——没有 SAN 的证书在高版本客户端上可能直接校验失败或退化到 CN 匹配。
强制加密的分层防御模型
「强制」不是一个开关,而是把「明文连接在哪一层被挡住」想清楚。一次连接要穿越四层,每层都有一个能一票否决明文的控制点:
应用进程
│ ① 客户端层:sslmode=verify-full —— 愿不愿发起明文?
▼
网络(内网/专线)
│ ② 网络层:防火墙/安全组收窄可达面 —— 明文流量还有路可走吗?
▼
PostgreSQL 服务器
│ ③ HBA 层:hostssl + hostnossl reject —— 明文连得进来吗?
▼
数据
└ ④ 平台层:云参数 rds.force_ssl / 代理逐段定档 —— 平台有没有旁路?| 层 | 挡住谁 | 典型漏洞形态 |
|---|---|---|
① 客户端 sslmode | 忘改配置的应用 | 新上线服务默认 prefer,静默明文 |
| ② 网络 | 不该出现的来源 | 运维临时开的防火墙规则残留 |
③ HBA hostssl | 一切明文 TCP | 漏改的 host 记录、新加记录写错类型 |
| ④ 平台 | 绕过 DBA 流程的接入 | 云控制台直连、代理后段明文 |
工程上的要点:第③层是唯一「服务端说了算」且覆盖全部 TCP 客户端的层,是强制的锚点;①②④ 各自补足「运维纪律、来源治理、平台旁路」。分层模型里最稳的结构是「锚点层兜底 + 其余层补位」:任何一层失守都不会击穿③层的拒绝逻辑。只做③也能达成技术上的强制,但没有①的配合,迁移过程会很难看(存量客户端集体被拒)——这正是后面迁移方案先改客户端、后收服务端的原因。
中间人攻击:为什么 require 不够
sslmode=require 下,客户端只要求「有加密」,不关心对面是谁。攻击模型:
合法客户端 ──加密──► 攻击者(假服务器) ──加密──► 真服务器
▲ 自签证书骗过 require 档 ▲ 用客户端的凭据直连
└── 认证信息、业务数据全程可见 ──┘攻击者出示自签证书,require 档照样握手成功;客户端把用户名密码(或 SCRAM 交换)交给假服务器,假服务器再拿凭据去连真服务器。verify-ca 要求攻击者的证书出自可信 CA——PKI 管控得当的情况下难度大增;verify-full 还要主机名匹配,攻击者除非能从你的 CA 签出目标主机名的证书,否则无法立足。强制 SSL 的完整含义因此是:强制加密 + 强制验证,即 verify-full。
语法与用法
服务端收口:hostssl 化改造
# 改造前(明文可达)
host app app_rw 10.0.1.0/24 scram-sha-256
# 改造后(只收加密连接)
hostssl app app_rw 10.0.1.0/24 scram-sha-256
# 兜底:明文连接显式拒绝
hostnossl all all 0.0.0.0/0 reject
hostnossl all all ::/0 reject改完的两步标准动作:pg_hba_file_rules 预检 + reload(详见 pg_hba.conf 详解 一页),然后监控认证失败——还在用 sslmode=disable 的存量客户端会以「no pg_hba.conf entry」被拒,这是迁移清单的天然来源。
客户端收口:连接串模板
# psql / libpq
psql "host=pg1.example.com dbname=app user=app_rw \
sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt"各语言客户端的等价配置(CA 文件分发见下一页;这里给最常用的四种):
// JDBC(pgjdbc)
String url = "jdbc:postgresql://pg1.example.com:5432/app"
+ "?sslmode=verify-full"
+ "&sslrootcert=/etc/pg-certs/ca.crt";
Connection conn = DriverManager.getConnection(url, "app_rw", password);// PHP PDO(PgSQL DSN 直接支持 sslmode 等 libpq 关键字)
$pdo = new PDO(
'pgsql:host=pg1.example.com;port=5432;dbname=app;sslmode=verify-full;sslrootcert=/etc/pg-certs/ca.crt',
'app_rw',
$password
);# Python psycopg(libpq 参数透传)
import psycopg
conn = psycopg.connect(
"host=pg1.example.com port=5432 dbname=app user=app_rw "
"sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt",
password=password,
)// Node.js(node-postgres):显式 ssl 对象最直观
const { Client } = require('pg');
const fs = require('fs');
const client = new Client({
host: 'pg1.example.com',
port: 5432,
database: 'app',
user: 'app_rw',
password: password,
ssl: {
rejectUnauthorized: true, // 验证 CA 链
ca: fs.readFileSync('/etc/pg-certs/ca.crt'), // 可信 CA
// checkServerIdentity 按默认实现校验主机名
},
});监控非 SSL 连接
服务端视角找出「还在明文」的连接,是迁移方案与长期巡检共用的一条查询(pg_stat_ssl 的字段见上一页):
SELECT a.username, a.application_name, a.client_addr, a.state, a.backend_start
FROM pg_stat_ssl s
JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend'
AND NOT s.ssl
AND a.client_addr IS NOT NULL
ORDER BY a.backend_start; username | application_name | client_addr | state | backend_start
----------+------------------+---------------+-------+-------------------------------
etl | airflow | 10.0.1.57 | idle | 2026-10-07 08:44:12.917+08
report | metabase | 10.0.3.201 | idle | 2026-10-07 09:02:41.330+08
(2 rows)client_addr IS NOT NULL 排除了本机 socket 连接(local 记录走 peer,没有 TLS 概念)。这份清单就是强制迁移的工作队列:清零后,它变成巡检基线——再出现即告警。
详细说明
用 pg_hba 强制的两种姿势
姿势一:全面 hostssl 化 + hostnossl 兜底(上一节所示),适合「全库强制」策略,简单彻底。
姿势二:分区策略——管理网段强制双因素(SSL + 证书),业务网段强制加密 + 密码,个别无法加密的老系统显式收窄到 /32 并加注解限期整改:
# 管理网段:加密 + 客户端证书
hostssl all +dba 10.0.9.0/24 scram-sha-256 clientcert=verify-ca
# 业务网段:强制加密
hostssl app app_rw 10.0.1.0/24 scram-sha-256
# 遗留系统:明文唯一通道,锁定到单机并限期
host app legacy 10.0.2.77/32 md5
# 兜底
hostnossl all all 0.0.0.0/0 reject
hostnossl all all ::/0 reject姿势二的本质是「把例外变成显式规则」:明文可达的入口从「任何 host 记录」收敛为「一条被注释与限期标记的记录」,审计时一眼可查。
verify-full 落地的两个前提
第一,CA 分发:客户端拿到 sslrootcert(企业 CA 或专用数据库 CA 的证书),没有 CA 文件,verify 档直接无法工作——CA 分发机制是强制 SSL 的隐性依赖,见下一页。
第二,主机名一致性:客户端连接用的主机名必须出现在服务端证书的 SAN 里。实践中大量「verify-full 连不上」不是 PKI 问题,而是客户端用了 IP、短主机名或未登记的别名。约定「数据库连接一律用 FQDN」,或为常用别名在 SAN 中显式登记。
云数据库的强制 TLS
托管服务通常提供「参数级」的强制开关,思路与自建一致(服务端拒绝非 TLS):
- AWS RDS / Aurora:参数组
rds.force_ssl = 1;客户端用 RDS 提供的 CA bundle(rds-ca 证书集)配sslmode=verify-full。开启后非 TLS 连接直接被拒。 - 其他云厂商:均有等价的「强制 SSL/Enforce TLS」开关,位置各异(参数组/控制台开关),核对自家文档。
- 自建上云混合:自建侧用
hostssl收口,两侧策略保持一致,避免「云上强制、自建明文」的漂移。
注意云数据库的 CA 是云厂商的 CA,与自建 PKI 不同——应用的 sslrootcert 在两个环境要分别管理,这是混合部署的常见翻车点。
代理与连接池的两段式强制
流量经过代理时,加密是「两段」:
- HAProxy 以 TCP 模式透传:TLS 端到端,
hostssl语义保持;以 HTTP/TLS 前端终止模式接入时,与数据库之间是明文——必须在后端段再配加密,或接受「内网可信」并写明例外。 - PgBouncer:
client_tls_sslmode(客户端段)与server_tls_sslmode(后端段)分别配置,两段各有证书链。客户端段设require以上、后端段设verify-full才是完整强制。 - 语义提醒:经池化后,数据库侧
pg_stat_ssl看到的是「池 → 库」这段;「客户端 → 池」的加密状态要在池侧监控。两段各建基线,缺一不可。
PgBouncer 的两段式配置落成文件大致是:
; pgbouncer.ini —— 两段各自强制
[pgbouncer]
; 客户端段:要求加密,并校验客户端拿到的证书链
client_tls_sslmode = require
client_tls_key_file = /etc/pgbouncer/server.key
client_tls_cert_file = /etc/pgbouncer/server.crt
client_tls_ca_file = /etc/pgbouncer/ca.crt
; 后端段:加密 + 校验真数据库的身份
server_tls_sslmode = verify-full
server_tls_key_file = /etc/pgbouncer/backend-client.key
server_tls_cert_file = /etc/pgbouncer/backend-client.crt
server_tls_ca_file = /etc/pgbouncer/backend-ca.crtverify-ca / verify-full 档在客户端段与后端段都受支持(较新版本的 PgBouncer 才有完整支持,按部署版本核对取值范围)。无论怎么取值,检查清单是固定的:两段各有 sslmode、各有证书链、各自的验证对象别搞混——后端段的 verify-full 校验的是「数据库服务器的证书」,客户端段的校验的是「池对外出示的证书」,两套 CA 在架构文档里要分别登记。
TLS 终止的四种拓扑对比
流量是否经过代理、代理是否解密,决定了「强制」的真实含义。四种拓扑一张表看清:
| 拓扑 | TLS 端点 | 数据库侧看到的 | 强制语义 | 适用 |
|---|---|---|---|---|
| ① 直连 | 数据库 | 端到端 TLS | hostssl + verify-full 完整生效 | 常规应用 |
| ② 代理 TCP 透传 | 数据库 | 端到端 TLS(代理只转发字节) | 同①,完整保留 | HAProxy/负载均衡 |
| ③ 代理终止 + 后段加密 | 代理 + 数据库(两段) | 「代理 → 库」这一段 | 两段各自强制,逐段验收 | 需要在代理层改写/分流 |
| ④ 代理终止 + 后段明文 | 仅代理 | 明文 | 数据库侧无法强制,只有代理到客户端这段是加密的 | 仅限受控内网且显式例外 |
拓扑②的 HAProxy 配置(TCP 模式,不解密):
# haproxy.cfg —— TCP 透传:TLS 由客户端与 PostgreSQL 端到端完成
frontend pg-frontend
bind *:5432
mode tcp
default_backend pg-backend
backend pg-backend
mode tcp
server pg1 10.0.0.11:5432 check透传模式下 verify-full 语义原封不动,但有一个必须想通的细节:客户端连接的主机名是「HAProxy 的主机名」,证书 SAN 必须包含它(或让客户端解析到代理地址的主机名与证书一致)。拓扑④则是反模式——它把「我们走 SSL」变成了半真半假的表述,数据库侧的 pg_stat_ssl 显示明文、审计上无法自证,若确需保留,按前文姿势二「显式规则 + 限期注释」处理。
与 GSSAPI 加密的互斥与互补
hostgssenc 提供了 TLS 之外的加密路径(Kerberos 会话加密)。若域内环境选择 GSS 加密替代 TLS,强制策略等价物是「hostgssenc 记录 + 客户端 gssencmode=require」,此时要防止的是客户端退化到 sslmode 路径——两种加密通道并存时,用 HBA 记录明确「哪些网段走哪条」,避免双通道都「可选」。
用连接日志做旁证
pg_stat_ssl 是「此刻」的快照,连接日志则是「历史」的账本,两者互补。开启 log_connections 后,连接建立会有固定格式的日志行;被强制策略拒绝的明文连接也会留下痕迹:
# 放行的加密连接(hostssl 记录命中)
LOG: connection received: host=10.0.1.57 port=52918
LOG: connection authorized: user=app_rw database=app application_name=etl
# 被拒的明文连接(hostnossl reject 或无匹配)
FATAL: pg_hba.conf rejects connection for host "10.0.2.77",
user "legacy", database "app", no encryption# 统计一个窗口内的明文拒绝来源:迁移收尾阶段它应当归零
grep 'no encryption' $PGDATA/log/postgresql-*.log \
| grep -o 'host "[0-9.:a-f]*"' | sort | uniq -c | sort -rn日志口径的价值在两个场景:其一,迁移验收——快照查询可能恰好赶上明文连接断开的间隙,日志能证明「整个窗口内没有明文流量」;其二,长期审计——拒绝日志突然出现的陌生来源,本身就是一次接入尝试的记录。把「快照 SQL + 日志 grep」两条都放进巡检脚本。
常见反模式
强制 SSL 落地时反复出现的几种形态,评审时对号入座:
- 「服务器开了 ssl=on,所以我们加密了」——默认
prefer与sslmode=disable都能明文连入,ssl=on只提供能力,不构成强制; - 「我们在 HBA 里写了 hostssl,但没有动旧的 host 记录」——旧记录仍在,明文路径继续存在;hostssl 化必须配套「删旧记录或 hostnossl reject 兜底」;
- 「客户端都配了 sslmode=require,很安全」——require 档可被中间人双向代理,安全表述里出现 require 就该追问「为什么不是 verify-full」;
- 「CA 文件直接打进代码仓库」——CA 证书虽是公开材料,但仓库里的副本没人更新、版本漂移;CA 分发应走配置管理统一路径;
- 「例外连了三年了」——没有责任人与限期的例外等于永久豁免,见上一节的例外管理;
- 「代理是加密的所以全链路安全」——拓扑④(代理终止 + 后段明文)下,代理之后的那段谁都没管;先画清拓扑,再谈强制。
- 「迁移文档里写着 verify-full,所以已经强制了」——文档不是证据,验收四证据才是;旧连接模板、脚本、定时代理里残留的旧 sslmode,靠文档发现不了。
- 「内网很安全,先不加密」——内网是「信任边界更近」,不是「没有对手」;强制策略的价值恰恰在限制内部横向移动。
- 「巡检 SQL 写进了脚本但没人看输出」——采集不等于监控,输出进告警通道才算闭环。
这些反模式的共同点是用局部正确替代整体验收。反着做就是把本页的验收四证据(明文被拒、verify 档生效、监控清零、变更留痕)逐条跑一遍。
验收:从「配置强制」到「证明强制」
强制 SSL 的验收不是「配置文件里写了 hostssl」,而是四个可执行证据:
- 明文连接被拒:
psql "host=... sslmode=disable"返回no pg_hba.conf entry ... no encryption; verify-full生效:把sslrootcert换成不相干 CA,连接必须失败;- 监控清零:
pg_stat_ssl中非 SSL 远程连接为 0,并已接告警; - 变更留痕:HBA diff、CA 分发记录、各端配置模板入库。
强制策略的例外管理
现实环境总有一两个「暂时无法加密」的系统(老硬件、封闭协议的商用软件)。强制的正确性不在于「零例外」,而在于例外是显式的、有主的、会过期的:
| 字段 | 示例 | 说明 |
|---|---|---|
| 来源 | 10.0.2.77/32 | 精确到主机,绝不写网段 |
| 责任人 | 王工(ERP 厂商接口) | 出问题有人可找 |
| 原因 | 厂商驱动不支持 TLS | 换成「谁批准的、为什么批准」 |
| 限期 | 2027-06-30(厂商 v3 版本交付) | 到期未整改则回收 |
# HBA 中对应的存在形态:独立一条、注释齐全、置于 hostssl 规则之后
# 例外:ERP 老接口,厂商不支持 TLS;责任人:王工;限期:2027-06-30
host app legacy 10.0.2.77/32 scram-sha-256管理动作三件套:例外清单与 HBA 注释保持同步(巡检时逐条核对);例外上线后监控它的流量(那条 SQL 里按 client_addr 过滤即可);到期未整改,直接删除记录让连接失败——例外到期不回收,比从来没有强制策略更糟,因为它教会了所有人「例外是永久的」。
例外清单本身纳入配置管理的版本控制,与 HBA 注释的 diff 每月核对一次,漂移当月修正。
实战示例
示例一:分阶段强制 SSL 迁移方案
目标:生产库从「SSL 可选」迁到「全强制 verify-full」,业务零中断。四个阶段,每阶段有明确的进入/退出条件。
阶段 0:基线盘点(不改任何配置)。
-- 明文连接清单:迁移的工作队列
SELECT a.username, a.application_name, a.client_addr, count(*) AS conns
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend' AND NOT s.ssl AND a.client_addr IS NOT NULL
GROUP BY 1, 2, 3 ORDER BY 4 DESC;阶段 1:服务端 hostssl 化(先加记录,不删旧的)。
# 在原 host 记录之前插入 hostssl 版本;旧 host 行暂留
hostssl app app_rw 10.0.1.0/24 scram-sha-256 # 新
host app app_rw 10.0.1.0/24 scram-sha-256 # 旧,阶段 3 删除由于首个匹配语义,新连接走 hostssl;sslmode=disable 的客户端仍走旧 host 行——先给所有客户端配好 sslmode,再动服务端。
阶段 2:客户端逐个升级到 verify-full。
# 对每个应用:发 CA 文件 → 改连接串 → 灰度验证
psql "host=pg1.example.com dbname=app user=app_rw \
sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt" -c "SELECT 1;"验收标准:pg_stat_ssl 中该应用的连接 ssl = t,且版本/套件符合基线。
阶段 3:服务端拔掉明文路径。
# 删除旧 host 行,加显式兜底
hostnossl all all 0.0.0.0/0 reject
hostnossl all all ::/0 rejectpsql -c "SELECT rule_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;"
psql -c "SELECT pg_reload_conf();"
# 3.1 明文连接应被拒(预期报错才是正确)
psql "host=pg1 dbname=app user=app_rw sslmode=disable" -c "SELECT 1;"psql: error: FATAL: pg_hba.conf rejects connection for host "10.0.1.57",
user "app_rw", database "app", no encryption-- 3.2 最终基线:远程连接全部加密
SELECT count(*) AS plaintext_remote
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend' AND NOT s.ssl AND a.client_addr IS NOT NULL; plaintext_remote
------------------
0
(1 row)示例二:四种语言客户端的统一配置片段
作为团队连接模板入库(假定 CA 统一分发到 /etc/pg-certs/ca.crt):
; ~/.pg_service.conf —— libpq 服务文件,客户端用 "service=prod_app" 引用
[prod_app]
host=pg1.example.com
dbname=app
sslmode=verify-full
sslrootcert=/etc/pg-certs/ca.crt// JDBC 模板
jdbc:postgresql://pg1.example.com:5432/app?sslmode=verify-full&sslrootcert=/etc/pg-certs/ca.crt# Python 模板
DSN = "host=pg1.example.com dbname=app sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt"// PHP PDO 模板
$dsn = 'pgsql:host=pg1.example.com;port=5432;dbname=app;sslmode=verify-full;sslrootcert=/etc/pg-certs/ca.crt';// Node 模板
const ssl = { rejectUnauthorized: true, ca: fs.readFileSync('/etc/pg-certs/ca.crt') };示例三:巡检 SQL 集合
-- 1) 明文远程连接(应恒为 0,非 0 即告警)
SELECT count(*) FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend' AND NOT s.ssl AND a.client_addr IS NOT NULL;
-- 2) 低版本协议连接(应恒为 0:TLSv1.0/1.1 已被 min 档排除)
SELECT count(*) FROM pg_stat_ssl
WHERE ssl AND version IN ('TLSv1', 'TLSv1.1');
-- 3) 加密连接的套件分布(收敛弱套件)
SELECT version, cipher, count(*) FROM pg_stat_ssl WHERE ssl GROUP BY 1, 2 ORDER BY 3 DESC;示例四:AWS RDS 的强制 TLS 接入
托管实例没有 pg_hba.conf,强制开关是参数组里的 rds.force_ssl:
# 1) 修改参数组(控制台或 CLI):rds.force_ssl = 1,应用后重启参数组生效
# 开启后非 TLS 连接直接被服务器拒绝# 2) 从 AWS 文档/控制台下载对应区域的 RDS CA bundle(如 rds-ca-rsa2048-g1.pem)
# 放到统一路径(配置管理分发,不进代码仓库)
export PGSSLROOTCERT=/etc/pg-certs/rds-ca-bundle.pem
export PGSSLMODE=verify-full
# 3) 连接(实例终端节点的主机名必须被 RDS 证书的 SAN 覆盖——RDS 证书天然满足)
psql "host=mydb.abc123.us-east-1.rds.amazonaws.com dbname=app user=app_rw" -c "\conninfo"SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)# 4) 验证强制生效:明文连接应被拒绝
psql "host=mydb.abc123.us-east-1.rds.amazonaws.com dbname=app user=app_rw \
sslmode=disable" -c "SELECT 1;"psql: error: server refused SSL connection三个迁移注意点:自建 PKI 的 CA 对 RDS 无效,连接模板必须按环境区分 sslrootcert(混合部署高频翻车点);RDS CA 有轮换周期(AWS 会公告新 CA 集合),订阅公告并纳入分发流程;rds.force_ssl 开启前先跑一遍明文连接盘点(同示例一的基线 SQL,RDS 的 pg_stat_ssl 同样可用)。
把这套动作固化成一页 runbook:参数组变更 → CA bundle 分发 → 连接模板 → 验证四段,新环境接入照抄执行。
示例五:HAProxy TCP 透传 + 端到端 verify-full
高可用入口用 HAProxy 做故障转移,TLS 保持端到端:
# haproxy.cfg(TCP 透传,不解密)
frontend pg-frontend
bind *:5432
mode tcp
default_backend pg-backend
backend pg-backend
mode tcp
option pgsql-check user pgcheck
server pg1 10.0.0.11:5432 check
server pg2 10.0.0.12:5432 check backup# pg_hba.conf:hostssl 语义不变,拒绝仍由数据库执行
hostssl app app_rw 10.0.0.0/24 scram-sha-256
hostnossl all all 0.0.0.0/0 reject# 客户端:连接的是 HAProxy 的主机名,证书 SAN 必须包含它
# (签发时把 pg-lb.example.com 写进服务端证书的 SAN,见 certificates 一页)
psql "host=pg-lb.example.com dbname=app user=app_rw \
sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt" -c "\conninfo"You are connected to database "app" as user "app_rw" on host "pg-lb.example.com" ...
SSL connection (protocol: TLSv1.3, ...)验收两条:pg_stat_ssl 里经代理进来的连接 ssl = t(透传保住了端到端 TLS);failover 切换后客户端不重新配置——因为两台后端共用同一张含代理主机名的证书,切换对客户端透明。若改用「每节点各自证书」的方案,SAN 就要同时覆盖代理主机名与各节点主机名。
注意事项
host 改 hostssl 前先看清存量明文流量。 直接替换记录会让所有 sslmode=disable 客户端立即被拒。用 pg_stat_ssl 的明文清单先驱动客户端改造,或利用「新旧记录并存 + 首个匹配」做灰度,别指望一刀切无感。
reject 兜底要覆盖 IPv4 与 IPv6。 只写 0.0.0.0/0 的 hostnossl reject,IPv6 明文连接仍会落到「无匹配拒绝」——结果一样,但显式双栈兜底让审计更清晰。
require 是过渡档,不是终点。 「我们要求所有连接走 SSL」的表述里必须追问「sslmode 是哪一档」。停留在 require 的强制只是加密强制,不是身份强制;迁移计划要把 verify-full 定为退出条件。
CA 分发与主机名约定是隐性依赖。 verify-full 落地失败的原因八成在客户端拿不到 CA 文件、或连接主机名不在 SAN 内。先修这两件事,再谈客户端升级。
本机 socket 连接不受 hostssl 约束。 local 记录没有 TLS 概念,明文统计查询要用 client_addr IS NOT NULL 排除,不要把本机 peer 连接计入「未加密」指标。
池化与代理后的监控对象变了。 数据库侧只能看到「池 → 库」段;「客户端 → 池」段在池侧观测(PgBouncer 日志/统计)。只看 pg_stat_ssl 全绿不等于全链路加密。
云托管实例的 CA 与自建不同。 RDS 用云厂商 CA bundle,自建 PKI 的 CA 文件对它无效;混合部署的连接模板必须区分环境,这也是连接串模板化管理(配置中心)而非硬编码的理由。
强制 SSL 会放大配置错误。 之前「连得上就行」掩盖的错误(主机名错、CA 过期)在 verify-full 下全部变成连接失败。迁移期准备回退路径(临时恢复旧 host 行 + reload),并把回退动作写进变更单。
流复制连接也要纳入 hostssl 化。 repl 账号的 HBA 记录如果还是 host replication repl ...,备库用明文照样能连。强制迁移时把复制记录一并改写为 hostssl replication ...,备库的 primary_conninfo 补上 sslmode=verify-ca 以上——这是清单里最容易被遗忘的一条。
拒绝日志是最好的完成证明。 pg_stat_ssl 只能证明「此刻」没有明文连接;把 no encryption 的拒绝日志接入留存与告警,才能证明「整个观察窗口内没有任何明文接入尝试」。两条证据链合起来才是完整的验收。
云与自建混合环境的 CA 要按环境登记。 云厂商的 CA bundle 与自建 PKI 的 CA 不同,同一套应用模板跨环境连接时 sslrootcert 必须按环境切换——把「环境 → CA 文件路径」做成模板变量,而不是靠人肉记忆兜底。
强制策略有自己的生命周期。 季度对账三件事:HBA 例外是否到期应回收、hostnossl reject 兜底是否仍在位、监控口径是否漂移。强制不是一次性配置,而是持续维护的状态——没有对账节奏的强制,一年内就会退化回「名义强制」。
回退预案也要验证。 变更单里的回退步骤(恢复旧 host 行 + reload)不演练过就不能算数:演练一次「恢复 → 旧客户端连通 → 再收紧」,确认回退路径本身没有埋雷——真出事时才发现回退命令写错,代价远高于事前五分钟。
最佳实践
- 以
verify-full为生产客户端的强制目标档。 迁移可以经过require过渡,但计划里必须有升档时间表,require 不作为终态。 - 服务端用 hostssl 改造 + hostnossl 双栈兜底。 让「明文不可达」在配置层显式成立,而不是依赖「客户端会自觉」。
- 迁移四阶段走完:盘点 → 服务端就位 → 客户端升档 → 拔除明文路径。 每阶段以 SQL 证据(明文连接数、拒绝日志)验收,不以「应该都改好了」为凭。
- CA 分发做成基础设施能力。 统一路径、统一版本、有更新通知机制;
sslrootcert不进代码仓库,进配置管理或镜像层。 - 连接主机名统一 FQDN 并写入 SAN。 团队规范里禁止用 IP 连库,从源头消除 verify-full 的主机名匹配失败。
- 明文连接监控永久化。
pg_stat_ssl的明文计数接入告警,清零不是终点,保持清零才是。 - 代理与池逐段定档并分别监控。 客户端→池、池→库两段的 sslmode 与证书链写进架构文档,巡检覆盖两段。
- 云与自建策略对齐。 RDS 的
rds.force_ssl与自建的 hostssl 化同步推进,混合环境用同一套验收 SQL。 - 例外必须显式、限时、可审计。 确实无法加密的遗留系统收窄到
/32,注释里写明责任人与整改日期,巡检清单跟踪。 - 把验收 SQL 沉淀为巡检脚本。 示例三的三条查询 + 拒绝日志抽样,构成强制 SSL 的长期证据链。
下一节
继续学习:证书生成与管理
参考链接
- PostgreSQL 官方文档:The pg_hba.conf File(hostssl / hostnossl)
- PostgreSQL 官方文档:SSL Support(libpq sslmode 语义)
- PostgreSQL 官方文档:Secure TCP/IP Connections with SSL
- PostgreSQL 官方文档:SSL Certificates
- PostgreSQL 官方文档:pg_stat_ssl 视图
- PostgreSQL 官方文档:Connection and Authentication 配置参数
- PostgreSQL 官方文档:Database Connection Control Functions(连接参数)
- PostgreSQL 官方文档:Environment Variables(PGSSLMODE 等)
- PostgreSQL 官方文档:Authentication Problems