1.问题描述
这不是低版本升级高版本导致的问题,而是 RenewTLS 自动续签通配符证书时发生。
使用场景
- 使用 DNS API 申请通配符证书:
*.example.com
- 节点实际使用域名:
node.example.com
- 使用 sing-box,安装 VLESS Reality Vision 和 TUIC
- 通配符证书部署到:
/etc/v2ray-agent/tls/node.example.com.crt
/etc/v2ray-agent/tls/node.example.com.key
- acme.sh 中同时残留以前申请失败产生的不完整子域名目录:
/root/.acme.sh/node.example.com_ecc
该目录只有 .key 和 .csr,没有有效的 .cer 或 fullchain.cer。
复现步骤
- 使用“任意组合安装”,选择 sing-box,并安装 Reality 与 TUIC。
- 通过 DNS API 申请
*.example.com 通配符证书。
- 使用
node.example.com 作为节点域名。
- 保留一个以前申请失败产生的
/root/.acme.sh/node.example.com_ecc 目录。
- 等待定时任务执行,或在证书满足续签条件时运行:
/bin/bash /etc/v2ray-agent/install.sh RenewTLS
- 检查
/etc/v2ray-agent/tls/node.example.com.crt、私钥及 sing-box 状态。
实际结果
acme.sh --cron 首先成功续签通配符证书,并自动部署到 sing-box 使用的路径:
Renew: *.example.com
Renewing: '*.example.com'
Cert success.
Installing key to: /etc/v2ray-agent/tls/node.example.com.key
Installing full chain to: /etc/v2ray-agent/tls/node.example.com.crt
随后 acme.sh 检测到不完整的子域名条目并跳过:
Renew: node.example.com
Renewing: 'node.example.com'
Skipping invalid cert for: node.example.com
Skipped node.example.com_ecc
但 v2ray-agent 的 renewalTLS() 在 --cron 后又执行:
sudo "$HOME/.acme.sh/acme.sh" \
--installcert \
-d "${domain}" \
--fullchainpath /etc/v2ray-agent/tls/"${domain}.crt" \
--keypath /etc/v2ray-agent/tls/"${domain}.key" \
--ecc
通配符场景中:
实际 ACME 证书名:*.example.com
${domain}:node.example.com
因为不完整的 node.example.com_ecc 目录仍然存在,第二次 installcert 进入安装流程,并再次写入相同的生产路径:
Installing key to: /etc/v2ray-agent/tls/node.example.com.key
Installing full chain to: /etc/v2ray-agent/tls/node.example.com.crt
最终观察到:
/etc/v2ray-agent/tls/node.example.com.crt 变成 0 字节
私钥也被不完整子域名目录中的旧私钥覆盖,sing-box 无法加载 TLS 配置。
从有效的通配符证书目录恢复证书和私钥后,sing-box 恢复运行。
问题原理
installTLS() 已经正确区分了普通证书和通配符证书:
if [[ "${installedDNSAPIStatus}" == "true" ]]; then
acme.sh --installcert -d "*.${dnsTLSDomain}" ...
else
acme.sh --installcert -d "${tlsDomain}" ...
fi
相关代码:
|
installTLS() { |
|
echoContent skyBlue "\n进度 $1/${totalProgress} : 申请TLS证书\n" |
|
readAcmeTLS |
|
local tlsDomain=${domain} |
|
|
|
# 安装tls |
|
if [[ -f "/etc/v2ray-agent/tls/${tlsDomain}.crt" && -f "/etc/v2ray-agent/tls/${tlsDomain}.key" && -n $(cat "/etc/v2ray-agent/tls/${tlsDomain}.crt") ]] || [[ -d "$HOME/.acme.sh/${tlsDomain}_ecc" && -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.key" && -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.cer" ]] || [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
echoContent green " ---> 检测到证书" |
|
renewalTLS |
|
|
|
if [[ -z $(find /etc/v2ray-agent/tls/ -name "${tlsDomain}.crt") ]] || [[ -z $(find /etc/v2ray-agent/tls/ -name "${tlsDomain}.key") ]] || [[ -z $(cat "/etc/v2ray-agent/tls/${tlsDomain}.crt") ]]; then |
|
if [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "*.${dnsTLSDomain}" --fullchainpath "/etc/v2ray-agent/tls/${tlsDomain}.crt" --keypath "/etc/v2ray-agent/tls/${tlsDomain}.key" --ecc >/dev/null |
|
else |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "${tlsDomain}" --fullchainpath "/etc/v2ray-agent/tls/${tlsDomain}.crt" --keypath "/etc/v2ray-agent/tls/${tlsDomain}.key" --ecc >/dev/null |
|
fi |
|
|
|
else |
|
if [[ -d "$HOME/.acme.sh/${tlsDomain}_ecc" && -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.key" && -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.cer" ]] || [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
if [[ -z "${lastInstallationConfig}" ]]; then |
|
echoContent yellow " ---> 如未过期或者自定义证书请选择[n]\n" |
|
read -r -p "是否重新安装?[y/n]:" reInstallStatus |
|
if [[ "${reInstallStatus}" == "y" ]]; then |
|
rm -rf /etc/v2ray-agent/tls/* |
|
installTLS "$1" |
|
fi |
|
fi |
|
fi |
|
fi |
|
|
|
elif [[ -d "$HOME/.acme.sh" ]] && [[ ! -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.cer" || ! -f "$HOME/.acme.sh/${tlsDomain}_ecc/${tlsDomain}.key" ]]; then |
|
switchDNSAPI |
|
if [[ -z "${dnsAPIType}" ]]; then |
|
echoContent yellow "\n ---> 不采用API申请证书" |
|
echoContent green " ---> 安装TLS证书,需要依赖80端口" |
|
allowPort 80 |
|
fi |
|
|
|
switchSSLType |
|
customSSLEmail |
|
selectAcmeInstallSSL |
|
|
|
if [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "*.${dnsTLSDomain}" --fullchainpath "/etc/v2ray-agent/tls/${tlsDomain}.crt" --keypath "/etc/v2ray-agent/tls/${tlsDomain}.key" --ecc >/dev/null |
|
else |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "${tlsDomain}" --fullchainpath "/etc/v2ray-agent/tls/${tlsDomain}.crt" --keypath "/etc/v2ray-agent/tls/${tlsDomain}.key" --ecc >/dev/null |
|
fi |
|
|
但 renewalTLS() 没有使用相同判断,而是始终使用节点域名:
acme.sh --installcert -d "${domain}" ...
相关代码:
|
renewalTLS() { |
|
|
|
if [[ -n $1 ]]; then |
|
echoContent skyBlue "\n进度 $1/1 : 更新证书" |
|
fi |
|
readAcmeTLS |
|
local domain=${currentHost} |
|
if [[ -z "${currentHost}" && -n "${tlsDomain}" ]]; then |
|
domain=${tlsDomain} |
|
fi |
|
|
|
if [[ -f "/etc/v2ray-agent/tls/ssl_type" ]]; then |
|
if grep -q "buypass" <"/etc/v2ray-agent/tls/ssl_type"; then |
|
sslRenewalDays=180 |
|
fi |
|
fi |
|
if [[ -d "$HOME/.acme.sh/${domain}_ecc" && -f "$HOME/.acme.sh/${domain}_ecc/${domain}.key" && -f "$HOME/.acme.sh/${domain}_ecc/${domain}.cer" ]] || [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
modifyTime= |
|
|
|
if [[ "${installedDNSAPIStatus}" == "true" ]]; then |
|
modifyTime=$(stat --format=%z "$HOME/.acme.sh/*.${dnsTLSDomain}_ecc/*.${dnsTLSDomain}.cer") |
|
else |
|
modifyTime=$(stat --format=%z "$HOME/.acme.sh/${domain}_ecc/${domain}.cer") |
|
fi |
|
|
|
modifyTime=$(date +%s -d "${modifyTime}") |
|
currentTime=$(date +%s) |
|
((stampDiff = currentTime - modifyTime)) |
|
((days = stampDiff / 86400)) |
|
((remainingDays = sslRenewalDays - days)) |
|
|
|
tlsStatus=${remainingDays} |
|
if [[ ${remainingDays} -le 0 ]]; then |
|
tlsStatus="已过期" |
|
fi |
|
|
|
echoContent skyBlue " ---> 证书检查日期:$(date "+%F %H:%M:%S")" |
|
echoContent skyBlue " ---> 证书生成日期:$(date -d @"${modifyTime}" +"%F %H:%M:%S")" |
|
echoContent skyBlue " ---> 证书生成天数:${days}" |
|
echoContent skyBlue " ---> 证书剩余天数:"${tlsStatus} |
|
echoContent skyBlue " ---> 证书过期前最后一天自动更新,如更新失败请手动更新" |
|
|
|
if [[ ${remainingDays} -le 1 ]]; then |
|
echoContent yellow " ---> 重新生成证书" |
|
handleNginx stop |
|
|
|
if [[ "${coreInstallType}" == "1" ]]; then |
|
handleXray stop |
|
elif [[ "${coreInstallType}" == "2" ]]; then |
|
handleSingBox stop |
|
fi |
|
|
|
sudo "$HOME/.acme.sh/acme.sh" --cron --home "$HOME/.acme.sh" |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "${domain}" --fullchainpath /etc/v2ray-agent/tls/"${domain}.crt" --keypath /etc/v2ray-agent/tls/"${domain}.key" --ecc |
|
reloadCore |
|
handleNginx start |
|
else |
|
echoContent green " ---> 证书有效" |
|
fi |
|
elif [[ -f "/etc/v2ray-agent/tls/${tlsDomain}.crt" && -f "/etc/v2ray-agent/tls/${tlsDomain}.key" && -n $(cat "/etc/v2ray-agent/tls/${tlsDomain}.crt") ]]; then |
关键调用:
|
sudo "$HOME/.acme.sh/acme.sh" --cron --home "$HOME/.acme.sh" |
|
sudo "$HOME/.acme.sh/acme.sh" --installcert -d "${domain}" --fullchainpath /etc/v2ray-agent/tls/"${domain}.crt" --keypath /etc/v2ray-agent/tls/"${domain}.key" --ecc |
|
reloadCore |
|
handleNginx start |
|
else |
acme.sh --cron 已包含证书自动部署流程
以下分析基于服务器实际使用的 acme.sh v3.1.4,对应官方仓库提交 3661fd86b6304115e42f43910e6dd452ab9866d6。
执行 installcert 时,acme.sh 不只是复制一次证书文件,还会通过 _savedomainconf 将部署目标持久化到该证书的配置文件中:
_savedomainconf "Le_RealCertPath" "$_real_cert"
_savedomainconf "Le_RealCACertPath" "$_real_ca"
_savedomainconf "Le_RealKeyPath" "$_real_key"
_savedomainconf "Le_ReloadCmd" "$_reload_cmd" "base64"
_savedomainconf "Le_RealFullChainPath" "$_real_fullchain"
源码位置:
其中与本问题直接相关的是:
Le_RealKeyPath
Le_RealFullChainPath
以后执行 acme.sh --cron 时,acme.sh 内部会调用 renewAll():
_TREAT_SKIP_AS_SUCCESS="1"
renewAll
源码位置:
renewAll() 会遍历证书目录,并对每个证书调用 renew():
renew "$d" "$_isEcc" "$_server"
源码位置:
renew() 随后加载该证书的 DOMAIN_CONF:
因此,之前由 installcert 保存的 Le_RealKeyPath、Le_RealFullChainPath 等变量会在续签时被恢复。
源码位置:
renew() 调用 issue() 时,会把这些已经恢复的部署路径一并传入:
issue \
"$Le_Webroot" \
"$Le_Domain" \
"$Le_Alt" \
"$Le_Keylength" \
"$Le_RealCertPath" \
"$Le_RealKeyPath" \
"$Le_RealCACertPath" \
"$Le_ReloadCmd" \
"$Le_RealFullChainPath" \
...
源码位置:
续签成功后,只要这些部署路径不为空,issue() 就会自动调用 _installcert():
if [ "$_real_cert$_real_key$_real_ca$_reload_cmd$_real_fullchain" ]; then
_savedomainconf "Le_RealCertPath" "$_real_cert"
_savedomainconf "Le_RealCACertPath" "$_real_ca"
_savedomainconf "Le_RealKeyPath" "$_real_key"
_savedomainconf "Le_ReloadCmd" "$_reload_cmd" "base64"
_savedomainconf "Le_RealFullChainPath" "$_real_fullchain"
if ! _installcert \
"$_main_domain" \
"$_real_cert" \
"$_real_key" \
"$_real_ca" \
"$_real_fullchain" \
"$_reload_cmd"; then
return 1
fi
fi
源码位置:
由此可以确认,完整流程是:
installcert
→ 保存 Le_RealKeyPath、Le_RealFullChainPath
→ acme.sh --cron
→ renewAll()
→ renew()
→ 加载 DOMAIN_CONF
→ issue()
→ _installcert()
→ 自动将续签后的证书部署到原路径
因此,只要此前已经通过 installcert 保存了正确的部署路径,acme.sh --cron 在续签成功后就会自动部署新证书。在 --cron 后再次显式执行 installcert 并不是完成续签部署所必需的,而是一次重复部署。
除此之外,并非所有通配符用户都会发生服务中断:
- 如果不存在子域名 ACME 目录,错误的第二次
installcert 通常会直接报错;
- 如果存在完整的子域名目录,可能部署错误证书;
- 如果存在不完整的子域名目录,可能覆盖私钥或清空线上证书。
当前脚本没有检查 --cron 和 --installcert 的返回值,失败后仍继续执行 reloadCore,因此会将证书部署失败扩大为服务中断。
期望结果
- 通配符场景使用实际证书名
*.example.com;
- 不使用节点子域名重复执行
installcert;
- 部署失败时保留原证书;
- 证书无效或私钥不匹配时不重载服务。
可能的修复方案
推荐删除 --cron 后重复执行的 installcert,因为 acme.sh 已经保存部署路径并会在续签后自动部署。
如果需要保留显式部署,应区分 ACME 证书名与节点域名:
local acmeDomain="${domain}"
if [[ "${installedDNSAPIStatus}" == "true" ]]; then
acmeDomain="*.${dnsTLSDomain}"
fi
sudo "$HOME/.acme.sh/acme.sh" \
--installcert \
-d "${acmeDomain}" \
--fullchainpath "/etc/v2ray-agent/tls/${domain}.crt" \
--keypath "/etc/v2ray-agent/tls/${domain}.key" \
--ecc
另外建议:
- 检查
--cron 和 --installcert 的退出状态;
- 先部署到临时文件;
- 检查证书和私钥非空;
- 验证证书与私钥匹配;
- 验证成功后再原子替换生产文件;
- 只有全部成功后才执行
reloadCore。
2.安装失败的log或者截图
Renew: *.example.com
Renewing: '*.example.com'
Cert success.
Installing key to: /etc/v2ray-agent/tls/node.example.com.key
Installing full chain to: /etc/v2ray-agent/tls/node.example.com.crt
Renew: node.example.com
Renewing: 'node.example.com'
Skipping invalid cert for: node.example.com
Skipped node.example.com_ecc
Installing key to: /etc/v2ray-agent/tls/node.example.com.key
Installing full chain to: /etc/v2ray-agent/tls/node.example.com.crt
故障后:
/etc/v2ray-agent/tls/node.example.com.crt 为 0 字节
sing-box 无法加载 TLS 配置
恢复有效通配符证书和私钥后,sing-box、Reality TCP 端口和 TUIC UDP 端口恢复正常。
3.系统版本
4.脚本版本
v3.5.25
master commit: 5c5e2b72a394356fb1d53ed05785d407b8743758
acme.sh: v3.1.4
5.服务端安装的内核、协议、安装方式
- 核心(1.xray-core、2.sing-box)
- 协议(0.VLESS+TLS_Vision+TCP)
VLESS Reality Vision + TUIC
6.客户端版本
不适用。本问题发生在服务端自动续签和证书部署阶段。
多个客户端同时无法连接,恢复服务端证书后均恢复正常。
1.问题描述
这不是低版本升级高版本导致的问题,而是
RenewTLS自动续签通配符证书时发生。使用场景
*.example.comnode.example.com该目录只有
.key和.csr,没有有效的.cer或fullchain.cer。复现步骤
*.example.com通配符证书。node.example.com作为节点域名。/root/.acme.sh/node.example.com_ecc目录。/etc/v2ray-agent/tls/node.example.com.crt、私钥及 sing-box 状态。实际结果
acme.sh --cron首先成功续签通配符证书,并自动部署到 sing-box 使用的路径:随后 acme.sh 检测到不完整的子域名条目并跳过:
但 v2ray-agent 的
renewalTLS()在--cron后又执行:通配符场景中:
因为不完整的
node.example.com_ecc目录仍然存在,第二次installcert进入安装流程,并再次写入相同的生产路径:最终观察到:
私钥也被不完整子域名目录中的旧私钥覆盖,sing-box 无法加载 TLS 配置。
从有效的通配符证书目录恢复证书和私钥后,sing-box 恢复运行。
问题原理
installTLS()已经正确区分了普通证书和通配符证书:相关代码:
v2ray-agent/install.sh
Lines 2138 to 2185 in 5c5e2b7
但
renewalTLS()没有使用相同判断,而是始终使用节点域名:acme.sh --installcert -d "${domain}" ...相关代码:
v2ray-agent/install.sh
Lines 2411 to 2470 in 5c5e2b7
关键调用:
v2ray-agent/install.sh
Lines 2463 to 2467 in 5c5e2b7
acme.sh --cron已包含证书自动部署流程以下分析基于服务器实际使用的 acme.sh v3.1.4,对应官方仓库提交
3661fd86b6304115e42f43910e6dd452ab9866d6。执行
installcert时,acme.sh 不只是复制一次证书文件,还会通过_savedomainconf将部署目标持久化到该证书的配置文件中:源码位置:
installcert()保存部署路径,第 6757–6764 行其中与本问题直接相关的是:
以后执行
acme.sh --cron时,acme.sh 内部会调用renewAll():_TREAT_SKIP_AS_SUCCESS="1" renewAll源码位置:
cron()调用renewAll(),第 7780–7802 行renewAll()会遍历证书目录,并对每个证书调用renew():源码位置:
renewAll()调用renew(),第 6318–6334 行renew()随后加载该证书的DOMAIN_CONF:因此,之前由
installcert保存的Le_RealKeyPath、Le_RealFullChainPath等变量会在续签时被恢复。源码位置:
renew()加载证书配置,第 6100–6124 行renew()调用issue()时,会把这些已经恢复的部署路径一并传入:issue \ "$Le_Webroot" \ "$Le_Domain" \ "$Le_Alt" \ "$Le_Keylength" \ "$Le_RealCertPath" \ "$Le_RealKeyPath" \ "$Le_RealCACertPath" \ "$Le_ReloadCmd" \ "$Le_RealFullChainPath" \ ...源码位置:
renew()将保存的部署路径传给issue(),第 6258–6272 行续签成功后,只要这些部署路径不为空,
issue()就会自动调用_installcert():源码位置:
issue()自动调用_installcert(),第 6057–6065 行由此可以确认,完整流程是:
因此,只要此前已经通过
installcert保存了正确的部署路径,acme.sh --cron在续签成功后就会自动部署新证书。在--cron后再次显式执行installcert并不是完成续签部署所必需的,而是一次重复部署。除此之外,并非所有通配符用户都会发生服务中断:
installcert通常会直接报错;当前脚本没有检查
--cron和--installcert的返回值,失败后仍继续执行reloadCore,因此会将证书部署失败扩大为服务中断。期望结果
*.example.com;installcert;可能的修复方案
推荐删除
--cron后重复执行的installcert,因为 acme.sh 已经保存部署路径并会在续签后自动部署。如果需要保留显式部署,应区分 ACME 证书名与节点域名:
另外建议:
--cron和--installcert的退出状态;reloadCore。2.安装失败的log或者截图
故障后:
恢复有效通配符证书和私钥后,sing-box、Reality TCP 端口和 TUIC UDP 端口恢复正常。
3.系统版本
4.脚本版本
5.服务端安装的内核、协议、安装方式
6.客户端版本