在不同Linux系统下用脚本实现SSL证书的自动更新
2026/5/21 计算机 次 0 条
好像是从去年开始,各大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。