yanchang
yanchang
发布于 2026-02-22 / 8 阅读
0
0

基于青龙面板的跨节点多域名 SSL 证书全自动续签与分发(含 EdgeOne 同步)

碎碎念:

碎碎念: 作为一个热衷于折腾 HomeLab 和各种自动化服务的技术人,我的服务器和域名之前都实现了自动化的证书续签。但是!!!!课题组的网站所有的证书都是需要手动维护的。 但这带来的痛苦是显而易见的:课题组的域名和服务器分布在不同的腾讯云账号下,而且随着项目的推进,像 machao.grouprockstime.com 这样的主域名下面,竟然裂变出了十几、二十个子域名站点! 每次证书快到期,都是周期性的痛苦,导致我不得不手动去各个服务器上点来点去。更抓狂的是,我还用上了腾讯云的 EdgeOne 边缘加速,源站证书更新了,边缘节点没更新,访客看到的还是过期证书。 忍无可忍,无需再忍。我决定彻底推翻现有的手动模式,利用青龙面板 + acme.sh + 腾讯云 API,撸一套“一次配置,终身无感”的究极 SSL 自动化下发架构。

🎯 架构设计思路

这套架构的设计核心在于:“中心化签发,分布式下发,解耦运行”

  1. 青龙中心节点:作为大脑,每天凌晨定时运行,利用 acme.sh 通过 DNS API 向 ZeroSSL/Let's Encrypt 申请泛域名证书(如 *.machao.group)。

  2. 源站多目录覆盖:一旦拿到新证书,通过 SCP 安全地将证书推送到各台目标服务器的缓冲目录,并通过 SSH 指令,瞬间“裂变复制”覆盖到该域名下的所有宝塔子站点目录(比如 lab.machao.group, toolapi.machao.group 等十几个目录)。

  3. 防弹级 Nginx 重载:采用三重容错机制 (systemctl -> init.d -> nginx 二进制) 强行重载 Nginx,确保新证书立即生效。

  4. 解耦的 EdgeOne 边缘同步:为了防止单点故障,我写了一个独立的子模块脚本。主脚本跑完后调用它。它通过校验本地证书的 MD5 指纹来判断是否更新,一旦发现新证书,立刻调用内置的 Python 脚本(处理恶心的腾讯云 V3 签名),通过 API 将证书上传并绑定到 EdgeOne 加速节点。

🛠️ 核心代码实现

有部分的环境变量在这里

为了方便管理,我将代码拆分成了两个脚本。

1. 主脚本:源站证书续签与分发 (renew_group_ssl.sh)

这个脚本是主力军,它负责与 CA 机构交涉,并将战利品分发到各个前线服务器。

核心亮点:

  • 自带 4 次失败重试机制:针对 DNS 延迟或 CA 接口抖动,脚本加入了优雅的 while 重试循环,加上 120秒 的 DNS 等待防抖,极大地提高了成功率。

  • 宝塔路径完美契合:抛弃了修改 Nginx 配置文件的方法,直接采用“鸠占鹊巢”的策略,把新证书重命名为 fullchain.pemprivkey.pem 覆盖宝塔默认目录,这样宝塔 UI 上的剩余天数也能正常显示了!

Bash

# ... (省略前面基础配置代码,详见文末源码) ...

    # 4. 提取与推送 (SCP)
    echo "✅ [$DOMAIN] 申请成功!准备提取推送..."
    LOCAL_SSL_DIR="/tmp/ssl_output_$DOMAIN"
    mkdir -p "$LOCAL_SSL_DIR"

    acme.sh --install-cert --home "$ACME_DIR" -d "$DOMAIN" --key-file "$LOCAL_SSL_DIR/$DOMAIN.key.pem" --fullchain-file "$LOCAL_SSL_DIR/$DOMAIN.pem" >/dev/null 2>&1

    DEPLOY_EXIT_CODE=0
    ssh $SSH_OPTS "$HOST_USER@$HOST_IP" "mkdir -p $SSL_DIR" 2>&1 || true
    scp $SSH_OPTS "$LOCAL_SSL_DIR/$DOMAIN.key.pem" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.key.pem" 2>&1 || DEPLOY_EXIT_CODE=$?
    scp $SSH_OPTS "$LOCAL_SSL_DIR/$DOMAIN.pem" "$HOST_USER@$HOST_IP:$SSL_DIR/$DOMAIN.pem" 2>&1 || DEPLOY_EXIT_CODE=$?

    if [ $DEPLOY_EXIT_CODE -eq 0 ]; then
        # 针对不同域名,横向覆盖宝塔十几个站点目录
        OVERWRITE_SITES=""
        if [ "$DOMAIN" == "machao.group" ]; then
            OVERWRITE_SITES="lab.machao.group,machao.group.11,tool.machao.group1,pabapi.machao.group,pap.machao.group,rockapi.machao.group,depthapi.rockstime.org,machao.group,www.machao.group,toolapi.machao.group"
        # ... 省略其他域名判断 ...
        fi

        if [ -n "$OVERWRITE_SITES" ]; then
            SITES=(${OVERWRITE_SITES//,/ })
            OVERWRITE_CMD=""
            for site in "${SITES[@]}"; do
                if [ -n "$site" ]; then
                    BT_CERT_DIR="/www/server/panel/vhost/cert/$site"
                    OVERWRITE_CMD="${OVERWRITE_CMD}mkdir -p ${BT_CERT_DIR} && cp -f ${SSL_DIR}/${DOMAIN}.pem ${BT_CERT_DIR}/fullchain.pem && cp -f ${SSL_DIR}/${DOMAIN}.key.pem ${BT_CERT_DIR}/privkey.pem; "
                fi
            done
            ssh $SSH_OPTS "$HOST_USER@$HOST_IP" "$OVERWRITE_CMD" 2>&1 || true
        fi

        # 三重容错重载 Nginx
        RELOAD_CMD="systemctl reload nginx 2>/dev/null || /etc/init.d/nginx reload 2>/dev/null || /www/server/nginx/sbin/nginx -s reload"
        INSTALL_LOG=$(ssh $SSH_OPTS "$HOST_USER@$HOST_IP" "$RELOAD_CMD" 2>&1) || DEPLOY_EXIT_CODE=$?

2. 子模块:EdgeOne 边缘节点同步 (renew_edgeone.sh)

源站更新了,如果访客走的是 CDN,拿到的还是旧证书。由于腾讯云 API V3 签名机制在 Shell 下极难实现,我在这里巧妙地写了一个“套娃”脚本:由 Bash 动态生成一段 Python 代码去发起 HTTPS 请求。

核心亮点:

  • 指纹识别(MD5)静默跳过:为了防止 API 滥用,脚本每次运行都会比对当前证书与上次记录的 MD5 值。如果不一致,才触发 API 同步;否则不到一秒直接静默退出。

Bash

    # 2. 计算 MD5 判断是否需要更新
    MD5_FILE="/ql/data/eo_cert_md5_$DOMAIN.txt"
    CURRENT_MD5=$(md5sum "$LOCAL_SSL_DIR/$DOMAIN.pem" | awk '{print $1}')
    OLD_MD5=""
    [ -f "$MD5_FILE" ] && OLD_MD5=$(cat "$MD5_FILE")

    if [ "$CURRENT_MD5" == "$OLD_MD5" ]; then
        echo "✅ [$DOMAIN] 证书 MD5 未变化,跳过 EdgeOne 同步。"
        continue
    fi

    # 3. MD5 变化,触发 Python 脚本同步 API (自动处理签名并调用 UploadCertificate 和 ModifyHostsCertificate)
    export Tencent_SecretId="$TC_ID"
    export Tencent_SecretKey="$TC_KEY"
    export EO_ZONE_ID="$EO_ZONE_ID"
    # ... 设置环境变量 ...
    python3 /tmp/eo_ssl_update.py

3. 父子联动与优雅的邮件报告

为了避免收到多封零散的邮件,主脚本在完成自己的工作后,会通过绝对路径调用 EdgeOne 子模块,读取其生成的日志,最后拼接成一封完整的战报发到我的邮箱。

Bash

# ================= 联动 EdgeOne 同步脚本 =================
EO_SCRIPT="/ql/data/scripts/renew_edgeone.sh"

if [ -f "$EO_SCRIPT" ]; then
    bash "$EO_SCRIPT"
    if [ -f "/tmp/eo_sync_report.txt" ]; then
        EMAIL_REPORT="${EMAIL_REPORT}\n$(cat /tmp/eo_sync_report.txt)\n"
        rm -f "/tmp/eo_sync_report.txt"
    fi
fi

踩坑记录 (Troubleshooting)

  1. Nginx reload invalid 报错: 在灰度测试时发现,由于服务器操作系统的差异,有的 CentOS 机器无法识别 systemctl reload nginx。最后改为 systemctlinit.d 和直接调用 Nginx 二进制文件三重 Fallback 才彻底解决。

  2. 青龙环境的路径迷局: 尝试用 dirname "$0" 获取子脚本路径时翻车了。因为青龙面板是通过内部调度器(Task)执行脚本的,相对路径会失效,最后老老实实写死了绝对路径 /ql/data/scripts/renew_edgeone.sh

  3. DNS 验证失败: 不要想着保留 _acme-challenge 的 TXT 记录来提高成功率,这反而会导致解析响应包过大和旧 Token 冲突。正确的做法是老老实实把 --dnssleep 拉长到 120 秒防抖。

结语

看着青龙面板里绿色的 Success 日志,以及邮箱里准时送达的合并报告,心里有一种说不出的舒坦。以后无论项目里再长出多少个 subdomain.machao.group,我只需要在数组里加个逗号,一切自动搞定。

生命苦短,让机器去干脏活累活吧!


碎碎念 (后记):

代码写完的那一刻,忽然觉得这简直就像是在排兵布阵。之前还在因为 WELL-LOG-PRO 项目的 DTW/DBA 算法头疼,或者在折腾那台 Hackintosh 笔记本的 AirDrop 和分区,现在却在这里搞运维和自动化。或许这才是技术的魅力,跨越边界,为了“偷懒”而疯狂写代码,最终换来系统的秩序与内心的宁静。



源码:

renew_edgeone.sh

renew_group_ssl.sh


评论