这次故障最容易误判的地方,是把“证书续签成功”和“线上证书已经更新”当成同一件事。acme.sh 负责签发和续签,CDN、DCDN 是否拿到新证书,还要看部署钩子有没有正确登记并执行。
下面先整理一套可复现的配置,再记录这次故障的判断依据和后续维护方法。
签发与部署
我的场景是使用阿里云 DNS 完成域名验证,为主域名和通配符域名签发同一张证书,再把证书部署到多个阿里云账号下的 CDN 和 DCDN。
安装 acme.sh
安装时设置联系邮箱,并通过 --log 和 --log-level 2 启用详细日志。没有给 --log 指定路径时,日志默认写入 $HOME/.acme.sh/acme.sh.log:
curl https://get.acme.sh | sh -s \
email=OWNER@example.com \
--log \
--log-level 2
acme.sh --set-default-ca --server letsencrypt通过阿里云 DNS 签发通配符证书
先在当前终端设置具有 DNS 解析权限的阿里云 AccessKey:
export Ali_Key="YOUR_ALIYUN_ACCESS_KEY_ID"
export Ali_Secret="YOUR_ALIYUN_ACCESS_KEY_SECRET"然后使用 dns_ali 完成 DNS 验证,同时签发主域名和通配符域名证书:
acme.sh --issue --dns dns_ali \
-d example.com \
-d '*.example.com'部署到同一阿里云账号
通过环境变量指定使用这张证书的 CDN、DCDN 域名:
export DEPLOY_ALI_CDN_DOMAIN="cdn.example.com api.example.com"
export DEPLOY_ALI_DCDN_DOMAIN="dcdn-api.example.com dcdn-ws.example.com"部署时在同一条命令中指定两个钩子,让 acme.sh 同时保存 ali_cdn 和 ali_dcdn:
acme.sh --deploy -d example.com \
--deploy-hook ali_cdn \
--deploy-hook ali_dcdn这个写法只适用于 CDN 和 DCDN 属于同一个阿里云账号的情况。两个钩子在一次执行中共用同一组 Ali_Key 和 Ali_Secret;acme.sh 会保存部署域名、凭据和钩子配置,后续续签成功时继续执行这两个钩子。
部署到多个阿里云账号
如果 CDN、DCDN 分属不同的阿里云账号,就不能只把内置的 ali_cdn 和 ali_dcdn 放进同一条命令。阿里云 DNS 和两个部署钩子共用全局的 Ali_Key、Ali_Secret,普通的多钩子命令不能为每个目标单独绑定账号。
acme.sh 3.1.2 之后提供了 multideploy,可以为多个服务编排部署环境。不过阿里云钩子的凭据还会由 dns_ali 写入全局 account.conf。为了让账号切换和 DNS 凭据恢复都能直接检查,我在这个场景里仍使用一个很小的自定义钩子。
手动部署时,可以为每条命令临时传入对应账号的凭据和域名:
Ali_Key="ACCOUNT_A_ACCESS_KEY_ID" \
Ali_Secret="ACCOUNT_A_ACCESS_KEY_SECRET" \
DEPLOY_ALI_CDN_DOMAIN="cdn-a.example.com" \
acme.sh --deploy -d example.com --deploy-hook ali_cdn
Ali_Key="ACCOUNT_B_ACCESS_KEY_ID" \
Ali_Secret="ACCOUNT_B_ACCESS_KEY_SECRET" \
DEPLOY_ALI_DCDN_DOMAIN="dcdn-b.example.com" \
acme.sh --deploy -d example.com --deploy-hook ali_dcdn这种方式可以完成当次部署,但每次 --deploy 都会覆盖 Le_DeployHook,最后只保留最后一次使用的钩子,不能直接作为自动续签方案。
自动部署时,把所有账号操作收口到一个自定义部署钩子,例如 ali_multi_account:
- 只把
ali_multi_account保存到Le_DeployHook。 - 钩子从权限为
0600的私有配置文件读取各账号凭据,不把凭据写进文章或 crontab。 - 钩子按账号依次设置
Ali_Key、Ali_Secret和目标域名,再调用对应的 CDN 或 DCDN 部署逻辑。 - 全部部署结束后,把 acme.sh 全局保存的阿里云凭据恢复为 DNS 验证账号,避免下次续签时无法修改 DNS 记录。
下面的示例包含两个 CDN 账号和一个 DCDN 账号。先创建私有配置目录与 $HOME/.acme.sh/private/aliyun-accounts.conf,再按实际情况填写凭据和域名:
mkdir -p "$HOME/.acme.sh/private"
chmod 700 "$HOME/.acme.sh/private"# DNS 验证账号
ALI_DNS_KEY="DNS_ACCOUNT_ACCESS_KEY_ID"
ALI_DNS_SECRET="DNS_ACCOUNT_ACCESS_KEY_SECRET"
# 第一个 CDN 账号
ALI_CDN_A_KEY="CDN_ACCOUNT_A_ACCESS_KEY_ID"
ALI_CDN_A_SECRET="CDN_ACCOUNT_A_ACCESS_KEY_SECRET"
ALI_CDN_A_DOMAINS="cdn-a.example.com api-a.example.com"
# 第二个 CDN 账号
ALI_CDN_B_KEY="CDN_ACCOUNT_B_ACCESS_KEY_ID"
ALI_CDN_B_SECRET="CDN_ACCOUNT_B_ACCESS_KEY_SECRET"
ALI_CDN_B_DOMAINS="cdn-b.example.com"
# DCDN 账号
ALI_DCDN_KEY="DCDN_ACCOUNT_ACCESS_KEY_ID"
ALI_DCDN_SECRET="DCDN_ACCOUNT_ACCESS_KEY_SECRET"
ALI_DCDN_DOMAINS="dcdn-api.example.com dcdn-ws.example.com"配置文件会被钩子作为 shell 文件加载,只能由当前用户修改:
chmod 600 "$HOME/.acme.sh/private/aliyun-accounts.conf"然后创建 $HOME/.acme.sh/deploy/ali_multi_account.sh。如果账号数量不同,配置项、完整性检查和部署调用要一起增删:
#!/usr/bin/env sh
ali_multi_account_deploy() {
_config_file="${ALI_MULTI_ACCOUNT_CONFIG:-$HOME/.acme.sh/private/aliyun-accounts.conf}"
if [ ! -r "$_config_file" ]; then
_err "Aliyun account config is not readable: $_config_file"
return 1
fi
. "$_config_file"
if [ -z "$ALI_DNS_KEY" ] || [ -z "$ALI_DNS_SECRET" ] ||
[ -z "$ALI_CDN_A_KEY" ] || [ -z "$ALI_CDN_A_SECRET" ] || [ -z "$ALI_CDN_A_DOMAINS" ] ||
[ -z "$ALI_CDN_B_KEY" ] || [ -z "$ALI_CDN_B_SECRET" ] || [ -z "$ALI_CDN_B_DOMAINS" ] ||
[ -z "$ALI_DCDN_KEY" ] || [ -z "$ALI_DCDN_SECRET" ] || [ -z "$ALI_DCDN_DOMAINS" ]; then
_err "Aliyun multi-account config is incomplete"
return 1
fi
_ali_cdn_hook="$(_findHook "$1" "$_SUB_FOLDER_DEPLOY" "ali_cdn")"
_ali_dcdn_hook="$(_findHook "$1" "$_SUB_FOLDER_DEPLOY" "ali_dcdn")"
if [ ! -r "$_ali_cdn_hook" ] || [ ! -r "$_ali_dcdn_hook" ]; then
_err "Built-in Aliyun deploy hooks were not found"
return 1
fi
. "$_ali_cdn_hook"
. "$_ali_dcdn_hook"
_deploy_result=0
Ali_Key="$ALI_CDN_A_KEY"
Ali_Secret="$ALI_CDN_A_SECRET"
DEPLOY_ALI_CDN_DOMAIN="$ALI_CDN_A_DOMAINS"
export Ali_Key Ali_Secret DEPLOY_ALI_CDN_DOMAIN
ali_cdn_deploy "$@" || _deploy_result=1
Ali_Key="$ALI_CDN_B_KEY"
Ali_Secret="$ALI_CDN_B_SECRET"
DEPLOY_ALI_CDN_DOMAIN="$ALI_CDN_B_DOMAINS"
export Ali_Key Ali_Secret DEPLOY_ALI_CDN_DOMAIN
ali_cdn_deploy "$@" || _deploy_result=1
Ali_Key="$ALI_DCDN_KEY"
Ali_Secret="$ALI_DCDN_SECRET"
DEPLOY_ALI_DCDN_DOMAIN="$ALI_DCDN_DOMAINS"
export Ali_Key Ali_Secret DEPLOY_ALI_DCDN_DOMAIN
ali_dcdn_deploy "$@" || _deploy_result=1
# 内置阿里云钩子会覆盖全局凭据,结束前恢复 DNS 验证账号。
Ali_Key="$ALI_DNS_KEY"
Ali_Secret="$ALI_DNS_SECRET"
_saveaccountconf_mutable Ali_Key "$Ali_Key"
_saveaccountconf_mutable Ali_Secret "$Ali_Secret"
unset Ali_Key Ali_Secret DEPLOY_ALI_CDN_DOMAIN DEPLOY_ALI_DCDN_DOMAIN
return "$_deploy_result"
}限制钩子文件权限:
chmod 700 "$HOME/.acme.sh/deploy/ali_multi_account.sh"自定义钩子准备好后,只登记它:
acme.sh --deploy -d example.com --deploy-hook ali_multi_account检查自动续期配置
查看 acme.sh 管理的证书和系统中的定时任务:
acme.sh --list
crontab -l确认这张证书保存了符合当前场景的部署钩子:
grep '^Le_DeployHook=' "$HOME/.acme.sh/example.com_ecc/example.com.conf"同一账号下同时使用 CDN 和 DCDN 时,结果应包含 ali_cdn 和 ali_dcdn;部署到多个账号时,结果应只包含 ali_multi_account。不要只检查证书是否续签,还要确认部署钩子与当前拓扑一致。
启用续期日志
acme.sh 默认创建的 cron 命令会把标准输出重定向到 /dev/null。如果没有另外启用文件日志,后续只能知道任务是否运行过,无法查看完整的续签和部署过程。
在当前用户下执行一次下面的命令,立即检查所有证书,并把日志路径和日志级别保存到 account.conf:
"$HOME/.acme.sh/acme.sh" --cron \
--home "$HOME/.acme.sh" \
--log "$HOME/.acme.sh/acme.sh.log" \
--log-level 2这条命令可能执行已经到期的续签和部署钩子;尚未到续签时间的证书会被跳过。日志配置保存后,原有 cron 命令也会继续写入同一个日志文件,不需要手工修改 crontab。
执行后检查配置和日志文件:
grep -E '^(LOG_FILE|LOG_LEVEL)=' "$HOME/.acme.sh/account.conf"
ls -lh "$HOME/.acme.sh/acme.sh.log"
tail -n 100 "$HOME/.acme.sh/acme.sh.log"需要观察下一次定时任务时,可以持续查看日志:
tail -f "$HOME/.acme.sh/acme.sh.log"问题排查
我发现一个由阿里云 DCDN 加速的业务域名证书已经过期。设备上有 acme.sh 定时任务,本地通配符证书也仍然有效。问题不在签发,而在部署。
当时能确认的事实有这些:
- crontab 每天执行一次 acme.sh;
- 本地通配符证书已按时续签,仍在有效期内;
- 对应的证书配置当时只保存了
Le_DeployHook='ali_cdn,'。 - 使用
ali_dcdn的业务域名因此没有收到续签后的新证书。 - 我手动执行
ali_dcdn部署后,线上证书才与本地证书恢复一致。
原因很直接:多次单独执行 acme.sh --deploy 时,后一次命令会覆盖证书配置中的 Le_DeployHook,不会在原有值后面追加。分别执行下面两条命令,最终只会留下 ali_dcdn:
acme.sh --deploy -d example.com --deploy-hook ali_cdn
acme.sh --deploy -d example.com --deploy-hook ali_dcdn同账号场景可以在一条命令里登记两个钩子:
acme.sh --deploy -d example.com \
--deploy-hook ali_cdn \
--deploy-hook ali_dcdn这次故障涉及多个阿里云账号,所以只登记前文的自定义钩子:
acme.sh --deploy -d example.com --deploy-hook ali_multi_account在自定义钩子完成前,按账号分别执行内置钩子只能用于恢复当次线上证书,下一次续签仍可能漏掉目标账号。
故障发生时,这台设备没有为 acme.sh 配置独立日志文件,cron 又把标准输出重定向到 /dev/null,所以拿不到当时的完整执行日志。最后只能用 crontab、证书文件时间、Le_DeployHook、线上证书有效期和指纹交叉判断。事后补上文件日志,主要是为了下次能够区分“没有续签”“续签成功但没有部署”和“部署调用失败”。
日常维护
检查证书和定时任务
定期查看 acme.sh 版本、已管理证书和 crontab:
"$HOME/.acme.sh/acme.sh" --version
"$HOME/.acme.sh/acme.sh" --list
crontab -l查看本地证书的有效期、序列号和指纹:
openssl x509 \
-in "$HOME/.acme.sh/example.com_ecc/fullchain.cer" \
-noout -subject -issuer -dates -serial -fingerprint -sha256查看线上域名实际提供的证书:
timeout 10 openssl s_client \
-connect cdn-a.example.com:443 \
-servername cdn-a.example.com </dev/null 2>/dev/null |
openssl x509 -noout -subject -issuer -dates -serial -fingerprint -sha256本地和线上的序列号、SHA-256 指纹应保持一致。多账号部署时,需要逐个检查所有 CDN、DCDN 域名,不能只验证其中一个。
查看日志和部署结果
查看最近 200 行日志:
tail -n 200 "$HOME/.acme.sh/acme.sh.log"筛选续签、部署和错误信息:
grep -Eni 'renew|deploy|error|failed|invalid' \
"$HOME/.acme.sh/acme.sh.log"需要立即执行一次与 cron 相同的检查时运行:
"$HOME/.acme.sh/acme.sh" --cron \
--home "$HOME/.acme.sh" \
--log "$HOME/.acme.sh/acme.sh.log" \
--log-level 2这不是只读命令。已经到续签时间的证书会被续签,随后执行已登记的部署钩子。运行结束后,不要只看日志里的 renew success,还要检查部署结果和线上证书指纹。
修改多账号配置或钩子
账号凭据或目标域名变化时,修改私有配置文件;部署流程变化时,修改自定义钩子:
${EDITOR:-vi} "$HOME/.acme.sh/private/aliyun-accounts.conf"
${EDITOR:-vi} "$HOME/.acme.sh/deploy/ali_multi_account.sh"修改后重新限制权限,并检查两个文件的 shell 语法:
chmod 700 "$HOME/.acme.sh/private"
chmod 600 "$HOME/.acme.sh/private/aliyun-accounts.conf"
chmod 700 "$HOME/.acme.sh/deploy/ali_multi_account.sh"
sh -n "$HOME/.acme.sh/private/aliyun-accounts.conf"
sh -n "$HOME/.acme.sh/deploy/ali_multi_account.sh"确认语法无误后,手动部署一次并检查日志和线上证书:
"$HOME/.acme.sh/acme.sh" --deploy \
-d example.com \
--deploy-hook ali_multi_account
tail -n 200 "$HOME/.acme.sh/acme.sh.log"更新 acme.sh
这里使用了自定义部署钩子,我选择手动更新 acme.sh,不让 cron 自动升级。更新前先确认私有配置和钩子存在,而且语法正确:
test -r "$HOME/.acme.sh/private/aliyun-accounts.conf"
test -r "$HOME/.acme.sh/deploy/ali_multi_account.sh"
sh -n "$HOME/.acme.sh/deploy/ali_multi_account.sh"手动更新到最新版,同时关闭以后由 cron 自动升级:
"$HOME/.acme.sh/acme.sh" --upgrade --auto-upgrade 0更新后再次检查版本、自定义钩子和日志:
"$HOME/.acme.sh/acme.sh" --version
test -r "$HOME/.acme.sh/deploy/ali_multi_account.sh"
sh -n "$HOME/.acme.sh/deploy/ali_multi_account.sh"
tail -n 200 "$HOME/.acme.sh/acme.sh.log"如果希望恢复自动更新,可以执行:
"$HOME/.acme.sh/acme.sh" --upgrade --auto-upgrade 1自动更新会在 cron 续签检查开始时执行。使用自定义钩子时,更新后仍要确认钩子文件存在,并留意日志中的兼容性错误。