Skip to content

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 可读
text
客户端                                   服务端
  │  ──① 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 总表 ​

参数默认值生效方式说明
ssloff重启SSL 总开关
ssl_cert_fileserver.crtreload服务器证书路径
ssl_key_fileserver.keyreload服务器私钥路径
ssl_ca_file空reload校验客户端证书的 CA
ssl_crl_file空reload证书吊销列表(单文件)
ssl_crl_dir空reload证书吊销列表目录(PG14+,OpenSSL hash 命名)
ssl_min_protocol_versionTLSv1.2reload最低协议版本(PG12 引入,PG13 起默认 TLSv1.2)
ssl_max_protocol_version空reload最高协议版本(空 = 允许最高可用版本)
ssl_ciphersHIGH:MEDIUM:+3DES:!aNULLreloadTLSv1.2 及以下的套件白名单(TLSv1.3 套件不由此参数控制)
ssl_prefer_server_ciphersonreload套件选择权:服务端优先
ssl_ecdh_curveprime256v1reloadECDH 椭圆曲线
ssl_dh_params_file空reload自定义 DH 参数(DHE 套件用,一般不需要)
ssl_passphrase_command空reload取私钥口令的命令(PG13+,支持加密私钥)
ssl_passphrase_command_support_reloadoffreloadreload 时是否也执行口令命令重新读取私钥(PG13+)

相对路径相对于数据目录($PGDATA)解析。一份典型的生产配置:

conf
# 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
bash
# 应用配置:ssl 与 ssl_* 文件参数修改后重启/重载
pg_ctl restart -D $PGDATA          # ssl: off → on 必须重启
psql -c "SELECT pg_reload_conf();" # 其余 ssl_* 参数 reload 即可

客户端连接写法 ​

bash
# 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 相关参数与同名环境变量一一对应(除大小写前缀),配置模板里两种写法都会见到:

连接参数环境变量默认值作用
sslmodePGSSLMODEprefer验证档位(六档见上表)
sslrootcertPGSSLROOTCERT~/.postgresql/root.crt校验服务端证书的 CA
sslcertPGSSLCERT~/.postgresql/postgresql.crt客户端证书
sslkeyPGSSLKEY~/.postgresql/postgresql.key客户端私钥
sslcrlPGSSLCRL无校验服务端证书的吊销列表

两条规则值得写进团队记忆:

  • 显式参数覆盖环境变量:连接串里写了 sslmode=... 时 PGSSLMODE 被忽略。排错时注意「环境变量明明设了却没生效」多半是连接串里藏着旧值;
  • 默认路径是隐式约定:什么都不配时,libpq 会去读 ~/.postgresql/root.crt 等默认文件——文件恰好存在时 verify-full 会「自动可用」,文件不存在时验证档直接失败。让客户端显式写 sslrootcert,消除对默认路径的偶然依赖。

把这张表贴进连接模板的注释区:新人抄配置时顺手就带上了正确参数。文档的最大作用不是供人查阅,而是让正确的写法成为默认。

验证连接是否加密 ​

方法一:pg_stat_ssl 视图(服务端视角,每个后端进程一行):

sql
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();
text
-[ 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 扩展(会话内自检):

sql
CREATE EXTENSION IF NOT EXISTS sslinfo;
SELECT ssl_is_used() AS encrypted,
       ssl_version() AS tls_version,
       ssl_cipher()  AS cipher;
text
 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(),新连接使用新证书,已有连接不受影响。

sql
-- reload 后新连接即可用新证书;可再连一次确认
SELECT pg_reload_conf();

一个容易忽略的点:reload 是「后端进程在建立连接时读取新上下文」,个别工作进程(如正在运行的后台 worker)的会话继续用旧上下文直到结束。轮换证书后用新建连接验证,而不是用既有会话验证。

私钥文件权限:硬性要求 ​

ssl_key_file 的权限在启动时被强制检查,不满足则启动失败:

text
FATAL:  private key file "server.key" has group or world access

规则:

  • 标准要求:属主为运行服务器的用户(postgres),权限 0600;
  • 打包变体:Debian 系常见 root 属主 + 0640 + 专用属组(如 ssl-cert,postgres 用户在其中),由打包脚本保证检查通过;
  • 证书文件 server.crt、CA、CRL 是公开材料,0644 即可,无需收紧。
bash
# 修正权限的三个标准动作
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 且文件有问题时,启动阶段的日志会直接给出原因,按症状对号入座:

text
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 才能凑齐验证路径。
bash
# 服务端:叶子 + 中间 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: 进 SANIP 变更会牵连证书重签
通配符仅在可控命名空间用(*.db.example.com)扩大单证书适用面,专用 CA 下才用
text
# 一份最小登记表(与证书台账同文件维护)
# 主机名              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 漏项从制度上就不会发生。

协议版本控制 ​

conf
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(连接参数)服务端证书是否被吊销
conf
# 服务端:启用客户端证书吊销检查
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 把「要口令」这件事交给外部命令,而不是交互提示:

conf
# 演示:直接 echo(生产应改为从密钥管理系统获取)
ssl_passphrase_command = 'echo "$PG_KEY_PASSPHRASE"'

# reload 时是否允许重新执行命令读取私钥(证书轮换场景)
ssl_passphrase_command_support_reload = on

两个权衡必须想清楚:加密私钥防的是「磁盘/备份上私钥文件被读走」,但口令命令本身要能无人工干预地取到口令(否则重启需人工到场);support_reload 打开后,reload 才能在轮换加密私钥时重新解密。明文私钥 + 0600 权限 + 严格的备份管控,在多数环境是性价比更高的选择。

套件与椭圆曲线 ​

bash
# 自定义 DH 参数(仅在需要 DHE 套件时)
openssl dhparam -out /etc/postgresql/dh2048.pem 2048
conf
ssl_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 有两个独立端点,配置文件里的两个参数族各管一段:

ini
; 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 以下」都是链路缺口。

监控与审计抓手 ​

sql
-- 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 ​

从零到「一条加密连接」的完整动作(证书生成过程见下一页,此处假定文件已就绪):

bash
# 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
conf
# 2) postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file  = 'server.key'
ssl_min_protocol_version = 'TLSv1.2'
bash
# 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
bash
# 4) 客户端验证
psql "host=pg1 dbname=postgres user=postgres sslmode=require" -c "\conninfo"
text
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)

示例二:给连接加密状态建立监控 ​

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    | 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)
bash
# 让 sslinfo 永久可用:模板库安装后新建库自动带
psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS sslinfo;"

示例三:协议版本与套件收紧 ​

conf
# 目标:TLSv1.2 起,套件白名单排除 3DES
ssl_min_protocol_version = 'TLSv1.2'
ssl_ciphers = 'HIGH:MEDIUM:!aNULL:!3DES'
bash
psql -c "SELECT pg_reload_conf();"
sql
-- 老客户端若仍在用低版本协议,会在这里现形(version 不再包含 TLSv1/TLSv1.1)
SELECT version, count(*) FROM pg_stat_ssl WHERE ssl GROUP BY 1;
text
 version | count
---------+-------
 TLSv1.3 |   118
 TLSv1.2 |    41
(2 rows)

示例四:JDBC 应用接入 verify-full ​

Java 应用是最常见的「环境变量帮不上忙」的场景,完整动作四步:

bash
# 1) CA 文件就位(配置管理分发,路径统一)
install -m 0644 ca.crt /etc/pg-certs/ca.crt
java
// 2) 连接串模板:sslmode 与 sslrootcert 都是 URL 参数
String url = "jdbc:postgresql://pg1.example.com:5432/app"
    + "?sslmode=verify-full"
    + "&sslrootcert=/etc/pg-certs/ca.crt";
bash
# 3) 灰度验证:同一应用先在预发环境跑一轮
psql "host=pg1.example.com dbname=app user=app_rw \
      sslmode=verify-full sslrootcert=/etc/pg-certs/ca.crt" -c "SELECT 1;"
sql
-- 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;
text
 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 版本、同文件权限布局——「测试机没问题」的结论,在版本与权限同构的前提下才能迁移到生产。

最佳实践 ​

  1. 服务端开箱配置四件套:ssl = on、证书、私钥、ssl_min_protocol_version = TLSv1.2。 这是现代实例的默认基线,新环境不要低于它。
  2. 私钥 0600 + 属主 postgres + 存放路径收紧。 私钥文件禁止进入备份分发链与代码仓库;使用加密私钥时配套 ssl_passphrase_command 并评估无人工干预重启的可行性。
  3. 证书与私钥放 $PGDATA 外统一管理。 目录化(如 /etc/postgresql/ssl/)、配置化管理、与实例目录解耦,轮换时不触碰数据目录。
  4. 客户端一律 sslmode=verify-full + 显式 sslrootcert(或 PG16+ 的 sslrootcert=system)。 把「验证谁签的、验证是不是那台服务器」做成连接串模板的固定部分。
  5. 用 pg_stat_ssl 建立加密状态监控。 未加密远程连接数、协议/套件分布、客户端证书有效期三个指标接入巡检,强制 SSL 迁移以它为基线。
  6. 协议版本只升不降。 min 档从 TLSv1.2 起步,新集群直接考虑排除旧协议的老客户端后再收紧到更高;max 档留空放行 TLSv1.3。
  7. ssl_ciphers 按合规基线调整,不凭感觉排套件。 默认值已排除明显弱项;确有基线要求(如禁 3DES)时改动并记录原因,TLSv1.3 套件另行处理。
  8. sslinfo 扩展提前部署到模板库。 ssl_is_used() 等函数让应用侧与会话侧的自检零成本,监控与排障都受益。
  9. 证书轮换走「新文件 + reload」流程并演练。 reload 生效、新建连接验证、旧连接自然收敛;全程不重启。每半年演练一次。
  10. 两段式架构(池/代理)逐段定档。 客户端→池、池→库各自的 sslmode 与证书链在架构文档中写明,检查清单逐段核对。

下一节

继续学习:强制 SSL 连接

参考链接 ​