yanchang
yanchang
发布于 2026-02-19 / 13 阅读
0
0

(2026.2.21更新)Acme.sh 泛域名证书自动化续签与部署在青龙面板

在管理个人服务器和各种内网服务的过程中,HTTPS 证书的自动续签一直是个绕不开的话题。很多同学(包括我自己)喜欢利用青龙面板的定时任务结合 acme.sh 来实现自动化。但在实际部署中,往往会遇到几个非常抓狂的“暗坑”:

  • 面板假死: 脚本明明执行完了,邮件也收到了,但青龙面板的日志界面左上角一直转圈,任务永远无法结束。

  • 莫名暴毙: 开启了 Bash 严格模式(set -e)后,脚本在判断“证书未过期跳过”时直接异常中断。

  • 跨服部署难: 申请下来的证书无法优雅地推送到远程目标服务器并重载 Nginx。

经过反复的底层排查和逻辑重构,我终于写出了两套堪称“坚如磐石”的自动化续签脚本。一套用于本地部署,一套用于跨服务器推送。今天就把这套终极方案分享出来。

核心痛点与解决思路

在放出脚本之前,先简单聊聊为什么以前的脚本会出问题。

但如果你直接照搬网上的普通脚本扔进青龙运行,大概率会遇到每天重复申请证书脚本无限转圈挂起依赖安装超时报错等一系列高血压问题。

经过几天的反复测试和深度排查,我终于摸索出了一套在青龙面板下完美运行 acme.sh 并通过 SCP 自动推送到远端 Nginx 的“终极方案”。今天就把这份防坑指南完整记录下来。

1. 容器重启,数据火葬场(重复申请地狱)

网上很多教程会把 acme.sh 安装在默认的 ~/.acme.sh(也就是容器的 /root/.acme.sh)下。 致命后果:青龙容器的 /root 目录默认是不持久化的。一旦容器重启或升级,本地的证书历史记录就会灰飞烟灭。下次定时任务执行时,acme.sh 查不到本地记录,就会无视证书还有 80 多天有效期,强行向 CA 机构重新申请。长期以往,极易触发 Let's Encrypt 或 ZeroSSL 的速率限制(Rate Limit),导致账号被封禁。

2. Alpine 源的网络玄学(依赖安装超时)

很多脚本喜欢在开头写一段 apk update && apk add ... 来自动安装 curlsocat 等依赖。 致命后果:青龙底层是 Alpine Linux,国内网络连 Alpine 官方源经常抽风。一旦 apk update 卡住超时,整个续签脚本直接宣告失败。

3. 幽灵进程与无限转圈(面板卡死)

有时候网络波动导致 acme.sh 内部的 curl 请求卡死,或者跨服务器的 SSH/SCP 连接断开,如果没有做好严谨的进程管控,这个任务就会在青龙面板里无限“运行中”,也就是经典的转圈圈,甚至吃满 CPU。

准备工作

第一步:弃用脚本安装,改用青龙面板持久化依赖

把专业的事交给专业的工具。不要再让脚本每次去跑 apk add 了,直接在青龙面板里配置,一劳永逸。

  1. 进入青龙面板 -> 依赖管理 -> Linux

  2. 点击右上角“添加依赖”。

  3. 批量填入以下名称并安装:

    Plaintext

    bash coreutils socat openssl git curl openssh-client
    
  4. 等待它们全部显示绿色打勾状态。就算以后容器销毁重建,青龙也会在启动时自动帮你装好。

第二步:配置跨服务器免密登录 (SSH Key)

因为我的证书是申请后要推送到另一台远端服务器(比如我的 Nginx 节点),所以需要持久化 SSH 密钥。 务必将私钥存放在青龙的持久化目录 /ql/data/ 下,例如 /ql/data/id_rsa_acme,并确保目标服务器已经添加了对应的公钥。

为了防止每次重新安装依赖,因此,需要将依赖托管到青龙面板


方案一:单机部署版 (适用于青龙面板与 Nginx 在同一台机器)

这个脚本适用于大多数一体化服务器环境(例如我用来管理 yanchang.cc 的主机)。它会在申请证书后,直接在宿主机进行配置重载。

环境准备

需要在青龙面板映射的 /ql/data/ 目录下准备好用于免密登录宿主机的 SSH 私钥 (id_rsa_acme)。

核心脚本

Bash

#!/bin/bash
##!name: 泛域名证书自动续签
##!desc: 自动续签 yanchang.cc (终极修复:彻底解决挂起与转圈问题及持久化丢失问题)
##!cron: 30 3 * * *

# ================= 核心优化:终极进程管控与断流 =================
set -euo pipefail

# 核弹级清理函数:杀光子孙进程,强制拔掉日志输出网线
cleanup() {
    # 1. 清理临时文件
    rm -f "/tmp/mail_$$.txt"
    
    # 2. 终极绝杀:强行关闭标准输出(1)和标准错误(2)
    # 这一步极其关键,立刻告诉青龙面板:“我彻底闭嘴了,立刻给我打勾!”
    exec 1>&-
    exec 2>&-
    
    # 3. 忽略 TERM 信号,防止我们发信号时连自己一起杀掉导致脚本中断
    trap '' TERM
    
    # 4. 株连九族:向当前进程组(-$$)发送终止信号,清理所有儿子和孙子幽灵进程
    kill -TERM -$$ 2>/dev/null || true
    sleep 1 # 给进程 1 秒钟优雅退出的时间
    kill -9 -$$ 2>/dev/null || true
}

# 绑定 EXIT 信号到 cleanup 函数
trap cleanup EXIT

# ================= 配置区域 =================
DOMAIN="yanchang.cc"
SSH_KEY_PATH="/ql/data/id_rsa_acme"
HOST_USER="yanchang"
HOST_IP="192.168.0.250" 
RECEIVER_EMAIL="yanchang@yanchang.cc"
SSL_DIR="/data/ssl_output"
# 临时文件加上 PID 防止并发冲突
MAIL_TMP="/tmp/mail_$$.txt"
# ===========================================

# 定义发送邮件函数
send_email() {
    local status="$1"
    local content=$(echo -e "$2")
    local subject="【青龙】yanchang的证书状态通知 - ${status}"
    
    echo "📧 正在发送邮件: $subject"
    
    if [ -z "${SMTP_SERVER:-}" ] || [ -z "${SMTP_EMAIL:-}" ] || [ -z "${SMTP_PASSWORD:-}" ]; then
        echo "❌ 邮件配置缺失:SMTP_SERVER/SMTP_EMAIL/SMTP_PASSWORD 未定义"
        return 1
    fi

    cat > "$MAIL_TMP" <<EOF
From: "yanchang.cc的证书监控" <$SMTP_EMAIL>
To: <$RECEIVER_EMAIL>
Subject: $subject
Content-Type: text/plain; charset=UTF-8

任务名称:泛域名证书自动续签
目标域名:$DOMAIN
执行时间:$(date "+%Y-%m-%d %H:%M:%S")
当前状态:$status

$content

-------------------------
此邮件由青龙面板自动发送
EOF

    # 优化:增加 -k 5s,如果 30 秒还没结束,再等 5 秒直接发 SIGKILL 强杀
    if ! timeout -k 5s 30s curl --silent --show-error --ssl-reqd \
        --url "smtps://$SMTP_SERVER:465" \
        --user "$SMTP_EMAIL:$SMTP_PASSWORD" \
        --mail-from "$SMTP_EMAIL" \
        --mail-rcpt "$RECEIVER_EMAIL" \
        --upload-file "$MAIL_TMP"; then
        echo "❌ 邮件发送失败"
        return 1
    fi
    return 0
}

# 1. 检查核心环境变量
if [ -z "${Ali_Key:-}" ] || [ -z "${Ali_Secret:-}" ]; then
    send_email "配置错误" "❌ 未检测到 Ali_Key 或 Ali_Secret 环境变量。" || true
    exit 1
fi

# 2. 检查 SSH 密钥
if [ ! -f "$SSH_KEY_PATH" ]; then
    msg="❌ 未找到持久化 SSH 密钥:$SSH_KEY_PATH\n请按照教程在容器内生成密钥并配置到宿主机。"
    echo -e "$msg"
    send_email "环境错误" "$msg" || true
    exit 1
fi

# 3. 检查核心依赖 (已交由青龙面板持久化管理)
echo "🛠️ 正在检查系统依赖..."
MISSING_DEPS=""
# 检查几个最核心的命令是否存在
for dep in bash socat curl git openssl ssh; do
    if ! command -v $dep >/dev/null 2>&1; then
        MISSING_DEPS="$MISSING_DEPS $dep"
    fi
done

if [ -n "$MISSING_DEPS" ]; then
    msg="❌ 缺少必要的系统依赖:$MISSING_DEPS\n请前往青龙面板网页端 ->【依赖管理】->【Linux】,添加以下依赖:\nbash coreutils socat openssl git curl openssh-client"
    echo -e "$msg"
    send_email "依赖缺失" "$msg" || true
    exit 1
else
    echo "✅ 系统核心依赖检查通过"
fi

# 4. 预创建证书部署目录
if [ ! -d "$SSL_DIR" ]; then
    echo "📁 创建证书目录:$SSL_DIR"
    mkdir -p "$SSL_DIR" || { send_email "目录创建失败" "❌ 无法创建证书目录 $SSL_DIR"; exit 1; }
fi

# 5. 准备 acme.sh (修复:更改为青龙持久化目录)
export ACME_DIR="/ql/data/acme"
if [ ! -f "$ACME_DIR/acme.sh" ]; then
    echo "⬇️ acme.sh 未找到,正在安装到持久化目录..."
    rm -rf /tmp/acme.sh
    timeout -k 10s 120s git clone https://gitee.com/neilpang/acme.sh.git /tmp/acme.sh >/dev/null 2>&1 || { send_email "安装失败" "git clone acme.sh 失败"; exit 1; }
    cd /tmp/acme.sh || exit 1
    # 强制指定安装目录为持久化目录
    ./acme.sh --install --home "$ACME_DIR" --nocron -m "${SMTP_EMAIL:-default@example.com}" >/dev/null 2>&1
    cd ~ || exit 1
else
    echo "✅ acme.sh 已安装"
fi
export PATH="$ACME_DIR:$PATH"

# 6. 注册账户 (容错处理) (修复:指定持久化配置目录)
timeout -k 5s 30s acme.sh --register-account --home "$ACME_DIR" -m "${SMTP_EMAIL:-default@example.com}" --server zerossl >/dev/null 2>&1 || true

# 7. === 核心:申请证书 ===
echo "🚀 开始检查证书状态..."

ISSUE_EXIT_CODE=0
# 增加 -k 参数防止死锁,并增加 --home 确保读取持久化历史记录
ISSUE_LOG=$(timeout -k 10s 180s bash "$ACME_DIR/acme.sh" --issue --home "$ACME_DIR" --dns dns_ali -d "$DOMAIN" -d "*.$DOMAIN" 2>&1) || ISSUE_EXIT_CODE=$?

CLEAN_LOG=$(echo "$ISSUE_LOG" | grep -v "integer expected" | grep -v "out of range" || true)

# 8. === 逻辑判断 ===
if [[ "$ISSUE_LOG" == *"Skipping"* ]] || [[ "$ISSUE_LOG" == *"Domains not changed"* ]]; then
    echo "✅ 证书无需更新 (Skipped)"
    
    NEXT_TIME=$(echo "$ISSUE_LOG" | grep -oE "Next renewal time is:.*" | head -n 1 || true)
    if [ -z "$NEXT_TIME" ]; then
        NEXT_TIME=$(echo "$CLEAN_LOG" | tail -n 3)
    fi
    
    send_email "未更新(有效期内)" "✅ 证书依然有效,跳过更新。\n\n[ $NEXT_TIME ]" || true
    exit 0
fi

if [ $ISSUE_EXIT_CODE -ne 0 ]; then
    echo "❌ acme.sh 执行出错"
    echo "$CLEAN_LOG"
    send_email "执行失败" "❌ 证书申请错误,日志:\n\n$CLEAN_LOG" || true
    exit 1
fi

echo "✅ 证书已重新申请,开始部署..."
RELOAD_CMD="ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no -o ConnectTimeout=10 $HOST_USER@$HOST_IP 'sudo systemctl reload nginx'"

DEPLOY_EXIT_CODE=0
# 修复:部署时带上 --home,防止 acme.sh 找不到刚刚申请的证书
INSTALL_LOG=$(timeout -k 10s 120s acme.sh --install-cert --home "$ACME_DIR" -d "$DOMAIN" \
--key-file       "$SSL_DIR/www.yanchang.key"  \
--fullchain-file "$SSL_DIR/www.yanchang.pem" \
--reloadcmd      "$RELOAD_CMD" 2>&1) || DEPLOY_EXIT_CODE=$?

if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
    echo "🎉 部署完成"
    send_email "已更新(Success)" "🎉 泛域名证书已成功续签并重载 Nginx!\n\n[部署日志]:\n$INSTALL_LOG" || true
else
    echo "❌ 部署失败"
    echo "$INSTALL_LOG"
    send_email "更新失败(Deploy Error)" "⚠️ 证书申请成功,但部署或 Nginx 重载失败。\n\n[错误日志]:\n$INSTALL_LOG" || true
    exit 1
fi

方案二:跨服务器 SCP 推送版 (适用于多节点集群管理)

随着设备的增多,我的目标是将青龙面板打造成一个中心化的“证书分发大脑”。比如给远程的 yanchang.pw 服务器续签时,我们就需要利用 scp 跨服推送文件,然后再触发远程 Nginx 的重启。

关键差异

  • 引入了本地临时目录 /tmp/ssl_output_$DOMAIN

  • 通过 scp 将证书安全地推送到目标主机的 /etc/nginx/ssl

  • 防冗余机制: 如果证书还在有效期(触发 Skipping),不仅不会申请,也绝对不会发起无意义的 SCP 传输和 Nginx 重载。

核心脚本

Bash

#!/bin/bash
##!name: yanchang.pw 泛域名证书续签
##!desc: 自动续签 *.yanchang.pw (修复跨服务器SCP推送问题及持久化依赖问题)
##!cron: 30 3 * * *

# ================= 核心优化:终极进程管控与断流 =================
set -euo pipefail

cleanup() {
    rm -f "/tmp/mail_$$.txt"
    rm -rf "/tmp/ssl_output_$DOMAIN" # 清理本地临时证书
    exec 1>&-
    exec 2>&-
    trap '' TERM
    kill -TERM -$$ 2>/dev/null || true
    sleep 1
    kill -9 -$$ 2>/dev/null || true
}
trap cleanup EXIT

# ================= 配置区域 =================
DOMAIN="yanchang.pw"
SSH_KEY_PATH="/ql/data/id_rsa_acme"
HOST_USER="yanchang" 
HOST_IP="8.137.122.123" 
RECEIVER_EMAIL="yanchang@yanchang.cc"
SSL_DIR="/etc/nginx/ssl"
MAIL_TMP="/tmp/mail_$$.txt"
# ===========================================

# 定义发送邮件函数
send_email() {
    local status="$1"
    local content=$(echo -e "$2")
    local subject="【青龙】yanchang证书状态通知 - ${status}"
    
    echo "📧 正在发送邮件: $subject"
    
    if [ -z "${SMTP_SERVER:-}" ] || [ -z "${SMTP_EMAIL:-}" ] || [ -z "${SMTP_PASSWORD:-}" ]; then
        echo "❌ 邮件配置缺失:SMTP_SERVER/SMTP_EMAIL/SMTP_PASSWORD 未定义"
        return 1
    fi

    cat > "$MAIL_TMP" <<EOF
From: "yanchang.pw的证书监控" <$SMTP_EMAIL>
To: <$RECEIVER_EMAIL>
Subject: $subject
Content-Type: text/plain; charset=UTF-8

任务名称:yanchang.pw 泛域名证书续签
目标域名:$DOMAIN
目标主机:$HOST_IP
执行时间:$(date "+%Y-%m-%d %H:%M:%S")
当前状态:$status

$content

-------------------------
此邮件由青龙面板自动发送
EOF

    if ! timeout -k 5s 30s curl --silent --show-error --ssl-reqd \
        --url "smtps://$SMTP_SERVER:465" \
        --user "$SMTP_EMAIL:$SMTP_PASSWORD" \
        --mail-from "$SMTP_EMAIL" \
        --mail-rcpt "$RECEIVER_EMAIL" \
        --upload-file "$MAIL_TMP"; then
        echo "❌ 邮件发送失败"
        return 1
    fi
    return 0
}

# 1. 检查核心环境变量
if [ -z "${Ali_Key:-}" ] || [ -z "${Ali_Secret:-}" ]; then
    send_email "配置错误" "❌ 未检测到 Ali_Key 或 Ali_Secret 环境变量。" || true
    exit 1
fi

# 2. 检查 SSH 密钥
if [ ! -f "$SSH_KEY_PATH" ]; then
    msg="❌ 未找到持久化 SSH 密钥:$SSH_KEY_PATH"
    echo -e "$msg"
    send_email "环境错误" "$msg" || true
    exit 1
fi

# 3. 检查核心依赖 (已交由青龙面板持久化管理)
echo "🛠️ 正在检查系统依赖..."
MISSING_DEPS=""
# 检查这几个最核心的命令是否存在(包含了推送需要的 scp 和 ssh)
for dep in bash socat curl git openssl scp ssh; do
    if ! command -v $dep >/dev/null 2>&1; then
        MISSING_DEPS="$MISSING_DEPS $dep"
    fi
done

if [ -n "$MISSING_DEPS" ]; then
    msg="❌ 缺少必要的系统依赖:$MISSING_DEPS\n请前往青龙面板网页端 ->【依赖管理】->【Linux】,添加以下依赖:\nbash coreutils socat openssl git curl openssh-client"
    echo -e "$msg"
    send_email "依赖缺失" "$msg" || true
    exit 1
else
    echo "✅ 系统核心依赖检查通过"
fi

# 4. 准备 acme.sh (修复:更改为青龙持久化目录)
export ACME_DIR="/ql/data/acme"
if [ ! -f "$ACME_DIR/acme.sh" ]; then
    echo "⬇️ acme.sh 未找到,正在安装到持久化目录..."
    rm -rf /tmp/acme.sh
    timeout -k 10s 120s git clone https://gitee.com/neilpang/acme.sh.git /tmp/acme.sh >/dev/null 2>&1 || exit 1
    cd /tmp/acme.sh || exit 1
    # 强制指定安装目录为持久化目录
    ./acme.sh --install --home "$ACME_DIR" --nocron -m "${SMTP_EMAIL:-default@example.com}" >/dev/null 2>&1
    cd ~ || exit 1
else
    echo "✅ acme.sh 已安装"
fi
export PATH="$ACME_DIR:$PATH"
# 注册账号时也强制指定工作目录
timeout -k 5s 30s acme.sh --register-account --home "$ACME_DIR" -m "${SMTP_EMAIL:-default@example.com}" --server zerossl >/dev/null 2>&1 || true

# 5. === 核心:申请证书 ===
echo "🚀 开始检查证书状态..."
ISSUE_EXIT_CODE=0
# 修复:加上 --home 参数确保读取持久化目录的历史记录
ISSUE_LOG=$(timeout -k 10s 180s bash "$ACME_DIR/acme.sh" --issue --home "$ACME_DIR" --dns dns_ali -d "$DOMAIN" -d "*.$DOMAIN" 2>&1) || ISSUE_EXIT_CODE=$?
CLEAN_LOG=$(echo "$ISSUE_LOG" | grep -v "integer expected" | grep -v "out of range" || true)

# 6. === 逻辑判断 ===
if [[ "$ISSUE_LOG" == *"Skipping"* ]] || [[ "$ISSUE_LOG" == *"Domains not changed"* ]]; then
    echo "✅ 证书无需更新 (Skipped)"
    NEXT_TIME=$(echo "$ISSUE_LOG" | grep -oE "Next renewal time is:.*" | head -n 1 || true)
    [ -z "$NEXT_TIME" ] && NEXT_TIME=$(echo "$CLEAN_LOG" | tail -n 3)
    
    send_email "未更新(有效期内)" "✅ 证书依然有效,跳过更新,未触发服务重载。\n\n[ $NEXT_TIME ]" || true
    exit 0
fi

if [ $ISSUE_EXIT_CODE -ne 0 ]; then
    echo "❌ acme.sh 执行出错"
    send_email "执行失败" "❌ 证书申请错误,日志:\n\n$CLEAN_LOG" || true
    exit 1
fi

# 7. === 关键修复:跨服务器推送证书 ===
echo "✅ 证书已重新申请,开始通过 SCP 推送至远端服务器..."

# 7.1 将证书提取到青龙容器的临时目录
LOCAL_SSL_DIR="/tmp/ssl_output_$DOMAIN"
mkdir -p "$LOCAL_SSL_DIR"

# 修复:加上 --home 参数确保能找到刚刚生成的证书
acme.sh --install-cert --home "$ACME_DIR" -d "$DOMAIN" \
    --key-file       "$LOCAL_SSL_DIR/$DOMAIN.key"  \
    --fullchain-file "$LOCAL_SSL_DIR/$DOMAIN.pem" >/dev/null 2>&1

DEPLOY_EXIT_CODE=0
INSTALL_LOG=""

# 7.2 通过 SCP 将临时目录的证书传送到目标主机的 /etc/nginx/ssl
echo "📤 正在上传 .key 和 .pem 文件..."
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$LOCAL_SSL_DIR/$DOMAIN.key" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.key" 2>&1 || DEPLOY_EXIT_CODE=$?
scp -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$LOCAL_SSL_DIR/$DOMAIN.pem" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.pem" 2>&1 || DEPLOY_EXIT_CODE=$?

# 7.3 推送成功后,重载远端 Nginx
if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
    echo "🔄 文件上传成功,准备重载远端 Nginx..."
    RELOAD_CMD="sudo systemctl reload nginx"
    INSTALL_LOG=$(ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$HOST_USER@$HOST_IP" "$RELOAD_CMD" 2>&1) || DEPLOY_EXIT_CODE=$?
else
    INSTALL_LOG="SCP 上传文件失败。请检查远端目录 $SSL_DIR 是否已赋予 $HOST_USER 写入权限 (sudo chown yanchang:yanchang $SSL_DIR)。"
fi

if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
    echo "🎉 部署完成"
    send_email "已更新(Success)" "🎉 泛域名证书已成功续签,推送到目标服务器并成功重载 Nginx!\n\n[部署日志]:\n推送与重载均执行成功。" || true
else
    echo "❌ 部署失败"
    send_email "更新失败(Deploy Error)" "⚠️ 证书申请成功,但推送或重载失败。\n\n[错误日志]:\n$INSTALL_LOG" || true
    exit 1
fi

给新手的特别提醒

如果你使用的是非 Root 普通用户进行跨服部署,请务必在目标服务器上做好两件事:

  1. 赋予该用户操作 /etc/nginx/ssl 目录的写入权限 (sudo chown -R 用户名:用户名 /etc/nginx/ssl)。

  2. 使用 visudo 为该用户配置 Nginx 的免密重启权限,否则脚本会在远端卡死在等待输入密码的环节。

结语

通过这两套脚本,我们彻底告别了青龙面板假死和意外中断的玄学问题。如果你有几十个分散在不同云厂商的域名,这套“中枢分发”架构能帮你省下极大的运维精力。折腾 HomeLab 的乐趣,不正是看着机器有条不紊地替我们干活嘛!


评论