愚蠢的地球人

在不同Linux系统下用脚本实现SSL证书的自动更新

好像是从去年开始,各大ssl证书服务商统一把免费证书的有效期从 1 年缩短到了 90 天,以前一年更新一次,手动操作还可以忍,现在每年要操作四到五次,就有点麻烦了。于是我在 Windows Server 云服务器上部署了 Win-acms,解决了证书更新的问题。但是我还有几台其他的 Linux 设备,也需要同步更新,于是我想到了通过计划任务定时执行脚本来实现证书自动更新。

首先,把 Win-acms 自动下载的证书挂到 ftp 下,直接用 IIS 内置的的 ftp 服务就行,注意开启 SSL。

原理很简单,写一个 sh 脚本检查证书的有效期,如果临近过期,则连接到远程 ftp 服务器,下载最新的证书并替换原来快过期的证书,然后重启 Web 服务器。最后把这个脚本添加到系统的计划任务中,每天定时执行就行了。

这一切在 Ubuntu 和 Debian 系统下都非常容易实现,直接贴代码,/etc/ssl/update_ssl.sh:

#!/usr/bin/bash
# 自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd)

# 如果不是在终端运行(说明是 crontab 触发),则启用静默和错误日志模式
if [ ! -t 1 ]; then
    exec 2>> "${SCRIPT_DIR}/update_ssl_err.log" >/dev/null
fi

# ==================== 配置区域 ====================
FTP_HOST="ftp.your_host"
FTP_USER="ftp_user"
FTP_PASS="password"
FTP_DIR="/ssl"
FTP_URL="ftp://${FTP_HOST}${FTP_DIR}"
CURL_OPTS="--ssl-reqd -k -sS -u ${FTP_USER}:${FTP_PASS}"

REMOTE_CERT_FILE="fairysoft.net-chain.pem" 
REMOTE_KEY_FILE="fairysoft.net-key.pem"  

LOCAL_CERT_FILE="/etc/ssl/fairysoft.net-chain.pem"
LOCAL_KEY_FILE="/etc/ssl/fairysoft.net-key.pem"

NGINX_RELOAD_CMD="systemctl reload nginx"
EXPIRE_DAYS=28
# =================================================

# 检查本地证书文件是否存在
if [ ! -f "$LOCAL_CERT_FILE" ]; then
    echo "本地证书文件不存在,将直接下载新证书。"
else
    # 获取当前证书的过期时间并计算剩余天数
    EXPIRE_DATE=$(openssl x509 -in "$LOCAL_CERT_FILE" -enddate -noout | cut -d= -f2)
    EXPIRE_SECS=$(date -d "$EXPIRE_DATE" +%s)
    CURRENT_SECS=$(date +%s)
    DAYS_LEFT=$(( ($EXPIRE_SECS - $CURRENT_SECS) / 86400 ))
    EXPIRE_DATE=$(date -d "$EXPIRE_DATE" '+%Y-%m-%d')
    # 判断是否需要更新
    if [ "$DAYS_LEFT" -ge "$EXPIRE_DAYS" ]; then
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,无需更新。"
        exit 0
    else
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,需要更新。"
    fi
fi

echo "开始从远程 FTP 服务器下载新证书..."

# 通过 curl 从 FTP 服务器下载最新证书
echo "正在下载证书..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_CERT_FILE}" -o "${LOCAL_CERT_FILE}.new"

echo "正在下载私钥..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_KEY_FILE}" -o "${LOCAL_KEY_FILE}.new"

# 检查下载是否成功
if [ -s "${LOCAL_CERT_FILE}.new" ] && [ -s "${LOCAL_KEY_FILE}.new" ]; then
    echo "下载成功!正在替换旧证书..."
    
    # 备份旧证书
    [ -f "$LOCAL_CERT_FILE" ] && mv "$LOCAL_CERT_FILE" "${LOCAL_CERT_FILE}.bak"
    [ -f "$LOCAL_KEY_FILE" ] && mv "$LOCAL_KEY_FILE" "${LOCAL_KEY_FILE}.bak"
    
    # 移动新证书到正式位置
    mv "${LOCAL_CERT_FILE}.new" "$LOCAL_CERT_FILE"
    mv "${LOCAL_KEY_FILE}.new" "$LOCAL_KEY_FILE"
    
    # 设置正确的权限
    chmod 644 "$LOCAL_CERT_FILE"
    chmod 600 "$LOCAL_KEY_FILE"
    
    # 重载 Nginx 使新证书生效
    echo "正在重载 Nginx 服务..."
    if eval $NGINX_RELOAD_CMD; then
        echo "SSL 证书更新并重载 Nginx 成功!"
    else
        echo "Nginx 重载失败,请检查配置文件!"
    fi
else
    echo "错误:从 FTP 下载证书失败或文件为空,更新终止。"
    rm -f "${LOCAL_CERT_FILE}.new" "${LOCAL_KEY_FILE}.new"
    exit 1
fi

echo "SSL 证书自动更新成功并已生效!"

然后用 crontab -e 编辑计划任务,添加:

0 2 * * * /usr/bin/bash /etc/ssl/update-ssl.sh

完成!就这么简单

在搞定了 Debian 服务器之后,我又开始研究威联通的 NAS,威联通的 QNAP 系统是深度定制的 Linux 系统,跟常见的 Ubuntu/Debian 系统区别很大。最大的区别,也是我踩过的最大的坑,就是证书的格式。

QNAP 虽然使用的是 Apache 作为 Web 服务器,但是证书管理却使用了 stunnel,配置文件如下:

SSLCertificateFile      "/etc/stunnel/stunnel.pem"  # 这里面同时包含私钥和网站证书
SSLCertificateChainFile "/etc/stunnel/uca.pem"      # 中继证书

从配置文件可以看出来,威联通使用的证书格式跟 Nginx 和 Apache 都不一样,私钥和网站证书合并成一个证书链,而中继证书却单独放。

经过几天时间的折腾,最后终于搞定了,/mnt/HDA_ROOT/.config/stunnel/update_ssl.sh:

#!/bin/bash
# 自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd)

# 如果不是在终端运行(说明是 crontab 触发),则启用静默和错误日志模式
if [ ! -t 1 ]; then
    exec 2>> "${SCRIPT_DIR}/update_ssl_err.log" >/dev/null
fi

# ==================== 配置区域 ====================
FTP_HOST="ftp.your_host"
FTP_USER="ftp_user"
FTP_PASS="password"
FTP_DIR="/ssl"
FTP_URL="ftp://${FTP_HOST}${FTP_DIR}"
CURL_OPTS="--ssl-reqd -k -sS -u ${FTP_USER}:${FTP_PASS}"

REMOTE_CRT_FILE="fairysoft.net-crt.pem"
REMOTE_KEY_FILE="fairysoft.net-key.pem"
REMOTE_CHAIN_FILE="fairysoft.net-chain-only.pem"

TMP_DIR="/tmp/ssl_download"
STUNNEL_DIR="/mnt/HDA_ROOT/.config/stunnel"
LOCAL_CERT_FILE="${STUNNEL_DIR}/stunnel.pem"
LOCAL_UCA_FILE="${STUNNEL_DIR}/uca.pem"
EXPIRE_DAYS=28
# =================================================

# 检查本地证书文件是否存在
if [ ! -f "$LOCAL_CERT_FILE" ]; then
    echo "本地证书文件不存在,将直接下载新证书。"
else
    # 获取当前证书的过期时间并计算剩余天数
    EXPIRE_DATE=$(openssl x509 -in "$LOCAL_CERT_FILE" -enddate -noout | cut -d= -f2)
    EXPIRE_SECS=$(date -d "$EXPIRE_DATE" +%s)
    CURRENT_SECS=$(date +%s)
    DAYS_LEFT=$(( ($EXPIRE_SECS - $CURRENT_SECS) / 86400 ))
    EXPIRE_DATE=$(date -d "$EXPIRE_DATE" '+%Y-%m-%d')
    # 判断是否需要更新
    if [ "$DAYS_LEFT" -ge "$EXPIRE_DAYS" ]; then
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,无需更新。"
        exit 0
    else
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,需要更新。"
    fi
fi

echo "开始从远程 FTP 服务器下载新证书..."

# 创建并清理临时下载目录
mkdir -p "$TMP_DIR"

# 通过 curl 从 FTP 服务器下载最新证书
echo "正在下载 crt 文件..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_CRT_FILE}" -o "${TMP_DIR}/${REMOTE_CRT_FILE}"

echo "正在下载 key 文件..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_KEY_FILE}" -o "${TMP_DIR}/${REMOTE_KEY_FILE}"

echo "正在下载 chain 文件..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_CHAIN_FILE}" -o "${TMP_DIR}/${REMOTE_CHAIN_FILE}"

# 校验下载的文件大小,防止下载了空文件导致服务瘫痪
if [ ! -s "${TMP_DIR}/${REMOTE_CRT_FILE}" ] || [ ! -s "${TMP_DIR}/${REMOTE_KEY_FILE}" ]; then
    echo "错误:从 FTP 下载证书失败或文件为空!更新终止。"
    rm -rf "$TMP_DIR"
    exit 1
fi

echo "下载完成!开始更新旧证书..."
# 备份旧证书
[ -f "$LOCAL_CERT_FILE" ] && mv "$LOCAL_CERT_FILE" "${LOCAL_CERT_FILE}.bak"
[ -f "$LOCAL_UCA_FILE" ] && mv "$LOCAL_UCA_FILE" "${LOCAL_UCA_FILE}.bak"

# 合并私钥与域名证书到 stunnel.pem,私钥在前,证书在后 
cat "${TMP_DIR}/${REMOTE_KEY_FILE}" "${TMP_DIR}/${REMOTE_CRT_FILE}" > "${LOCAL_CERT_FILE}"

# 写入中间证书链
if [ -s "${TMP_DIR}/${REMOTE_CHAIN_FILE}" ]; then
    cat "${TMP_DIR}/${REMOTE_CHAIN_FILE}" > "${LOCAL_UCA_FILE}"
fi

# 覆盖两个 backup 文件
cat "${TMP_DIR}/${REMOTE_CRT_FILE}" > "${STUNNEL_DIR}/backup.cert"
cat "${TMP_DIR}/${REMOTE_KEY_FILE}" > "${STUNNEL_DIR}/backup.key"

# chmod 600 "${LOCAL_CERT_FILE}" "${LOCAL_UCA_FILE}"

# 清理临时文件
rm -rf "$TMP_DIR"

echo "正在重启系统服务使证书生效..."

# 重启核心 stunnle、web 与反向代理服务
/etc/init.d/stunnel.sh stop
sleep 3
/etc/init.d/Qthttpd.sh restart 2>&1 | grep -v "php_ext.ini"
sleep 3
/etc/init.d/thttpd.sh restart
sleep 3
/etc/init.d/stunnel.sh start
sleep 3
if [ -f "/etc/init.d/reverse_proxy.sh" ]; then
    /etc/init.d/reverse_proxy.sh reload  2>&1 | grep -v "AH00558"
fi

echo "SSL 证书自动更新成功并已生效!"

脚本跑通之后就是添加计划任务,威联通系统添加计划任务的方法跟其他 Linux 系统不一样,网上有很多教程,很容易就能搜到。需要先编辑 /etc/config/crontab,可以使用 vi 或者 nano,然后再执行指令:

crontab /etc/config/crontab && /etc/init.d/crond.sh restart

在搞定了威联通之后,我又把目光转到了我家里的一台 Home Assistant 服务器,HAOS 系统更加特殊,虽然底层是 Linux,但是默认并没有开启 SSH,需要先下载一个 SSH工具。

在 HA 插件商店里,搜索并安装 Advanced SSH & Web Terminal,开启 SSH,编写脚本测试,运行正常,/ssl/update_ssl.sh:

#!/bin/bash
# 自动获取当前脚本所在的绝对路径
SCRIPT_DIR=$(cd "$(dirname "$0")"; pwd)

# 如果不是在终端运行(说明是 crontab 触发),则启用静默和错误日志模式
if [ ! -t 1 ]; then
    exec 2>> "${SCRIPT_DIR}/update_ssl_err.log" >/dev/null
fi
# ==================== HAOS 专用配置区域 ====================
FTP_HOST="ftp.your_host"
FTP_USER="ftp_user"
FTP_PASS="password"
FTP_DIR="/ssl"
FTP_URL="ftp://${FTP_HOST}${FTP_DIR}"
CURL_OPTS="--ssl-reqd -k -sS -u ${FTP_USER}:${FTP_PASS}"

REMOTE_CERT_FILE="fairysoft.net-chain.pem" 
REMOTE_KEY_FILE="fairysoft.net-key.pem"  

LOCAL_CERT_FILE="/ssl/fairysoft.net-chain.pem"
LOCAL_KEY_FILE="/ssl/fairysoft.net-key.pem"

NGINX_RELOAD_CMD="ha apps restart core_nginx_proxy"
EXPIRE_DAYS=28
# =========================================================


# 检查本地证书文件是否存在
if [ ! -f "$LOCAL_CERT_FILE" ]; then
    echo "本地证书文件不存在,将直接下载新证书。"
else
    # 获取 OpenSSL 的原始英文时间
    RAW_DATE=$(openssl x509 -in "$LOCAL_CERT_FILE" -enddate -noout | cut -d= -f2)
    # 示例格式: May 30 23:59:59 2026 GMT
    
    # 针对 HAOS (Alpine) 环境,用 awk 将英文月份转换为纯数字格式 (YYYY-MM-DD HH:MM:SS)
    CLEAN_DATE=$(echo "$RAW_DATE" | awk '
    BEGIN {
        split("Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec", months, "|");
        for (i=1; i<=12; i++) m_num[months[i]] = sprintf("%02d", i);
    }
    {
        print $4 "-" m_num[$1] "-" sprintf("%02d", $2) " " $3
    }')

    EXPIRE_DATE=$(date -d "$CLEAN_DATE" '+%Y-%m-%d')

    # 计算具体的剩余天数 $DAYS_LEFT
    EXPIRE_SECS=$(date -d "$CLEAN_DATE" +%s)
    CURRENT_SECS=$(date +%s)
    DAYS_LEFT=$(( ($EXPIRE_SECS - $CURRENT_SECS) / 86400 ))

    # 检查证书是否在指定的 $EXPIRE_DAYS 天数内过期
    CHECK_SECS=$(( EXPIRE_DAYS * 86400 ))
    if openssl x509 -in "$LOCAL_CERT_FILE" -checkend $CHECK_SECS > /dev/null; then
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,无需更新。"
        exit 0
    else
        echo "当前证书有效期($EXPIRE_DATE),将在 $DAYS_LEFT 天后过期 ,需要更新。"
    fi
fi

echo "开始从远程 FTP 服务器下载新证书..."

# 通过 curl 从 FTP 服务器下载最新证书
echo "正在下载证书..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_CERT_FILE}" -o "${LOCAL_CERT_FILE}.new"

echo "正在下载私钥..."
curl $CURL_OPTS "${FTP_URL}/${REMOTE_KEY_FILE}" -o "${LOCAL_KEY_FILE}.new"

# 检查下载是否成功
if [ -s "${LOCAL_CERT_FILE}.new" ] && [ -s "${LOCAL_KEY_FILE}.new" ] && ! grep -q -E "html|Error" "${LOCAL_CERT_FILE}.new"; then
    echo "下载成功!正在替换旧证书..."
    
    # 备份旧证书
    [ -f "$LOCAL_CERT_FILE" ] && mv "$LOCAL_CERT_FILE" "${LOCAL_CERT_FILE}.bak"
    [ -f "$LOCAL_KEY_FILE" ] && mv "$LOCAL_KEY_FILE" "${LOCAL_KEY_FILE}.bak"
    
    # 移动新证书到正式位置
    mv "${LOCAL_CERT_FILE}.new" "$LOCAL_CERT_FILE"
    mv "${LOCAL_KEY_FILE}.new" "$LOCAL_KEY_FILE"
    
    # 设置正确的权限
    chmod 644 "$LOCAL_CERT_FILE"
    chmod 600 "$LOCAL_KEY_FILE"
    
    # 重载 Nginx
    echo "正在重载 Nginx 服务..."
    if eval $NGINX_RELOAD_CMD; then
        echo "SSL 证书更新并重启 Nginx 成功!"
    else
        echo "Nginx 重启失败,请检查命令行!"
    fi
else
    echo "错误:从 FTP 下载证书失败,更新终止。"
    rm -f "${LOCAL_CERT_FILE}.new" "${LOCAL_KEY_FILE}.new"
    exit 1
fi

echo "SSL 证书自动更新成功并已生效!"

脚本跑通之后,下一步就是添加计划任务了,但这时候问题来了,计划任务却怎么都跑不起来。

问 AI,告诉我说 HAOS 不支持计划任务,让我使用系统自带的自动化功能配合 Shell Command 来实现,在 AI 的引导下,一顿操作猛如虎,折腾了一上午,脚本硬是没有执行起来。

只好自己想办法排查问题,经过一番排查,发现 Shell Command 根本没有 /ssl 目录的写入权限,然后我改目录,证书倒是能下载了,最后却又没有 ha apps restart core_nginx_proxy 的执行权限。

被一堆权限问题搞得焦头烂额之后,我只好换了一家 AI,请出 Gemini 大哥出场,他让我直接在 Advanced SSH & Web Terminal 的配置页面里编辑YAML:

init_commands:
  - echo "00 03 * * * /bin/bash /ssl/update_ssl.sh" > /etc/crontabs/root
  - echo "" >> /etc/crontabs/root
  - chmod 600 /etc/crontabs/root
  - crond

改完之后点保存,系统会自动重启 Advanced SSH & Web Terminal,测试正常,还得是Gemini。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

相关推荐

最新留言

最近发表

网站分类

Tags列表

友情链接

控制面板

站点统计