证书续期别再靠人肉: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 记录失败"。
定位过程:
curl error 3对应CURLE_URL_MALFORMAT(URL 格式错误),说明调用 DNS 厂商 API 时构造出的请求有问题;- 检查凭据是否还在:
bash grep -o 'SAVED_Tencent_SecretI[dK]*' ~/.acme.sh/account.conf
凭据字段存在; - 检查 DNS 插件是否还在:
bash ls ~/.acme.sh/dnsapi/ | grep tencent # dns_tencent.sh
插件存在; - 检查是否残留了上次的挑战记录:
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 天就告警。
⚠️ 有个坑:证书已经过期时,上面的代码会在握手阶段直接抛
SSLCertVerificationError,getpeercert()根本没机会执行。
想读"过期了多久",必须捕获异常后降级成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 字段——如果它是空的,那你的证书就是在裸奔。
系列阅读
- NET::ERR_CERT_DATE_INVALID 排查实录:证书过期的根因二分与独立到期预警——红屏出现之后怎么办:根因二分排查法、能读出"过期了多久"的检测脚本、独立到期预警。
本文环境:acme.sh v3.1.4 / Nginx 1.18.0 / Ubuntu 20.04.6 LTS / Let's Encrypt(ECC 通配符,DNSPod DNS-01 校验)
文中命令与输出均来自真实环境实测;厂商 API 相关规则请以官方文档为准。