Let's Encrypt 证书自动化实战:acme.sh 泛域名签发、DNS API 验证与到期告警全流程

证书过期是站长最不该犯的错

对一个个人站长来说,HTTPS 证书过期几乎是最低级却最致命的失误:浏览器直接弹出"您的连接不是私密连接"的红页,访客秒退,搜索引擎也会降低信任度。而免费证书 Let's Encrypt 的绝大多数申请方式——不管是宝塔面板的一键、还是 certbot 的默认配置——都是手动触发或者三个月自动续期。自动续期一旦失败(比如 DNS API 挂了、验证文件被 CDN 缓存、80 端口被占用),你又没收到告警,那到期那天就是事故现场。

本文介绍用 acme.sh 做全自动证书管理:泛域名证书、DNS API 验证、Nginx 无缝重载、到期告警一次配齐。acme.sh 是纯 shell 脚本实现的 ACME 客户端,不依赖 Python,体积小、依赖少,特别适合小内存 VPS。

第一步:安装 acme.sh

官方一键脚本会把它装到 ~/.acme.sh:

curl https://get.acme.sh | sh -s email=you@example.com

它会自动做三件事:下载脚本到 ~/.acme.sh、创建一个 cron 定时任务每天检查续期、把 acme.sh 加进 shell 环境。装完记得重新登录或 source ~/.bashrc,让 acme.sh 命令生效。也可以换成更短的别名 alias acme.sh=~/.acme.sh/acme.sh。

默认 CA 会切换到 ZeroSSL(需要注册账号),如果只想用 Let's Encrypt,先执行:

acme.sh --set-default-ca --server letsencrypt

第二步:DNS API 验证,签发泛域名证书

签发证书有两种验证方式:HTTP 验证(在网站根目录放一个验证文件,需要 80 端口可达)和 DNS 验证(在域名解析里加一条 TXT 记录)。泛域名证书只能走 DNS 验证,因为 *.example.com 无法对应任何具体的 HTTP 路径。而且 DNS 验证不受 CDN、防火墙、80 端口占用的影响,是最稳的方式。

acme.sh 内置了几十家 DNS 服务商的 API,以 Cloudflare 为例。先去 Cloudflare 面板创建一个 API Token(权限给 Zone:DNS:Edit),然后用环境变量传入:

export CF_Token="你的_api_token"
export CF_Account_ID="你的_account_id"

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

--keylength ec-256 表示用 ECDSA P-256 密钥,比传统的 RSA 2048 更短、握手更快,现代浏览器全支持。泛域名 *.example.com 加上根域名 example.com,一张证书就能覆盖 www、api、blog 等所有子域,非常方便。

acme.sh 会自动加上 TXT 记录、等待 DNS 生效、完成验证,然后把证书和私钥保存到 ~/.acme.sh/example.com_ecc/。但注意:不要直接把这个目录里的文件配给 Nginx,因为续期时 acme.sh 会更新内部文件,而它推荐用 --install-cert 把证书复制到一个稳定路径。

第三步:安装证书并使用 reloadcmd 自动重载 Nginx

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

这条命令做了三件事:把证书私钥、完整证书链复制到 /etc/nginx/ssl/,并注册一条 reloadcmd——之后每一次自动续期成功,acme.sh 都会自动执行这条命令,让 Nginx 加载新证书。nginx -t 先做配置检查,通过才 reload,避免把坏配置推上线。

Nginx 侧的配置:

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

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

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    # ... 其余站点配置
}

这里用 fullchain.pem(叶证书 + 中间证书)而不是 cert.pem,是为了让浏览器能验证完整信任链,否则部分客户端会报"证书链不完整"。

第四步:自动续期真的会跑吗

acme.sh 安装时会写一条 cron,你可以确认一下:

crontab -l | grep acme
# 正常输出类似:
# 23 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null

这条任务每天跑一次,acme.sh 会检查所有已签发证书,只有当剩余有效期少于 30 天时才真正续期,所以不用担心频繁签发触发 Let's Encrypt 的速率限制。手动强制续期测试:

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

续期日志在 ~/.acme.sh/acme.sh.log,出问题先看这里。常见失败原因:DNS API Token 权限不足或已过期、域名解析被其他厂商托管但 Token 配了错的厂商、网络不通 Let's Encrypt 的验证服务器。

第五步:给它加上到期告警(关键!)

自动续期再好,也有失败的可能。你必须有一个"如果证书快到期还没续成功,就通知我"的兜底机制。最简单的是写一个检查脚本,把剩余天数推送到你的告警渠道(邮件、Telegram、钉钉、企业微信机器人)。

#!/bin/bash
# /root/check_cert.sh —— 检查证书剩余天数,少于 15 天告警
DOMAIN="example.com"
PORT=443
END=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN:$PORT" 2>/dev/null \
      | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
[ -z "$END" ] && { echo "无法获取 $DOMAIN 证书"; exit 1; }

END_TS=$(date -d "$END" +%s)
NOW_TS=$(date +%s)
DAYS=$(( (END_TS - NOW_TS) / 86400 ))
echo "$DOMAIN 证书剩余 $DAYS 天"

if [ "$DAYS" -lt 15 ]; then
    # 这里替换成你的告警方式,例如 curl 一个 webhook
    curl -s -X POST "https://your-webhook-url" \
      -H 'Content-Type: application/json' \
      -d "{\"msg\":\"$DOMAIN 证书仅剩 $DAYS 天,请检查续期!\"}"
fi

加进 cron 每天跑:

0 9 * * * /root/check_cert.sh >> /var/log/cert_check.log 2>&1

注意这个脚本是从外部(真实访问 443 端口)读取证书的剩余天数,它验证的是"浏览器实际看到的那张证书",比只看文件更靠谱——能顺带发现证书没被 reload、Nginx 还在用旧证书这类问题。

进阶:多域名批量管理与证书复用

如果一台服务器上跑着好几个站,可以用一张泛域名证书覆盖主域名下的所有子域,也可以为不同根域名分别签发。查看当前管理的所有证书:

acme.sh --list

删除某张证书(不再续期):

acme.sh --remove -d example.com --ecc

一个小技巧:证书私钥文件权限要收紧,只有 root 可读,Nginx 用 root 启动后降权读取没问题:

chmod 600 /etc/nginx/ssl/example.com.key

和 certbot 比,为什么推荐 acme.sh

certbot 功能强大但依赖 Python 环境,在没有装 Python 或 Python 版本混乱的老系统上容易出问题;它也偏重"为 Apache/Nginx 自动改配置"的模式,对自定义 Nginx 布局反而别扭。acme.sh 是纯 shell、零依赖,配合 --install-cert --reloadcmd 的模式,把"签发"和"部署"解耦得很干净,重装系统、迁移服务器时也容易复现。对于个人站长的场景,acme.sh 更轻、更可控。

小结

全自动证书管理就四步:装 acme.sh、用 DNS API 签发泛域名证书、用 --install-cert 部署并挂上 reloadcmd、再补一个外部到期告警兜底。做完这套,三年都不用再碰证书这件事——除非你敢赌 DNS Token 永远不过期。顺手把 check_cert.sh 也加上吧,它救的不只是证书,是整个站点的可信度。

补充一:HTTP 验证方式与 80 端口的坑

如果你不想用 DNS API(比如域名解析不在大厂、拿不到 API Token),也可以用 HTTP 验证。acme.sh 提供两种 HTTP 模式:

  • --webroot:把验证文件写到网站根目录的 .well-known/acme-challenge/ 下,需要你知道网站的真实根路径。
  • --standalone:acme.sh 自己起一个临时 HTTP 服务监听 80 端口来接收验证请求,要求 80 端口空闲。
# webroot 模式
acme.sh --issue -d example.com -w /var/www/html --keylength ec-256

# standalone 模式(80 端口空闲时)
acme.sh --issue -d example.com --standalone --keylength ec-256

HTTP 验证的坑主要有三个:一是 --standalone 时 Nginx 正占着 80 端口,导致起不来,必须先 systemctl stop nginx;二是站点接了 CDN,验证请求被 CDN 拦到边缘节点,源站根本收不到;三是 .well-known 目录被伪静态规则重写到了业务系统。而 DNS 验证没有这些问题,这也是前面推荐它的原因。只有当域名托管服务商不提供 API、或你完全不想碰解析记录时,才退回 HTTP 验证。

补充二:证书链、OCSP 装订与 HTTP/2

拿到证书只是开始,让 TLS 握手又快又稳还需要几项配套。推荐在 Nginx 里加上 OCSP Stapling:它让 Nginx 主动去 CA 查询证书吊销状态并缓存结果,握手时直接带给浏览器,省掉浏览器自己去查询的一跳,既快又保护了访客隐私。

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/example.com.fullchain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

注意 ssl_trusted_certificate 要用完整链文件,否则 OCSP 响应无法验证。resolver 必须配置,因为 Nginx 需要用它解析 CA 的 OCSP 服务器地址。ssl_protocols 建议只留 TLSv1.2 和 TLSv1.3,把存在已知问题的 TLSv1.0/1.1 关掉。加密套件用 ssl_ciphers 指定现代组合,或者干脆交给 TLSv1.3 的默认套件。部署完可以用 openssl s_client -connect example.com:443 -servername example.com 检查证书链是否完整,用在线工具检查协议与套件评级。

补充三:HSTS 与证书的配套考虑

有了有效证书,就可以上 HSTS(HTTP Strict Transport Security),告诉浏览器在指定时间内强制用 HTTPS 访问本站,避免用户第一次被降级劫持后长期留在 HTTP:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

但 HSTS 有个"一旦启用就难以撤销"的特性:max-age 一旦下发,浏览器在有效期内会记住并强制 HTTPS,如果之后你的证书再次出问题,访客连"点继续访问"的选项都没有,直接进不去。所以经验做法是:先用一个很短的 max-age(比如 300 秒)测试几天,确认全站 HTTPS 无死角,再逐步加到 31536000。确认无误前不要加 preload,preload 列表是硬编码进浏览器的,撤销周期以月计。

把证书自动化和 HSTS 放在一起考虑,本质上是同一件事:HTTPS 一旦成为强约束,任何一次证书疏漏都从"警告"升级成"网站打不开"。这也正是本节开头那份告警脚本存在的意义——它让你在窗口关闭之前,就发现问题。

Last modification:October 9th, 2026 at 12:24 pm

相关文章

暂无相关文章

Leave a Comment