证书过期导致的全站不可用,几乎每年都会上几次技术新闻。根因很少是"没续期",而是证书生命周期没有自动化、没有监控、没有明确的责任边界。与此同时,TLS 证书早已不只是"给网站配 HTTPS"——服务间 mTLS、代码签名、Kubernetes 准入控制、零信任身份都建立在 PKI 之上。本文系统梳理 PKI 的信任模型、证书的签发与轮换、吊销与透明度,以及如何把整个生命周期管成一条自动化的流水线。
一、PKI 的组成与信任链
公钥基础设施(Public Key Infrastructure,PKI)解决的是"如何信任一个从未见过的公钥"。核心机制是信任链(chain of trust):一个受信的根证书,逐级向下为中间证书、终端实体证书签名。
Root CA(自签名,离线保管,有效期 20 年)
└── Intermediate CA(在线签发,有效期 5~10 年)
└── Leaf / End-entity(服务器或客户端证书,有效期 90 天~1 年)
关键规则:
- 根证书必须离线。一旦根私钥泄露,整个信任体系崩塌,所有下游证书都要重签。
- 中间证书是消耗品。浏览器只信任根,服务器必须把完整的中间链一并下发,否则部分客户端会报
unable to get local issuer certificate。 - 信任是终端行为。客户端(浏览器/操作系统)内置了受信根列表(如 Mozilla CA Program),自建 CA 若不在列表里,就必须手动导入。
1.1 X.509 证书里真正重要的字段
| 字段 | 含义 | 常见坑 |
|---|---|---|
Subject | 主体标识(CN 等) | 现代校验不看 CN,看 SAN |
Subject Alternative Name | 覆盖的域名/IP | 漏配即报域名不匹配 |
Basic Constraints | 是否 CA、路径长度 | 叶子证书必须 CA:FALSE |
Key Usage | 密钥用途 | 服务器证书需 digitalSignature, keyEncipherment |
Extended Key Usage | 扩展用途 | 服务器 serverAuth,客户端 clientAuth |
Not Before / Not After | 有效期 | 时钟漂移导致 certificate not yet valid |
Authority Key Identifier | 签发者密钥 ID | 用于在链中选对中间证书 |
Common Name(CN)在 2017 年后被主流浏览器弃用为域名来源,只作为展示用。所有域名必须写进 SAN,哪怕是单个域名。
二、证书签发:CSR 与 CA
签发的起点是证书签名请求(Certificate Signing Request,CSR)。私钥在本地生成、永不出门,CSR 只包含公钥与主体信息。
2.1 用 openssl 生成密钥与 CSR
# 1. 生成 2048 位(或 4096 位)RSA 私钥,或更现代的 ECDSA
openssl ecparam -genkey -name prime256v1 -out server.key
# 2. 写 CSR 配置文件,把域名放进 SAN
cat > csr.cnf <<'EOF'
[req]
distinguished_name = dn
req_extensions = ext
prompt = no
[dn]
CN = app.example.com
[ext]
subjectAltName = DNS:app.example.com,DNS:www.example.com,IP:10.0.0.5
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
EOF
# 3. 生成 CSR
openssl req -new -key server.key -out server.csr -config csr.cnf
# 4. 自签(仅测试用)或提交给 CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 90 -extfile csr.cnf -extensions ext -out server.crt
2.2 验签与查看
# 查看证书主体、SAN、有效期
openssl x509 -in server.crt -noout -subject -ext subjectAltName -dates
# 验证完整信任链(需要提供中间证书)
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt server.crt
# 模拟 TLS 握手,查看服务端下发的链
openssl s_client -connect app.example.com:443 -servername app.example.com -showcerts </dev/null
s_client 是排查线上问题最有效的工具:它能一次性告诉你证书链是否完整、域名是否匹配、协议与密码套件协商结果、是否启用 OCSP Stapling。
2.3 证书与密钥的格式
PKI 工具链里最常见的沟通障碍是"格式对不上"。三类容器要分清:
| 格式 | 编码 | 内容 | 典型用途 |
|---|---|---|---|
| PEM | Base64(-----BEGIN...) | 任意,可含链 | Nginx、OpenSSL 默认 |
| DER | 二进制 | 单张证书 | Java keystore、Windows |
| PKCS#12(.p12/.pfx) | 二进制 | 证书 + 私钥 + 链 | 导入浏览器/客户端 |
| PKCS#8 | 私钥容器 | 单个私钥 | 统一的私钥格式 |
常用转换:
# PEM 证书 → DER
openssl x509 -in server.crt -outform der -out server.der
# 证书 + 私钥 + 链 → PKCS#12
openssl pkcs12 -export -inkey server.key -in server.crt \
-certfile chain.pem -out bundle.p12 -name "app"
# 传统 RSA 私钥 → 加密的 PKCS#8
openssl pkcs8 -topk8 -in server.key -out server.pk8 -v2 aes-256-cbc
# 从 p12 拆出证书与私钥
openssl pkcs12 -in bundle.p12 -nodes -out all.pem
一个高频事故:把包含私钥的 .pem 误提交进 Git。密钥必须与证书分开存放,并用 密钥与凭证管理
的机制加密托管。
三、自动化签发与轮换
手工签发证书是事故的温床。目标应当是:证书从签发到轮换全自动,人类只在监控告警里出现。
3.1 ACME 与 Let’s Encrypt
ACME(RFC 8555)是自动化签发的事实标准,Let’s Encrypt 是其最流行的实现。核心挑战有两种:
| 挑战类型 | 原理 | 适用场景 |
|---|---|---|
http-01 | CA 访问 /.well-known/acme-challenge/<token> | 公网可达的 Web 服务 |
dns-01 | 在 DNS 加一条 TXT 记录 | 通配符证书、内网服务 |
tls-alpn-01 | 在 443 端口用特殊 ALPN 响应 | 无法改 Web 配置时 |
# certbot 手动跑一次,验证流程
certbot certonly --webroot -w /var/www/html \
-d app.example.com -d www.example.com \
--email ops@example.com --agree-tos --no-eff-email
# 通配符证书必须用 dns-01
certbot certonly --manual --preferred-challenges dns -d '*.example.com'
Let’s Encrypt 证书有效期 90 天,官方建议每 60 天续期一次。certbot renew 应放进 cron/systemd timer,并配置续期后的 reload 钩子。
3.2 cert-manager 与 Kubernetes
在 Kubernetes 里,cert-manager 把证书抽象成 CRD,自动签发、续期、注入 Secret:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-account
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: app-tls
namespace: prod
spec:
secretName: app-tls
duration: 2160h # 90 天
renewBefore: 720h # 到期前 30 天开始续期
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- app.example.com
- www.example.com
要点:
renewBefore应设为证书有效期的 1/3 左右,给失败重试留足窗口。- 用
ClusterIssuer而非Issuer,让所有命名空间共享同一签发配置。 - 结合 Kubernetes 安全加固 的 RBAC 限制,证书 Secret 只对需要的 ServiceAccount 可见。
- 监控
certmanager_certificate_expiration_timestamp_seconds指标,比"到期前 7 天告警"更早发现问题。
3.3 内部 PKI
内网服务不应依赖公共 CA,应自建内部 PKI(可用 Vault PKI、step-ca、cfssl)。内部 CA 的优势:可签发任意长有效期(如服务间 24 小时)、可自定义扩展、可即时吊销。内部根证书同样要离线保管,并定期轮换中间 CA。
3.4 轮换的原子性与热加载
证书文件被替换的瞬间,正在处理的连接不能中断。正确做法是先写临时文件再原子 rename,然后向服务进程发信号触发重载:
# 原子替换:新文件就位后再 rename,避免读到半截文件
cp new.crt /etc/nginx/ssl/app.crt.tmp
mv /etc/nginx/ssl/app.crt.tmp /etc/nginx/ssl/app.crt
nginx -t && nginx -s reload
# HAProxy / Envoy 等支持热加载的组件类似
注意两点:
- 不要用
cp直接覆盖正在被读取的文件,可能读到不完整内容。 - reload 之前先
nginx -t做语法与证书校验,避免把坏证书推上线导致进程启动失败。
对于不能 reload 的长连接服务,需要在应用层实现"双证书加载"——同时加载新旧证书,按握手时间选择,等旧证书上的连接自然结束后再卸载。
四、证书透明度与吊销
4.1 证书透明度(CT)
CT(Certificate Transparency,RFC 6962)要求 CA 把签发的每张证书登记到公开的、只能追加的日志(CT Log)里。任何人都能审计"某域名被签发了哪些证书",从而发现误签发或恶意签发。现代浏览器要求证书必须携带 SCT(Signed Certificate Timestamp),否则拒绝信任。运维侧可以用 crt.sh 反查自己域名下的所有证书,及时发现异常签发。
4.2 吊销机制对比
| 机制 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| CRL | 下载完整吊销列表 | 简单 | 列表膨胀、延迟高 |
| OCSP | 实时查询单张证书状态 | 精确 | 增加延迟、隐私泄露 |
| OCSP Stapling | 服务端缓存 OCSP 响应随握手下发 | 无额外延迟、保护隐私 | 需服务端正确配置 |
| Must-Staple | 证书声明必须带 stapling | 强制生效 | 配置错误即不可用 |
生产环境应启用 OCSP Stapling:服务端定期向 CA 拉取 OCSP 响应并缓存,握手时直接附带,客户端无需再连 CA。
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
吊销本身是个"尽力而为"的机制——客户端可能因网络或缓存错过吊销信息。因此短有效期正在取代吊销:90 天甚至 24 小时的证书,让"等它自然过期"比"等吊销生效"更快。这也正是自动化轮换如此重要的原因。
4.3 用 CT 反查自己域名的证书
CT 日志对防守方同样有价值:任何人为你的域名申请的证书都会出现在公开日志里。定期巡检可以发现误签发、影子 IT 或钓鱼域名:
# 查询某域名在 CT 日志中登记过的所有证书(crt.sh JSON 接口)
curl -s "https://crt.sh/?q=%25.example.com&output=json" \
| jq -r '.[] | "\(.not_before) \(.issuer_name) \(.name_value)"' \
| sort -u | head -40
把这条查询接进定时任务,凡是出现未知 CA、未知子域的签发记录就告警。注意结果里 %.example.com 会匹配所有子域,需人工确认哪些属于自己。
五、mTLS 与零信任身份
双向 TLS(mutual TLS,mTLS)要求客户端与服务端互相出示证书。它把"网络位置"从信任依据里剔除,是 零信任微隔离 的基石。
# 用 s_client 验证 mTLS:客户端证书必须被服务端信任
openssl s_client -connect api.internal:8443 \
-cert client.crt -key client.key \
-CAfile ca-bundle.crt -servername api.internal
落地要点:
- 客户端证书有效期要短(小时级),由内部 CA 自动轮换。
- 校验 SAN/URI:不要只看"证书由我们 CA 签发",还要看它声明的身份是否匹配调用方,否则任意一张内部证书都能冒充所有服务。
- 服务网格(Istio/Linkerd) 会自动注入 sidecar 并管理证书,证书身份用 SPIFFE ID(
spiffe://cluster/ns/prod/sa/orders)表达,把身份绑定到 Kubernetes 的 ServiceAccount。 - 私钥保护是前提:密钥必须加密存储、限制文件权限,见 现代密码学 中的密钥管理实践。
六、监控与过期治理
证书事故的最后一公里是监控。至少要采集三类指标:
| 指标 | 来源 | 告警阈值 |
|---|---|---|
| 剩余有效期(天) | 定期扫描证书 | < 21 天(warn),< 7 天(crit) |
| 续期任务是否成功 | certbot/cert-manager | 连续 2 次失败 |
| 握手错误率 | 负载均衡/网关 | 突增即告警 |
一个简单的探测脚本,扫描一个网段的所有 HTTPS 端点:
#!/usr/bin/env bash
# 输出 "host days_left"
for host in $(cat hosts.txt); do
end=$(echo | openssl s_client -connect "$host:443" -servername "$host" 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
echo "$host $left"
done
把它接进 Prometheus(写成一个 textfile collector 或 blackbox exporter 探针),就能用统一告警规则覆盖所有证书,包括那些"藏在某个角落、没人记得的"内部服务。
治理层面的三条硬规矩:
- 任何证书都必须有 owner 和自动续期,禁止手工导入的长期证书。
- 有效期上限:公网证书 ≤ 90 天,内部服务证书 ≤ 24 小时,客户端证书 ≤ 数小时。
- 过期演练:定期在预发环境强制让证书过期,验证告警与自动续期是否真的生效。
6.1 常见故障排查表
| 报错 | 根因 | 处理 |
|---|---|---|
certificate has expired | 未续期或续期失败 | 检查 renew 任务与钩子 |
unable to get local issuer certificate | 未下发中间证书 | 拼接 fullchain.pem |
hostname mismatch | SAN 未包含该域名 | 重签 CSR,域名写进 SAN |
certificate not yet valid | 客户端时钟漂移 | 校准 NTP |
self signed certificate | 客户端不信任自建 CA | 导入根证书到信任库 |
no suitable key share | 协议/套件不兼容 | 检查 TLS 版本与密码套件 |
| 握手成功但 mTLS 拒绝 | 客户端证书 SAN 不匹配 | 校验 SAN/URI,而非只看签发者 |
排查顺序建议固定为:openssl s_client 看链 → openssl verify 验链 → openssl x509 -dates 看有效期 → 对比系统时间。四步能覆盖 90% 的线上证书问题。
小结
PKI 与证书管理的本质是把信任的建立、传递、撤销都变成可自动化的流程。信任链保证"公钥可信",ACME/cert-manager 保证"证书常新",OCSP Stapling 与短有效期保证"失效可控",监控保证"意外可见"。把这四件事都工程化之后,证书就不再是需要人盯的定时炸弹,而是基础设施里一个安静的、自动运转的部件。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。