告别 Certbot:acme.sh 泛域名证书自动续期实战,DNS API 验证与续期静默失效排查

告别 Certbot:用 acme.sh 给个人站做泛域名证书自动续期实战

免费 SSL 证书这件事,大部分站长都是从 Certbot 起步的。Certbot 确实好用,但它有一个绕不开的限制:想签泛域名证书(*.example.com),必须用 DNS-01 验证方式,而 Certbot 的 DNS 插件配置起来比较死板,很多国内 DNS 服务商的 API 它根本不支持,导致你只能手动去 DNS 面板加 TXT 记录——每 90 天手动续一次,烦得要命。

acme.sh 是另一个 ACME 客户端,纯 Shell 实现,零依赖,支持 100 多家 DNS 服务商的 API 自动验证。配好之后,证书续期完全是后台自动跑,你三个月都不会想起它的存在。这篇文章讲它在我几台 VPS 上的完整落地过程,包括最容易翻车的几个点。

一、为什么选 acme.sh 而不是 Certbot

先把两者的差异摆清楚,免得你以为我在贬低 Certbot:

  • 依赖:Certbot 是 Python 程序,装的时候会带一堆 pip 依赖,还容易和系统的 Python 版本打架;acme.sh 是纯 Shell 脚本,几十 KB,扔到哪个 Linux 上都能跑。
  • DNS API 支持:Certbot 的 DNS 验证需要装对应的 dns- 插件包,不少国内服务商(阿里云、DNSPod、华为云)的支持要么缺失要么要额外装第三方插件;acme.sh 内置了 dnspod、aliyun、cloudflare 等一大票 API,一个参数就搞定。
  • 证书格式:acme.sh 默认签发的是 ECC(ECDSA)证书,比 RSA 更快更小,对性能敏感的站有实际收益。
  • 部署钩子:acme.sh 的 --install-cert 可以指定续期后自动执行的 reload 命令,把「续期 → 部署 → 重载 Nginx」串成一条,很省心。

当然 Certbot 也有优势,比如它是 EFF 官方项目、和 Nginx/Apache 的自动配置集成做得更好(一条 certbot --nginx 全搞定)。但对有泛域名需求、或者用国内 DNS 的站长来说,acme.sh 更顺手。

二、安装

官方推荐方式是 curl 在线安装:

curl https://get.acme.sh | sh -s email=你的邮箱@example.com

# 重新加载 shell,让 acme.sh 的命令可用
source ~/.bashrc

# 确认版本
acme.sh --version

安装脚本干了三件事:把程序装到 ~/.acme.sh/、创建别名、注册一个 cron 任务(这就是自动续期的核心)。看一下你的 crontab 就知道了:

crontab -l | grep acme
# 大概长这样:
# 8 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

这条 cron 每天凌晨跑一次,检查所有证书的剩余有效期,离到期 30 天以内就自动续期。所以你只要不动 crontab,续期就是自动的。

第一个坑:有些精简系统没装 curl,或者系统时间不对。acme.sh 的安装和续期都依赖正确的系统时间(TLS 握手和 ACME 协议对时间敏感)。装之前先看一眼:

date
# 如果时间跑偏了,先修好时间再装
timedatectl status

三、申请泛域名证书(以 Cloudflare 为例)

泛域名证书必须走 DNS-01 验证。以 Cloudflare 做 DNS 为例,先去后台拿一个 API Token(建议用「编辑区域 DNS」权限,不要用全局 API Key)。

export CF_Token="你的cloudflare_api_token"
export CF_Account_ID="你的account_id"   # 有些场景需要

acme.sh --issue --dns dns_cf \
  -d example.com \
  -d '*.example.com' \
  --keylength ec-256

注意 *.example.com 这个通配符不包含裸域名 example.com,所以两个都要写。这是新手最常犯的错:只签了通配符,结果访问 https://example.com(不带 www)时证书不匹配,浏览器报错。

执行过程中,acme.sh 会自动调用 Cloudflare API 创建一条 _acme-challenge 的 TXT 记录,等 DNS 生效、验证通过后自动删除。整个过程几十秒,你能看到它一步步打印日志。

第二个坑:DNS 传播慢导致验证失败。有些 DNS 服务商的 TXT 记录生效要几分钟,acme.sh 默认等待时间可能不够,会报 Timeout 或 Verify error。可以加参数延长等待:

acme.sh --issue --dns dns_cf -d example.com -d '*.example.com' \
  --dnssleep 120

国内用 DNSPod 的换成 --dns dns_dp,环境变量是 DP_Id 和 DP_Key;阿里云是 --dns dns_ali,变量是 Ali_Key 和 Ali_Secret。具体名字可以去 acme.sh 的 wiki 查,变量名大小写千万别写错,写错了它不会报「变量不存在」,而是直接验证失败,很难查。

四、把证书部署给 Nginx

签完证书后,不要直接引用 ~/.acme.sh/ 目录下的文件。这个目录是 acme.sh 的内部工作区,续期时会重建,而且权限是私有的。正确做法是用 --install-cert 把证书拷贝到你自己的目录,并绑定 reload 命令:

mkdir -p /etc/nginx/ssl/example.com

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

这里的 --ecc 很重要:因为前面用 --keylength ec-256 签的是 ECC 证书,不写 ecc 它可能去 RSA 目录找,找不到就报错。

Nginx 配置里引用这两个文件:

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate     /etc/nginx/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
}

第三个坑:reload 权限。如果你是用 root 跑 acme.sh,systemctl reload nginx 没问题。但如果你是用普通用户跑(更安全),reload 会因为权限不足失败。这时候需要配 sudo 免密:

# /etc/sudoers.d/acme-nginx
acmeuser ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx
# 然后 reloadcmd 改成
--reloadcmd "sudo systemctl reload nginx"

另外,证书文件权限建议收紧,别让全站可读:

chmod 700 /etc/nginx/ssl/example.com
chmod 600 /etc/nginx/ssl/example.com/privkey.pem
chmod 644 /etc/nginx/ssl/example.com/fullchain.pem

五、验证续期真的能跑

这是最容易被忽略的一步。很多人配完就走了,等到 90 天后证书过期才发现「续期根本没生效」,网站被浏览器拦了。所以一定要手动演练一次:

# 强制续期一次(加 --force 忽略剩余天数)
acme.sh --renew -d example.com --ecc --force

# 看有没有真的重载 nginx、证书时间有没有更新
ls -l /etc/nginx/ssl/example.com/
openssl x509 -in /etc/nginx/ssl/example.com/fullchain.pem -noout -dates

如果 notAfter 时间变成了新的,说明整条链路是通的。我还建议加一个到期告警,防止哪天 acme.sh 静默失败你自己不知道:

# 一行检查,剩余天数小于 15 就告警(可塞进每日 cron)
end=$(openssl x509 -in /etc/nginx/ssl/example.com/fullchain.pem -noout -enddate | cut -d= -f2)
left=$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))
[ "$left" -lt 15 ] && echo "证书仅剩 $left 天,请检查!"

六、证书过期最常见的原因排查顺序

如果你发现 acme.sh 该续期时没续成,按这个顺序查:

  1. cron 还在不在:crontab -l,看看那条 acme.sh --cron 是否还在。有的服务器软件(如某些面板)会覆写 crontab,把它冲掉。
  2. 日志说了什么:acme.sh 的日志在 ~/.acme.sh/acme.sh.log,直接看尾部,报错一般很明确。也可以手动跑一次 acme.sh --cron --home ~/.acme.sh 看输出。
  3. DNS API 凭证失效:Cloudflare Token 过期、DNSPod Key 被重置,都会导致验证失败。手动跑一次立刻就能复现。
  4. 系统时间错乱:时间偏差过大时 TLS 握手失败,续期报各种奇怪错误。先 date 一下。
  5. 80/443 端口被占用或重定向异常:如果你用的是 HTTP-01 验证(不是 DNS-01),那 80 端口必须能被外网访问到,不能被防火墙拦,也不能被强制 301 到 443。
  6. reloadcmd 失败:证书其实续成功了,但 nginx 没重载,还在用旧证书。这种最坑,因为 acme.sh 认为成功了,实际上网站还是过期的。

七、常见误区:acme.sh 与 Certbot 能共存吗

能,但没必要,而且很容易出事。我见过有站长两个都装了,结果同一个域名被两个客户端反复签发,导致 Let's Encrypt 的速率限制被触发(同一注册域名每周最多 50 张证书),最后谁也签不出来,网站直接断证书。

如果你确实要从 Certbot 迁到 acme.sh,建议按这个顺序来:

  1. 先用 acme.sh 签出证书,验证能正常签发(--issue 成功即可,先不部署)。
  2. 把 Nginx 的 ssl_certificate 路径切到 acme.sh 部署出来的新路径,reload 验证网站正常。
  3. 确认无误后,停掉 Certbot 的自动续期:systemctl disable --now certbot.timer(如果是 systemd timer)或者 crontab -l 里删掉 certbot 的条目。
  4. 最后再考虑 apt remove certbot,但注意别把已有的 /etc/letsencrypt 目录一起删了,万一要回滚就没退路。

关键原则:同一时间只让一个 ACME 客户端管同一个域名。两个客户端抢着续期,是证书混乱最常见的根因。

八、用 webhook 做续期通知

前面提到的到期告警是一道防线,但更主动的做法是让 acme.sh 在续期成功后直接通知你。它内置了 webhook 支持,比如接到一个自定义 URL:

export DEPLOY_DOCKER_CONTAINER_LABEL="sh.acme.autoload.domain=example.com"

# 或者用通用通知:续期成功后 POST 到你的接口
acme.sh --install-cert -d example.com --ecc \
  --key-file       /etc/nginx/ssl/example.com/privkey.pem \
  --fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
  --reloadcmd      "systemctl reload nginx && curl -s 'https://你的通知接口/?msg=renewed'"

把通知拼在 reloadcmd 后面是最简单的做法——reload 成功才发通知,万一 reload 失败你也能从「没收到通知」这件事察觉异常。对个人站来说,接一个 Telegram Bot 或者企业微信机器人的 Webhook 都行,成本几乎为零。

九、小结

acme.sh 的好处可以总结成一句话:配一次,忘三年。泛域名证书 + DNS API 自动验证 + reload 钩子,三件套配好之后,你基本上不需要再操心证书这件事。对同时管着好几个域名、好几个二级域名的站长来说,这个省心程度是 Certbot 手动加 TXT 记录完全比不了的。

唯一要记住的,是演练一次强制续期,再挂一个到期告警。自动化的东西最怕的就是「以为它在跑,其实早就失败了」——证书过期导致整站报错,是所有站长事故里最冤枉的一种。

Last modification:September 28th, 2026 at 10:23 pm

Leave a Comment