证书生成与管理
前两页把 SSL 的配置与强制策略讲完了,留了一个「从哪来」的问题:server.crt、ca.crt、客户端证书这些文件是怎么产生的?答案是 PKI(Public Key Infrastructure,公钥基础设施)——自己搭一个小型 CA(Certificate Authority,证书颁发机构)用 OpenSSL 签发,或接入企业内部 CA,或借助 Let's Encrypt / cert-manager 等自动化体系。本页把「证书全生命周期」讲完:生成、部署、验证、轮换、吊销,以及配套的保管纪律。
为什么值得为数据库专门讲证书管理?因为 PostgreSQL 的证书要求有几个「项目特有」的硬约束:私钥文件的权限检查会在启动时直接拒绝放行的实例(server.key 必须 0600/0640 且属主正确,见 SSL 配置 一页);verify-full 的主机名校验依赖 SAN(subjectAltName)的正确书写;客户端证书的 CN 直接决定数据库角色名(见 证书认证 一页)。通用 PKI 知识套用到数据库时,这几处最容易翻车。
本页的实战部分给出一份可直接执行的脚本:一个 CA、三个数据库节点的服务端证书、一个客户端证书,一条命令生成完毕;再补上 CRL 吊销与验证检查单。学完这页,16.3 整章的「配置 → 强制 → 供给」三步就闭环了。
前置知识
建议先阅读:强制 SSL 连接
基础概念
PKI 的三个角色
| 角色 | 是什么 | 在数据库场景的对应物 |
|---|---|---|
| CA | 签发与吊销证书的权威,持有 CA 私钥 | 专用数据库 CA(自建或企业 PKI) |
| 证书 | 绑定「身份 + 公钥」并由 CA 签名的文件 | server.crt(服务端)、postgresql.crt(客户端) |
| 私钥 | 与证书配对的秘密,证明身份的本体 | server.key、postgresql.key |
三个角色的保密等级依次是:证书可公开,服务端/客户端私钥需严格保管,CA 私钥是整个体系的天花板——它泄露等于所有证书报废,应当离线保存、永不上服务器。
信任链:证书是怎么被信起来的
根 CA 证书(ca.crt,自签)
│ 签名
▼
服务端证书(server.crt) 客户端证书(etl.crt)
│ 客户端用 ca.crt 验证 │ 服务端用 ssl_ca_file 验证
▼ ▼
「对面是 pg1.example.com」 「对面是角色 etl」自签 CA 意味着「信任由我们自己分发」:ca.crt 就是信任的种子,谁拿到它谁就信任这张 CA 签的一切。它不是秘密(可公开分发),但它的分发渠道必须是可信的——CA 证书被调包,整个 verify 体系就为攻击者背书了。
PEM 格式与文件长相
PostgreSQL 与 OpenSSL 默认使用 PEM(Privacy-Enhanced Mail)格式——Base64 文本加固定头尾,肉眼可辨:
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUK...
-----END CERTIFICATE----------BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w...
-----END PRIVATE KEY-----BEGIN CERTIFICATE 是证书,BEGIN PRIVATE KEY(PKCS#8)或 BEGIN RSA PRIVATE KEY(PKCS#1)是私钥,BEGIN X509 CRL 是吊销列表。多个证书可以串联在同一 PEM 文件里(证书链场景:服务器证书 + 中间 CA)。
证书文件家族一览
操作过程中会冒出六七种扩展名,先建立一张地图避免混淆:
| 扩展名 | 内容 | 在 PostgreSQL 里的角色 |
|---|---|---|
.crt / .pem | X.509 证书(PEM) | ssl_cert_file、ssl_ca_file、客户端 sslcert |
.key | 私钥(PEM) | ssl_key_file、客户端 sslkey,权限要求最严 |
.csr | 证书签名请求(申请文件) | 交给 CA 签发后即完成使命,可删 |
.crl | 吊销列表(PEM) | ssl_crl_file / ssl_crl_dir |
.srl | 签发序列号(-CAcreateserial 生成) | CA 侧台账,保证序列号不重复 |
.p12 / .pfx | 证书+私钥的打包容器(带口令) | PostgreSQL 不直接使用,需要先拆出 PEM |
.der | 二进制编码的证书/私钥 | 同上,需转回 PEM(见下文转换命令) |
两个易混点:.crt 与 .pem 都描述「内容是 PEM 编码的什么」,扩展名本身不决定内容——file 命令或直接看首行才是判断依据;.p12/.pfx 常由 Java 工具链产出,给 PostgreSQL 用之前必须拆出证书与私钥两个 PEM 文件。
给这张表配一条工作纪律:交付给 PostgreSQL 的每个文件,先能回答「你是表里哪一行、权限是多少、谁在何时放到这里」——三个问题答不上来的文件,按可疑处理,先隔离再核实。
语法与用法
OpenSSL 命令速查
本页会用到的命令全集,按用途分组:
# ---------- CA ----------
openssl req -new -x509 -days 3650 -nodes \
-keyout ca.key -out ca.crt \
-subj "/CN=Demo DB CA" # 自签生成 CA(证书 + 私钥)
# ---------- 服务端证书 ----------
openssl req -new -nodes \
-subj "/CN=pg1.example.com" \
-addext "subjectAltName=DNS:pg1.example.com,DNS:pg1,IP:10.0.0.11" \
-keyout server.key -out server.csr # 生成私钥(-nodes 不加密)与 CSR
openssl x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-days 365 -out server.crt \
-extfile <(printf "subjectAltName=DNS:pg1.example.com,DNS:pg1,IP:10.0.0.11") # 用 CA 签发(SAN 需经 extfile 带入)
# ---------- 验证 ----------
openssl verify -CAfile ca.crt server.crt # 验证证书链
openssl verify -CAfile ca.crt -crl_check -CRLfile ca.crl server.crt # 连 CRL 一起验
# ---------- 查看与体检 ----------
openssl x509 -in server.crt -noout -text | grep -A2 "Subject Alternative Name"
openssl x509 -enddate -noout -in server.crt # 有效期
openssl x509 -noout -modulus -in server.crt | openssl md5 # 公钥指纹(RSA)
openssl rsa -noout -modulus -in server.key | openssl md5 # 私钥指纹(两者必须一致)三个高频细节:
-nodes表示私钥不加密(no DES);数据库无人值守重启场景一般选它,加密私钥则要配套ssl_passphrase_command(见 SSL 配置 一页)。- CSR 里的
-addext subjectAltName不会自动进入签发后的证书,签发命令必须用-extfile把 SAN 再写一遍(或使用支持-copy_extensions的新版 OpenSSL)——只写 CSR 不写 extfile 是「证书里没有 SAN」的头号原因。 -CAcreateserial首次签发时生成序列号文件ca.srl,后续签发可继续使用(-CAserial ca.srl)。
部署时的权限与属主
# 服务端三件套的标准落位($PGDATA 内示例)
install -o postgres -g postgres -m 0644 server.crt $PGDATA/server.crt
install -o postgres -g postgres -m 0600 server.key $PGDATA/server.key
install -o postgres -g postgres -m 0644 ca.crt $PGDATA/ca.crt| 文件 | 属主 | 权限 | 说明 |
|---|---|---|---|
server.key | postgres | 0600 | 硬性要求,否则启动失败;打包变体可 root 属主 + 0640 + 专用属组 |
server.crt | postgres | 0644 | 公开材料 |
ca.crt / CRL | postgres | 0644 | 公开材料 |
详细说明
自签 CA 的设计取舍
自签 CA 适合的场景:数据库实例数量可控(几台到几十台)、有配置管理工具、希望完全自主可控。它的问题也很清楚:CA 私钥要自己保管、信任分发靠人工(把 ca.crt 放到每个客户端)、没有自动化吊销生态。与之相对的企业级选择在下面两节。决策口诀:小环境自签够用,大环境接内部 CA,公网可达才考虑 Let's Encrypt。
SAN:verify-full 的生命线
verify-full 的主机名校验逻辑是「证书 SAN 里的 DNS 条目 == 客户端连接的主机名」(无 SAN 时退回 CN)。签发服务端证书时:
- 主名 + 常用别名都写进 SAN:
DNS:pg1.example.com(FQDN)、DNS:pg1(短名,若客户端真的会用)、IP:10.0.0.11(确实有用 IP 连的客户端才写,IP 变了证书要重签); - 通配符谨慎使用:
*.db.example.com方便但扩大了单证书的适用面,专用数据库 CA 下可控时再用; - 多节点各签各的:pg1、pg2、pg3 各一张证书、各含自己的 SAN,不要共享一张「大而全」证书(私钥多机复制会扩大泄露面;但注意:高可用切换后新主机的证书必须已就位——failover 场景应在所有节点预置全套证书)。
通配符证书的适用边界
节点多、命名规整的环境会想用通配符证书(*.db.example.com)一张打天下。规则与代价:
| 维度 | 说明 |
|---|---|
| 匹配规则 | 通配符只允许出现在最左一个完整标签(*.db.example.com 匹配 pg1.db.example.com,不匹配 pg1.rack1.db.example.com,也不匹配裸域 db.example.com) |
| 私钥分发面 | 一张证书一个私钥覆盖 N 台主机,私钥落点同步放大到 N——泄露影响面按主机数倍增 |
| 吊销粒度 | 吊销通配符证书等于同时吊销所有主机的证书,无法只踢一台 |
| 适用判断 | 专用数据库 CA 之下、主机数可控(个位数到十位数)时收益明显;更大规模建议逐机签发 |
用通配符时补两条纪律:私钥仍然一机一钥地分发与保管(可以共用同一张证书,但尽量减少持久副本);台账按主机维度登记「用了哪张通配符证书」,否则轮换时会发现「不知道有几台在用它」。
CN 与 O 等字段的实践
Subject 字段的填法建议:
/CN=pg1.example.com/O=Example Corp/OU=Database
/CN=etl/OU=ServiceAccounts ← 客户端证书:CN 直接用数据库角色名- 服务端证书:CN 习惯用 FQDN(即使 SAN 才是校验依据,CN 与之一致便于人工阅读);
- 客户端证书:CN 直接等于数据库角色名(或与
pg_ident.conf映射规则对齐),这是 证书认证 一页定下的约定; - O/OU 组织字段按企业规范填写即可,PostgreSQL 默认不校验它们(
clientname=DN的场景除外——那时 DN 的每个字段都参与匹配)。
证书格式转换:PEM / DER / PKCS#8
从各处收来的文件格式不齐是常态,部署前统一转换:
# 1) 看首行判断格式
head -1 server.key # BEGIN PRIVATE KEY → PKCS#8 的 PEM(目标形态)
# BEGIN RSA PRIVATE KEY → PKCS#1 旧式,建议统一
head -1 server.crt # BEGIN CERTIFICATE → PEM 证书
# 2) PEM ↔ DER(证书)
openssl x509 -in server.crt -outform DER -out server.der
openssl x509 -in server.der -inform DER -out server.pem
# 3) 私钥统一为 PKCS#8 编码 PEM(新版本 OpenSSL 的 pkey 默认输出)
openssl pkey -in legacy-rsa.key -out server.key
# 4) 拆解 .p12 打包(Java/浏览器工具链的产物)
openssl pkcs12 -in bundle.p12 -nokeys -out server.crt
openssl pkcs12 -in bundle.p12 -nocerts -nodes -out server.key操作纪律三条:转换在受控环境做(私钥的中间产物不落共享盘/仓库);转换后重做配对校验(modulus/公钥指纹法,见语法一节);.p12 拆解时 -nodes 会输出无口令私钥,命令历史与临时文件的清理要跟上。
ECC 与 RSA 的选择
两个选项都是正解,差异在运维权衡:
| 维度 | RSA | ECC(如 prime256v1 / secp384r1) |
|---|---|---|
| 同等安全强度的密钥长度 | 2048~3072 位 | 256~384 位 |
| 兼容性 | 最广,老客户端无一例外可用 | 现代客户端全支持,极老栈可能缺曲线 |
| 签发与握手开销 | 略高 | 更短的密文、更快的签名验证 |
| 工具链支持 | 无差别 | OpenSSL 均支持,个别老旧打包工具需确认 |
实践取向:不确定就 RSA 2048/3072——兼容性零风险,尺寸开销完全可接受;客户端面貌可控(新版本驱动占绝对多数)时选 ECC 256。两点别混淆:证书的密钥类型(签发时决定)与 ssl_ecdh_curve(握手时密钥交换用的曲线参数)是两件事,后者不影响前者,也无需为 ECC 证书特意调整;ssl_dh_params_file 只影响 DHE 套件,RSA/ECC 证书下通常不需要配。
有效期与轮换节奏
| 证书 | 建议有效期 | 轮换方式 |
|---|---|---|
| CA 证书 | 3~10 年 | 换 CA 是大工程,到期前做「新旧 CA 并行签发」过渡 |
| 服务端证书 | 90~365 天 | 新文件 + reload,无需重启 |
| 客户端证书 | 90~180 天 | 重新签发 + 配置管理分发 |
轮换是「例行公事」而不是「应急抢修」,流程标准化:生成新证书(新序列号)→ 部署到服务器(旧文件保留为 .old)→ SELECT pg_reload_conf(); → 新建连接验证 → 观察无失败后清理旧文件。客户端证书轮换多一步「先分发新证书、再吊销旧证书」的重叠窗口,避免批量失效。
提前轮换的触发条件除日历(到期前 30 天)外还有事件:节点迁移(SAN 变化)、私钥疑似泄露(直接吊销重签)、CA 更新。把「所有在用证书的到期日清单」纳入巡检——服务端侧还有现成的运行时抓手:pg_stat_ssl 的 not_after 列能看到活跃连接所用客户端证书的有效期。
内部 CA 签发(企业级)
企业已有 PKI 时,流程变为「申请 → 审核 → 签发 → 部署」:
- 生成 CSR(含 SAN、CN 按 16.2 约定),提交给内部 CA 平台;
- CA 侧按流程签发(可能含中间 CA 链,把「服务器证书 + 中间 CA」串联进
server.crt,ssl_ca_file对应根 CA); - 部署与 reload 同自签流程。
优点是信任链纳入企业统一治理(审计、自动化、吊销策略都有平台背书);注意点是「SAN 与 CN 的登记规范」要与 DBA 约定对齐——证书平台不知道「CN=etl 对应数据库角色」,这份对照文档由数据库团队维护。
Let's Encrypt / cert-manager 思路
Let's Encrypt 提供免费、自动化的公网信任证书,用于数据库的可行性与注意点:
- 验证方式是关键:HTTP-01 需要 80 端口可达,数据库服务器通常不满足;应使用 DNS-01(向 DNS 加 TXT 记录完成验证),配合 API 自动化。
- 90 天有效期 + 自动续期:
certbot renew配 deploy hook 触发数据库 reload(--deploy-hook "pg_ctl reload -D $PGDATA"),或用 systemd timer。 - 证书是公网信任的:任何能解析你主机名的人都能用
verify-full验证它——防中间人能力更强,但「暴露主机名到公共 CT 日志」的隐私影响要评估。 - Kubernetes 场景:cert-manager 原生支持 ACME 与内部 CA,可自动签发、续期、挂载 Secret 给 PostgreSQL 容器,续期后触发实例重载——云原生部署的首选路径,细节按集群配置展开。
CRL 生成与配置
吊销走 OpenSSL CA 数据库流程。先准备最小 CA 目录结构:
mkdir -p /etc/pki/dbca/{newcerts}
touch /etc/pki/dbca/index.txt
echo 1000 > /etc/pki/dbca/serial
echo 1000 > /etc/pki/dbca/crlnumber# /etc/pki/dbca/ca.cnf —— 最小可用 CA 配置
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/pki/dbca
database = $dir/index.txt
new_certs_dir = $dir/newcerts
serial = $dir/serial
crlnumber = $dir/crlnumber
certificate = $dir/ca.crt
private_key = $dir/ca.key
default_md = sha256
default_days = 365
default_crl_days = 7
policy = policy_any
x509_extensions = v3_ca
[ policy_any ]
commonName = supplied
organizationName = optional
[ v3_ca ]
basicConstraints = CA:TRUE吊销与重新生成 CRL:
# 1) 吊销指定证书
openssl ca -config ca.cnf -revoke server.crt
# 2) 重新生成 CRL(默认 7 天有效,建议以短周期定时生成)
openssl ca -config ca.cnf -gencrl -out /etc/pki/dbca/ca.crl
# 3) 分发到数据库服务器并 reload
scp /etc/pki/dbca/ca.crl pg1:/etc/postgresql/ssl/ca.crl# postgresql.conf
ssl_crl_file = '/etc/postgresql/ssl/ca.crl'SELECT pg_reload_conf();# 4) 验证吊销生效
openssl verify -CAfile ca.crt -crl_check -CRLfile ca.crl server.crtserver.crt: FAILED
error 23 at 0 depth lookup: certificate revokedPG14+ 的多 CA 环境可用 ssl_crl_dir:目录内文件按 OpenSSL hash 命名(openssl rehash <目录> 生成软链),适合「多个 CA 各自维护 CRL」的形态。
CRL 自身也是有有效期的对象(上面 ca.cnf 的 default_crl_days = 7 决定了 -gencrl 产物的寿命):发布节奏必须小于等于其有效期,否则下游各端会陆续持有过期 CRL,吊销体系名存实亡。把 -gencrl 挂上与 default_crl_days 同步的定时任务,并把 CRL 的到期纳入证书巡检(见示例四的 openssl crl -nextupdate 检查),比记住手工执行可靠。
把 CRL 纳入与证书同一张台账:发放日期、nextUpdate、发布通道、reload 时间四列——CRL 管理的绝大多数事故,都出在「忘了它停在哪一步」。
客户端证书的分发与保管
客户端私钥是「谁能冒充这个角色」的本体,分发纪律:
- 经配置管理/密管系统分发(Ansible Vault、HashiCorp Vault、云 KMS),禁止 Git、聊天工具、邮件附件;
- 落盘即
0600,属主为运行连接的应用 OS 用户; - 一机一证、一角色一证:不共享客户端证书文件;轮换时先分发新证、后吊销旧证;
- 有台账:谁在哪个节点持有哪张证书(CN、序列号、到期日)——吊销时按序列号精确定位。
排错
| 报错 / 症状 | 根因 | 处理 |
|---|---|---|
certificate verify failed(客户端) | CA 不匹配 / 证书过期 / 主机名不在 SAN | openssl verify 链路体检;核对 sslrootcert 与 SAN |
tlsv1 alert unknown ca(服务端日志) | 客户端证书的 CA 不在 ssl_ca_file 信任链内 | 把签发该客户端证书的 CA 加入并 reload |
| 主机名不匹配(verify-full) | SAN 缺失或不含连接主机名 | 重新签发并带上正确 SAN;客户端改用 SAN 内的主机名 |
private key file "server.key" has group or world access | 私钥权限不符 | chown postgres + chmod 0600 |
key values mismatch | 证书与私钥不配对 | 用 modulus 指纹对比法找对配对文件 |
certificate is not yet valid | 服务器时钟偏差 / notBefore 未到 | 校时(NTP);重新签发 |
| reload 后仍用旧证书 | 看的是老连接 / 忘了 reload | 新建连接验证;确认 pg_reload_conf() 已执行 |
# 一条龙体检脚本:链、SAN、有效期、配对
openssl verify -CAfile ca.crt server.crt
openssl x509 -in server.crt -noout -ext subjectAltName
openssl x509 -enddate -noout -in server.crt
[ "$(openssl x509 -noout -modulus -in server.crt | openssl md5)" = \
"$(openssl rsa -noout -modulus -in server.key | openssl md5)" ] \
&& echo "cert/key PAIRED" || echo "cert/key MISMATCH"实战示例
示例一:三节点脚本化生成
一条脚本生成:1 个 CA + 3 个节点的服务端证书(各含自己的 SAN)+ 1 个客户端证书。所有节点主机名按 pgN.example.com 约定。
#!/usr/bin/env bash
# gen-pg-certs.sh —— 用法: ./gen-pg-certs.sh <输出目录>
set -euo pipefail
OUT=${1:?usage: $0 <output-dir>}; mkdir -p "$OUT"; cd "$OUT"
CA_DAYS=3650; SVC_DAYS=365; CLI_DAYS=180
NODES=(pg1.example.com pg2.example.com pg3.example.com)
CLI_USER=etl
# ---------- 1. 自签 CA ----------
openssl req -new -x509 -days $CA_DAYS -nodes \
-keyout ca.key -out ca.crt -subj "/CN=Demo DB CA"
# ---------- 2. 各节点服务端证书 ----------
for host in "${NODES[@]}"; do
openssl req -new -nodes -subj "/CN=$host/O=Example Corp/OU=Database" \
-keyout "$host.key" -out "$host.csr"
openssl x509 -req -in "$host.csr" \
-CA ca.crt -CAkey ca.key -CAcreateserial -days $SVC_DAYS -out "$host.crt" \
-extfile <(printf "subjectAltName=DNS:%s\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth" "$host")
rm -f "$host.csr"
done
# ---------- 3. 客户端证书(CN = 数据库角色名)----------
openssl req -new -nodes -subj "/CN=$CLI_USER/OU=ServiceAccounts" \
-keyout "$CLI_USER.key" -out "$CLI_USER.csr"
openssl x509 -req -in "$CLI_USER.csr" \
-CA ca.crt -CAkey ca.key -days $CLI_DAYS -out "$CLI_USER.crt" \
-extfile <(printf "keyUsage=digitalSignature\nextendedKeyUsage=clientAuth")
rm -f "$CLI_USER.csr"
# ---------- 4. 自检 ----------
openssl verify -CAfile ca.crt "${NODES[@]/%/.crt}" "$CLI_USER.crt"
for host in "${NODES[@]}"; do
openssl x509 -in "$host.crt" -noout -ext subjectAltName
done
echo "---- 产物 ----"; ls -lpg1.example.com.crt: OK
pg2.example.com.crt: OK
pg3.example.com.crt: OK
etl.crt: OK
X509v3 Subject Alternative Name:
DNS:pg1.example.com
...部署(到每个节点):
for i in 1 2 3; do
n="pg$i.example.com"
install -o postgres -g postgres -m 0644 "$n.crt" "/etc/postgresql/ssl/$n.crt"
install -o postgres -g postgres -m 0600 "$n.key" "/etc/postgresql/ssl/$n.key"
install -o postgres -g postgres -m 0644 ca.crt "/etc/postgresql/ssl/ca.crt"
done# 各节点 postgresql.conf(文件名按节点改)
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/pg1.example.com.crt'
ssl_key_file = '/etc/postgresql/ssl/pg1.example.com.key'
ssl_min_protocol_version = 'TLSv1.2'pg_ctl restart -D $PGDATA # ssl 开关需要重启;后续换证书只需 reload示例二:CRL 吊销全流程
承接示例一的产物,吊销 etl.crt:
# 前提:CA 目录结构已初始化(见正文「CRL 生成与配置」节)
cd /etc/pki/dbca
openssl ca -config ca.cnf -revoke "$OUT/etl.crt"
openssl ca -config ca.cnf -gencrl -out ca.crl
scp ca.crl pg1:/etc/postgresql/ssl/ca.crlRevoking Certificate 1000.
Data Base Updated# pg1: postgresql.conf 增加(或已配置)
ssl_crl_file = '/etc/postgresql/ssl/ca.crl'SELECT pg_reload_conf();# 验证:该客户端证书再连应失败
psql "host=pg1.example.com dbname=app user=etl sslmode=verify-full \
sslcert=etl.crt sslkey=etl.key sslrootcert=ca.crt" -c "SELECT 1;"psql: error: SSL error: certificate verify failed示例三:证书部署检查单
每次签发/轮换后逐项打勾:
# 1. 证书链验证通过
openssl verify -CAfile ca.crt server.crt client.crt
# 2. SAN 与主机名约定一致
openssl x509 -in server.crt -noout -ext subjectAltName
# 3. 证书与私钥配对
[ "$(openssl x509 -noout -modulus -in server.crt | openssl md5)" = \
"$(openssl rsa -noout -modulus -in server.key | openssl md5)" ] && echo PAIRED
# 4. 服务端文件权限与属主
stat -c '%a %U:%G %n' server.key server.crt ca.crt
# 期望:600 postgres:postgres server.key;644 其余
# 5. reload 生效并新建连接验证
psql -c "SELECT pg_reload_conf();"
psql "host=pg1.example.com sslmode=verify-full sslrootcert=ca.crt" -c \
"SELECT ssl, version, cipher FROM pg_stat_ssl WHERE pid = pg_backend_pid();" ssl | version | cipher
-----+---------+---------------------------
t | TLSv1.3 | TLS_AES_256_GCM_SHA384
(1 row)示例四:证书到期巡检脚本
证书过期是「静默失败」型故障——到期那一刻所有连接一起断。巡检脚本把它变成可见的倒计时:
#!/usr/bin/env bash
# check-cert-expiry.sh —— 扫描证书目录,输出剩余天数并告警
set -euo pipefail
CERT_DIR=/etc/postgresql/ssl
WARN_DAYS=30
now=$(date +%s)
for cert in "$CERT_DIR"/*.crt; do
end=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
end_ts=$(date -d "$end" +%s) # GNU date;BSD/macOS 换 date -j -f 格式
days=$(( (end_ts - now) / 86400 ))
line=$(printf '%-55s end=%-25s %4s days' "$cert" "$end" "$days")
if [ "$days" -lt "$WARN_DAYS" ]; then
echo "$line <== 告警:不足 ${WARN_DAYS} 天"
else
echo "$line"
fi
done/etc/postgresql/ssl/server.crt end=Mar 30 12:00:00 2027 GMT 174 days
/etc/postgresql/ssl/ca.crt end=Jun 30 08:00:00 2026 GMT 62 days
/etc/postgresql/ssl/client-etl.crt end=Oct 15 08:00:00 2026 GMT 8 days <== 告警:不足 30 天再补两类「会过期但藏在别处」的检查:
# 1) CRL 的有效期(nextUpdate 字段;过期的 CRL 与过期证书同级别的问题)
openssl crl -in crl.pem -noout -lastupdate -nextupdate
# 2) 运行时视角:活跃连接里客户端证书的最快到期时间
psql -c "SELECT min(not_after) AS earliest_client_cert_expiry
FROM pg_stat_ssl WHERE ssl AND client_dn IS NOT NULL;" earliest_client_cert_expiry
-----------------------------------
2026-10-15 08:00:00+00
(1 row)脚本挂进监控 cron(日频)并把「不足 30 天」推送到告警通道,与 SLA 告警同级——证书告警不该只在事故群里出现。
巡检输出本身也入库留存:每次跑完把结果追加到带日期的日志文件,轮换后翻「历史倒计时曲线」就能确认新证书确实生效——巡检不止是发现过期,也是轮换动作的验收记录。
脚本与台账放同一个版本库:改脚本必对台账字段,两者长期对齐,巡检体系才不会在人员更替后走样。
注意事项
CA 私钥的天花板地位。 ca.key 离线保管(加密介质/独立密管),永不需要出现在数据库服务器或 CI 上;它的泄露意味着所有证书必须全部重签、所有客户端 CA 重新分发——把「CA 私钥丢失预案」提前写好。
CSR 的 SAN 不自动进证书。 -addext 只作用于 CSR,签发命令不带 -extfile(或 -copy_extensions)时 SAN 会丢失,verify-full 随即失效。把 SAN 写进签发命令模板,而不是靠记忆。
证书到期没有提醒,只有故障。 所有证书建立到期台账(含序列号),配合巡检脚本提前 30 天告警;运行时侧用 pg_stat_ssl.not_after 兜底观察活跃连接的客户端证书。
权限检查在启动时一票否决。 server.key 权限错误实例直接起不来;证书放 $PGDATA 外时同时检查目录权限与 SELinux/AppArmor。变更先在测试实例演练。
吊销依赖 CRL 分发节奏。 生成 CRL 但没分发/reload,吊销就不生效;把「吊销 → gencrl → 分发 → reload → 连接验证」做成单一脚本,杜绝手工漏步。
多节点与 failover 的证书预置。 高可用切换后新主的证书必须在切换前就位(SAN 含原 VIP 或双方主机名策略一致),否则切换成功、连接全挂——这是 HA + SSL 组合的经典事故。
公网 CA(Let's Encrypt)的主机名暴露。 证书进入公共 CT 日志等于公布主机名清单;内网命名(pg1.internal)本来也不满足公网 CA 的验证条件。混合场景按「公网入口用 LE、内部通道用内部 CA」划分。
证书不是权限体系的替代。 CN 映射只解决「是谁」,角色权限仍由 GRANT 体系管理;证书认证的账号同样要做最小权限与定期审计。
巡检要覆盖「链条上的每一份文件」。 服务器证书、CA、中间 CA、CRL、客户端证书都在同一张巡检清单里——只盯 server.crt 的人,最后都倒在「CA 过期」「CRL 过期」这类旁支文件上。示例四脚本用 *.crt 通配扫描 + openssl crl 补查,就是为此。
证书序列号是吊销与台账的钥匙。 台账每一行都记录序列号(openssl x509 -serial -noout -in xx.crt),吊销与对账按序列号定位,而不是按「哪个主机的哪张」——序列号是 CA 与被吊销对象之间唯一的公共语言。
分发的每一份文件先核对指纹再落位。 证书跨环境流转时接收方执行 openssl x509 -fingerprint -sha256 与台账比对;指纹不符即文件截断/替换/编码损坏——这类损坏 downstream 表现出极具迷惑性的 certificate verify failed,指纹核对一次就绕开了全部排查。
通配符证书是「方便」与「管控」的交换。 一张证书覆盖多台主机,私钥分发面与泄露影响面同步放大,且吊销无法只针对一台。规模小(个位数主机)建议逐机签发;确要用通配符,限制在专用 CA 内并按主机维度登记使用情况。
最佳实践
- 用脚本生成证书,不用手工敲命令。 示例一的脚本入库版本管理;任何「手工签发」都会造成 SAN、有效期、算法的漂移。
- 专用数据库 CA,不复用 Web CA。 签发范围小、吊销面小、CN 命名空间不受 Web 证书规范干扰。
- SAN 完整覆盖连接约定,签发模板固化。 FQDN + 常用别名一次写全;客户端统一用 FQDN 连接。
- 客户端证书「CN = 角色名(或映射规则)」制度化。 与 证书认证 一页的映射约定保持一份对照文档。
- 有效期分级:CA 长、服务端 90~365 天、客户端 90~180 天。 到期前 30 天自动告警,轮换脚本化、reload 生效、新建连接验证。
- 私钥三重纪律:
0600、不入库、一机一钥。 CA 私钥离线,服务端/客户端私钥经配置管理或密管分发。 - CRL 短周期生成 + 自动分发 + 吊销演练。 每季度跑一次完整吊销演练,验证「被吊销证书连接失败」这条闭环。
- 证书台账与
pg_stat_ssl巡检双轨。 台账管「发出去什么」,运行时视图管「正在用什么」,两轨对齐才算受控。 - HA 与多节点场景预置全套证书。 切换后的新主即刻可用;证书路径与 reload 流程写进 HA runbook。
- K8s 环境用 cert-manager,传统环境用脚本 + 配置管理。 选定一种自动化路线并坚持,混合手工是最坏状态。
下一节
继续学习:审计日志(log_statement)
参考链接
- PostgreSQL 官方文档:Secure TCP/IP Connections with SSL
- PostgreSQL 官方文档:SSL Certificates
- PostgreSQL 官方文档:Connection and Authentication 配置参数(ssl_* GUC)
- PostgreSQL 官方文档:SSL Support(libpq)
- PostgreSQL 官方文档:Authentication Methods(cert 方法)
- PostgreSQL 官方文档:The pg_hba.conf File(clientcert)
- PostgreSQL 官方文档:pg_stat_ssl 视图
- PostgreSQL 官方文档:Authentication Problems