Skip to content

强制 SSL 连接 ​

上一页把 SSL 配置讲全了,但有一个隐藏的前提没有挑明:「服务器支持 SSL」与「所有连接都被加密」是两回事。默认的 ssl = on 只是「可以加密」,客户端用 sslmode=disable 照样明文连进来;pg_hba 里一条 host 记录同样兼容明文。这一页解决的就是收口问题——把加密从「可选能力」变成「唯一路径」,让任何一条非加密连接都无法建立。

强制加密没有单点开关,而是四层手段的组合拳:pg_hba 记录层面(hostssl 只放行加密连接)、客户端层面(sslmode=require/verify-ca/verify-full 拒绝降级)、配置卫生层面(移除或收紧明文可达的 host 记录)、平台层面(云数据库参数、代理层强制)。四层各管一段,任何一层单独存在都有绕过方式,叠加起来才构成真正的强制。

另一个必须在此页说透的概念是「加密 ≠ 验证」。客户端的 require 档只保证密文传输,不验证服务端身份——中间人可以在正中间做「双向代理」,两段都加密而内容尽收。真正强制的是 verify-full 档 + 正确的 CA 分发,这也是迁移方案的终点档位。我们按「手段对比 → 服务端收口 → 客户端收口 → 验证监控 → 分阶段迁移」的顺序展开。

前置知识

建议先阅读:SSL 配置(ssl / ssl_cert_file)

基础概念 ​

四种强制手段对比 ​

手段作用层强度局限
① hostssl 记录服务端 HBA高:非 SSL 连接在该记录上不可见只约束命中该记录的连接;漏网 host 记录仍可明文
② 客户端 sslmode客户端高:require 以上拒绝明文逐个客户端改配置,漏一台就漏一路
③ 移除/收紧明文记录服务端 HBA高:消除明文路径需要先盘点存量明文流量
④ 平台强制(云参数/代理)平台中高:一刀切能力随平台而定,配置在库外

四层是「与」的关系:服务端 hostssl 决定「能不能明文连」,客户端 sslmode 决定「愿不愿明文连」,两者任一达标即可阻断明文——但单边强制的另一半是兜底,双端都收口才没有「谁忘了改配置就退化到明文」的缝隙。

host / hostssl / hostnossl 的准确语义 ​

记录类型匹配的连接常见误解
hostTCP 连接,无论是否 SSL「host = 明文」是误解;它也匹配 SSL 连接
hostssl仅 SSL/TLS 连接强制加密的正确载体
hostnossl仅非 SSL 连接显式给明文连接单独立规则(通常是更严的规则)

由语义直接推出两条收口技巧:

  • 把业务规则从 host 改写为 hostssl,等于「这条规则只对加密连接生效」;
  • 再用一条 hostnossl ... reject 兜底,把所有明文连接显式拒绝——虽然「无匹配即拒绝」也会拒掉它们,但显式 reject 让拒绝原因在日志与配置评审中一目了然。

证书校验的三层递进 ​

客户端 sslmode 的后三档是递进关系:

text
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 匹配。

强制加密的分层防御模型 ​

「强制」不是一个开关,而是把「明文连接在哪一层被挡住」想清楚。一次连接要穿越四层,每层都有一个能一票否决明文的控制点:

text
 应用进程
    │  ① 客户端层:sslmode=verify-full —— 愿不愿发起明文?
    ▼
 网络(内网/专线)
    │  ② 网络层:防火墙/安全组收窄可达面 —— 明文流量还有路可走吗?
    ▼
 PostgreSQL 服务器
    │  ③ HBA 层:hostssl + hostnossl reject —— 明文连得进来吗?
    ▼
 数据
    └  ④ 平台层:云参数 rds.force_ssl / 代理逐段定档 —— 平台有没有旁路?
层挡住谁典型漏洞形态
① 客户端 sslmode忘改配置的应用新上线服务默认 prefer,静默明文
② 网络不该出现的来源运维临时开的防火墙规则残留
③ HBA hostssl一切明文 TCP漏改的 host 记录、新加记录写错类型
④ 平台绕过 DBA 流程的接入云控制台直连、代理后段明文

工程上的要点:第③层是唯一「服务端说了算」且覆盖全部 TCP 客户端的层,是强制的锚点;①②④ 各自补足「运维纪律、来源治理、平台旁路」。分层模型里最稳的结构是「锚点层兜底 + 其余层补位」:任何一层失守都不会击穿③层的拒绝逻辑。只做③也能达成技术上的强制,但没有①的配合,迁移过程会很难看(存量客户端集体被拒)——这正是后面迁移方案先改客户端、后收服务端的原因。

中间人攻击:为什么 require 不够 ​

sslmode=require 下,客户端只要求「有加密」,不关心对面是谁。攻击模型:

text
合法客户端 ──加密──► 攻击者(假服务器) ──加密──► 真服务器
             ▲ 自签证书骗过 require 档            ▲ 用客户端的凭据直连
             └── 认证信息、业务数据全程可见 ──┘

攻击者出示自签证书,require 档照样握手成功;客户端把用户名密码(或 SCRAM 交换)交给假服务器,假服务器再拿凭据去连真服务器。verify-ca 要求攻击者的证书出自可信 CA——PKI 管控得当的情况下难度大增;verify-full 还要主机名匹配,攻击者除非能从你的 CA 签出目标主机名的证书,否则无法立足。强制 SSL 的完整含义因此是:强制加密 + 强制验证,即 verify-full。

语法与用法 ​

服务端收口:hostssl 化改造 ​

text
# 改造前(明文可达)
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」被拒,这是迁移清单的天然来源。

客户端收口:连接串模板 ​

bash
# psql / libpq
psql "host=pg1.example.com dbname=app user=app_rw \
      sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt"

各语言客户端的等价配置(CA 文件分发见下一页;这里给最常用的四种):

java
// 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
// 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
# 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,
)
javascript
// 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 的字段见上一页):

sql
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;
text
 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 并加注解限期整改:

text
# 管理网段:加密 + 客户端证书
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 的两段式配置落成文件大致是:

ini
; 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.crt

verify-ca / verify-full 档在客户端段与后端段都受支持(较新版本的 PgBouncer 才有完整支持,按部署版本核对取值范围)。无论怎么取值,检查清单是固定的:两段各有 sslmode、各有证书链、各自的验证对象别搞混——后端段的 verify-full 校验的是「数据库服务器的证书」,客户端段的校验的是「池对外出示的证书」,两套 CA 在架构文档里要分别登记。

TLS 终止的四种拓扑对比 ​

流量是否经过代理、代理是否解密,决定了「强制」的真实含义。四种拓扑一张表看清:

拓扑TLS 端点数据库侧看到的强制语义适用
① 直连数据库端到端 TLShostssl + verify-full 完整生效常规应用
② 代理 TCP 透传数据库端到端 TLS(代理只转发字节)同①,完整保留HAProxy/负载均衡
③ 代理终止 + 后段加密代理 + 数据库(两段)「代理 → 库」这一段两段各自强制,逐段验收需要在代理层改写/分流
④ 代理终止 + 后段明文仅代理明文数据库侧无法强制,只有代理到客户端这段是加密的仅限受控内网且显式例外

拓扑②的 HAProxy 配置(TCP 模式,不解密):

text
# 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 后,连接建立会有固定格式的日志行;被强制策略拒绝的明文连接也会留下痕迹:

text
# 放行的加密连接(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
bash
# 统计一个窗口内的明文拒绝来源:迁移收尾阶段它应当归零
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」,而是四个可执行证据:

  1. 明文连接被拒:psql "host=... sslmode=disable" 返回 no pg_hba.conf entry ... no encryption;
  2. verify-full 生效:把 sslrootcert 换成不相干 CA,连接必须失败;
  3. 监控清零:pg_stat_ssl 中非 SSL 远程连接为 0,并已接告警;
  4. 变更留痕:HBA diff、CA 分发记录、各端配置模板入库。

强制策略的例外管理 ​

现实环境总有一两个「暂时无法加密」的系统(老硬件、封闭协议的商用软件)。强制的正确性不在于「零例外」,而在于例外是显式的、有主的、会过期的:

字段示例说明
来源10.0.2.77/32精确到主机,绝不写网段
责任人王工(ERP 厂商接口)出问题有人可找
原因厂商驱动不支持 TLS换成「谁批准的、为什么批准」
限期2027-06-30(厂商 v3 版本交付)到期未整改则回收
text
# 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:基线盘点(不改任何配置)。

sql
-- 明文连接清单:迁移的工作队列
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 化(先加记录,不删旧的)。

text
# 在原 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。

bash
# 对每个应用:发 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:服务端拔掉明文路径。

text
# 删除旧 host 行,加显式兜底
hostnossl all  all  0.0.0.0/0  reject
hostnossl all  all  ::/0       reject
bash
psql -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;"
text
psql: error: FATAL:  pg_hba.conf rejects connection for host "10.0.1.57",
        user "app_rw", database "app", no encryption
sql
-- 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;
text
 plaintext_remote
------------------
                0
(1 row)

示例二:四种语言客户端的统一配置片段 ​

作为团队连接模板入库(假定 CA 统一分发到 /etc/pg-certs/ca.crt):

ini
; ~/.pg_service.conf —— libpq 服务文件,客户端用 "service=prod_app" 引用
[prod_app]
host=pg1.example.com
dbname=app
sslmode=verify-full
sslrootcert=/etc/pg-certs/ca.crt
java
// JDBC 模板
jdbc:postgresql://pg1.example.com:5432/app?sslmode=verify-full&sslrootcert=/etc/pg-certs/ca.crt
python
# Python 模板
DSN = "host=pg1.example.com dbname=app sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt"
php
// PHP PDO 模板
$dsn = 'pgsql:host=pg1.example.com;port=5432;dbname=app;sslmode=verify-full;sslrootcert=/etc/pg-certs/ca.crt';
javascript
// Node 模板
const ssl = { rejectUnauthorized: true, ca: fs.readFileSync('/etc/pg-certs/ca.crt') };

示例三:巡检 SQL 集合 ​

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:

text
# 1) 修改参数组(控制台或 CLI):rds.force_ssl = 1,应用后重启参数组生效
#    开启后非 TLS 连接直接被服务器拒绝
bash
# 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"
text
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)
bash
# 4) 验证强制生效:明文连接应被拒绝
psql "host=mydb.abc123.us-east-1.rds.amazonaws.com dbname=app user=app_rw \
      sslmode=disable" -c "SELECT 1;"
text
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 保持端到端:

text
# 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
text
# 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
bash
# 客户端:连接的是 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"
text
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)不演练过就不能算数:演练一次「恢复 → 旧客户端连通 → 再收紧」,确认回退路径本身没有埋雷——真出事时才发现回退命令写错,代价远高于事前五分钟。

最佳实践 ​

  1. 以 verify-full 为生产客户端的强制目标档。 迁移可以经过 require 过渡,但计划里必须有升档时间表,require 不作为终态。
  2. 服务端用 hostssl 改造 + hostnossl 双栈兜底。 让「明文不可达」在配置层显式成立,而不是依赖「客户端会自觉」。
  3. 迁移四阶段走完:盘点 → 服务端就位 → 客户端升档 → 拔除明文路径。 每阶段以 SQL 证据(明文连接数、拒绝日志)验收,不以「应该都改好了」为凭。
  4. CA 分发做成基础设施能力。 统一路径、统一版本、有更新通知机制;sslrootcert 不进代码仓库,进配置管理或镜像层。
  5. 连接主机名统一 FQDN 并写入 SAN。 团队规范里禁止用 IP 连库,从源头消除 verify-full 的主机名匹配失败。
  6. 明文连接监控永久化。 pg_stat_ssl 的明文计数接入告警,清零不是终点,保持清零才是。
  7. 代理与池逐段定档并分别监控。 客户端→池、池→库两段的 sslmode 与证书链写进架构文档,巡检覆盖两段。
  8. 云与自建策略对齐。 RDS 的 rds.force_ssl 与自建的 hostssl 化同步推进,混合环境用同一套验收 SQL。
  9. 例外必须显式、限时、可审计。 确实无法加密的遗留系统收窄到 /32,注释里写明责任人与整改日期,巡检清单跟踪。
  10. 把验收 SQL 沉淀为巡检脚本。 示例三的三条查询 + 拒绝日志抽样,构成强制 SSL 的长期证据链。

下一节

继续学习:证书生成与管理

参考链接 ​