From beb3273a08a2cc29afcbbce89b340439a7886ab2 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Mon, 23 Mar 2026 00:11:03 +0800 Subject: [PATCH] power --- README.md | 68 ++++++- update_acme_cert.sh | 454 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 497 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e32de41..af1f1a7 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,29 @@ ## 🎯 项目概述 -本项目包含三个核心脚本,用于解决 SSL 证书管理的常见痛点: +本项目包含四个核心脚本,用于解决 SSL 证书管理的常见痛点: +- **新域名证书创建**:一键为新域名创建 Let's Encrypt SSL 证书 - **自动证书更新**:智能检测证书有效期并自动更新 - **环境诊断**:全面检测 ACME 挑战环境配置 - **一键管理**:简化复杂的证书管理流程 ## 📋 功能特性 +### 🆕 新域名证书创建 (`create_new_domain_cert.sh`) +- **交互式向导**:友好的中文交互界面,引导用户完成证书创建 +- **自动安装 acme.sh**:智能检测并自动安装/升级 acme.sh 工具 +- **智能验证**:自动验证域名格式和端口范围(1-65535) +- **Webroot 自动配置**:自动创建和授权 ACME 挑战目录 +- **配置冲突清理**:智能检测并清理可能存在的 nginx 配置冲突 +- **证书有效性检测**:检查现有证书状态,避免重复申请 +- **智能重试机制**:支持 3 次自动重试,采用指数退避策略 +- **域名可达性检测**:自动检测域名 80 端口可达性 +- **多方式 nginx 重载**:支持 systemctl、service、nginx -s 等多种重载方式 +- **幂等性支持**:自动备份旧配置,支持重复运行 +- **彩色中文日志**:详细的操作日志,便于阅读和调试 +- **一键 HTTPS 配置**:自动生成 HTTP → HTTPS 强制跳转配置 +- **最终验证流程**:测试 HTTPS 访问、HTTP 跳转、SSL 证书有效性 + ### 🔄 自动证书更新 (`update_acme_cert.sh`) - **智能过期检测**:自动检查证书剩余有效期(默认30天预警) - **多种更新模式**:支持自动更新、强制更新、仅安装模式 @@ -74,7 +90,29 @@ location ^~ /.well-known/acme-challenge/ { ## 📖 使用指南 -### 1. 环境诊断(推荐先运行) +### 1. 新域名证书创建 + +为新域名快速创建 Let's Encrypt SSL 证书: + +```bash +# 交互式创建(推荐) +sudo ./create_new_domain_cert.sh +``` + +脚本将引导您完成以下步骤: +1. **输入域名**:例如 `example.com` +2. **输入后端端口**:例如 `3000` +3. **指定证书目录**:默认 `/etc/nginx/tls/example.com` + +创建流程包括: +- 自动安装/升级 acme.sh +- 创建并授权 ACME 挑战目录 +- 清理现有 nginx 配置冲突 +- 申请 Let's Encrypt 证书 +- 生成 HTTPS 配置 +- 验证证书和跳转 + +### 2. 环境诊断(推荐先运行) 在使用证书更新功能前,建议先运行环境诊断脚本: @@ -96,7 +134,7 @@ sudo ./acme_test.sh example.com /var/www/letsencrypt - ✅ HTTP 访问可用性 - ✅ 端口监听状态 -### 2. 证书更新 +### 3. 证书更新 #### 基本用法 ```bash @@ -127,7 +165,7 @@ sudo ./update_acme_cert.sh 8. **Nginx 重载**:使新证书生效 9. **最终验证**:确认证书安装成功 -### 3. 配置文件 +### 4. 配置文件 #### 主要配置项 编辑脚本文件修改以下配置: @@ -154,6 +192,28 @@ LOG_FILE="/var/log/acme_update.log" - **自定义 CA**:修改 `--server` 参数 - **通知设置**:可集成邮件/Webhook 通知 +### 新域名证书创建脚本配置项 + +`create_new_domain_cert.sh` 的主要配置项: + +```bash +# acme.sh 路径 +ACME_SH="/root/.acme.sh/acme.sh" + +# ACME 挑战目录 +WEBROOT="/var/www/letsencrypt" + +# Nginx 配置目录 +NGINX_CONF_DIR="/etc/nginx/conf.d" + +# Nginx sites 目录 +NGINX_AVAILABLE="/etc/nginx/sites-available" +NGINX_ENABLED="/etc/nginx/sites-enabled" + +# 管理员邮箱(用于 acme.sh) +EMAIL="admin@$(hostname -f)" +``` + ## 🔧 高级功能 ### 证书指纹验证 diff --git a/update_acme_cert.sh b/update_acme_cert.sh index 779de90..1817070 100644 --- a/update_acme_cert.sh +++ b/update_acme_cert.sh @@ -1,9 +1,30 @@ #!/bin/bash # # 脚本名称: update_acme_cert.sh -# 功能: 自动检测并更新 ACME SSL 证书 +# 功能: 自动检测并更新 ACME SSL 证书(增强版) # 作者: Assistant -# 日期: 2026-03-03 +# 日期: 2026-03-23 +# 用法: +# sudo ./update_acme_cert.sh +# +# 交互式输入: +# - 域名 (例如: example.com) +# +# 新增增强功能: +# 1. 自动安装/升级 acme.sh(如未安装) +# 2. 自动备份旧证书配置 +# 3. nginx 配置冲突智能清理 +# 4. 域名可达性自动检测 +# 5. 证书更新失败自动重试(最多3次,指数退避) +# 6. 多维度最终验证(HTTPS访问、HTTP跳转、SSL证书) +# 7. 详细的诊断建议和排查步骤 +# +# 前提条件: +# - 需要 root 权限运行 +# - 需要安装 nginx +# - 需要开放 80 和 443 端口 +# - 域名需正确解析到本服务器 +# # sudo chmod +x update_acme_cert.sh # ================= 配置区域 ================= @@ -12,6 +33,7 @@ NGINX_CONF_DIR="/etc/nginx/conf.d" WEBROOT="/var/www/letsencrypt" TLS_BASE_DIR="/etc/nginx/tls" LOG_FILE="/var/log/acme_update.log" +EMAIL="admin@$(hostname -f)" # =========================================== # 彩色输出(方便阅读) @@ -19,6 +41,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' +CYAN='\033[0;36m' NC='\033[0m' # No Color # 日志函数 - 中文输出 @@ -26,6 +49,7 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" 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"; } +log_success() { echo -e "${CYAN}[完成]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; } # 检查是否 root 权限 check_root() { @@ -35,6 +59,71 @@ check_root() { fi } +# 检查并安装 acme.sh +install_acme_sh() { + log_step "检查 acme.sh 安装状态..." + + if [[ -f "$ACME_SH" ]]; then + log_success "acme.sh 已安装: $ACME_SH" + + local version=$("$ACME_SH" --version 2>/dev/null | head -n1) + log_info "当前版本: $version" + + echo "" + read -p "$(echo -e "${YELLOW}[确认]${NC} 是否升级 acme.sh 到最新版本? [y/N]: ")" upgrade + if [[ "$upgrade" =~ ^[Yy]$ ]]; then + log_info "正在升级 acme.sh..." + if "$ACME_SH" --upgrade --auto-upgrade; then + log_success "acme.sh 升级成功" + else + log_warn "升级失败,继续使用当前版本" + fi + fi + else + log_warn "未检测到 acme.sh,开始自动安装..." + + log_info "执行安装命令: curl https://get.acme.sh | sh -s email=$EMAIL" + if curl https://get.acme.sh | sh -s email=$EMAIL; then + log_success "acme.sh 安装成功" + + if [[ -f "$ACME_SH" ]]; then + log_info "验证安装: $ACME_SH" + else + log_error "acme.sh 安装验证失败" + exit 1 + fi + else + log_error "acme.sh 安装失败,请检查网络连接" + exit 1 + fi + fi + + log_info "设置默认 CA 为 Let's Encrypt..." + "$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1 || true +} + +# 备份旧配置(幂等性支持) +backup_old_configs() { + log_step "检查并备份旧配置..." + + local backup_count=0 + local timestamp=$(date +%s) + + # 备份证书目录 + if [[ -d "$CERT_DIR" ]] && [[ -n "$(ls -A $CERT_DIR 2>/dev/null)" ]]; then + local backup_dir="${CERT_DIR}.bak-${timestamp}" + cp -r "$CERT_DIR" "$backup_dir" + log_info "已备份证书目录: $backup_dir" + backup_count=$((backup_count + 1)) + fi + + if [[ $backup_count -eq 0 ]]; then + log_info "未发现旧配置,无需备份" + else + log_success "共备份 $backup_count 个旧配置" + fi +} + # 输入域名并验证 input_domain() { echo "" @@ -90,6 +179,136 @@ extract_cert_path() { mkdir -p "$CERT_DIR" } +# 清理 nginx 配置冲突 +cleanup_nginx_conflicts() { + log_step "清理 nginx 配置冲突..." + + local cleaned=0 + + # 1. 清理所有包含此域名的配置文件 + log_info "搜索包含 ${DOMAIN} 的配置文件..." + local conflict_files=$(grep -rl "server_name.*${DOMAIN}" /etc/nginx/ 2>/dev/null | grep -v ".bak-" | grep -v ".bak/") + + if [[ -n "$conflict_files" ]]; then + echo "$conflict_files" | while read file; do + log_warn "发现冲突配置: $file" + + local count=$(grep -c "server_name" "$file" 2>/dev/null || echo 0) + + if [[ $count -gt 1 ]]; then + log_warn "文件包含多个域名,仅移除此域名的配置" + log_error "需要手动处理: $file" + elif [[ $count -eq 1 ]]; then + local backup="${file}.bak-conflict-$(date +%s)" + mv "$file" "$backup" + log_info "已备份并移除冲突配置: $backup" + cleaned=$((cleaned + 1)) + fi + done + fi + + # 2. 清理 sites-enabled 中的符号链接 + if [[ -L "/etc/nginx/sites-enabled/${DOMAIN}.conf" ]]; then + rm -f "/etc/nginx/sites-enabled/${DOMAIN}.conf" + log_info "已删除符号链接: /etc/nginx/sites-enabled/${DOMAIN}.conf" + cleaned=$((cleaned + 1)) + fi + + # 3. 清理 sites-available 中的配置 + if [[ -f "/etc/nginx/sites-available/${DOMAIN}.conf" ]]; then + rm -f "/etc/nginx/sites-available/${DOMAIN}.conf" + log_info "已删除配置: /etc/nginx/sites-available/${DOMAIN}.conf" + cleaned=$((cleaned + 1)) + fi + + # 4. 清理 conf.d 中所有相关配置 + local confd_files=$(find /etc/nginx/conf.d/ -name "*${DOMAIN}*.conf" -type f 2>/dev/null) + echo "$confd_files" | while read file; do + if [[ "$file" != "$CONF_FILE" ]]; then + local backup="${file}.bak-old-$(date +%s)" + mv "$file" "$backup" + log_info "已备份旧配置: $backup" + cleaned=$((cleaned + 1)) + fi + done + + if [[ $cleaned -eq 0 ]]; then + log_info "未发现配置冲突" + else + log_success "已清理 $cleaned 个冲突配置" + log_warn "建议运行: systemctl restart nginx" + fi +} + +# 检测域名 80 端口可达性 +check_domain_reachability() { + log_step "检测域名 80 端口可达性..." + + local test_url="http://${DOMAIN}/.well-known/acme-challenge/test" + log_info "测试 URL: $test_url" + + local response + response=$(curl -I -s --connect-timeout 10 "$test_url" 2>&1) + local http_code=$(echo "$response" | grep -i "HTTP/" | head -n1 | awk '{print $2}') + local location=$(echo "$response" | grep -i "^Location:" | head -n1 | awk '{print $2}' | tr -d '\r') + + log_info "HTTP 响应码: ${http_code:-无响应}" + + case "$http_code" in + 200) + log_success "✅ 域名 80 端口可达性检测通过 (HTTP 200)" + log_info "ACME 挑战路径正常响应" + return 0 + ;; + 404) + log_success "✅ 域名 80 端口可达性检测通过 (HTTP 404)" + log_warn "ACME 挑战路径返回 404,但 nginx 已响应" + log_info "nginx 会自动创建挑战文件,检测通过" + return 0 + ;; + 301|302) + log_warn "检测到 HTTP 跳转 (HTTP $http_code)" + if [[ -n "$location" ]]; then + log_info "跳转目标: $location" + + if [[ "$location" =~ ^https:// ]]; then + log_warn "⚠️ HTTPS 跳转已配置生效!" + log_info "这说明之前已经成功配置过 HTTPS" + + echo "" + echo -e "${YELLOW}检测到域名 $DOMAIN 已配置 HTTPS 跳转${NC}" + echo "这可能是之前运行的配置残留" + echo "" + + if [[ "$location" == "https://" ]] || [[ "$location" == "https://$DOMAIN" ]] || [[ "$location" =~ ^https://.*$ ]]; then + log_info "HTTPS 跳转配置正常,域名可达" + log_info "可以继续证书更新流程" + return 0 + else + log_error "跳转目标异常: $location" + return 1 + fi + else + log_warn "跳转到非 HTTPS 地址: $location" + return 1 + fi + else + log_warn "收到 HTTP $http_code 但无 Location 头" + return 1 + fi + ;; + 000) + log_error "❌ 连接失败 (HTTP $http_code)" + log_error "域名可能未解析或防火墙阻止" + return 1 + ;; + *) + log_error "❌ 意外的 HTTP 响应码: $http_code" + return 1 + ;; + esac +} + # 检查证书是否过期(返回 0=已过期,1=未过期) check_cert_expired() { local cert_file="$1" @@ -166,6 +385,84 @@ renew_certificate() { fi } +# 带重试的证书更新函数 +renew_certificate_with_retry() { + local max_retries=3 + local retry_count=0 + local wait_seconds=5 + + while [[ $retry_count -lt $max_retries ]]; do + retry_count=$((retry_count + 1)) + + echo "" + log_step "尝试更新证书 (第 $retry_count/$max_retries 次)..." + + # 调用基本的证书更新函数 + if renew_certificate; then + log_success "✅ 证书更新成功!" + return 0 + else + # 更新失败,分析原因 + log_warn "⚠️ 第 $retry_count 次更新失败" + + # 检查是否是配置问题导致的失败 + if grep -q "Invalid response.*404" /var/log/acme_update.log 2>/dev/null || grep -q "conflicting server name" /var/log/nginx/error.log 2>/dev/null; then + log_warn "检测到配置冲突问题,尝试清理并重试..." + + # 再次清理配置 + cleanup_nginx_conflicts + + # 完全重启 nginx + log_info "重启 nginx 服务..." + if systemctl restart nginx >/dev/null 2>&1 || systemctl reload nginx >/dev/null 2>&1 || service nginx restart >/dev/null 2>&1; then + log_success "nginx 重启成功" + else + log_error "nginx 重启失败,请手动检查" + fi + + # 等待一下让 nginx 完全启动 + sleep $wait_seconds + fi + + # 如果不是最后一次尝试,等待后重试 + if [[ $retry_count -lt $max_retries ]]; then + echo "" + log_info "等待 ${wait_seconds} 秒后重试..." + sleep $wait_seconds + + # 增加等待时间(指数退避) + wait_seconds=$((wait_seconds * 2)) + fi + fi + done + + # 所有重试都失败 + log_error "❌ 经过 $max_retries 次尝试,证书更新仍然失败" + echo "" + log_error "可能的原因:" + echo " 1. 域名未正确解析到本服务器" + echo " 2. 防火墙阻止了 80 端口" + echo " 3. nginx 配置存在冲突" + echo " 4. webroot 目录权限不足" + echo "" + echo "建议的排查步骤:" + echo " # 1. 检查域名解析" + echo " dig ${DOMAIN}" + echo "" + echo " # 2. 测试 80 端口" + echo " curl -I http://${DOMAIN}/.well-known/acme-challenge/test" + echo "" + echo " # 3. 检查 nginx 配置" + echo " grep -r 'server_name.*${DOMAIN}' /etc/nginx/" + echo "" + echo " # 4. 检查 webroot 权限" + echo " ls -la ${WEBROOT}" + echo " sudo chown -R www-data:www-data ${WEBROOT}" + echo "" + + return 1 +} + # 验证证书内容(匹配用户提供的特征) verify_cert_success() { local cert_file="$1" @@ -230,13 +527,117 @@ set_cert_permissions() { # 重载 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 重载成功" + if systemctl restart nginx >/dev/null 2>&1; then + log_success "nginx 重启成功" + elif systemctl reload nginx >/dev/null 2>&1; then + log_success "nginx 重载成功" + elif service nginx restart >/dev/null 2>&1; then + log_success "nginx 重启成功 (service 命令)" + elif service nginx reload >/dev/null 2>&1; then + log_success "nginx 重载成功 (service 命令)" + elif service nginx force-reload >/dev/null 2>&1; then + log_success "nginx 重载成功 (force-reload)" + elif nginx -s reload >/dev/null 2>&1; then + log_success "nginx 重载成功 (nginx -s)" else - log_warn "⚠️ nginx 重载失败,请手动执行: service nginx force-reload" + log_error "nginx 重启/重载失败,请手动执行: systemctl restart nginx" + exit 1 fi } +# 增强的最终验证 +enhanced_final_verification() { + log_step "开始最终验证流程..." + + # 测试 HTTPS 访问 + echo "" + echo -e "${BLUE}测试 1: HTTPS 访问${NC}" + local https_response=$(curl -I -s --connect-timeout 10 "https://${DOMAIN}" 2>&1) + local https_code=$(echo "$https_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}') + + if [[ "$https_code" == "200" ]] || [[ "$https_code" == "301" ]] || [[ "$https_code" == "302" ]]; then + log_success "✅ HTTPS 访问成功 (HTTP $https_code)" + else + log_warn "⚠️ HTTPS 响应异常: ${https_code:-无响应}" + fi + + # 测试 HTTP 跳转 + echo "" + echo -e "${BLUE}测试 2: HTTP 跳转测试${NC}" + local http_response=$(curl -I -s --connect-timeout 10 "http://${DOMAIN}" 2>&1) + local http_code=$(echo "$http_response" | grep -i "HTTP/" | head -n1 | awk '{print $2}') + local location=$(echo "$http_response" | grep -i "^Location:" | head -n1 | sed 's/Location: *//i' | tr -d '\r') + + if [[ "$http_code" == "301" ]] || [[ "$http_code" == "302" ]]; then + log_success "✅ HTTP 跳转正常 (HTTP $http_code)" + if [[ -n "$location" ]]; then + log_info "跳转目标: $location" + + # 验证跳转目标是否正确 + if [[ "$location" =~ ^https://${DOMAIN} ]] || [[ "$location" =~ ^https://${DOMAIN}/ ]]; then + log_success "✅ 跳转目标正确: $location" + else + log_warn "⚠️ 跳转目标可能不正确: $location" + log_info "期望格式: https://${DOMAIN}..." + fi + else + log_warn "⚠️ 收到 HTTP $http_code 但没有 Location 头" + log_error "这可能导致浏览器无法自动跳转" + fi + else + log_warn "⚠️ HTTP 跳转异常: ${http_code:-无响应}" + echo "" + log_error "完整响应头:" + echo "$http_response" | head -n 10 + echo "" + log_error "如果显示 nginx 默认页面,可能原因:" + echo " 1. nginx 使用了旧的配置文件" + echo " 2. 80 端口有多个 server 块冲突" + echo " 3. 需要完全重启 nginx 而不是重载" + fi + + # 验证 SSL 证书 + echo "" + echo -e "${BLUE}测试 3: SSL 证书验证${NC}" + local ssl_verify=$(echo | openssl s_client -connect "${DOMAIN}:443" -servername "${DOMAIN}" 2>/dev/null | openssl x509 -noout -subject -dates 2>/dev/null) + if [[ -n "$ssl_verify" ]]; then + log_success "✅ SSL 证书有效" + echo "$ssl_verify" | while read line; do + log_info " $line" + done + else + log_warn "⚠️ SSL 证书验证失败" + fi + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} ✅ SSL 证书更新成功! ${NC}" + echo -e "${GREEN}========================================${NC}" + echo "" + echo -e "${BLUE}📋 配置信息:${NC}" + echo " 域名: $DOMAIN" + echo " 证书文件: $CERT_FILE" + echo " 密钥文件: $KEY_FILE" + echo " Nginx 配置: $CONF_FILE" + echo " 日志文件: $LOG_FILE" + echo "" + echo -e "${BLUE}🧪 验证命令:${NC}" + echo " 1. 测试 HTTPS 访问:" + echo -e " ${GREEN}curl -I https://${DOMAIN}${NC}" + echo "" + echo " 2. 查看证书详情:" + echo -e " ${GREEN}openssl x509 -in $CERT_FILE -noout -text | head -20${NC}" + echo "" + echo " 3. 在线验证 SSL 配置:" + echo -e " ${GREEN}https://www.ssllabs.com/ssltest/analyze.html?d=${DOMAIN}${NC}" + echo "" + echo -e "${YELLOW}⚠️ 提示:${NC}" + echo " - HTTP 请求会自动 301 跳转到 HTTPS" + echo " - 证书将在 60 天后自动续期" + echo " - 如需手动续期: $ACME_SH --renew -d $DOMAIN" + echo "" +} + # 主流程 main() { echo -e "${BLUE}========================================${NC}" @@ -245,10 +646,20 @@ main() { echo -e "${BLUE}========================================${NC}" check_root + install_acme_sh input_domain check_nginx_conf extract_cert_path + echo "" + log_info "开始执行证书更新流程..." + echo "" + + backup_old_configs + + # 清理可能存在的 nginx 配置冲突 + cleanup_nginx_conflicts + local need_renew=false local do_install=false @@ -269,14 +680,18 @@ main() { esac fi + # 只有在需要更新证书时才执行验证和更新流程 if [[ "$need_renew" == "true" ]]; then - log_step "准备执行证书更新..." - - if renew_certificate; then - # 更新成功(或跳过)后,必须执行安装 - do_install=true + if check_domain_reachability; then + if renew_certificate_with_retry; then + # 更新成功(或跳过)后,必须执行安装 + do_install=true + else + log_error "❌ 证书更新失败,流程终止" + exit 1 + fi else - log_error "❌ 证书更新失败,流程终止" + log_error "域名可达性检测失败,无法继续" exit 1 fi fi @@ -286,13 +701,10 @@ main() { set_cert_permissions reload_nginx - # 最终验证(验证生产环境中的文件) - if verify_cert_success "$CERT_FILE"; then - log_info "🎉 域名 [$DOMAIN] 证书更新/安装全流程完成!" - else - log_error "❌ 新证书验证失败,请手动检查 $CERT_FILE" - exit 1 - fi + # 使用增强的最终验证 + enhanced_final_verification + + log_success "🎉 域名 [$DOMAIN] 证书更新/安装全流程完成!" else log_error "❌ 证书安装失败" exit 1 @@ -301,9 +713,9 @@ main() { log_info "✨ 域名 [$DOMAIN] 证书无需更新,任务结束" fi - echo -e "${GREEN}========================================${NC}" - echo -e "${GREEN} ✅ 脚本执行完毕 ${NC}" - echo -e "${GREEN}========================================${NC}" + echo "" + log_success "🎉 域名 [$DOMAIN] SSL 证书更新完成!" + echo "" } # 执行主函数