W weiserv
← 返回博客

证书续期别再靠人肉:acme.sh + 腾讯云 DNSPod 通配符证书自动续期与排障实录

证书续期别再靠人肉:acme.sh + 腾讯云 DNSPod 通配符证书自动续期与排障实录

有天我例行检查自己的站点,顺手看了一眼证书有效期,结果后背一凉:

notAfter=Oct  4 14:49:46 2026 GMT

距离过期还有 29 天。 而这个证书的签发方式是"手动跑一次 acme.sh"——也就是说,如果我不记得手动续,29 天后整个站点的 HTTPS 会直接失守,浏览器拦全屏红屏。

更要命的是,在排查过程中我发现了一个极具欺骗性的假象:机器上确实有一个证书相关的定时任务在跑,但它跟我的证书毫无关系。

这篇文章记录完整的排查、配置、以及一次真实的续期失败定位。所有命令与输出均来自真实环境。


一、第一步不是"续期",而是确认"到底有没有自动续期"

很多人的第一反应是直接跑一次续期命令,看到成功就以为万事大吉。这是错的——手动续一次只解决这一次,不解决下一期。

先做三件事确认现状:

# 1) 看证书真实有效期(以 Nginx 实际加载的证书为准)
openssl x509 -in /etc/nginx/ssl/weiserv.com/fullchain.cer -noout -enddate -subject

# 2) 看 acme.sh 管理了哪些证书、下次续期时间
~/.acme.sh/acme.sh --list

# 3) 看有没有真的定时任务
crontab -l | grep -i acme
systemctl list-timers | grep -i acme

我的实测结果:

检查项 结果 判断
证书有效期 notAfter=Oct 4 2026 剩 29 天
acme.sh --list 有记录,Renew 字段为空 无自动续期计划
用户 crontab 无 acme 相关条目 🔴 完全没有自动续期
systemd timer 无 acme 相关 🔴 同上

结论很清楚:续期完全靠人肉


二、三个极易误判的信号(我逐个踩过)

信号 1:certbot.timer 在跑 ≠ 证书会被续

这台机器上装过 certbot,certbot.timer 状态是 enabled + active,看起来"证书有人管"。

但执行:

sudo certbot certificates

返回的是 No certs found——certbot 管理的证书库是空的。我的证书是 acme.sh 签的,certbot 根本不知道它的存在。

这个 timer 每 12 小时触发一次,勤勤恳恳地"续"一个空列表,制造出一种"证书很安全"的错觉。这是最危险的假象。

处理:

sudo systemctl disable --now certbot.timer

判断标准很简单:谁签的证书,谁负责续。混用两套工具时,务必确认当前生效的证书到底归谁管。

信号 2:--dry-run 未必是你的 acme.sh 支持的选项

我想先"演练"一次续期,于是:

~/.acme.sh/acme.sh --renew -d weiserv.com --dry-run

结果不是演练,而是报错——这个版本的 acme.sh(v3.1.4)不认识 --dry-run 参数

想验证续期链路是否通畅,在这个版本里只能真跑一次。好消息是:Let's Encrypt 证书续期不影响现有证书,即使失败,手上的证书也不会立刻失效(前提是还没过期)。

信号 3:续期"成功"不等于 Nginx 用上了新证书

acme.sh 续期成功只是把新证书写进了它自己的目录。如果没有配置 reloadcmd,Nginx 进程里加载的仍然是旧证书——磁盘上是新的,内存里是旧的,浏览器照样报过期。

这一点必须在安装证书时就配好(见下一节),事后补会很别扭。


三、配置自动续期(两行命令)

acme.sh 自带安装定时任务的能力:

~/.acme.sh/acme.sh --install-cronjob

装好后在 crontab 里能看到类似这一行(每天 4 个时间点各跑一次,--cron 会自动判断哪些证书该续、哪些还早):

17 3,9,15,21 * * * "$HOME/.acme.sh"/acme.sh --cron --home "$HOME/.acme.sh" > /dev/null

验证:

crontab -l | grep acme

同时确认 reloadcmd 已配置(这是信号 3 的根治办法)。我的证书配置里 Le_ReloadCmd 是:

sudo systemctl reload nginx

如果之前签发时没配,可以补:

~/.acme.sh/acme.sh --install-cert -d weiserv.com \
  --key-file       /etc/nginx/ssl/weiserv.com/weiserv.com.key \
  --fullchain-file /etc/nginx/ssl/weiserv.com/fullchain.cer \
  --reloadcmd      "sudo systemctl reload nginx"

四、一次真实的续期失败与定位

配好定时任务后,我手工强制续一次,验证整条链路真的能跑通:

~/.acme.sh/acme.sh --renew -d weiserv.com --force --debug 2

第一次:失败了。 关键输出:

Adding TXT value: aMr14CXHbdQlN0CCwpy5lQTxTXGD0K3u8p_UYZ-vobo for domain:  _acme-challenge.weiserv.com
记录已经存在,无需再次添加。
Error adding TXT record to domain: _acme-challenge.weiserv.com
Please add '--debug' or '--log' to see more information.
Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: 3

这里有个自相矛盾的地方值得说:它先说"记录已经存在,无需再次添加"(听起来是成功的),紧接着又报"添加 TXT 记录失败"。

定位过程

  1. curl error 3 对应 CURLE_URL_MALFORMAT(URL 格式错误),说明调用 DNS 厂商 API 时构造出的请求有问题;
  2. 检查凭据是否还在:
    bash grep -o 'SAVED_Tencent_SecretI[dK]*' ~/.acme.sh/account.conf
    凭据字段存在;
  3. 检查 DNS 插件是否还在:
    bash ls ~/.acme.sh/dnsapi/ | grep tencent # dns_tencent.sh
    插件存在;
  4. 检查是否残留了上次的挑战记录:
    bash dig +short TXT _acme-challenge.weiserv.com
    返回空——说明域名上并没有真的残留 TXT 记录。

凭据在、插件在、DNS 上没残留 ⇒ 配置和代码都没问题,那么最可能的就是 DNS 厂商 API 的瞬时故障(或是当时的请求构造偶发异常)。

第二次:原样重跑,成功了。

Cert success.
Your cert is in: ~/.acme.sh/<你的域名>_ecc/<你的域名>.cer
Installing key to: /etc/nginx/ssl/weiserv.com/weiserv.com.key
Installing full chain to: /etc/nginx/ssl/weiserv.com/fullchain.cer
Running reload cmd: sudo systemctl reload nginx
Reload successful

复查:

openssl x509 -in /etc/nginx/ssl/weiserv.com/fullchain.cer -noout -enddate
# notAfter=Dec  4 14:31:41 2026 GMT

从 Oct 4 延到了 Dec 4,Nginx 也重载成功。

经验:DNS-01 校验链路长(acme.sh → 厂商 API → DNS 生效 → CA 校验),任一环瞬时抖动都会失败。
遇到这类报错,先复核凭据/插件/残留记录三件事,都正常就直接重跑一次——不要急着改配置,改错了反而把好的配置弄坏。


五、让它可观测:怎么确认续期真的在跑

自动续期配好之后,真正的风险变成"它悄悄不工作了"。建议做两件事:

1. 定期看一眼计划续期时间

~/.acme.sh/acme.sh --list

输出里 Renew 字段会给出下次续期时间。新版 acme.sh 会读取 CA 的 ARI(ACME Renewal Info)建议窗口,例如我的证书显示:

weiserv.com  "ec-256"  *.weiserv.com  LetsEncrypt.org  2026-09-05T15:30:18Z  2026-11-03T21:17:45Z

意思是建议在 2026-11-03 之后续期——说明续期计划已经排上了,不再是空的。

2. 加一个到期预警(别只依赖续期)

续期可能失败,预警是最后一道防线。用纯标准库就能读证书剩余天数:

import ssl, socket, datetime

def days_left(host, port=443):
    ctx = ssl.create_default_context()
    with socket.create_connection((host, port), timeout=10) as sock:
        with ctx.wrap_socket(sock, server_hostname=host) as s:
            cert = s.getpeercert()
    not_after = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
    return (not_after - datetime.datetime.utcnow()).days

print(days_left("weiserv.com"))

把剩余天数挂进定时任务,低于 20 天就告警。

⚠️ 有个坑:证书已经过期时,上面的代码会在握手阶段直接抛 SSLCertVerificationErrorgetpeercert() 根本没机会执行。
想读"过期了多久",必须捕获异常后降级成 CERT_NONE 再连一次。这个坑的完整解法(双 context 降级读取、退出码分级、对公开过期站点的实证)在《NET::ERR_CERT_DATE_INVALID 排查实录》一文里展开。


小结

阶段 动作 关键点
排查 openssl 看有效期 + --list 看计划 + crontab 看任务 三者缺一不可,只看有效期会漏掉"没自动续期"
排干扰 确认 certbot 是否真的是证书的管理者 空跑的 timer 是最危险的假象
配置 --install-cronjob + 确认 reloadcmd 没有 reloadcmd,续了也不生效
验证 --renew --force 真跑一次 该版本无 --dry-run
兜底 到期预警脚本 续期会失败,预警是最后防线

整套做下来不到半小时,换来的是再也不用记住"该续证书了"这件事

如果你也在用 acme.sh,现在就可以花一分钟跑一下 acme.sh --list 看看 Renew 字段——如果它是空的,那你的证书就是在裸奔。

系列阅读


本文环境:acme.sh v3.1.4 / Nginx 1.18.0 / Ubuntu 20.04.6 LTS / Let's Encrypt(ECC 通配符,DNSPod DNS-01 校验)
文中命令与输出均来自真实环境实测;厂商 API 相关规则请以官方文档为准。

广告位占位 · post-inline