SSL 配置(ssl / ssl_cert_file)
认证解决「你是谁」,SSL/TLS 解决「路上有没有人偷看、有没有人篡改、对面是不是真服务器」。16.2 的五种认证方法里,password 方法的明文、md5 缺失的通道绑定、cert 依赖的握手环节——全都以「连接走 TLS」为前提或前提之一。本页开始 16.3 的三页内容:先把服务端与客户端的 SSL 配置参数讲全(本页),再讲如何把加密从「可选项」变成「强制项」(强制 SSL 连接),最后讲证书怎么签出来、怎么轮换(证书生成与管理)。
PostgreSQL 的 SSL 由编译进服务器的 OpenSSL 提供,配置分两侧:服务端围绕一组以 ssl_ 开头的 GUC(ssl、ssl_cert_file、ssl_key_file、ssl_ca_file……),核心是三份文件与几个策略开关;客户端由 libpq 的 sslmode 等连接参数控制,核心是六档验证强度(disable 到 verify-full)的选择。两侧各自独立配置、在握手时协商——「服务端开了 SSL」不等于「客户端验证了服务端」,反之亦然,这是大量「自以为加密了」事故的根源。
本页还会给出两个「眼见为实」的验证手段:pg_stat_ssl 视图与 sslinfo 扩展。配置完不验证等于没配,这两个工具也会是下一页强制 SSL 方案里的监控抓手。
前置知识
建议先阅读:peer / ident 认证
基础概念
服务端三份文件与信任链
| 文件 | 参数 | 作用 | 属主/权限 |
|---|---|---|---|
| 服务器证书 | ssl_cert_file | 证明「我是这台服务器」,内含公钥与身份 | postgres 可读(0644 即可) |
| 服务器私钥 | ssl_key_file | 与证书配对的私钥,TLS 握手的根本 | 必须 0600/0640,属主 postgres |
| CA 证书 | ssl_ca_file | 校验客户端证书的信任锚(启用 cert/clientcert 时必需) | postgres 可读 |
客户端 服务端
│ ──① ClientHello(可加密的套件列表)──► │
│ ◄─② ServerHello + 服务器证书 ──────── │
│ ──③ 校验证书(按 sslmode 档位)────► │
│ ◄─④ 要求客户端证书(若 HBA 要求)───── │
│ ──⑤ 客户端证书 + 密钥交换/签名 ──────► │
│ ◄═══⑥ 会话密钥协商完成,加密通道建立══►│只配 ssl_cert_file + ssl_key_file 时,服务器在握手不会要求客户端出示证书;ssl_ca_file 是「备好验客户端证书的能力」,是否索取由 pg_hba 记录(cert 方法、clientcert 选项)决定——这一分工在 证书认证 一页已详细展开。
客户端 sslmode:六档验证强度
| sslmode | 加密? | 验证服务端证书? | 校验主机名? | 典型用途 |
|---|---|---|---|---|
disable | 否 | — | — | 明文内网(不推荐) |
allow | 尽力 | 否 | 否 | 兼容性探测,几乎不用 |
prefer(默认) | 尽力 | 否 | 否 | 历史默认;加密但不防中间人 |
require | 必须 | 否 | 否 | 有加密诉求但不做校验 |
verify-ca | 必须 | 验证 CA 链 | 否 | 内部 PKI 受控主机名场景 |
verify-full | 必须 | 验证 CA 链 | 验证(SAN/CN) | 生产推荐 |
注意两件事:其一,默认 prefer 只保证「加密了」,不验证证书,中间人可以出示自签证书完成一次「加密的劫持」;其二,verify-full 的主机名校验依赖服务端证书的 SAN(subjectAltName),这正是下一页讲中间人攻击时的关键,也是证书签发时必须写 SAN 的原因。
客户端文件参数
与验证强度配套的是四组文件参数(完整表格见 证书认证 一页):sslrootcert(校验服务端证书的 CA,默认 ~/.postgresql/root.crt;PG16 起可设为 system 直接使用操作系统信任库)、sslcert/sslkey(客户端证书与私钥,仅当服务端索取客户端证书时使用)、sslcrl(校验服务端证书的吊销列表)。
语法与用法
服务端 GUC 总表
| 参数 | 默认值 | 生效方式 | 说明 |
|---|---|---|---|
ssl | off | 重启 | SSL 总开关 |
ssl_cert_file | server.crt | reload | 服务器证书路径 |
ssl_key_file | server.key | reload | 服务器私钥路径 |
ssl_ca_file | 空 | reload | 校验客户端证书的 CA |
ssl_crl_file | 空 | reload | 证书吊销列表(单文件) |
ssl_crl_dir | 空 | reload | 证书吊销列表目录(PG14+,OpenSSL hash 命名) |
ssl_min_protocol_version | TLSv1.2 | reload | 最低协议版本(PG12 引入,PG13 起默认 TLSv1.2) |
ssl_max_protocol_version | 空 | reload | 最高协议版本(空 = 允许最高可用版本) |
ssl_ciphers | HIGH:MEDIUM:+3DES:!aNULL | reload | TLSv1.2 及以下的套件白名单(TLSv1.3 套件不由此参数控制) |
ssl_prefer_server_ciphers | on | reload | 套件选择权:服务端优先 |
ssl_ecdh_curve | prime256v1 | reload | ECDH 椭圆曲线 |
ssl_dh_params_file | 空 | reload | 自定义 DH 参数(DHE 套件用,一般不需要) |
ssl_passphrase_command | 空 | reload | 取私钥口令的命令(PG13+,支持加密私钥) |
ssl_passphrase_command_support_reload | off | reload | reload 时是否也执行口令命令重新读取私钥(PG13+) |
相对路径相对于数据目录($PGDATA)解析。一份典型的生产配置:
# postgresql.conf —— SSL 基础配置
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ca_file = 'ca.crt'
ssl_crl_file = 'ca.crl'
ssl_min_protocol_version = 'TLSv1.2'
ssl_max_protocol_version = '' # 上限放开,由 min 档兜底
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
ssl_prefer_server_ciphers = on# 应用配置:ssl 与 ssl_* 文件参数修改后重启/重载
pg_ctl restart -D $PGDATA # ssl: off → on 必须重启
psql -c "SELECT pg_reload_conf();" # 其余 ssl_* 参数 reload 即可客户端连接写法
# psql:完整校验
psql "host=pg1.example.com port=5432 dbname=app user=app_rw \
sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt"
# 环境变量等价写法
export PGSSLMODE=verify-full
export PGSSLROOTCERT=/etc/pg-certs/ca.crt
psql "host=pg1.example.com dbname=app user=app_rw"连接池与代理在中间时(如 PgBouncer),加密是两段式的:客户端 → 池、池 → 数据库,两侧参数分别配置,语义见下一页的迁移方案。
客户端 SSL 参数与环境变量速查
libpq 的 SSL 相关参数与同名环境变量一一对应(除大小写前缀),配置模板里两种写法都会见到:
| 连接参数 | 环境变量 | 默认值 | 作用 |
|---|---|---|---|
sslmode | PGSSLMODE | prefer | 验证档位(六档见上表) |
sslrootcert | PGSSLROOTCERT | ~/.postgresql/root.crt | 校验服务端证书的 CA |
sslcert | PGSSLCERT | ~/.postgresql/postgresql.crt | 客户端证书 |
sslkey | PGSSLKEY | ~/.postgresql/postgresql.key | 客户端私钥 |
sslcrl | PGSSLCRL | 无 | 校验服务端证书的吊销列表 |
两条规则值得写进团队记忆:
- 显式参数覆盖环境变量:连接串里写了
sslmode=...时PGSSLMODE被忽略。排错时注意「环境变量明明设了却没生效」多半是连接串里藏着旧值; - 默认路径是隐式约定:什么都不配时,libpq 会去读
~/.postgresql/root.crt等默认文件——文件恰好存在时verify-full会「自动可用」,文件不存在时验证档直接失败。让客户端显式写sslrootcert,消除对默认路径的偶然依赖。
把这张表贴进连接模板的注释区:新人抄配置时顺手就带上了正确参数。文档的最大作用不是供人查阅,而是让正确的写法成为默认。
验证连接是否加密
方法一:pg_stat_ssl 视图(服务端视角,每个后端进程一行):
SELECT pid, ssl, version, cipher, bits,
client_dn, client_serial, issuer_dn,
not_before, not_after
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();-[ RECORD 1 ]-+---------------------------
pid | 25143
ssl | t
version | TLSv1.3
cipher | TLS_AES_256_GCM_SHA384
bits | 256
client_dn |
client_serial |
issuer_dn |
not_before |
not_after |字段速查:ssl 是否启用;version/cipher/bits 协议与套件;client_dn/client_serial/issuer_dn 客户端证书身份(走 cert/clientcert 时有值);not_before/not_after 客户端证书有效期——巡检客户端证书过期用它。
方法二:sslinfo 扩展(会话内自检):
CREATE EXTENSION IF NOT EXISTS sslinfo;
SELECT ssl_is_used() AS encrypted,
ssl_version() AS tls_version,
ssl_cipher() AS cipher; encrypted | tls_version | cipher
-----------+-------------+---------------------------
t | TLSv1.3 | TLS_AES_256_GCM_SHA384
(1 row)psql 的 \conninfo 也会直接显示当前连接的 SSL 状态,快速肉眼验证用它。
详细说明
ssl = on 与重启边界
ssl 这个总开关在 postmaster 启动时创建 SSL 上下文,因此 off → on(或反向)必须重启。除此之外的所有 ssl_* 参数都支持 reload 生效——这意味着证书轮换不需要重启:替换文件内容(或改路径)后 pg_reload_conf(),新连接使用新证书,已有连接不受影响。
-- reload 后新连接即可用新证书;可再连一次确认
SELECT pg_reload_conf();一个容易忽略的点:reload 是「后端进程在建立连接时读取新上下文」,个别工作进程(如正在运行的后台 worker)的会话继续用旧上下文直到结束。轮换证书后用新建连接验证,而不是用既有会话验证。
私钥文件权限:硬性要求
ssl_key_file 的权限在启动时被强制检查,不满足则启动失败:
FATAL: private key file "server.key" has group or world access规则:
- 标准要求:属主为运行服务器的用户(
postgres),权限0600; - 打包变体:Debian 系常见 root 属主 +
0640+ 专用属组(如ssl-cert,postgres 用户在其中),由打包脚本保证检查通过; - 证书文件
server.crt、CA、CRL 是公开材料,0644即可,无需收紧。
# 修正权限的三个标准动作
chown postgres:postgres /var/lib/pgsql/18/data/server.key
chmod 0600 /var/lib/pgsql/18/data/server.key文件位置:$PGDATA 内外与外挂
相对路径相对 $PGDATA 解析。两种部署习惯:
- 放
$PGDATA(默认):与实例生命周期绑定,pg_basebackup/备份工具会一并处理。注意:私钥随数据目录备份扩散的读写面要纳入备份策略评估。 - 放
$PGDATA外(推荐生产):如/etc/postgresql/ssl/,ssl_cert_file = '/etc/postgresql/ssl/server.crt'。证书管理(签发、轮换、权限)与数据库目录解耦,CI/CD 与配置管理工具(Ansible 等)可以独立维护,也避免pg_basebackup把私钥拖到备库。
两种都要保证:目录属主收紧(root 或 postgres + 0700),且 SELinux/AppArmor(若启用)允许 postgres 进程读取对应路径——「文件明明存在却加载失败」的隐形原因常常是 MAC 策略。
启动检查与日志
ssl = on 且文件有问题时,启动阶段的日志会直接给出原因,按症状对号入座:
FATAL: could not load server certificate file "server.crt": No such file or directory
FATAL: private key file "server.key" has group or world access
FATAL: could not load private key file "server.key": Permission denied
FATAL: could not load root certificate file "ca.crt": ...启动失败不产生半可用状态:SSL 配置损坏时实例起不来,因此先在测试实例上演练证书变更、再滚动到生产,是标准流程。运行中 reload 遇到坏文件则保持旧上下文并记日志,与 pg_hba reload 的失败行为一致。
证书链与中间 CA
企业 PKI 常用「根 CA → 中间 CA → 叶子证书」三级结构,文件怎么组织直接影响握手成败:
- 服务端证书文件(
ssl_cert_file):应为「叶子证书 + 中间 CA」的串联(PEM 允许多证书共存于一个文件)——服务器要在握手中把中间 CA 一起发给客户端,只发叶子时客户端拼不出完整链条,verify-full报certificate verify failed; - 客户端的
sslrootcert:放根 CA 一张即可(根 CA 不需要在链文件里出现); ssl_ca_file(校验客户端证书):放根 CA,或「根 + 中间」的集合——当客户端证书由中间 CA 签发时,服务端需要中间 CA 才能凑齐验证路径。
# 服务端:叶子 + 中间 CA 串联成 ssl_cert_file 指向的文件
cat server.crt intermediate-ca.crt > /etc/postgresql/ssl/server-chain.crt
chown postgres:postgres /etc/postgresql/ssl/server-chain.crt
chmod 0644 /etc/postgresql/ssl/server-chain.crt
# 验证链条完整性(-untrusted 模拟握手中的中间 CA 提供)
openssl verify -CAfile root-ca.crt -untrusted intermediate-ca.crt server.crt排查链问题时盯一个信号:单发证书能通过 openssl verify、真实连接却失败,几乎总是「握手中途少了中间 CA」。用 openssl s_client -connect pg1:5432 -showcerts 看服务器实际送出的证书序列,与预期比对。
验证客户端证书链条同理:把客户端证书与其签发路径放进同一份 openssl verify 命令核对,再上真实连接——链问题在命令行暴露的成本远低于在生产连接报错里排查。
sslmode 六档的深层语义
六档按「三层校验」逐级叠加,把表格翻译成威胁模型:
| 档位 | 防被动嗅探 | 防篡改 | 防假服务器(中间人) |
|---|---|---|---|
disable | ✗ | ✗ | ✗ |
allow / prefer | ✓ | ✓ | ✗ |
require | ✓ | ✓ | ✗ |
verify-ca | ✓ | ✓ | 部分(假服务器需持有可信 CA 签的证书) |
verify-full | ✓ | ✓ | ✓(还需主机名匹配) |
require 与 verify-ca 之间的本质差别常被低估:require 只对「链路上有没有人」负责,中间人自己跟客户端建一条加密连接、自己跟服务器建另一条,全程加密但内容尽收眼底。生产客户端的基线应是 verify-full(配合正确的 CA 与主机名),下一页展开。
客户端主机名的登记规范
verify-full 的主机名校验把「连接用什么名字」变成了安全机制的一部分,名字约定要从「运维习惯」升级为「登记制度」:
| 约定项 | 推荐做法 | 理由 |
|---|---|---|
| 主连接名 | FQDN(pg1.example.com) | SAN 的第一登记项,全团队统一 |
| 常用别名 | 登记进 SAN(DNS:pg1、DNS:pg-lb) | 消除「verify-full 连不上」的头号来源 |
| IP 直连 | 尽量禁止;确需则 IP: 进 SAN | IP 变更会牵连证书重签 |
| 通配符 | 仅在可控命名空间用(*.db.example.com) | 扩大单证书适用面,专用 CA 下才用 |
# 一份最小登记表(与证书台账同文件维护)
# 主机名 SAN 条目 变更记录
# pg1.example.com DNS:pg1.example.com 2026-10 初签
# pg1 DNS:pg1 2026-10 补登(运维短名)
# pg-lb.example.com DNS:pg-lb.example.com 2026-11 新增(HAProxy 入口)登记表的另一半是客户端纪律:连接串只允许用 SAN 里登记过的名字。新加别名走变更流程(先签证书、后改连接),而不是反过来——后者会产生一段「所有客户端 verify-full 失败」的窗口。新实例接入时按这张表生成签发请求,SAN 漏项从制度上就不会发生。
协议版本控制
ssl_min_protocol_version = 'TLSv1.2' # PG13+ 默认值
ssl_max_protocol_version = '' # 空 = 允许最高版本(如 TLSv1.3)要点:
- PG12 引入这两个参数时默认
TLSv1(兼容优先),PG13 起默认提高到TLSv1.2——安全基线扫描报「支持 TLSv1.0/1.1」的实例,基本是 PG12 且未调参,改 min 值即可,无需升级。 - TLSv1.3 的套件选择独立于
ssl_ciphers(由 OpenSSL 管理),想限制 1.3 套件只能升 min 档或控制 OpenSSL 版本。 ssl_max_protocol_version留空(放开 1.3)是常态;除非对端兼容性测试确有需要,不要人为设上限把 1.3 拒之门外。- 调整 min 档只影响新连接,既有会话不受影响——灰度期间新旧协议版本并存是正常态。
吊销检查的取舍与节奏
SSL 链路两侧都有吊销检查的入口,先分清谁校验谁:
| 位置 | 参数 | 校验对象 |
|---|---|---|
| 服务端 | ssl_crl_file / ssl_crl_dir(PG14+) | 客户端证书是否被吊销(配 cert/clientcert 认证时才有意义) |
| 客户端 | sslcrl(连接参数) | 服务端证书是否被吊销 |
# 服务端:启用客户端证书吊销检查
ssl_crl_file = '/etc/postgresql/ssl/crl.pem' # 单文件;多 CA 用 ssl_crl_dir(PG14+)取舍与节奏三个要点:
- 检查本身很便宜,CRL 逐一比对的每握手开销可忽略;真正的成本是「生成 → 分发 → reload」这条运维链的节奏维护(生成命令见 证书生成与管理 一页);
- 没配吊销检查是一种显式状态:被泄露的证书在有效期内一直可用。小环境可以接受,但要写进台账并配替代控制(短有效期轮换),而不是默认「不会发生」;
- 节奏绑定:CRL 的生成周期不大于其自身有效期,分发后 reload 生效;换 CA 时整套 CRL 重建。把这条链路挂定时任务,并纳入证书巡检(CRL 也有到期时间)。
ssl_crl_dir的哈希文件名要随 CA 变动重算:openssl rehash重新执行一次即可,巡检清单里给它留一行。
一个前提值得点破:若环境里根本没有 cert/clientcert 认证,服务端两个 CRL 参数可以不配——「客户端证书吊销」的校验对象不存在时,配了也只是无意义的维护负担。参数取舍跟着认证方案走,不跟感觉走。
加密私钥与 ssl_passphrase_command
私钥可以加口令保护(如 openssl pkey -aes256 -in server.key -out server.key.enc),服务器启动时需要口令解密。PG13 起用 ssl_passphrase_command 把「要口令」这件事交给外部命令,而不是交互提示:
# 演示:直接 echo(生产应改为从密钥管理系统获取)
ssl_passphrase_command = 'echo "$PG_KEY_PASSPHRASE"'
# reload 时是否允许重新执行命令读取私钥(证书轮换场景)
ssl_passphrase_command_support_reload = on两个权衡必须想清楚:加密私钥防的是「磁盘/备份上私钥文件被读走」,但口令命令本身要能无人工干预地取到口令(否则重启需人工到场);support_reload 打开后,reload 才能在轮换加密私钥时重新解密。明文私钥 + 0600 权限 + 严格的备份管控,在多数环境是性价比更高的选择。
套件与椭圆曲线
# 自定义 DH 参数(仅在需要 DHE 套件时)
openssl dhparam -out /etc/postgresql/dh2048.pem 2048ssl_dh_params_file = '/etc/postgresql/dh2048.pem'
ssl_ecdh_curve = 'prime256v1'
ssl_prefer_server_ciphers = on现代默认已经足够安全:ECDHE + AES-GCM 是握手与数据面的主力,ssl_ciphers 的默认值 HIGH:MEDIUM:+3DES:!aNULL 排除了匿名套件与过弱算法。有合规基线(如禁用 3DES)时改 ssl_ciphers;ssl_ecdh_curve 一般不动,除非与客户端生态有兼容冲突。
性能影响与会话复用
TLS 的成本集中在握手(非对称运算),数据面是 AES-GCM 对称加密,现代 CPU 有 AES-NI 加持,吞吐损失通常在个位数百分比。实践上:
- 握手成本被会话复用摊薄:libpq 与服务器支持 SSL 会话复用(session resumption),短连接频繁重连的场景(连接池穿透、监控探针)复用显著减少握手次数。
- 长连接为主的应用几乎无感:OLTP 应用经连接池维持长连接,加密开销摊到连接生命周期内可忽略。
- 大流量场景要实测:批量
COPY、大结果集导出在弱 CPU 实例上加密损耗更明显,上线前按真实数据量压测。 - 验证档位的增量成本很小:verify 档比 require 多的是证书链校验与主机名比对,毫秒级——不要为省这点开销降档。
- PG17 起 libpq 支持
sslnegotiation=direct,可在握手阶段省一次协议协商往返(默认仍为postgres协商方式)——纯微优化,常规配置保持默认即可。 - 复用由 OpenSSL 双侧自动协商,客户端无需额外配置;监控里「握手占比」异常升高时,先排查是否出现了短连接风暴。
连接池场景的两段式 SSL 配置
PgBouncer 站在中间时,TLS 有两个独立端点,配置文件里的两个参数族各管一段:
; pgbouncer.ini —— 客户端段(应用 → 池)
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| 参数族 | 证书的「身份」 | 谁验证它 |
|---|---|---|
client_tls_* | 池对外的服务器证书 | 应用客户端(若用 verify 档) |
server_tls_* | 池对内的客户端证书 | 数据库(配 clientcert/cert 时) |
理解这张表就理解了两段式的全部要点:同一个池,在左段是服务器、在右段是客户端,两个角色各要一张证书、各有一套信任锚。数据库侧若启用了 cert/clientcert 认证,右段的 server_tls_cert_file 就是「池的客户端证书」,其 CN 按数据库 HBA 的规则匹配角色。逐段验收的方法上一页已给:池左侧看池的日志与统计,池右侧看数据库的 pg_stat_ssl——两侧的加密档位都要有基线,任何一段的「require 以下」都是链路缺口。
监控与审计抓手
-- 1) 当前有多少连接未加密(client_addr 非空 = 远程连接)
SELECT count(*) FILTER (WHERE NOT s.ssl AND a.client_addr IS NOT NULL) AS remote_plaintext,
count(*) FILTER (WHERE s.ssl) AS encrypted
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.backend_type = 'client backend';
-- 2) 套件/版本分布:收敛旧协议的使用面
SELECT version, cipher, count(*)
FROM pg_stat_ssl
WHERE ssl
GROUP BY 1, 2
ORDER BY 3 DESC;
-- 3) 客户端证书有效期巡检(cert/clientcert 场景)
SELECT a.usename, s.client_dn, s.not_after
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE s.client_dn IS NOT NULL
ORDER BY s.not_after;把这三条查询接进巡检脚本(配合定时任务与告警),是下一页强制 SSL 迁移的观察仪表盘。
实战示例
示例一:最小可用的服务端 SSL
从零到「一条加密连接」的完整动作(证书生成过程见下一页,此处假定文件已就绪):
# 1) 文件就位
install -o postgres -g postgres -m 0644 server.crt /var/lib/pgsql/18/data/server.crt
install -o postgres -g postgres -m 0600 server.key /var/lib/pgsql/18/data/server.key
install -o postgres -g postgres -m 0644 ca.crt /var/lib/pgsql/18/data/ca.crt# 2) postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_min_protocol_version = 'TLSv1.2'# 3) 重启(ssl 开关必须重启)并检查日志
pg_ctl restart -D /var/lib/pgsql/18/data
grep -i ssl /var/lib/pgsql/18/data/log/postgresql-$(date +%F).log | tail -5# 4) 客户端验证
psql "host=pg1 dbname=postgres user=postgres sslmode=require" -c "\conninfo"You are connected to database "postgres" as user "postgres" on host "pg1" (address "10.0.0.11") at port 5432.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)示例二:给连接加密状态建立监控
-- 未加密的远程连接清单(下一页强制迁移的基线数据)
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 | active | 2026-10-07 09:12:04.113+08
dbmon | telegraf | 10.0.7.3 | idle | 2026-10-07 09:12:11.802+08
(2 rows)# 让 sslinfo 永久可用:模板库安装后新建库自动带
psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS sslinfo;"示例三:协议版本与套件收紧
# 目标:TLSv1.2 起,套件白名单排除 3DES
ssl_min_protocol_version = 'TLSv1.2'
ssl_ciphers = 'HIGH:MEDIUM:!aNULL:!3DES'psql -c "SELECT pg_reload_conf();"-- 老客户端若仍在用低版本协议,会在这里现形(version 不再包含 TLSv1/TLSv1.1)
SELECT version, count(*) FROM pg_stat_ssl WHERE ssl GROUP BY 1; version | count
---------+-------
TLSv1.3 | 118
TLSv1.2 | 41
(2 rows)示例四:JDBC 应用接入 verify-full
Java 应用是最常见的「环境变量帮不上忙」的场景,完整动作四步:
# 1) CA 文件就位(配置管理分发,路径统一)
install -m 0644 ca.crt /etc/pg-certs/ca.crt// 2) 连接串模板:sslmode 与 sslrootcert 都是 URL 参数
String url = "jdbc:postgresql://pg1.example.com:5432/app"
+ "?sslmode=verify-full"
+ "&sslrootcert=/etc/pg-certs/ca.crt";# 3) 灰度验证:同一应用先在预发环境跑一轮
psql "host=pg1.example.com dbname=app user=app_rw \
sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt" -c "SELECT 1;"-- 4) 服务端确认该应用的连接已加密(按 application_name 过滤)
SELECT a.application_name, s.ssl, s.version, s.cipher, count(*)
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.application_name LIKE 'app-%'
GROUP BY 1, 2, 3, 4; application_name | ssl | version | cipher | count
------------------+-----+---------+-------------------------+-------
app-order-pool | t | TLSv1.3 | TLS_AES_256_GCM_SHA384 | 20
(1 row)两个 JDBC 特有的坑:其一,连接池组件(HikariCP 等)的「JDBC URL 后缀」要写进 jdbcUrl 而不是数据源属性——写错位置参数会被静默吞掉;其二,verify 档失败时 pgjdbc 的报错是 SSL error: certificate verify failed 的包装栈,看到它先核对 URL 里的 sslrootcert 路径在应用容器内是否可达(容器路径与宿主机路径混淆是高频原因)。
注意事项
「加密了」不等于「防中间人」。 默认的 prefer 与常被误当安全档的 require 都不验证服务端证书。面向生产的客户端配置,验证档位必须到 verify-ca 或 verify-full——这是本页与下一页最重要的一句话。
ssl = on 的变更必须重启。 计划外重启是事故源,把「首次开启 SSL」安排进维护窗口;其余 ssl_* 参数 reload 即可,但要通过新建连接验证效果。
私钥权限错误会阻止启动。 0600 + 属主 postgres 是标准答案;文件放 $PGDATA 外时要同时确认目录权限与 SELinux/AppArmor 策略,避免「路径存在却读不到」。
默认值有版本差异。 ssl_min_protocol_version 在 PG12/PG13 间默认值不同;ssl_crl_dir、ssl_passphrase_command、sslnegotiation 各有其引入版本。跨版本维护多套实例时,以「实例的实际 SHOW 值」为准建立配置基线,而不是以文档默认值为准。
TLSv1.3 不受 ssl_ciphers 控制。 想通过改 ssl_ciphers 排除 TLSv1.3 套件是徒劳的;合规要求排除某些 1.3 套件时,只能在 OpenSSL 层面处理或调整 min/max 档。
客户端证书有效期只在连接时可见。 pg_stat_ssl.not_after 只反映当前活跃连接的客户端证书,长期驻留的连接可能「拿着快过期的证书一直跑」。证书巡检要覆盖证书文件本身(在签发方/分发清单里做),不能只依赖运行时视图。
代理层会拆散端到端语义。 HAProxy 终止 TLS 后与数据库之间是明文,除非再配一段加密;PgBouncer 的 client_tls_sslmode/server_tls_sslmode 控制两段各自的行为。在架构图上明确「每一段的加密档位」,不要笼统地说「我们走 SSL」。
加密不是零成本。 大流量导入导出、弱 CPU 实例先压测;同时确认会话复用生效(短连接风暴场景收益明显),必要时在客户端侧固定连接复用策略。
环境变量会被显式参数覆盖。 PGSSLMODE=verify-full 设好了,但连接串里残留 sslmode=require 时,生效的是后者。连接串模板化(配置中心/服务文件)的目的就是消除这种「双处配置」的漂移。
OpenSSL 版本决定能力上限。 服务端与客户端各自链接的 OpenSSL 决定可用的协议版本与套件——ssl_min_protocol_version 设了 TLSv1.3,但某台老客户端的 OpenSSL 不支持 1.3,连接照样建立不起来。收紧 min 档前,先盘点存量客户端的 OpenSSL 能力(openssl version),再定灰度顺序。
吊销通道缺失是一种显式状态。 没配 ssl_crl_file / sslcrl 时,被泄露的证书在有效期内一直可用。小环境可以接受这个取舍,但要写进台账并配替代控制(如短有效期轮换),而不是默认「不会发生」。
验证档位发生在客户端,服务端只能筛选。 sslmode 是客户端行为——服务器无法「替客户端」选择 verify 档,只能通过 HBA 的 hostssl 决定「不加密的进不来」。这就是为什么强制加密(下一页)必须客户端与服务端两侧配合。
证书变更的演练环境要贴近生产。 至少同 OpenSSL 版本、同文件权限布局——「测试机没问题」的结论,在版本与权限同构的前提下才能迁移到生产。
最佳实践
- 服务端开箱配置四件套:
ssl = on、证书、私钥、ssl_min_protocol_version = TLSv1.2。 这是现代实例的默认基线,新环境不要低于它。 - 私钥
0600+ 属主 postgres + 存放路径收紧。 私钥文件禁止进入备份分发链与代码仓库;使用加密私钥时配套ssl_passphrase_command并评估无人工干预重启的可行性。 - 证书与私钥放
$PGDATA外统一管理。 目录化(如/etc/postgresql/ssl/)、配置化管理、与实例目录解耦,轮换时不触碰数据目录。 - 客户端一律
sslmode=verify-full+ 显式sslrootcert(或 PG16+ 的sslrootcert=system)。 把「验证谁签的、验证是不是那台服务器」做成连接串模板的固定部分。 - 用
pg_stat_ssl建立加密状态监控。 未加密远程连接数、协议/套件分布、客户端证书有效期三个指标接入巡检,强制 SSL 迁移以它为基线。 - 协议版本只升不降。 min 档从
TLSv1.2起步,新集群直接考虑排除旧协议的老客户端后再收紧到更高;max 档留空放行 TLSv1.3。 ssl_ciphers按合规基线调整,不凭感觉排套件。 默认值已排除明显弱项;确有基线要求(如禁 3DES)时改动并记录原因,TLSv1.3 套件另行处理。- sslinfo 扩展提前部署到模板库。
ssl_is_used()等函数让应用侧与会话侧的自检零成本,监控与排障都受益。 - 证书轮换走「新文件 + reload」流程并演练。 reload 生效、新建连接验证、旧连接自然收敛;全程不重启。每半年演练一次。
- 两段式架构(池/代理)逐段定档。 客户端→池、池→库各自的 sslmode 与证书链在架构文档中写明,检查清单逐段核对。
下一节
继续学习:强制 SSL 连接
参考链接
- PostgreSQL 官方文档:Secure TCP/IP Connections with SSL
- PostgreSQL 官方文档:SSL Certificates
- PostgreSQL 官方文档:Connection and Authentication 配置参数(ssl_* GUC)
- PostgreSQL 官方文档:SSL Support(libpq 客户端)
- PostgreSQL 官方文档:Database Connection Control Functions
- PostgreSQL 官方文档:Environment Variables(PGSSLMODE 等)
- PostgreSQL 官方文档:pg_stat_ssl 视图
- PostgreSQL 官方文档:sslinfo 扩展
- PostgreSQL 官方文档:Server Applications(pg_ctl)