泛域名 SSL 证书自动化避坑:Acme.sh + DNS API 申请、90 天自动续签与失败排查

为什么 *.example.com 泛域名证书必须用 DNS-01 验证?本文讲清 acme.sh 安装、Cloudflare/DNSPod/阿里云 DNS API 配置、crontab 续签与 Nginx/Xray 证书 reload,以及 DNS API 权限过期、CA 限制等失败排查。

很多买国外 VPS 的朋友,第一次碰到泛域名证书(就是那张能盖住 a.example.com、b.example.com、随便什么子域名的 *.example.com)时都会懵:明明单域名证书用 Certbot 一条命令就搞定了,为什么一上通配符就各种报错?为什么网上教程全在让你去 DNS 后台加 TXT 记录?这篇文章把泛域名证书的来龙去脉、以及用 acme.sh 把这事儿彻底自动化(装好之后 90 天自动续、不用管)一次讲透,顺便把最常见的几个翻车点列出来。

延伸阅读

更多相关攻略推荐:BGP 是互联网的"高德地图":打开网页时,数据是这样找路的【优化线路 02】BGP / AS4837 常规线路年付 10 美元高级玩家玩出花:如何在 VPS 上广播自己的 IP 地址?BYOIP计算机科学两大难题之一:缓存失效与 CDN 瞬时刷新奥秘Cloudflare 国内慢、还弹验证码?CDN 替代方案盘点与两台

一、为什么泛域名证书必须用 DNS 验证?

先说清楚一件事:不是 acme.sh 故意刁难你,而是 Let's Encrypt / ZeroSSL 这些免费 CA 对泛域名证书强制要求 DNS-01 验证,HTTP-01 验证根本不支持通配符。原因得从两种验证方式的原理讲起。

HTTP-01 验证的思路是:CA 给你一串随机字符,要求你在 http://example.com/.well-known/acme-challenge/xxxx 这个 URL 下能访问到一个对应的文件,CA 自己的服务器过来抓取,抓到了就证明"你控制这台服务器 / 这个域名"。问题来了——泛域名 *.example.com 意味着"example.com 下面的所有子域名",而 HTTP-01 的验证请求是打到一个具体主机上的(比如 www.example.com 那台机器)。CA 没法通过验证"某一台主机"来推断"你对每一个可能的子域名都有控制权",所以它直接拒绝用 HTTP-01 发泛域名证书。

DNS-01 验证的思路更聪明:你不往服务器塞文件,而是去 DNS 里给 _acme-challenge.example.com 加一条 TXT 记录,值就是 CA 给的那串随机字符。CA 去查这条 TXT 记录,查到了就证明"你掌控这个域名的 DNS"——而掌控了 DNS,本质上就等于掌控了这个域名下所有子域名的解析。验证完,工具会自动把那条临时 TXT 记录删掉,干干净净。这个机制天然适合泛域名,所以所有通配符证书都走 DNS-01。

手写 TXT 记录当然能申请,但每 90 天就要手动加一次、删一次,纯属自虐。真正省心的方式是让工具通过 DNS 服务商的 API 自动完成加记录、查记录、删记录——这就是 acme.sh 的拿手好戏。

二、acme.sh 是什么、怎么装?

acme.sh 是一个纯 Shell 写的 ACME 协议客户端,几乎零依赖(走 DNS API 这条路只需要 bash、curl、openssl,连 socat 都不用)。它最香的地方有三点:一是极轻量,装完占几 MB;二是支持上百家 DNS 服务商的 API 自动验证;三是自带定时续签,装好基本可以忘了它。

安装就一行(root 和普通用户都能装,区别只是证书默认放哪儿):

curl https://get.acme.sh | sh

或者用 wget:

wget -O - https://get.acme.sh | sh

装完后它会把自己放到 ~/.acme.sh/,并自动给你的 shell 加一个 alias、顺手把续签的 cron 也注册了。为了让当前终端立刻能用 acme.sh 命令,重载一下配置:

source ~/.bashrc
acme.sh --version

如果你的 shell 比较特别(比如 zsh 且 alias 没生效),也可以直接用完整路径调用:~/.acme.sh/acme.sh。后面所有命令我都用 acme.sh 简写。

三、三大 DNS 服务商的 API 配置

acme.sh 通过环境变量读取各家 DNS 的 API 凭证,然后用 --dns 插件名 指定用哪家。先配置好环境变量,再发证书。下面列国内玩家最常用的三家。

Cloudflare(推荐用 Scoped API Token,别用 Global Key)

Cloudflare 现在强烈建议用"区域级 API Token"而不是那个能改你账号一切设置的 Global API Key。在 CF 后台 Create Token,权限给 Zone → DNS → Edit,并且 Scope 限定到你要签发的具体 zone。然后导出两个变量:

export CF_Token="你的_cloudflare_API_Token"
export CF_Zone_ID="你的_zone_id"

如果你一个账号下挂了好几个 zone,懒得填 Zone ID,可以用 Account ID 代替:export CF_Account_ID="你的_account_id"。老教程里常见的 CF_Key + CF_Email 组合(Global API Key)现在还能用,但权限太大、一旦泄露等于把整个 CF 账号交出去,不推荐

DNSPod.cn(传统 DNSPod 账号)

很多老域名还托管在 dnspod.cn。在 DNSPod 控制台"用户中心 → API Token"里创建一条,会得到一对"ID"和"Token":

export DP_Id="你的_dnspod_token_id_数字"
export DP_Key="你的_dnspod_token_字符串"

注意 DP_Id 是数字、DP_Key 是字符串,别填反。对应的插件名是 dns_dp

阿里云 Aliyun

阿里云用 RAM 的 AccessKey(ID + Secret),记得给这个 AccessKey 至少加上 AliyunDNSFullAccess 或细粒度的 alidns:* 权限,否则会卡在"无权修改解析":

export Ali_Key="你的_AccessKey_Id"
export Ali_Secret="你的_AccessKey_Secret"

对应插件名 dns_ali

腾讯云 DNSPod(走腾讯云 CAM)

如果你的 DNSPod 区域其实是挂在腾讯云账号下的,那要用腾讯云统一的 CAM 密钥,而不是上面的 DP_Id/DP_Key:

export Tencent_SecretId="你的_SecretId"
export Tencent_SecretKey="你的_SecretKey"

对应插件名 dns_tencent。选错插件名是导致"明明密钥对了却签不下来"的高发原因,照着上面三家对号入座。

小提示:这些凭证首次签发成功后,acme.sh 会自己存进 ~/.acme.sh/account.conf,后续自动续签会直接复用,不用你每次都 export。

四、一条命令申请泛域名证书

环境变量设好后,一条命令同时把根域名和泛域名都签了(*.example.com 一定要用单引号包住,否则 shell 会当成通配符去展开文件):

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

想用更省体积、握手更快的 ECDSA 证书,加 --keylength ec-256

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

dns_cf 换成 dns_dp / dns_ali / dns_tencent 即可对应不同服务商。成功后证书躺在 ~/.acme.sh/example.com/ 下面(有 fullchain.cerexample.com.key 等)。但记住一句话:不要直接在 Nginx 配置里引用这个目录的文件,因为目录结构、权限、文件名以后可能变。正确姿势是下一步"安装"到稳定路径。

五、自动续签:cron 与 install-cronjob

acme.sh 安装时就帮你注册了 cron 定时任务,可以用 crontab -l 看到类似这样一行:

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

如果哪天你发现没注册上(比如换过家目录),手动补一下:

acme.sh --install-cronjob

续签逻辑是这样的:证书有效期 90 天,acme.sh 默认在剩余不到 30 天时才真正去续(也就是大约第 60 天左右动手),所以你平时根本不用管它。想手动模拟一次续签(不强制、未到时间会跳过):

acme.sh --renew -d example.com

想无视时间窗口强制重签(调试或换 CA 时有用):

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

也可以直接跑 acme.sh --cron 触发一遍完整的定时逻辑。总之,只要你别手贱删了 cron,证书基本不会过期。

六、续签后自动 reload:Nginx 与 Xray 证书热加载

这里有个极易被忽略的坑:acme.sh 把证书续新了,但 Nginx / Xray 还攥着内存里旧证书的副本,不重新加载的话,浏览器看到的还是快过期的老证书,等于白续。所以你得告诉 acme.sh:"续签成功后,顺手让相关服务重新读一下证书"。

关键原则:用 reload(热加载)而不是 restart 整个服务,避免正在跑的连接被一刀切断。两者区别很大:

  • Nginx 支持优雅 reload:nginx -s reloadsystemctl reload nginx,旧 worker 会把当前请求处理完再退出,新请求用新证书,用户无感知。
  • Xray / v2ray 的证书是在进程启动时一次性读进内存的,没有"重读证书不重启"的机制,所以只能重启进程:systemctl restart xray。注意这只是重启这个服务(毫秒级),不是重启整台 VPS;客户端连接会由握手逻辑自动重连,影响极小。

把这个动作绑进"安装证书"那一步的 --reloadcmd 里,以后每次续签都会自动执行:

acme.sh --installcert -d example.com \
  --key-file /etc/ssl/example.com.key \
  --fullchain-file /etc/ssl/example.com.cer \
  --reloadcmd "systemctl reload nginx"

如果你的证书同时喂给 Nginx 和 Xray,用分号串起来:--reloadcmd "systemctl reload nginx; systemctl restart xray"。再次强调,Nginx 用 reload、Xray 用 restart,这是各自机制决定的,不是我写错了。

七、证书部署到指定路径:installcert 与 deploy

前面说了别直接引用 ~/.acme.sh/ 里的文件。正确做法是把证书"安装"到你自己控制的稳定目录,比如 /etc/ssl/。上面那条 --installcert 命令干的就是这事:它把 key 和 fullchain 拷贝到你指定的路径,并记住 --reloadcmd。之后 Nginx 配置里就写:

ssl_certificate     /etc/ssl/example.com.cer;
ssl_certificate_key /etc/ssl/example.com.key;

除了 installcert 这种"拷贝到本地"的模式,acme.sh 还有更强大的 deploy hook 体系:--deploy --deploy-hook nginx 能直接帮你 reload Nginx;--deploy-hook remote 能把证书推到别的机器;还有各种面板、CDN、负载均衡的 hook。常用 hook 列表看 acme.sh --list-deploy-hooks。对绝大多数单机 VPS 玩家来说,installcert + reloadcmd 已经完全够用。

八、续签失败的坑:API 过期、CA 限速、如何切换 CA

真出问题时,多半是下面几类:

1)DNS API 权限过期 / 密钥错误

这是头号翻车原因。DNS 服务商那边你改了 Token、收回了权限、或者子账号 RAM 策略被改,都会导致 acme.sh 加 TXT 记录失败,日志里常见 "Incorrect TXT record"、"Timeout"、"API error"、"permission denied"。排查清单:重新生成 Token;确认 Cloudflare Token 权限是 Zone:DNS:Edit 且 Scope 覆盖目标 zone;确认 DP_Id/DP_Key 配对正确;确认阿里云 AccessKey 仍有效且 RAM 权限含 alidns:*。可以先手动在 DNS 后台加一条测试 TXT 看是否生效,排除 API 本身的问题。

2)CA 速率限制

Let's Encrypt 有几个常被踩的限流:每个注册域名每周最多 50 张证书;同一组域名(含相同 SAN 组合)的重复证书每周最多 5 张;验证失败每小时每域名最多 5 次。新手最容易触发"重复证书限制"——反复用同样的 -d 组合重签测着玩,第 6 次就被拦一周。ZeroSSL 通过 ACME 现在也能无限发 90 天免费证书,但需要 EAB 凭据(下面讲切换时会自动处理)。

3)切换 CA:Let's Encrypt 与 ZeroSSL 互转

觉得 LE 被限流了,或者想换根证书(比如某些老设备对 ISRG 根信任有问题),可以换 CA:

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

首次切到 ZeroSSL 需要注册账号并拉取 EAB(自动完成):

acme.sh --register-account --server zerossl

换回 Let's Encrypt 就是 --set-default-ca --server letsencrypt。注意切换 CA 后,原来 LE 签的证书还是 90 天,只是后续续签走新 CA。

4)CAA 记录挡路

如果你在 DNS 里设了 CAA 记录,限定"只允许某几家 CA 发证书",而你现在用的 CA 不在白名单,签发会直接失败。检查:dig CAA example.com,确认里面有 letsencrypt.orgsectigo.com(ZeroSSL 根属 Sectigo)。没有就补一条,或临时删掉 CAA 限制。

5)VPS 时钟歪了 / 调试技巧

ACME 全流程依赖准确时间,VPS 时钟偏差太大(比如没开 NTP)会导致 TLS 握手和 nonce 校验异常。先 timedatectl set-ntp on 校准。想看详细过程就加 --debug--debug 2,日志在 ~/.acme.sh/acme.sh.log,对照上面几条逐一排查。

#VPS #SSL #泛域名证书 #acme.sh #DNS验证 #LetEncrypt #ZeroSSL #证书续签