#!/bin/bash # # 脚本名称: update_acme_cert.sh # 功能: 自动检测并更新 ACME SSL 证书 # 作者: Assistant # 日期: 2026-03-03 # sudo chmod +x acme_renew.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(可选) 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 # 检查是否包含证书结束标记(用户提供的特征) # 使用 -- 避免 grep 将证书内容当作选项处理 if grep -q -- "-----END CERTIFICATE-----" "$cert_file"; then # 可选:检查特定指纹(按需启用) # if grep -q "O1CA0HAB8LbWS" "$cert_file"; then log_info "✅ 证书内容验证通过" return 0 # fi 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 重载成功" # 主流程 main() {