#!/bin/bash
# =================配置区域=================
# 证书源目录的基础路径 (脚本会自动追加 IPv4 地址)
# 最终寻找路径例如: /etc/ssl/ip/1.2.3.4/fullchain.pem
SOURCE_BASE_DIR="/etc/ssl/ip"
# 距离到期还剩多少天时进行替换 (默认 1 天)
EXPIRY_DAYS=1
# 日志文件路径
LOG_FILE="/var/log/panel_ssl_update.log"
# =================配置结束=================
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 日志函数
log() {
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $1" | tee -a "$LOG_FILE"
}
# 获取公网 IPv4
get_public_ip() {
# 尝试多个接口获取 IP
IP=$(curl -s4m 5 https://api.ipify.org || curl -s4m 5 https://ifconfig.me || curl -s4m 5 https://myip.ipip.net | grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
if [[ -z "$IP" ]]; then
log "${RED}[Error] 无法获取公网 IPv4 地址,请检查网络连接。${NC}"
exit 1
fi
echo "$IP"
}
# 核心逻辑:检测面板并生成任务列表
detect_targets() {
# 定义全局数组存储任务
# 格式: "证书路径|私钥路径|重启命令|任务描述|文件权限用户"
TARGETS=()
# ---------------------------------------------------------
# 1. CyberPanel 检测模块
# ---------------------------------------------------------
# 检测 CyberPanel 面板端口 (8090) - 这是必须存在的
if [[ -f "/usr/local/lscp/conf/cert.pem" ]]; then
log "${BLUE}[Info] 检测到 CyberPanel 面板服务 (Port 8090)。${NC}"
# 添加 8090 面板任务
TARGETS+=("/usr/local/lscp/conf/cert.pem|/usr/local/lscp/conf/key.pem|systemctl restart lscpd|CyberPanel_8090|lsadm")
# 接下来检测 LSWS WebAdmin (Port 7080)
# 逻辑:先找 webadmin.crt,找不到再找 admin.crt
if [[ -f "/usr/local/lsws/admin/conf/webadmin.crt" ]]; then
log "${BLUE}[Info] 检测到 LSWS WebAdmin (Port 7080) - 使用 webadmin.crt 路径。${NC}"
TARGETS+=("/usr/local/lsws/admin/conf/webadmin.crt|/usr/local/lsws/admin/conf/webadmin.key|systemctl restart lsws|LSWS_WebAdmin_7080|lsadm")
elif [[ -f "/usr/local/lsws/admin/conf/cert/admin.crt" ]]; then
log "${BLUE}[Info] 检测到 LSWS WebAdmin (Port 7080) - 使用 admin.crt 路径。${NC}"
TARGETS+=("/usr/local/lsws/admin/conf/cert/admin.crt|/usr/local/lsws/admin/conf/cert/admin.key|systemctl restart lsws|LSWS_WebAdmin_7080|lsadm")
else
log "${YELLOW}[Warn] 未找到 LSWS WebAdmin (7080) 的证书文件,跳过该端口的处理。${NC}"
fi
# ---------------------------------------------------------
# 2. aaPanel/宝塔 检测模块
# ---------------------------------------------------------
elif [[ -f "/www/server/panel/ssl/certificate.pem" ]]; then
log "${BLUE}[Info] 检测到 aaPanel/宝塔面板。${NC}"
# 宝塔不需要指定特定用户(root即可),这里留空或填root
TARGETS+=("/www/server/panel/ssl/certificate.pem|/www/server/panel/ssl/privateKey.pem|bt restart|aaPanel_Main|root")
else
log "${RED}[Error] 未检测到任何已知的面板证书路径。无法继续。${NC}"
exit 1
fi
}
# 检查证书状态
# 返回 0 表示需要替换,返回 1 表示不需要
check_cert_status() {
local cert_path=$1
# 文件不存在,必须替换
if [[ ! -f "$cert_path" ]]; then return 0; fi
local issuer=$(openssl x509 -in "$cert_path" -noout -issuer 2>/dev/null)
# 1. 检查是否为 Let's Encrypt
if [[ "$issuer" != *"Let's Encrypt"* ]]; then
log "[Check] ${cert_path} 不是 Let's Encrypt 证书,准备替换。"
return 0
fi
# 2. 检查过期时间
local end_date_str=$(openssl x509 -in "$cert_path" -noout -enddate | cut -d= -f2)
local end_date_epoch=$(date -d "$end_date_str" +%s)
local current_epoch=$(date +%s)
local threshold_seconds=$((EXPIRY_DAYS * 86400))
local remaining_seconds=$((end_date_epoch - current_epoch))
if [[ $remaining_seconds -lt $threshold_seconds ]]; then
log "[Check] ${cert_path} 即将过期 (剩余 $((remaining_seconds/86400)) 天),准备替换。"
return 0
else
log "[Check] ${cert_path} 有效期充足,跳过。"
return 1
fi
}
# 执行替换流程
process_targets() {
local ip=$1
local src_fullchain="${SOURCE_BASE_DIR}/${ip}/fullchain.pem"
local src_key="${SOURCE_BASE_DIR}/${ip}/private.key"
# 检查源证书是否存在
if [[ ! -s "$src_fullchain" ]] || [[ ! -s "$src_key" ]]; then
log "${RED}[Error] 源 IP 证书不存在或为空: ${src_fullchain}${NC}"
log "${YELLOW}[Tip] 请确认证书是否已申请并放置在 /etc/ssl/ip/${ip}/ 目录下${NC}"
exit 1
fi
# 遍历任务列表
for target in "${TARGETS[@]}"; do
IFS='|' read -r t_cert t_key t_cmd t_desc t_user <<< "$target"
check_cert_status "$t_cert"
if [ $? -eq 0 ]; then
log "${YELLOW}[Action] 开始替换: $t_desc ...${NC}"
# 1. 备份
cp -f "$t_cert" "${t_cert}.bak_$(date +%F_%H%M)" 2>/dev/null
cp -f "$t_key" "${t_key}.bak_$(date +%F_%H%M)" 2>/dev/null
# 2. 覆盖 (使用 cat 保持 inode 和部分属性)
cat "$src_fullchain" > "$t_cert"
cat "$src_key" > "$t_key"
# 3. 权限修正
if [[ -n "$t_user" ]] && [[ "$t_user" != "root" ]]; then
if id "$t_user" &>/dev/null; then
chown "$t_user:$t_user" "$t_cert" "$t_key"
fi
fi
chmod 600 "$t_cert" "$t_key"
# 4. 重启服务
log "[Restart] 执行重启: $t_cmd"
eval "$t_cmd" >/dev/null 2>&1
if [ $? -eq 0 ]; then
log "${GREEN}[Success] $t_desc 替换完成。${NC}"
else
log "${RED}[Error] $t_desc 重启命令失败。${NC}"
fi
fi
done
}
# 添加 Crontab
add_cron() {
local script_path=$(readlink -f "$0")
local cron_job="0 1 * * * /bin/bash $script_path >> $LOG_FILE 2>&1"
if ! crontab -l 2>/dev/null | grep -Fq "$script_path"; then
(crontab -l 2>/dev/null; echo "$cron_job") | crontab -
log "[Init] 已添加计划任务 (每天凌晨 1 点)。"
fi
}
# =================主逻辑入口=================
# 必须 root 运行
if [[ $EUID -ne 0 ]]; then echo "请使用 root 运行此脚本"; exit 1; fi
PUBLIC_IP=$(get_public_ip)
log "=== 开始执行面板 SSL 维护脚本 (IP: $PUBLIC_IP) ==="
# 1. 探测环境
detect_targets
# 2. 确保计划任务
add_cron
# 3. 执行处理
process_targets "$PUBLIC_IP"
log "=== 脚本执行结束 ==="