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

🎯 架构设计思路
这套架构的设计核心在于:“中心化签发,分布式下发,解耦运行”。
青龙中心节点:作为大脑,每天凌晨定时运行,利用
acme.sh通过 DNS API 向 ZeroSSL/Let's Encrypt 申请泛域名证书(如*.machao.group)。源站多目录覆盖:一旦拿到新证书,通过 SCP 安全地将证书推送到各台目标服务器的缓冲目录,并通过 SSH 指令,瞬间“裂变复制”覆盖到该域名下的所有宝塔子站点目录(比如
lab.machao.group,toolapi.machao.group等十几个目录)。防弹级 Nginx 重载:采用三重容错机制 (
systemctl->init.d->nginx 二进制) 强行重载 Nginx,确保新证书立即生效。解耦的 EdgeOne 边缘同步:为了防止单点故障,我写了一个独立的子模块脚本。主脚本跑完后调用它。它通过校验本地证书的 MD5 指纹来判断是否更新,一旦发现新证书,立刻调用内置的 Python 脚本(处理恶心的腾讯云 V3 签名),通过 API 将证书上传并绑定到 EdgeOne 加速节点。
🛠️ 核心代码实现
有部分的环境变量在这里
为了方便管理,我将代码拆分成了两个脚本。
1. 主脚本:源站证书续签与分发 (renew_group_ssl.sh)
这个脚本是主力军,它负责与 CA 机构交涉,并将战利品分发到各个前线服务器。
核心亮点:
自带 4 次失败重试机制:针对 DNS 延迟或 CA 接口抖动,脚本加入了优雅的
while重试循环,加上120秒的 DNS 等待防抖,极大地提高了成功率。宝塔路径完美契合:抛弃了修改 Nginx 配置文件的方法,直接采用“鸠占鹊巢”的策略,把新证书重命名为
fullchain.pem和privkey.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)
Nginx reload invalid 报错: 在灰度测试时发现,由于服务器操作系统的差异,有的 CentOS 机器无法识别
systemctl reload nginx。最后改为systemctl、init.d和直接调用 Nginx 二进制文件三重 Fallback 才彻底解决。青龙环境的路径迷局: 尝试用
dirname "$0"获取子脚本路径时翻车了。因为青龙面板是通过内部调度器(Task)执行脚本的,相对路径会失效,最后老老实实写死了绝对路径/ql/data/scripts/renew_edgeone.sh。DNS 验证失败: 不要想着保留
_acme-challenge的 TXT 记录来提高成功率,这反而会导致解析响应包过大和旧 Token 冲突。正确的做法是老老实实把--dnssleep拉长到 120 秒防抖。
结语
看着青龙面板里绿色的 Success 日志,以及邮箱里准时送达的合并报告,心里有一种说不出的舒坦。以后无论项目里再长出多少个 subdomain.machao.group,我只需要在数组里加个逗号,一切自动搞定。
生命苦短,让机器去干脏活累活吧!
碎碎念 (后记):
代码写完的那一刻,忽然觉得这简直就像是在排兵布阵。之前还在因为 WELL-LOG-PRO 项目的 DTW/DBA 算法头疼,或者在折腾那台 Hackintosh 笔记本的 AirDrop 和分区,现在却在这里搞运维和自动化。或许这才是技术的魅力,跨越边界,为了“偷懒”而疯狂写代码,最终换来系统的秩序与内心的宁静。

