Files
acme_renew/update_acme_cert.sh
jeremygan2021 5d12be811e first commit
2026-03-03 15:01:07 +08:00

310 lines
9.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
#
# 脚本名称: update_acme_cert.sh
# 功能: 自动检测并更新 ACME SSL 证书
# 作者: Assistant
# 日期: 2026-03-03
# sudo chmod +x update_acme_cert.sh
# ================= 配置区域 =================
ACME_SH="/root/.acme.sh/acme.sh"
NGINX_CONF_DIR="/etc/nginx/conf.d"
WEBROOT="/var/www/letsencrypt"
TLS_BASE_DIR="/etc/nginx/tls"
LOG_FILE="/var/log/acme_update.log"
# ===========================================
# 彩色输出(方便阅读)
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数 - 中文输出
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log_warn() { echo -e "${YELLOW}[警告]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log_error() { echo -e "${RED}[错误]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
log_step() { echo -e "${BLUE}[步骤]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; }
# 检查是否 root 权限
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "请使用 root 权限运行此脚本"
exit 1
fi
}
# 输入域名并验证
input_domain() {
echo ""
read -p "$(echo -e "${BLUE}[输入]${NC} 请输入要更新的域名例如example.com: ")" DOMAIN
if [[ -z "$DOMAIN" ]]; then
log_error "域名不能为空"
exit 1
fi
log_info "目标域名: $DOMAIN"
}
# 检查 nginx 配置文件是否存在
check_nginx_conf() {
CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.conf"
if [[ ! -f "$CONF_FILE" ]]; then
log_warn "未找到配置文件: $CONF_FILE"
# 尝试模糊匹配
CONF_FILE=$(grep -l "server_name.*$DOMAIN" ${NGINX_CONF_DIR}/*.conf 2>/dev/null | head -n1)
if [[ -z "$CONF_FILE" ]]; then
log_error "$NGINX_CONF_DIR 中未找到包含域名 $DOMAIN 的 nginx 配置"
exit 1
fi
log_info "通过模糊匹配找到配置文件: $CONF_FILE"
else
log_info "找到配置文件: $CONF_FILE"
fi
}
# 从 nginx 配置中提取证书路径
extract_cert_path() {
log_step "解析配置文件,提取证书路径..."
# 提取 ssl_certificate 路径
CERT_PATH=$(grep -E "^\s*ssl_certificate\s+" "$CONF_FILE" | head -n1 | awk '{print $2}' | tr -d ';')
KEY_PATH=$(grep -E "^\s*ssl_certificate_key\s+" "$CONF_FILE" | head -n1 | awk '{print $2}' | tr -d ';')
if [[ -z "$CERT_PATH" || -z "$KEY_PATH" ]]; then
log_warn "未在配置中找到 ssl_certificate 或 ssl_certificate_key 指令"
# 使用默认路径
CERT_DIR="${TLS_BASE_DIR}/${DOMAIN}"
CERT_FILE="${CERT_DIR}/cert.pem"
KEY_FILE="${CERT_DIR}/key.pem"
log_info "使用默认证书目录: $CERT_DIR"
else
CERT_DIR=$(dirname "$CERT_PATH")
CERT_FILE="$CERT_PATH"
KEY_FILE="$KEY_PATH"
log_info "证书文件: $CERT_FILE"
log_info "密钥文件: $KEY_FILE"
fi
# 确保目录存在
mkdir -p "$CERT_DIR"
}
# 检查证书是否过期(返回 0=已过期1=未过期)
check_cert_expired() {
local cert_file="$1"
if [[ ! -f "$cert_file" ]]; then
log_warn "证书文件不存在: $cert_file,视为需要更新"
return 0
fi
# 获取过期时间
EXPIRE_DATE=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2)
if [[ -z "$EXPIRE_DATE" ]]; then
log_warn "无法读取证书过期时间,视为需要更新"
return 0
fi
EXPIRE_TIMESTAMP=$(date -d "$EXPIRE_DATE" +%s 2>/dev/null)
NOW_TIMESTAMP=$(date +%s)
DAYS_LEFT=$(( (EXPIRE_TIMESTAMP - NOW_TIMESTAMP) / 86400 ))
log_info "证书过期时间: $EXPIRE_DATE"
log_info "距离过期还有: ${DAYS_LEFT}"
# 提前 30 天预警更新
if [[ $DAYS_LEFT -lt 30 ]]; then
log_warn "证书即将过期(<30天需要更新"
return 0
else
log_info "证书仍在有效期内,无需更新"
return 1
fi
}
# 使用 acme.sh 颁发/更新证书
renew_certificate() {
log_step "开始更新证书: $DOMAIN"
# 方式1: 设置默认 CA可选
log_info "【方式1】设置默认 CA 为 Let's Encrypt..."
if ! "$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1; then
log_warn "设置 CA 失败,继续尝试颁发证书..."
fi
# 方式2: 颁发证书webroot 模式)
log_info "【方式2】使用 webroot 模式颁发证书..."
log_info "Webroot 路径: $WEBROOT"
local cmd="$ACME_SH --issue -d $DOMAIN --webroot $WEBROOT"
if [[ "$FORCE_RENEW" == "true" ]]; then
cmd="$cmd --force"
log_info "⚠️ 已启用强制更新模式"
fi
# 捕获输出用于分析,同时也显示在终端
local tmp_out=$(mktemp)
$cmd 2>&1 | tee "$tmp_out"
local ret=${PIPESTATUS[0]}
# 分析结果
if [[ $ret -eq 0 ]]; then
log_info "✅ 证书颁发/更新成功!"
rm -f "$tmp_out"
return 0
else
# 检查是否是因为已经更新而跳过
if grep -qE "Domains not changed|Skipping|already issued" "$tmp_out"; then
log_warn "⚠️ ACME 提示证书无需更新或已存在,视为成功"
rm -f "$tmp_out"
return 0
else
log_error "❌ 证书颁发失败,请检查域名解析和 webroot 权限"
rm -f "$tmp_out"
return 1
fi
fi
}
# 验证证书内容(匹配用户提供的特征)
verify_cert_success() {
local cert_file="$1"
if [[ ! -f "$cert_file" ]]; then
log_warn "验证失败: 证书文件不存在 -> $cert_file"
return 1
fi
# 检查是否包含证书结束标记
if grep -q "END CERTIFICATE" "$cert_file"; then
log_info "✅ 证书内容验证通过"
return 0
fi
log_warn "证书内容验证未通过 - 未找到结束标记"
return 1
}
# 安装证书到指定位置
install_certificate() {
log_step "安装证书到生产环境..."
INSTALL_CMD="$ACME_SH --install-cert -d $DOMAIN \
--key-file ${KEY_FILE} \
--fullchain-file ${CERT_FILE} \
--reloadcmd \"service nginx force-reload\""
log_info "执行安装命令: $INSTALL_CMD"
if eval "$INSTALL_CMD"; then
log_info "✅ 证书安装成功"
return 0
else
log_error "❌ 证书安装失败"
return 1
fi
}
# 设置证书文件权限
set_cert_permissions() {
log_step "设置证书文件权限..."
# 确保使用实际目录
CERT_DIR=$(dirname "$CERT_FILE")
if sudo chown www-data:www-data "${CERT_DIR}"/*.pem 2>/dev/null; then
log_info "✅ 文件所有者已设置为 www-data:www-data"
else
log_warn "⚠️ chown 执行失败,请手动检查权限"
fi
if sudo chmod 600 "${CERT_DIR}"/*.pem 2>/dev/null; then
log_info "✅ 文件权限已设置为 600仅所有者可读写"
else
log_warn "⚠️ chmod 执行失败,请手动检查权限"
fi
# 确保 nginx 可读www-data 是 nginx 运行用户)
log_info "✅ 权限设置完成nginx 可正常读取证书"
}
# 重载 nginx 使配置生效
reload_nginx() {
log_step "重载 nginx 配置..."
if service nginx force-reload >/dev/null 2>&1 || nginx -s reload >/dev/null 2>&1; then
log_info "✅ nginx 重载成功"
else
log_warn "⚠️ nginx 重载失败,请手动执行: service nginx force-reload"
fi
}
# 主流程
main() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} 🔄 ACME SSL 证书自动更新脚本 (Robust) ${NC}"
echo -e "${BLUE} 日志文件: $LOG_FILE ${NC}"
echo -e "${BLUE}========================================${NC}"
check_root
input_domain
check_nginx_conf
extract_cert_path
local need_renew=false
local do_install=false
# 检查证书是否需要更新
if check_cert_expired "$CERT_FILE"; then
need_renew=true
else
echo ""
echo -e "${YELLOW}当前证书仍在有效期内,请选择操作:${NC}"
echo "1) 强制更新证书 (Force Renew)"
echo "2) 跳过更新,仅执行安装和重载 (Install Only)"
echo "3) 退出脚本 (Exit)"
read -p "请输入选项 [1-3]: " choice
case "$choice" in
1) need_renew=true; FORCE_RENEW=true ;;
2) need_renew=false; do_install=true ;;
*) log_info "用户选择退出"; exit 0 ;;
esac
fi
if [[ "$need_renew" == "true" ]]; then
log_step "准备执行证书更新..."
if renew_certificate; then
# 更新成功(或跳过)后,必须执行安装
do_install=true
else
log_error "❌ 证书更新失败,流程终止"
exit 1
fi
fi
if [[ "$do_install" == "true" ]]; then
if install_certificate; then
set_cert_permissions
reload_nginx
# 最终验证(验证生产环境中的文件)
if verify_cert_success "$CERT_FILE"; then
log_info "🎉 域名 [$DOMAIN] 证书更新/安装全流程完成!"
else
log_error "❌ 新证书验证失败,请手动检查 $CERT_FILE"
exit 1
fi
else
log_error "❌ 证书安装失败"
exit 1
fi
else
log_info "✨ 域名 [$DOMAIN] 证书无需更新,任务结束"
fi
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} ✅ 脚本执行完毕 ${NC}"
echo -e "${GREEN}========================================${NC}"
}
# 执行主函数
main "$@"