运维实践

证书自动续期:在有效期缩到 47 天前搭好流水线

作者: · 最后核实

证书自动续期,就是让 ACME 客户端按计划获取新证书、部署到所有需要它的服务器、重载服务,并在任何一步失败时通知你。这已经不是可选项:公网 TLS 证书最长有效期从 2026 年 3 月 15 日起是 200 天,2029 年 3 月 15 日起只有 47 天,手工续期意味着每张证书每年要做十来次。

为什么手动续期行不通

47 天证书按 2/3 寿命续期,每年约要续期 11.6 次,手工流程跟不上。SC-081v3 投票 规定的公网 TLS 证书最长有效期如下:

签发时间 最长有效期 按 2/3 寿命续期,每年次数 域名验证复用期
2026-03-15 之前 398 天 约 1.4 次 398 天
2026-03-15 起 200 天 约 2.7 次 200 天
2027-03-15 起 100 天 约 5.5 次 100 天
2029-03-15 起 47 天 约 11.6 次 10 天

最后一列和第一列同样重要。到 2029 年,CA 只能复用 10 天内的域名验证结果,几乎每次续期都要重新做一次挑战。“运维把文件发给同事、同事再传到网站根目录”这种流程根本跟不上。

Let's Encrypt 比规则走得更快:可选的 tlsserver 配置从 2026 年 5 月 13 日起签发 45 天证书;默认的 classic 配置在 2027 年 2 月 10 日从 90 天改为 64 天,2028 年 2 月 16 日改为 45 天;shortlived 配置现在就签发 160 小时的证书。国内云厂商的免费证书也已缩短到 3 个月左右(以控制台说明为准)。完整日期见 证书有效期时间线,背后的原因见 47 天证书。

续期流水线的四个环节

只有当客户端看到新证书,续期才算完成。多数事故出在签发之后的环节,而不是签发本身。

  +-----------+     +-----------+     +-----------+     +-----------+
  |   获取    | --> |   分发    | --> |   重载    | --> |   验证    |
  | ACME 订单 |     | 复制到每个|     | nginx、   |     | 从外部检查|
  | + CSR     |     | 节点、SLB、|     | HAProxy、 |     | 线上实际  |
  |           |     | CDN       |     | Java 应用 |     | 出示的证书|
  +-----------+     +-----------+     +-----------+     +-----+-----+
        ^                                                     |
        +------------------- 失败告警 ------------------------+
  • 获取:ACME 客户端完成挑战,下载新证书链。
  • 分发:证书和私钥送到每个终止 TLS 的地方:其他 Web 节点、负载均衡、CDN、邮件服务器、硬件设备。
  • 重载:每个进程加载新文件。nginx、HAProxy 需要 reload;很多 Java、Node.js 应用需要重启或重新加载 keystore。
  • 验证:从网络外部连上去,检查实际出示的到期时间和证书链。

何时续期:ARI 优先,1/3 兜底

最好的续期时间是 CA 建议的时间。ACME Renewal Information(RFC 9773)允许 CA 为每张证书公布续期窗口,并在计划批量吊销时把窗口提前。Let's Encrypt 集成指南给出三条规则:

  1. 每张证书每天至少查两次 ARI,在建议窗口内续期。
  2. 拿不到 ARI 时,在剩余 1/3 寿命时续期;有效期不足 10 天的证书在一半时续期。
  3. 在一天中的随机时间运行,不要卡在整点;失败后退避重试:1 分钟、10 分钟、100 分钟,之后每天一次。

对 90 天证书,兜底规则意味着到期前 30 天续期;对 45 天证书则是到期前约 15 天。Let's Encrypt 的公告特别提醒:写死“每 60 天续期”的做法将不再可行,并建议在 cron、包装脚本和运维文档里搜一搜 60、80、83 这类数字。

Certbot:定时器与部署钩子

大多数发行版安装 Certbot 时会带一个 systemd 定时器或 cron,每天运行两次 certbot renew。Certbot 4.0 起,证书剩余寿命不足 1/3 即视为到期;4.0 之前是固定的 30 天,对短期证书来说永远来不及。

# 看看有没有装定时器
systemctl list-timers | grep certbot

# 用测试环境完整演练一遍续期
sudo certbot renew --dry-run

# 只在续期成功后重载 nginx
sudo certbot renew --deploy-hook "systemctl reload nginx"

部署钩子只在证书真正续期后执行,而 --pre-hook、--post-hook 每次尝试都会执行。也可以把可执行脚本放进 /etc/letsencrypt/renewal-hooks/deploy/,对所有证书生效,不用改每个续期配置。Certbot 会给部署钩子传 RENEWED_LINEAGE、RENEWED_DOMAINS 环境变量,足够用来把文件复制到其他主机。

acme.sh:cron、DNS API 与 install-cert

acme.sh 安装时会加一个每日 cron,并在自己的目录里保存证书副本。关键一步是 --install-cert:它记住要把文件复制到哪里、每次续期后执行什么命令。国内用户最常见的是配合 DNSPod 或阿里云解析做 dns-01:

# 默认 CA 不是 Let's Encrypt,需要的话先切换
acme.sh --set-default-ca --server letsencrypt

# DNSPod(腾讯云 DNS 解析)的 API Token
export DP_Id="123456"
export DP_Key="your-dnspod-token"
acme.sh --issue --dns dns_dp -d example.com -d '*.example.com'

# 阿里云解析则用 dns_ali,配合最小权限 RAM 子账号的 AccessKey
# export Ali_Key="..." ; export Ali_Secret="..."
# acme.sh --issue --dns dns_ali -d example.com -d '*.example.com'

acme.sh --install-cert -d example.com \
  --key-file       /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.fullchain.pem \
  --reloadcmd      "systemctl reload nginx"

不要让 nginx 直接引用 acme.sh 工作目录里的文件,内部目录结构可能随版本变化。API 密钥会被 acme.sh 保存到 account.conf,所以务必使用只能改 DNS 记录的子账号。

宝塔面板与云厂商的自动化

宝塔面板的 SSL 页面申请 Let's Encrypt 证书时,会同时添加一个计划任务负责续签。它省事,但有两个常见坑:计划任务被误删或执行失败时,面板不会主动告诉你;站点开启了强制 HTTPS、防盗链或 CDN 回源规则后,/.well-known/acme-challenge/ 访问不到,http-01 会失败,这时改用面板里的 DNS 验证方式。

证书部署在阿里云、腾讯云的 CDN、负载均衡(SLB/CLB)、WAF 上时,手工在控制台上传新证书最容易遗漏。两家云都提供把证书一键部署到云产品、并在证书更新后同步的功能,也提供 OpenAPI,可以在 ACME 部署钩子里调用(具体能力以产品文档为准)。无论用哪种方式,都要在外部检查每个接入点实际出示的证书。

自带 ACME 的服务器

最简单的流水线是服务器自己续期自己的证书。Caddy 和 Traefik 默认自动签发和续期,Apache httpd 有 mod_md,nginx 在 2025 年 8 月推出了官方的 ngx_http_acme_module:

resolver 223.5.5.5;   # 模块需要解析器来访问 ACME 服务器

acme_issuer letsencrypt {
    uri https://acme-v02.api.letsencrypt.org/directory;
    state_path /var/cache/nginx/acme-letsencrypt;
    accept_terms_of_service;
}
acme_shared_zone zone=ngx_acme_shared:1M;

server {
    listen 443 ssl;
    server_name example.com;
    acme_certificate letsencrypt;
    ssl_certificate       $acme_certificate;
    ssl_certificate_key   $acme_certificate_key;
    ssl_certificate_cache max=2;
}

server {
    listen 80;           # http-01 挑战在这里应答
    location / { return 404; }   # 挑战请求由模块自行处理
}

内置 ACME 能省掉单机的分发和重载环节,但外部监控仍然不能少。

dns-01 自动化与密钥权限

通配符、内网主机和多机集群适合用 dns-01,但它把 DNS 密钥带进了签发流程。三种方法可以缩小风险:

  • 如果 DNS 服务商支持,使用只能修改某个域名 TXT 记录的受限 Token 或 RAM 子账号。
  • 用 CNAME 委托验证,例如 _acme-challenge.example.com CNAME example.com.acme.example.net.,让密钥只能控制一个专门的“验证区”。
  • 在一台专用主机上集中签发,再把证书推送到 Web 服务器,而不是让每台 Web 服务器都保存 DNS 密钥。

多台服务器与负载均衡

服务器不止一台时,先决定由哪台机器负责签发。每个节点各自跑客户端,会造成重复下单、触发频率限制,而且 CA 的 http-01 请求可能落到没有 token 的节点上。常见做法:

模式 做法 注意
集中签发 一台主机用 dns-01 续期,再通过 Ansible、rsync 或密钥管理服务分发 推送失败、私钥分发安全
共享挑战路径 负载均衡把 /.well-known/acme-challenge/ 转发到同一台节点 规则要经得起配置变更
在边缘终止 负载均衡或 CDN 自己申请证书 回源证书仍需续期
Kubernetes cert-manager 把证书写进 Secret,供 Ingress 使用 Ingress 控制器要能重新加载 Secret

Kubernetes 与 cert-manager

在 Kubernetes 里,cert-manager 是自动化证书的标准方案。Certificate 资源写明域名和 Issuer,cert-manager 默认在证书寿命过去 2/3 时续期,结果写入 Secret。

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
spec:
  secretName: example-com-tls
  dnsNames: ["example.com", "www.example.com"]
  issuerRef:
    name: letsencrypt
    kind: ClusterIssuer
  renewBeforePercentage: 33

优先用 renewBeforePercentage 而不是固定的 renewBefore:CA 缩短有效期后,固定时长可能超过新证书的寿命,导致反复续期,cert-manager 文档专门提醒过这一点。

监控与告警

监控能把静悄悄的续期失败变成一张工单,而不是一次故障。Let's Encrypt 在 2025 年 6 月 4 日停止发送到期邮件,理由是成本和保存数百万邮箱带来的隐私负担,所以你得自己盯。

  • 从外部检查:像真实客户端一样带 SNI 连接每个公网域名和端口。磁盘上的文件说明不了负载均衡实际出示的是什么。单个主机用 SSL 检测,一批域名用 批量 SSL 检测。
  • 按剩余比例告警,不按固定天数:剩余不足 1/3 寿命时提醒,不足 1/6 时紧急告警。对 47 天证书来说,固定的“30 天前提醒”几乎在签发后就会触发。
  • 监控续期任务本身:记录客户端的退出码或最近一次成功时间。
  • 关注证书透明度:CT 日志记录了为你的域名签发的每一张证书,既能发现被遗忘的主机,也能发现意外签发,见 证书透明度。

上线前先演练

从没完整跑过的续期流水线只是一种猜测。趁当前证书还有充足余量时演练:

  • 每次改动防火墙、安全组、负载均衡规则或 DNS 密钥后,都对测试环境跑一遍,例如 certbot renew --dry-run。
  • 选一个低风险域名强制真实续期一次,确认部署钩子执行了、每个节点都重载了、外部检查看到了新的序列号。
  • 用短期证书做“金丝雀”:在测试域名上用 6 天证书,每周把整条流水线跑两次左右,哪一步坏了几天内就会暴露。
  • 故意弄坏一个环节,比如停掉金丝雀主机上的续期定时器,确认告警真的能送到人手里。

故障排查清单

大多数续期失败出自下面几种原因,按顺序排查:

现象 可能原因 处理
Timeout during connect 80 端口被防火墙或云安全组挡住,或未备案被拦截 放行 80 端口或改用 dns-01,见 certbot 超时
挑战 URL 返回 404 请求落到了别的服务器,或跳转规则破坏了路径 把 /.well-known/acme-challenge/ 路由到签发主机
CAA 错误 CAA 记录里没有你的 CA 修改记录,见 CAA 记录
触发频率限制 反复失败或重复下单 先在测试环境修好,见 频率限制
磁盘上是新证书,线上还是旧证书 服务没重载,或某个节点没更新 加部署钩子,逐个节点检查
续期后证书链报错 只部署了叶子证书,没带中间证书 部署 fullchain.pem,见 缺少中间证书
私钥与证书不匹配 私钥和证书来自不同批次 两个文件一起部署,见 私钥不匹配

需要查看服务器到底出示了什么,可以参考 连接测试命令手册 和 故障排查命令手册。

常见问题

续期任务多久跑一次合适?

每天至少两次,时间随机到分钟。没有证书到期时任务几乎不耗资源,频繁运行成本很低,而且支持 ARI 的客户端能在 CA 提前续期窗口后几小时内作出反应。

有了续期定时任务就够了吗?

不够。续期成功了,服务却可能还在用旧证书:没人重载服务,或者负载均衡后面的另一台机器根本没收到新文件。要加一个部署钩子负责重载,再加一个外部检查,在线上证书剩余寿命不足约 1/3 时告警。

47 天证书该设多少天续期?

在寿命约 2/3 处续期,也就是到期前 15~16 天左右,并让 ARI 在 CA 需要时把时间提前。写死“到期前 30 天”或“每 60 天”的脚本都会出问题。

阿里云、腾讯云买的证书也能自动续期吗?

要看签发方式。如果背后的 CA 提供 ACME 接口和 EAB 凭据,可以用同一套客户端自动化;云厂商控制台的证书一般通过其“证书托管/自动部署”类功能或 OpenAPI 更新到 CDN、负载均衡等产品,具体以产品文档为准。纯手工下载上传的方式在 47 天时代不可持续。

资料来源

  1. CA/Browser Forum Ballot SC081v3
  2. Let's Encrypt:Integration Guide(When to Renew)
  3. Let's Encrypt:Decreasing Certificate Lifetimes to 45 Days
  4. Let's Encrypt:Ending Support for Expiration Notification Emails
  5. Certbot 用户指南:续期
  6. cert-manager:Certificate 资源
  7. nginx:ngx_http_acme_module