Skip to content

证书认证 ​

密码认证的固有弱点是「密码可以被猜、被钓、被复用」。证书认证(cert 方法)换了一种证明方式:客户端出示一张由可信 CA 签发的 X.509 数字证书,私钥只在客户端本地、永不落网;服务器验证证书链后,直接从证书的 CN(Common Name)或 DN 中取出用户名完成登录——没有密码这个环节,自然也就没有密码可攻击。对无人值守的自动化作业(ETL、备份、监控)、对要求 mTLS 的强合规环境,证书认证都是比「密码放进 .pgpass」更干净的答案。

需要先厘清一件容易混淆的事:SSL/TLS 在这个体系里有两个角色。其一是传输加密——16.3 整章的主题,任何连接都可以用;其二是身份认证——客户端用证书证明「我是谁」,这正是 cert 方法做的事。同一张基础设施(CA、证书)可以同时服务两个角色,但两件事的配置与语义并不相同。本页聚焦认证侧,传输侧的参数细节(sslmode 六档等)见 16.3 的 SSL 配置 一页。

此外还有一个中间形态:在 scram-sha-256 等密码方法上加 clientcert= 选项,要求客户端同时出示有效证书——证书当入场券,密码做身份证明,构成双因素。cert 方法则更进一步,用证书直接替代密码。两种形态本页都会讲到,先把边界划清:clientcert 不免除密码校验,cert 方法没有密码。

前置知识

建议先阅读:LDAP / Kerberos 认证

基础概念 ​

一次 cert 认证里发生了什么 ​

text
┌──────────────┐                        ┌──────────────────┐
│    客户端     │  ① TLS 握手            │    PostgreSQL     │
│              │ ─────────────────────► │                  │
│ 持有:        │  ② 出示客户端证书       │ 信任锚:           │
│  cert + key  │ ─────────────────────► │  ssl_ca_file     │
│              │  ③ 用私钥签名完成       │ 校验:             │
│              │     挑战证明            │  · 证书链可信     │
│              │ ◄───────────────────── │  · 未过期         │
│              │  ④ 握手完成             │  · 未被吊销(CRL)  │
└──────────────┘                        │  · CN == 角色名   │
                                        └──────────────────┘

关键性质:客户端证书从不在网络上传输完整私钥。握手过程中客户端用私钥对协商数据签名,服务器用证书里的公钥验证——知道「证书内容」无法冒充,必须持有私钥文件。这与 SCRAM 的「密码不出网」是同一思想,但证明材料(私钥)可以做到从不进入任何人的记忆或配置字符串。

服务端证书 vs 客户端证书 ​

服务端证书客户端证书
证明服务器的身份(防假服务器)客户端的身份(防冒充用户)
由谁验证客户端(按 sslmode 的档位)服务端(按 HBA 记录的 cert/clientcert)
存放$PGDATA/server.crt + server.key客户端机器 ~/.postgresql/postgresql.crt + .key
身份字段SAN / CN 里的主机名CN / DN 里的用户名

同一张 CA 可以签发两类证书。服务器视角的三个文件分工明确:ssl_cert_file/ssl_key_file 证明「我是这台服务器」,ssl_ca_file 定义「我信任哪些客户端」。

两层校验的语义:verify-ca 与 verify-full ​

clientcert 选项(以及客户端侧 sslmode 的对应档位)有两个强度:

档位校验内容结论
verify-ca证书由可信 CA 签发、未过期、未吊销证明「是我们发出去的证书」,但不校验持有者是不是本人——证书被复制到别的机器、CN 与登录账号不符都能通过
verify-fullverify-ca 全部内容 + CN/DN 与登录用户名匹配证明「持有这张证书的人就是登录的那个角色」

cert 方法固定按 verify-full 语义工作:证书可信是前提,CN/DN 决定用户名。密码方法上挂 clientcert=verify-full 时,CN 匹配逻辑相同,只是身份仍以密码为准。

证书内容解剖(openssl x509 -text) ​

排错与签发验收时都绕不开「看看这张证书里到底写了什么」:

bash
openssl x509 -in etl.crt -noout -text
text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 4096 (0x1000)
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=Demo DB CA
        Validity
            Not Before: Oct  1 08:00:00 2026 GMT
            Not After :  Dec 30 08:00:00 2026 GMT
        Subject: CN=etl, OU=ServiceAccounts
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication

逐个字段与 cert 认证的关系:

字段含义与 cert 认证的关系
Version: 3X.509 v3 格式,现代证书的标准v3 才有扩展段(SAN、EKU 等)
Serial NumberCA 为每张证书分配的唯一序号吊销与台账按序列号定位
Issuer签发它的 CA 的 Subject服务端校验时它必须落在 ssl_ca_file 的信任链内
Validity(Not Before / Not After)有效期过期即握手失败;巡检盯 Not After
Subject证书持有者身份(CN、O、OU…)clientname=CN 时 CN 直接当角色名;DN 时整串参与匹配
Public Key Info公钥及其算法私钥须与之配对(modulus 指纹核对)
Basic Constraints: CA:FALSE声明这不是 CA 证书端实体证书的标准约束
Key Usage / Extended Key Usage用途限定客户端证书惯例带 clientAuth、服务端证书带 serverAuth,签发模板应区分

读证书的输出比记签发命令更能暴露问题:CN 拼写、有效期、签发 CA 是否认错,肉眼核对这三处即可拦截大半配置事故。

语法与用法 ​

pg_hba 记录:cert 方法 ​

text
# TYPE     DATABASE  USER    ADDRESS       METHOD  OPTIONS
hostssl    app       etl     10.0.9.0/24   cert
hostssl    app       all     10.0.9.0/24   cert map=certmap
hostssl    app       all     10.0.9.0/24   cert clientname=DN

三条铁律:

  1. 必须配 hostssl。cert 依赖 TLS 握手中的客户端证书环节,非 SSL 连接上它无从谈起。写在 host 记录上时,非加密连接无法命中该记录的预期语义,一律用 hostssl。
  2. cert 方法没有密码环节,也不接受 clientcert=no——要求客户端证书是它的定义本身。
  3. 身份字段的选取用 clientname=:CN(默认)取证书 Subject 的 CN 字段,DN 取整个可分辨名称(CN 无法区分用户时用 DN,通常配合 map 正则提取)。

pg_hba 记录:clientcert 选项(双因素形态) ​

text
# 密码方法 + 证书入场券
hostssl    app    app_rw   10.0.1.0/24   scram-sha-256 clientcert=verify-ca
hostssl    app    dba      10.0.9.0/24   scram-sha-256 clientcert=verify-full
  • verify-ca:要求客户端证书可信,CN 不作要求——适合「证书是设备/机器证书」的场景(每台应用服务器一张机器证书,账号仍各用各的密码)。
  • verify-full:CN 还必须匹配登录角色名——适合「一人一证」且要求双因素的管理账号。
  • 除 cert 外的方法都可以挂 clientcert;gss、sspi 也支持(证书校验与 GSS 认证叠加)。

服务端配置 ​

conf
# postgresql.conf —— 服务端三件套 + 吊销列表
ssl = on
ssl_cert_file = 'server.crt'      # 服务端证书(相对路径相对 $PGDATA)
ssl_key_file  = 'server.key'      # 服务端私钥,权限必须 0600/0640
ssl_ca_file   = 'ca.crt'          # 校验客户端证书的信任锚
ssl_crl_file  = 'ca.crl'          # 可选:证书吊销列表

ssl_ca_file 的存在本身不改变行为——它只是「备好了验客户端证书的能力」,真正要求客户端出示证书的是 HBA 记录中的 cert 或 clientcert。反过来,只配 ssl_cert_file/ssl_key_file(最常见的「开 SSL」)时,服务器不会要求客户端证书。

客户端配置 ​

libpq 三参数 + 对应环境变量:

libpq 参数环境变量默认值作用
sslcertPGSSLCERT~/.postgresql/postgresql.crt客户端证书文件
sslkeyPGSSLKEY~/.postgresql/postgresql.key客户端私钥文件
sslrootcertPGSSLROOTCERT~/.postgresql/root.crt校验服务端证书的 CA
sslcrlPGSSLCRL无校验服务端证书的吊销列表
bash
# psql 命令行方式
psql "host=pg1 dbname=app user=etl sslmode=verify-full \
      sslcert=/etc/pg-certs/etl.crt sslkey=/etc/pg-certs/etl.key \
      sslrootcert=/etc/pg-certs/ca.crt"

# 环境变量方式(自动化脚本常用)
export PGSSLCERT=/etc/pg-certs/etl.crt
export PGSSLKEY=/etc/pg-certs/etl.key
export PGSSLROOTCERT=/etc/pg-certs/ca.crt

客户端私钥文件建议 0600 收紧(服务端 server.key 的 0600 是硬性要求,见 16.3 的 SSL 配置 一页)。

吊销:ssl_crl_file 与 ssl_crl_dir ​

私钥泄露、人员离职时,光删数据库角色不够——那张证书还能用于其他角色或未来重建的角色,必须让服务器「拒绝这张证书」:

conf
# 单个 CRL 文件(PEM 格式,可含多个 CA 的 CRL)
ssl_crl_file = 'ca.crl'

# PG14+:CRL 目录(文件需按 OpenSSL hash 命名,用 openssl rehash 生成)
ssl_crl_dir = '/etc/postgresql/crl'

CRL 更新后 SELECT pg_reload_conf(); 即生效,无需重启。客户端侧对称地有 sslcrl 参数校验服务端证书是否被吊销。

详细说明 ​

cert 认证的完整时序 ​

把一次 cert 登录按阶段拆开,每个失败点对应一种典型报错:

  1. TCP + TLS 握手:客户端 sslmode 协商出加密;服务端出示 server.crt。失败 → 客户端报 SSL error: certificate verify failed(客户端没带 sslrootcert 或 CA 不匹配)。
  2. 客户端证书出示:HBA 记录要求证书时,TLS 握手中客户端回送 sslcert 指向的证书。失败 → 服务器日志 could not accept SSL connection: certificate verify failed(客户端证书不在 ssl_ca_file 的信任链内,日志/客户端可能表现为 tlsv1 alert unknown ca)。
  3. 证书链与状态校验:签名链、有效期、(若配置了)CRL。失败 → 握手被拒,日志注明原因(过期/吊销/链不完整)。
  4. 身份提取:按 clientname 取 CN 或 DN;有 map= 时先过映射。失败 → certificate authentication failed for user "xxx"(CN 与角色名对不上)。
  5. 进入数据库:以提取出的角色名完成登录,后续是常规的权限体系。

clientname:CN 还是 DN ​

默认从 Subject 里取 CN。CN 的局限在「重名 + 命名不规范」的 CA 上:如果 CA 证书的 CN 是「Zhang San」而数据库角色是 zhang_san,每次都要靠映射翻译,不如直接用 DN:

text
hostssl  app  all  10.0.9.0/24  cert clientname=DN map=dnmap

# pg_ident.conf:从完整 DN 中用正则提取角色名
dnmap   /^CN=([^,]+),OU=DBUsers,O=Example,C=CN$   \1

DN 匹配是全字符串精确匹配(或走映射正则),因此 clientname=DN 几乎总是与 map= 搭配出现。

map 映射:cert 与 pg_ident.conf ​

cert 方法支持 map=,行为与 peer / ident 认证 一页描述的映射机制完全一致:映射表里的每一行都是一条「证书身份 → 数据库角色」的显式授权,不在映射表里的组合一律拒绝。两个典型用法:

text
# pg_ident.conf
# ① CN 恰好是规范角色名时的白名单式直通
certmap    etl-01         etl          etl
# ② CN 是工号时翻译成角色名
certmap    staff          /^(emp|etl)\-(\d+)$    svc_\2

用映射而不是「让 CN 恰好等于角色名」的隐藏好处:证书签发与数据库建角色解耦,CN 的命名规范由 PKI 团队定,角色名由 DBA 定,中间靠映射文件对齐——这份映射文件就是审计时的「授权对照表」。

证书吊销的工程现实 ​

CRL 的理论很完整,工程上要认账几件事:

  • CRL 是快照。签发方每次吊销都要重新生成 CRL 并分发到所有 PostgreSQL 实例(reload 生效),「忘记分发」是吊销失效的头号原因,必须纳入变更流程。
  • ssl_crl_dir(PG14+)适合多 CA 环境。目录里按 OpenSSL hash 命名放多份 CRL,openssl rehash 维护软链,比单文件聚合好维护。
  • OCSP 未被服务端侧支持。PostgreSQL 服务端校验吊销只走 CRL,没有在线 OCSP 查询;对吊销时效要求极高的场景(证书一吊销分钟级生效),要么压缩 CRL 发布周期,要么在 LB/代理层做。

证书格式与转换:PEM / DER / PKCS#8 / 加密私钥 ​

PostgreSQL 与 libpq 系客户端要求 PEM 格式;从 Java 生态或旧工具拿到的文件经常是 DER、PKCS#1 或带口令的私钥,上线前统一转换:

bash
# 1) 先认清手里是什么格式(看首行)
head -1 etl.crt          # -----BEGIN CERTIFICATE----- → PEM 证书
head -1 etl.key          # -----BEGIN PRIVATE KEY----- → PEM 私钥(PKCS#8)
                         # -----BEGIN RSA PRIVATE KEY----- → PKCS#1(旧式 RSA)

# 2) PEM ↔ DER 转换(证书)
openssl x509 -in etl.crt -outform DER -out etl.der
openssl x509 -in etl.der -inform DER -out etl.pem

# 3) 私钥统一为 PKCS#8 编码的 PEM(libpq 系客户端的稳妥形态)
openssl pkey -in legacy-rsa.key -out etl.key

# 4) 给私钥加口令 / 去掉口令
openssl pkey -in etl.key -aes256 -out etl.key.enc
openssl pkey -in etl.key.enc -out etl.key

# 5) 确认转换后证书与私钥仍然配对
[ "$(openssl x509 -noout -modulus -in etl.pem | openssl md5)" = \
  "$(openssl pkey -pubout -in etl.key | openssl md5)" ] && echo PAIRED

三点实践提醒:

  • 服务端可以用加密私钥,代价是配套 ssl_passphrase_command(见 16.3 的 SSL 配置 一页);客户端私钥以「不加密 + 0600 权限」为稳妥默认——各语言驱动对加密客户端私钥的支持程度不一,无人值守场景更是以「能自动读取」为第一优先。
  • 证书(.crt)是公开材料,DER 转换常用于导入浏览器信任库、Java keystore 等外部工具链;私钥(.key)的转换动作只在受控环境做,转换过程中的中间文件用完即删。
  • 配对校验的 modulus 法对 RSA 私钥有效;上面第 5 条用「公钥指纹」的写法对 ECC 私钥同样适用,优先采用。

与 SCRAM 组合的双因素设计 ​

「证书 + 密码」双因素是强合规环境的常见要求,PG 原生支持且无需任何扩展:

text
# 管理网段:持有个人证书(verify-full 绑定到人)+ 知道密码
hostssl  all  +dba   10.0.9.0/24   scram-sha-256 clientcert=verify-full

设计上的三个要点:

  • verify-full 保证 CN 与角色名绑定,证书不可转借给其他角色使用;
  • 密码仍走 SCRAM 挑战-响应,不因证书而退化;
  • 两个因素的失效域独立:私钥丢失时吊销证书(密码仍有效,可临时摘掉 clientcert 恢复),密码泄露时改密码(证书仍有效)。应急路径要预先写进 runbook。

与 GSS/LDAP 方案的取舍 ​

方案强项弱项
cert无密码、无外部依赖、自动化友好证书生命周期管理(签发/分发/吊销)要自建
gss/Kerberos与企业 SSO 天然集成依赖 KDC,域外设备用不了
ldap账号生命周期外包给目录密码仍要人输/存,管道配置多

纯机器到机器的连接,cert 通常是最优解——机器没有「记住密码」的烦恼,也没有 Kerberos 票据缓存过期的问题。人在回路的场景,若企业已有 AD,Kerberos/LDAP 更顺;cert 当人的双因素之一也很常见。

短有效期:吊销的工程替代 ​

CRL 链路(生成 → 分发 → reload)的运维成本不小,现代 PKI 实践给出了另一条路:把有效期压短,用「到期自灭」代替「主动吊销」。

  • 客户端证书 30~90 天自动轮换,私钥泄露后的暴露窗口从「CRL 下一次分发前」缩短到「最长一个有效期」;
  • 轮换由签发脚本 + 配置管理自动完成(参考 16.3 证书生成与管理 一页的脚本化生成与 deploy hook 思路),人不参与日常轮换;
  • 两者不是二选一:短有效期为主 + CRL 兜底是稳妥组合——自动化轮换坏了的时候,CRL 还是那条「立即踢出去」的应急通道。

判断标准很简单:如果轮换已经完全自动化(脚本/平台托管),可以把有效期压短、淡化 CRL;如果轮换还靠人,那就必须把 CRL 链路维护好,并把有效期放宽到 180 天以减少轮换频次。

证书从哪来 ​

本页只定协议,签发细节在 16.3 的 证书生成与管理 一页完整展开(自签 CA、OpenSSL 命令、SAN、有效期轮换、内部 CA 与 cert-manager 思路)。做 cert 认证前先明确三件事:谁当 CA(专用小 CA 比复用公司大 CA 更可控)、有效期多长(90~365 天,配合自动化轮换)、CN 命名规范(与数据库角色名或映射规则对齐)。

客户端证书在各语言驱动中的配置 ​

证书文件落位是统一的,差异只在「驱动怎么读它」。四种常见形态:

bash
# ① libpq 系(psql、pg_dump、PgBouncer 等):环境变量最省事
export PGSSLCERT=/etc/pg-certs/etl.crt
export PGSSLKEY=/etc/pg-certs/etl.key
export PGSSLROOTCERT=/etc/pg-certs/ca.crt
export PGSSLMODE=verify-full
java
// ② JDBC(pgjdbc):URL 参数直接支持 sslcert / sslkey / sslrootcert
String url = "jdbc:postgresql://pg1.example.com:5432/app"
    + "?sslmode=verify-full"
    + "&sslrootcert=/etc/pg-certs/ca.crt"
    + "&sslcert=/etc/pg-certs/etl.crt"
    + "&sslkey=/etc/pg-certs/etl.key";
// 注意:sslkey 对私钥格式的要求随驱动版本不同(早期版本倾向 PKCS#8),
// 以所用 pgjdbc 版本的文档为准。
python
# ③ Python psycopg:libpq 参数透传进 conninfo
import psycopg
conn = psycopg.connect(
    "host=pg1.example.com dbname=app user=etl "
    "sslmode=verify-full "
    "sslcert=/etc/pg-certs/etl.crt sslkey=/etc/pg-certs/etl.key "
    "sslrootcert=/etc/pg-certs/ca.crt",
)
javascript
// ④ Node.js(node-postgres):ssl 对象即 TLS 选项,cert/key/ca 与 Node tls 一致
const { Client } = require('pg');
const fs = require('fs');
const client = new Client({
  host: 'pg1.example.com',
  database: 'app',
  user: 'etl',
  ssl: {
    rejectUnauthorized: true,
    ca:   fs.readFileSync('/etc/pg-certs/ca.crt'),
    cert: fs.readFileSync('/etc/pg-certs/etl.crt'),
    key:  fs.readFileSync('/etc/pg-certs/etl.key'),
  },
});

三个共同点值得写进团队规范:

  1. 证书路径统一:所有客户端都从同一个目录(如 /etc/pg-certs/)读证书,由配置管理分发,避免散落在各应用的私有路径里;
  2. 验证服务端不可省:无论哪种语言,sslmode=verify-full(或等价的 rejectUnauthorized + ca)都要保留——客户端证书只解决「别人冒充我」,不解决「我连到假服务器」;
  3. 进程属主与文件权限对齐:证书私钥必须能被应用进程读到(systemd 的 User=、容器的 UID),属主错位是「文件明明在却读不到」的第一大原因。

排错路径速查 ​

报错 / 症状根因方向
客户端 certificate verify failed服务端证书链不被客户端信任:缺 sslrootcert、CA 不匹配、主机名不在 SAN
服务端日志 tlsv1 alert unknown ca客户端证书不在服务端 ssl_ca_file 信任链内
服务端日志 peer did not return a certificate(客户端侧或见 tlsv1 alert certificate required)服务端要求证书而客户端没有出示:sslcert 路径错、文件读不到、驱动未配置
certificate authentication failed for user "xxx"CN/DN 与角色名不匹配:检查 clientname、映射表
连上但 permission denied认证成功了,是角色权限问题——回到 16.1 的权限体系排查
客户端 key 相关报错sslkey 路径/格式(必须为 PEM 格式)问题;私钥与证书不配对

定位技巧:先在服务器日志里看 TLS 层报错(比客户端报错信息丰富得多),再决定是修信任链还是修映射。

实战示例 ​

示例一:ETL 机器的全证书(无密码)接入 ​

目标:ETL 主机用证书认证连 etl 角色,零密码、零 .pgpass。

bash
# ---------- PKI 侧:为 ETL 主机签发客户端证书(流程详见 16.3 certificates 一页)----------
openssl req -new -nodes \
  -subj "/CN=etl" \
  -keyout etl.key -out etl.csr
openssl x509 -req -in etl.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 180 -out etl.crt
chmod 0600 etl.key

# 分发到 ETL 主机的固定位置
scp etl.crt etl.key ca.crt etl-host:/etc/pg-certs/
conf
# ---------- 服务端 postgresql.conf ----------
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file  = 'server.key'
ssl_ca_file   = 'ca.crt'        # 信任这张 CA 签的客户端证书
text
# ---------- pg_hba.conf ----------
hostssl  app  etl  10.0.9.57/32  cert
sql
-- 数据库侧准备角色
CREATE ROLE etl LOGIN PASSWORD NULL;   -- 无密码角色,只能靠证书进来
GRANT USAGE ON SCHEMA staging TO etl;
bash
# ---------- 服务端生效与预检 ----------
psql -c "SELECT rule_number, error FROM pg_hba_file_rules WHERE error IS NOT NULL;"
psql -c "SELECT pg_reload_conf();"
bash
# ---------- ETL 主机验证 ----------
psql "host=pg1 dbname=app user=etl sslmode=verify-full \
      sslcert=/etc/pg-certs/etl.crt sslkey=/etc/pg-certs/etl.key \
      sslrootcert=/etc/pg-certs/ca.crt" \
  -c "SELECT current_user;"
text
 current_user
--------------
 etl
(1 row)

注意:客户端虽然走 cert 认证,sslmode=verify-full + sslrootcert 仍然必要——它校验的是服务端证书,防止 ETL 机器连到假数据库。

示例二:DBA 双因素(证书 + SCRAM) ​

text
# pg_hba.conf:管理网段双因素
hostssl  all  +dba  10.0.9.0/24   scram-sha-256 clientcert=verify-full

# pg_ident.conf 可省略:verify-full 在密码方法上只要求 CN == 登录角色名
bash
# 签发个人证书:CN 必须与数据库角色名一致(alice)
openssl req -new -nodes -subj "/CN=alice" -keyout alice.key -out alice.csr
openssl x509 -req -in alice.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 90 -out alice.crt
sql
CREATE ROLE alice LOGIN IN ROLE dba PASSWORD 'Her#SCRAM-Pass' ;
bash
# 登录:证书 + 密码缺一不可
psql "host=pg1 dbname=app user=alice sslmode=verify-full channel_binding=require \
      sslcert=alice.crt sslkey=alice.key sslrootcert=ca.crt"
text
Password:  (SCRAM 密码)
psql (18.x)
SSL connection (protocol: TLSv1.3, ...)
Type "help" for help.

app=> SELECT current_user;
 current_user
--------------
 alice

示例三:吊销一张泄露的证书 ​

bash
# 1) 在 CA 侧吊销(openssl ca 流程,详见 16.3 certificates 一页)
openssl ca -config ca.cnf -revoke etl.crt

# 2) 重新生成 CRL 并分发到数据库服务器
openssl ca -config ca.cnf -gencrl -crldays 7 -out ca.crl
scp ca.crl pg1:/var/lib/pgsql/crl/

# 3) 服务端 reload 生效
psql -c "SELECT pg_reload_conf();"
bash
# 4) 验证:ETL 主机再次连接应被拒绝
psql "host=pg1 dbname=app user=etl sslmode=verify-full \
      sslcert=etl.crt sslkey=etl.key sslrootcert=ca.crt" -c "SELECT 1;"
text
psql: error: SSL error: certificate verify failed

示例四:多语言客户端统一挂载客户端证书 ​

同一个 ETL 角色的证书,三种运行形态(shell 脚本、Python 作业、Node 服务)统一从 /etc/pg-certs/ 读取:

bash
# /etc/pg-certs/ 的统一布局(配置管理分发,属主为运行账号)
#   etl.crt / etl.key  客户端证书与私钥
#   ca.crt             校验服务端的 CA
ls -l /etc/pg-certs/
text
-rw------- 1 etl etl 1704 Oct  1 08:00 etl.key
-rw-r--r-- 1 etl etl 1289 Oct  1 08:00 etl.crt
-rw-r--r-- 1 etl etl 1216 Oct  1 08:00 ca.crt
bash
# shell 形态:环境变量放进 systemd EnvironmentFile 或 wrapper 脚本
export PGSSLMODE=verify-full
export PGSSLCERT=/etc/pg-certs/etl.crt
export PGSSLKEY=/etc/pg-certs/etl.key
export PGSSLROOTCERT=/etc/pg-certs/ca.crt
psql -h pg1.example.com -U etl -d app -c "SELECT current_user;"
python
# Python 形态:读取同一组文件
import psycopg
conn = psycopg.connect(
    "host=pg1.example.com dbname=app user=etl sslmode=verify-full "
    "sslcert=/etc/pg-certs/etl.crt sslkey=/etc/pg-certs/etl.key "
    "sslrootcert=/etc/pg-certs/ca.crt",
)
javascript
// Node 形态:同一个目录、同一个 CA
ssl: {
  rejectUnauthorized: true,
  ca:   fs.readFileSync('/etc/pg-certs/ca.crt'),
  cert: fs.readFileSync('/etc/pg-certs/etl.crt'),
  key:  fs.readFileSync('/etc/pg-certs/etl.key'),
}
sql
-- 服务端确认三类连接都带了客户端证书(client_dn 有值即证书已出示)
SELECT a.application_name, s.ssl, s.client_dn, s.not_after
FROM pg_stat_ssl s JOIN pg_stat_activity a USING (pid)
WHERE a.usename = 'etl';
text
 application_name | ssl |            client_dn            |       not_after
------------------+-----+---------------------------------+------------------------
 etl-shell        | t   | CN=etl,OU=ServiceAccounts       | 2026-12-30 08:00:00+00
 etl-python       | t   | CN=etl,OU=ServiceAccounts       | 2026-12-30 08:00:00+00
 etl-node         | t   | CN=etl,OU=ServiceAccounts       | 2026-12-30 08:00:00+00
(3 rows)

这套布局的核心收益是换证书只动一处:轮换时配置管理把新文件推到 /etc/pg-certs/,三种形态的应用进程重启后自动生效,数据库侧无需任何变更。

注意事项 ​

cert 方法不能跑在非 SSL 连接上。 记录一定写 hostssl;把 cert 写在 host 上时,明文连接永远到不了这个方法的处理逻辑,行为会与预期不符。预检时用 pg_hba_file_rules 确认记录类型。

ssl_ca_file 存在不等于要求客户端证书。 客户端是否必须出示证书,只由 HBA 的 cert/clientcert 决定。只在 postgresql.conf 配了 ssl_ca_file 的实例,普通 hostssl + scram 连接不会索要客户端证书。

CN 匹配是大小写敏感的精确比较。 角色名 ETL 与证书 CN etl 不匹配(PostgreSQL 标识符未加引号折叠为小写,但证书 CN 的字符串比较发生在标识符折叠之前,务必让 CN 与角色字面一致,或用映射消除歧义)。

证书有效期比密码更「硬」。 密码过期前有无数提醒手段,证书过期只有一次无声的握手失败。把所有在用证书的 not_after 做成监控清单(pg_stat_ssl 的 not_after 列可以看到已连接会话的客户端证书有效期),提前 30 天告警。

客户端私钥的权限与备份。 私钥文件 0600;私钥不是「备份友好物」——泄露的私钥等于密码本,只能吊销重发。私钥丢失的恢复路径是重新签发,不是找回文件。

cert 认证失败与权限失败要分清。 certificate authentication failed 是认证层(CN/映射问题),permission denied for table 是授权层(角色权限问题)。两者报错位置不同,混在一起排查会浪费大量时间。

CRL 分发是吊销生效的必要条件。 吊销动作只改变 CA 侧的列表,服务器 reload 的那份 CRL 才是裁决依据。把「吊销 → 生成 → 分发 → reload → 验证」做成一个脚本,别凭记忆手工五连。

证书认证绕过了密码策略,不等于绕过了账号治理。 没有密码的 cert 角色同样要遵守最小权限、定期审计;VALID UNTIL 对 cert 认证无效(它只作用于密码认证),账号停用必须用 NOLOGIN 或吊销证书实现。

客户端证书文件的属主必须与运行进程一致。 证书签发时属主往往是 root 或签发管理员,分发后忘了 chown 给应用运行账号,服务就起不来或读到空文件——把「属主 + 0600(私钥)」写成分发清单的固定两列。

同一张证书经映射可以放行多个角色。 pg_ident.conf 里同一 SYSTEM-USERNAME 写多行 PG-USERNAME 是合法的;但每多一行,「证书 → 角色」的授权面就多一格,映射文件评审时按最小必需原则裁剪。

最佳实践 ​

  1. 机器连接用 cert,人连接用「密码 + 可选证书」或 Kerberos。 无人值守场景是证书认证收益最大的地方:没有密码就意味着没有 .pgpass、没有密码轮换与 .pgpass 分发。
  2. 为 cert 认证设立专用 CA。 不要复用给 Web 服务签证书的大 CA;专用 CA 吊销面小、命名空间干净,泄露时爆炸半径可控。
  3. CN 命名规范与角色名/映射规则对齐后再开闸。 先定「CN 直接等于角色名」还是「CN 走映射翻译」,写成文档,再签第一张证书。
  4. 用 map= 显式白名单。 即使 CN 与角色名完全一致,也建议经映射放行——映射文件就是「哪些证书可以进库」的审计清单。
  5. 客户端一律 sslmode=verify-full + sslrootcert。 客户端证书只防「别人冒充我」,verify-full 才防「别人冒充服务器」;两者是不同方向的威胁,都要覆盖。
  6. 服务端私钥 0600、客户端私钥 0600、CA 私钥离线保管。 三类密钥的保密等级从低到高:客户端证书、服务端证书、CA 私钥;CA 私钥丢失等于整个信任体系报废。
  7. 有效期 90~180 天 + 自动化轮换。 短有效期把「证书泄露」的窗口期压小;轮换脚本化(见 16.3 的 证书生成与管理 一页),人工签发是漂移之源。
  8. CRL 分发做成自动化闭环并定期演练吊销。 每次吊销必须以「目标证书连接失败」的验证收尾;建议每季度演练一次,确保链路没有悄悄坏掉。
  9. 双因素场景用 clientcert=verify-full 而不是裸密码。 管理账号(+dba、超级用户)挂上证书入场券,成本是每人一张证书,收益是「拿到密码也进不来」。
  10. 监控在用证书的有效期。 基于 pg_stat_ssl.not_after(客户端证书)与服务端证书文件做成有效期看板,提前 30 天告警,杜绝「周一上班证书过期」。
  11. 把 PKI 操作纳入版本管理。 签发脚本、CA 配置(不含 CA 私钥)、CRL 生成脚本进 Git;谁在什么时候签了什么证书,有据可查。
  12. 排错先看服务端日志。 TLS 层的失败原因(unknown ca、过期、吊销、协议错误)服务端日志永远比客户端报错详细,养成从服务端日志起步的习惯。

下一节

继续学习:peer / ident 认证

参考链接 ​