3、证书轮换策略:轮换周期设计、蓝绿部署与金丝雀发布中的证书轮换、自动化轮换脚本

证书轮换这事儿,说白了就是跟时间赛跑。你想想看,一张证书从签发那天起,倒计时就开始了。我见过太多团队,证书过期前一晚才手忙脚乱地换,结果业务中断半小时。嗯,这一章咱们就把轮换策略聊透。

3.1 轮换周期设计:不是越短越好

轮换周期怎么定?我个人习惯先问三个问题:

  • 证书被泄露的概率有多大?
  • 轮换一次的成本有多高?
  • 业务能接受多长的停机窗口?

答案不同,周期就不同。下面这张表是我在多个项目里总结出来的经验值:

场景 推荐周期 理由
内部服务 mTLS 30 天 集群内证书流转快,自动化程度高
面向用户的 Web 证书 90 天 CDN 缓存、客户端兼容性需要缓冲
根 CA / 中间 CA 2-5 年 轮换成本极高,需手动审计
IoT 设备证书 1 年 设备离线时间长,更新通道窄
注意: 我曾经遇到一个团队把内部证书周期设为 7 天,结果轮换脚本一次失败,整个集群的 gRPC 调用全断了。周期越短,对自动化可靠性的要求就越高。别为了「安全」盲目缩短周期。

3.2 蓝绿部署中的证书轮换

蓝绿部署的核心思路是「两套环境,一次切换」。证书轮换在这里天然有优势——你可以在绿环境里先换上新的证书,验证没问题了,再把流量切过去。

具体步骤我一般这么走:

  1. 准备绿环境:部署新版本应用,挂载新证书。
  2. 健康检查:用 curl 或 openssl 验证绿环境的 TLS 握手是否正常。
  3. 流量切换:从负载均衡器把流量从蓝切到绿。
  4. 观察期:保持蓝环境运行 24 小时,确认无异常后再回收。
  5. 回收旧证书:从蓝环境卸载旧证书,吊销或归档。
小技巧: 我习惯在蓝绿切换前,先做一次「证书预加载」。也就是在绿环境里提前 24 小时挂上新证书,但不对外暴露。这样能提前发现证书链不完整、OCSP 响应慢等问题。

3.3 金丝雀发布中的证书轮换

金丝雀发布比蓝绿更精细。你只替换一小部分实例的证书,观察业务指标,再逐步扩大范围。这种方式特别适合「证书变更可能影响客户端兼容性」的场景。

举个例子,有一次我们要把 RSA 证书换成 ECDSA 证书。ECDSA 的签名算法跟 RSA 不一样,有些老旧的客户端(比如 Android 4.x)可能不认。怎么办?

  • 第一步:从 100 个实例里挑 2 个,挂上 ECDSA 证书。
  • 第二步:监控这 2 个实例的 TLS 握手失败率、4xx/5xx 错误率。
  • 第三步:如果指标正常,扩大到 10 个实例。
  • 第四步:逐步推进到 50%、100%。
关键点: 金丝雀轮换必须搭配「灰度路由」。也就是说,只有特定比例的流量会命中新证书实例。我一般用 Istio 的权重路由或者 Nginx 的 split_clients 来实现。

3.4 自动化轮换脚本:从手动到无人值守

手动轮换证书?说实话,2025 年了,这不应该再发生。我写自动化脚本时,会遵循一个原则:脚本只做三件事——签发、部署、验证。

下面是一个基于 Bash + OpenSSL + systemd 的简化版轮换脚本。它适用于单机部署的场景:

#!/bin/bash
# cert-rotate.sh — 自动化证书轮换脚本
# 用法: ./cert-rotate.sh <域名> <证书路径>

set -euo pipefail

DOMAIN="${1}"
CERT_DIR="${2:-/etc/ssl/certs}"
DAYS_BEFORE_EXPIRY=30

# 1. 检查当前证书是否即将过期
CERT_FILE="${CERT_DIR}/${DOMAIN}.pem"
if [ -f "$CERT_FILE" ]; then
    EXPIRY=$(openssl x509 -enddate -noout -in "$CERT_FILE" | cut -d= -f2)
    EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
    NOW_EPOCH=$(date +%s)
    REMAINING=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
    if [ "$REMAINING" -gt "$DAYS_BEFORE_EXPIRY" ]; then
        echo "证书还有 ${REMAINING} 天过期,无需轮换"
        exit 0
    fi
fi

# 2. 签发新证书(这里假设有内部 CA)
openssl req -x509 -newkey rsa:2048 -keyout "${CERT_DIR}/${DOMAIN}.key" \
    -out "${CERT_DIR}/${DOMAIN}.pem" -days 90 -nodes \
    -subj "/CN=${DOMAIN}"

# 3. 部署新证书(重启服务)
systemctl reload nginx

# 4. 验证新证书是否生效
sleep 2
echo | openssl s_client -connect localhost:443 -servername "$DOMAIN" 2>/dev/null | \
    openssl x509 -noout -dates

echo "证书轮换完成:${DOMAIN}"
注意: 上面的脚本只是演示。生产环境里,你还需要处理证书链、私钥权限、备份旧证书、发送告警通知。我踩过最大的坑是——脚本里忘了 set -euo pipefail,结果私钥生成失败,但脚本继续往下跑,最后 reload 了一个空证书,服务直接挂了。

3.5 轮换策略的整体流程

下面这张图,是我自己画的一个轮换策略总览。它把周期设计、蓝绿/金丝雀、自动化脚本串在了一起:

证书轮换策略总览 证书即将过期 / 安全事件 蓝绿部署 金丝雀发布 绿环境挂载新证书 健康检查 → 流量切换 2% 实例挂新证书 逐步扩至 10% → 50% → 100% 自动化脚本执行 核心原则:先验证,再切换;先小范围,再全量;先自动化,再无人值守

3.6 避坑指南

最后,分享几个我亲身踩过的坑:

  • 证书链不完整:有一次轮换后,客户端报错「certificate unknown」。查了半天,发现新证书只发了叶子证书,没带中间 CA。从那以后,我每次轮换都会用 openssl verify -CAfile ca.pem cert.pem 做完整链验证。
  • 私钥权限:自动化脚本跑完,私钥文件权限变成 644,所有人都能读。嗯,这个低级错误我犯过一次。现在脚本里强制 chmod 600。
  • 时钟偏移:证书的 notBefore 和 notAfter 是基于签发时间的。如果服务器时钟快了 5 分钟,新证书可能还没到生效时间。我建议在签发时加上 -offset -300(提前 5 分钟生效)。
  • 回滚预案:自动化轮换脚本必须带回滚能力。我习惯在部署新证书前,先备份旧证书到 /var/backup/certs/,并保留最近 3 个版本。
我的习惯: 每次轮换完成后,我会手动跑一遍 openssl s_client -connect <host>:443 -showcerts,确认证书链、有效期、SNI 都正常。虽然自动化了,但关键步骤还是看一眼放心。