证书运维

acme.sh 说续期成功了,证书却还是旧的

acme.sh 的定时任务显示成功,浏览器里的证书却没变。本文按「有没有续下来」和「续下来了但没生效」两条岔路,逐一排查路径不匹配、reloadcmd 未执行、宝塔证书目录、版本过旧等成因,每一步都给出确认命令。

acme.sh 的定时任务跑完没报错,日志里写着 Cert success,浏览器里看到的却还是那张旧证书 —— 甚至已经过期了。

这句「成功」和你看到的现象不矛盾,因为它们说的不是同一件事。acme.sh 的成功只到「新证书拿到手并写进文件」为止,把它交给正在跑的服务是另一件事,而那一步失败时通常是静默的。

所以排查的第一步不是看日志,是先判断自己在哪条岔路上。

先分岔:是没续下来,还是续下来了没生效

# ① acme.sh 自己那份,什么时候签的
acme.sh --info -d example.com | grep -E "Le_CertCreateTimeStr|Le_NextRenewTimeStr"

# ② 服务器实际发给访客的那份
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates
① 和 ② 的关系你在这条岔路
① 是新的,② 是旧的续下来了,但没生效 → 第 1、2、3 节
① 也是旧的根本没续下来 → 第 4、5 节
--info 什么都没输出域名不在这个 acme.sh 账户下,或者你查的用户不对 → 第 4 节第一段

别用浏览器判断 ②。浏览器会缓存连接和中间证书,你刷新看到的可能不是访客看到的。上面那条 openssl 命令问的是服务器当下真正发出的东西。

1. reloadcmd 没配,或者配了但没执行

这是最常见的一种。--install-cert 时如果没给 --reloadcmd,acme.sh 会把新证书写到目标路径,然后什么也不做 —— nginx 仍然握着启动时读进内存的那份旧证书。

acme.sh --info -d example.com | grep Le_ReloadCmd

空的就是没配。补上:

acme.sh --install-cert -d example.com \
  --key-file       /path/to/key.pem \
  --fullchain-file /path/to/fullchain.pem \
  --reloadcmd      "nginx -s reload"

--install-cert 是幂等的,重复执行只是更新配置,不会重新签发。

2. 宝塔面板的 nginx 不吃 force-reload

很多教程里的 reloadcmd 写的是 service nginx force-reload。宝塔自带的 nginx 不支持这个动作,命令直接失败,而 acme.sh 不会因为 reloadcmd 失败就把整次续期判为失败。

宝塔环境下改成:

--reloadcmd "/etc/init.d/nginx reload"

改完之后手动跑一次 reloadcmd 本身,确认它真的返回 0:

/etc/init.d/nginx reload; echo "退出码 $?"

3. 宝塔把证书存在自己的目录里

宝塔管理的站点,证书读的是它自己的路径:

/www/server/panel/vhost/cert/example.com/fullchain.pem
/www/server/panel/vhost/cert/example.com/privkey.pem

而 acme.sh 默认把证书放在 ~/.acme.sh/example.com_ecc/(ECC 证书带 _ecc 后缀)。两边是两份文件,acme.sh 更新了自己那份,宝塔那份纹丝不动。

不要手工复制,那只能解决这一次。让 --install-cert 直接写进宝塔的路径:

acme.sh --install-cert -d example.com --ecc \
  --key-file       /www/server/panel/vhost/cert/example.com/privkey.pem \
  --fullchain-file /www/server/panel/vhost/cert/example.com/fullchain.pem \
  --reloadcmd      "/etc/init.d/nginx reload"

--ecc 不能省,如果你的证书是 ECC 的。acme.sh 用它区分同一域名下的 RSA 与 ECC 两套目录,漏掉时会提示找不到证书,或者安装的是另一套。

4. 定时任务里的路径和实际安装位置不一致

acme.sh 的定时任务长这样:

0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

如果安装时用的是别的用户、或者中途换过 --home,crontab 里这个路径就会指向一个空目录 —— 任务照常运行、照常退出 0、什么也没做。而 > /dev/null 把唯一能看出问题的输出也丢掉了。

# crontab 里写的是哪个路径
crontab -l | grep acme

# 证书实际在哪
ls -d ~/.acme.sh/*/ 2>/dev/null

两者不一致就改 crontab。顺便把 > /dev/null 换成一个日志文件,下次就不用猜了:

0 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" >> /var/log/acme.log 2>&1

root 装的 acme.sh,定时任务也必须在 root 的 crontab 里。sudo crontab -lcrontab -l 看到的是两份不同的表,这是「我明明配了定时任务」最常见的误会来源。

5. acme.sh 版本太旧

acme.sh 装上之后不会自己升级,除非开了自动升级。旧版本会在 CA 那边的接口调整后突然失效,而且报错通常发生在网络层,看起来像是网络问题。

acme.sh --version
acme.sh --upgrade
acme.sh --upgrade --auto-upgrade   # 以后自动升级

升级不会动已有证书和配置。

确认真的修好了

不要等下一次自动续期。强制跑一次完整流程:

acme.sh --renew -d example.com --ecc --force

然后回到本文开头那条 openssl s_client 命令,确认服务器发出的日期变了。只有这一条能证明修好了 —— acme.sh 自己的日志只能证明前半段。

如果你不想再维护这条链路

上面五种成因有一个共同点:它们都不是 acme.sh 的 bug,而是这条链路上环节太多。签发、写文件、重载服务、定时任务、用户与路径,任何一环安静地断掉,结果都是「日志说成功,证书是旧的」,而你只会在证书过期那天知道。

它还依赖一件事:那台跑 cron 的机器要一直活着、一直被维护。

本站的证书申请把这条链路缩短了一半 —— 验证环节用一条 CNAME 委派后不再需要人工,签发与续期由我们完成。部署那一步仍然在你这边,这一点我们不打算含糊其辞:证书还是要装到你的服务器上。

真正的区别是,需要一直活着的那台机器不再是你的。

另外,无论你用哪种方式续期,都建议把域名加入证书监控:它检查的是访客实际拿到的那张证书,也就是本文开头的 ②。这正是所有「续期脚本说成功了」的故障唯一逃不过的那一关。