Skip to content

证书生成与管理 ​

前两页把 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 私钥是整个体系的天花板——它泄露等于所有证书报废,应当离线保存、永不上服务器。

信任链:证书是怎么被信起来的 ​

text
根 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 文本加固定头尾,肉眼可辨:

text
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgIUK...
-----END CERTIFICATE-----
text
-----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 / .pemX.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 命令速查 ​

本页会用到的命令全集,按用途分组:

bash
# ---------- 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)。

部署时的权限与属主 ​

bash
# 服务端三件套的标准落位($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.keypostgres0600硬性要求,否则启动失败;打包变体可 root 属主 + 0640 + 专用属组
server.crtpostgres0644公开材料
ca.crt / CRLpostgres0644公开材料

详细说明 ​

自签 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 字段的填法建议:

text
/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 ​

从各处收来的文件格式不齐是常态,部署前统一转换:

bash
# 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 的选择 ​

两个选项都是正解,差异在运维权衡:

维度RSAECC(如 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 时,流程变为「申请 → 审核 → 签发 → 部署」:

  1. 生成 CSR(含 SAN、CN 按 16.2 约定),提交给内部 CA 平台;
  2. CA 侧按流程签发(可能含中间 CA 链,把「服务器证书 + 中间 CA」串联进 server.crt,ssl_ca_file 对应根 CA);
  3. 部署与 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 目录结构:

bash
mkdir -p /etc/pki/dbca/{newcerts}
touch /etc/pki/dbca/index.txt
echo 1000 > /etc/pki/dbca/serial
echo 1000 > /etc/pki/dbca/crlnumber
ini
# /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:

bash
# 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
conf
# postgresql.conf
ssl_crl_file = '/etc/postgresql/ssl/ca.crl'
sql
SELECT pg_reload_conf();
bash
# 4) 验证吊销生效
openssl verify -CAfile ca.crt -crl_check -CRLfile ca.crl server.crt
text
server.crt: FAILED
error 23 at 0 depth lookup: certificate revoked

PG14+ 的多 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 不匹配 / 证书过期 / 主机名不在 SANopenssl 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() 已执行
bash
# 一条龙体检脚本:链、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 约定。

bash
#!/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 -l
text
pg1.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
...

部署(到每个节点):

bash
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
conf
# 各节点 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'
bash
pg_ctl restart -D $PGDATA   # ssl 开关需要重启;后续换证书只需 reload

示例二:CRL 吊销全流程 ​

承接示例一的产物,吊销 etl.crt:

bash
# 前提: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.crl
text
Revoking Certificate 1000.
Data Base Updated
conf
# pg1: postgresql.conf 增加(或已配置)
ssl_crl_file = '/etc/postgresql/ssl/ca.crl'
sql
SELECT pg_reload_conf();
bash
# 验证:该客户端证书再连应失败
psql "host=pg1.example.com 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

示例三:证书部署检查单 ​

每次签发/轮换后逐项打勾:

bash
# 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();"
text
 ssl | version |         cipher
-----+---------+---------------------------
 t   | TLSv1.3 | TLS_AES_256_GCM_SHA384
(1 row)

示例四:证书到期巡检脚本 ​

证书过期是「静默失败」型故障——到期那一刻所有连接一起断。巡检脚本把它变成可见的倒计时:

bash
#!/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
text
/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 天

再补两类「会过期但藏在别处」的检查:

bash
# 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;"
text
        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 内并按主机维度登记使用情况。

最佳实践 ​

  1. 用脚本生成证书,不用手工敲命令。 示例一的脚本入库版本管理;任何「手工签发」都会造成 SAN、有效期、算法的漂移。
  2. 专用数据库 CA,不复用 Web CA。 签发范围小、吊销面小、CN 命名空间不受 Web 证书规范干扰。
  3. SAN 完整覆盖连接约定,签发模板固化。 FQDN + 常用别名一次写全;客户端统一用 FQDN 连接。
  4. 客户端证书「CN = 角色名(或映射规则)」制度化。 与 证书认证 一页的映射约定保持一份对照文档。
  5. 有效期分级:CA 长、服务端 90~365 天、客户端 90~180 天。 到期前 30 天自动告警,轮换脚本化、reload 生效、新建连接验证。
  6. 私钥三重纪律:0600、不入库、一机一钥。 CA 私钥离线,服务端/客户端私钥经配置管理或密管分发。
  7. CRL 短周期生成 + 自动分发 + 吊销演练。 每季度跑一次完整吊销演练,验证「被吊销证书连接失败」这条闭环。
  8. 证书台账与 pg_stat_ssl 巡检双轨。 台账管「发出去什么」,运行时视图管「正在用什么」,两轨对齐才算受控。
  9. HA 与多节点场景预置全套证书。 切换后的新主即刻可用;证书路径与 reload 流程写进 HA runbook。
  10. K8s 环境用 cert-manager,传统环境用脚本 + 配置管理。 选定一种自动化路线并坚持,混合手工是最坏状态。

下一节

继续学习:审计日志(log_statement)

参考链接 ​