From 5d12be811e884e5c11205de971bc8cc7f3a2b45f Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Tue, 3 Mar 2026 15:01:07 +0800 Subject: [PATCH] first commit --- README.md | 305 +++++++++++++++++++++++++++++++++++++++++++ acme_test.sh | 250 +++++++++++++++++++++++++++++++++++ test_head.sh | 243 ++++++++++++++++++++++++++++++++++ test_head_clean.sh | 239 ++++++++++++++++++++++++++++++++++ update_acme_cert.sh | 310 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1347 insertions(+) create mode 100644 README.md create mode 100644 acme_test.sh create mode 100644 test_head.sh create mode 100644 test_head_clean.sh create mode 100644 update_acme_cert.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..e32de41 --- /dev/null +++ b/README.md @@ -0,0 +1,305 @@ +# ACME SSL 证书自动管理工具集 + +一个功能强大的 ACME SSL 证书自动管理工具集合,专为 Nginx 服务器环境设计,提供证书更新、环境诊断和自动化管理功能。 + +## 🎯 项目概述 + +本项目包含三个核心脚本,用于解决 SSL 证书管理的常见痛点: +- **自动证书更新**:智能检测证书有效期并自动更新 +- **环境诊断**:全面检测 ACME 挑战环境配置 +- **一键管理**:简化复杂的证书管理流程 + +## 📋 功能特性 + +### 🔄 自动证书更新 (`update_acme_cert.sh`) +- **智能过期检测**:自动检查证书剩余有效期(默认30天预警) +- **多种更新模式**:支持自动更新、强制更新、仅安装模式 +- **Nginx 集成**:自动解析 Nginx 配置文件,提取证书路径 +- **权限管理**:自动设置正确的文件权限和所有者 +- **安全验证**:证书内容验证和指纹检查 +- **彩色日志**:中文彩色输出,便于阅读和调试 +- **完整日志**:详细的操作日志记录到 `/var/log/acme_update.log` + +### 🔍 环境诊断 (`acme_test.sh`) +- **DNS 解析检查**:验证域名解析是否正确指向服务器 +- **Webroot 权限测试**:检测挑战文件写入权限 +- **Nginx 配置检查**:验证 ACME 挑战 location 规则 +- **HTTP 访问测试**:模拟 Let's Encrypt 验证过程 +- **网络连通性**:检测端口监听和外部访问 +- **智能清理**:自动清理测试文件 + +### 🛠️ 核心优势 +- **零配置依赖**:自动检测和适配现有环境 +- **错误恢复**:详细的错误提示和解决方案建议 +- **安全加固**:遵循最佳安全实践 +- **中文友好**:完整的中文界面和日志输出 +- **生产就绪**:经过充分测试,适用于生产环境 + +## 🚀 快速开始 + +### 环境要求 +- **操作系统**:Linux (Ubuntu/CentOS/Debian 等) +- **权限要求**:root 权限或 sudo 访问 +- **依赖软件**: + - Nginx Web 服务器 + - acme.sh 证书工具 + - curl、openssl、grep 等基础工具 + +### 安装步骤 + +1. **克隆项目** +```bash +git clone <项目地址> +cd acme-ssl-manager +``` + +2. **设置执行权限** +```bash +chmod +x *.sh +``` + +3. **安装 acme.sh**(如果尚未安装) +```bash +curl https://get.acme.sh | sh +``` + +4. **配置 Nginx** +确保您的 Nginx 配置包含 ACME 挑战 location 规则: +```nginx +location ^~ /.well-known/acme-challenge/ { + allow all; + root /var/www/letsencrypt; +} +``` + +## 📖 使用指南 + +### 1. 环境诊断(推荐先运行) + +在使用证书更新功能前,建议先运行环境诊断脚本: + +```bash +# 交互式模式 +sudo ./acme_test.sh + +# 直接指定域名 +sudo ./acme_test.sh example.com + +# 指定域名和Webroot +sudo ./acme_test.sh example.com /var/www/letsencrypt +``` + +诊断脚本将检查: +- ✅ DNS 解析是否正确 +- ✅ Webroot 目录权限 +- ✅ Nginx 配置完整性 +- ✅ HTTP 访问可用性 +- ✅ 端口监听状态 + +### 2. 证书更新 + +#### 基本用法 +```bash +# 交互式更新 +sudo ./update_acme_cert.sh + +# 直接更新指定域名(需要修改脚本) +# 编辑脚本设置 DOMAIN 变量 +``` + +#### 操作模式 +脚本运行时会根据证书状态提供不同选项: + +1. **证书即将过期**(<30天):自动执行更新 +2. **证书仍有效**:提供三个选项 + - **强制更新**:重新颁发证书 + - **仅安装**:跳过更新,仅安装现有证书 + - **退出**:不做任何操作 + +#### 更新流程 +1. **权限检查**:验证 root 权限 +2. **域名输入**:交互式输入目标域名 +3. **配置解析**:自动查找 Nginx 配置文件 +4. **证书检测**:检查当前证书有效期 +5. **证书更新**:使用 acme.sh 颁发新证书 +6. **证书安装**:安装到指定位置 +7. **权限设置**:设置正确的文件权限 +8. **Nginx 重载**:使新证书生效 +9. **最终验证**:确认证书安装成功 + +### 3. 配置文件 + +#### 主要配置项 +编辑脚本文件修改以下配置: + +```bash +# acme.sh 路径 +ACME_SH="/root/.acme.sh/acme.sh" + +# Nginx 配置目录 +NGINX_CONF_DIR="/etc/nginx/conf.d" + +# Webroot 路径 +WEBROOT="/var/www/letsencrypt" + +# 证书存储目录 +TLS_BASE_DIR="/etc/nginx/tls" + +# 日志文件 +LOG_FILE="/var/log/acme_update.log" +``` + +#### 高级配置 +- **强制更新**:设置 `FORCE_RENEW="true"` +- **自定义 CA**:修改 `--server` 参数 +- **通知设置**:可集成邮件/Webhook 通知 + +## 🔧 高级功能 + +### 证书指纹验证 +支持自定义证书指纹验证: +```bash +# 在 verify_cert_success 函数中启用指纹检查 +if grep -q "YOUR_FINGERPRINT" "$cert_file"; then + log_info "✅ 证书指纹验证通过" + return 0 +fi +``` + +### 多域名支持 +支持通配符域名和多域名证书: +```bash +# 修改 renew_certificate 函数 +local cmd="$ACME_SH --issue -d $DOMAIN -d *.${DOMAIN} --webroot $WEBROOT" +``` + +### 自动续期 +设置定时任务实现自动续期: +```bash +# 编辑 crontab +crontab -e + +# 添加以下内容(每天凌晨检查) +0 2 * * * /path/to/update_acme_cert.sh >> /var/log/acme_cron.log 2>&1 +``` + +## 📊 日志和监控 + +### 日志文件 +- **更新日志**:`/var/log/acme_update.log` +- **定时任务日志**:`/var/log/acme_cron.log` + +### 日志格式 +``` +[INFO] 2026-03-03 10:30:45 - 目标域名: example.com +[步骤] 2026-03-03 10:30:45 - 解析配置文件,提取证书路径... +[警告] 2026-03-03 10:30:45 - 证书即将过期(<30天),需要更新 +``` + +### 监控建议 +- **日志轮转**:配置 logrotate 管理日志文件 +- **监控告警**:监控日志中的错误关键词 +- **证书监控**:设置证书有效期监控告警 + +## 🐛 故障排除 + +### 常见问题 + +#### 1. DNS 解析失败 +```bash +# 检查域名解析 +dig +short your-domain.com +# 或 +nslookup your-domain.com +``` + +#### 2. Webroot 权限问题 +```bash +# 修正权限 +sudo chown -R www-data:www-data /var/www/letsencrypt +sudo chmod -R 755 /var/www/letsencrypt +``` + +#### 3. Nginx 配置错误 +```bash +# 检查 Nginx 配置 +sudo nginx -t +# 重载配置 +sudo service nginx force-reload +``` + +#### 4. 端口未监听 +```bash +# 检查端口监听 +sudo ss -tlnp | grep ':80' +# 检查防火墙 +sudo ufw status +sudo iptables -L +``` + +#### 5. acme.sh 未安装 +```bash +# 安装 acme.sh +curl https://get.acme.sh | sh +# 重新加载环境 +source ~/.bashrc +``` + +### 错误代码说明 + +| 错误类型 | 说明 | 解决方案 | +|---------|------|----------| +| DNS 解析不一致 | 域名解析 IP 与服务器 IP 不匹配 | 检查 DNS 设置,确认域名指向正确 | +| Webroot 写入失败 | 无法写入挑战文件 | 检查目录权限和磁盘空间 | +| HTTP 访问失败 | 无法通过 HTTP 访问挑战文件 | 检查 Nginx 配置和端口监听 | +| 证书颁发失败 | acme.sh 无法颁发证书 | 检查域名解析、Webroot 配置 | + +## 🔒 安全考虑 + +### 文件权限 +- **证书文件**:设置为 600(仅所有者可读写) +- **所有者**:设置为 www-data(Nginx 运行用户) +- **目录权限**:Webroot 目录设置为 755 + +### 最佳实践 +1. **定期更新**:及时更新证书避免过期 +2. **监控告警**:设置证书有效期监控 +3. **备份配置**:定期备份 Nginx 配置和证书 +4. **访问控制**:限制对证书目录的访问 +5. **日志审计**:定期检查操作日志 + +## 🤝 贡献指南 + +欢迎提交 Issue 和 Pull Request! + +### 开发规范 +- 遵循 Bash 最佳实践 +- 添加适当的错误处理 +- 保持中文界面友好 +- 更新相关文档 + +### 测试要求 +- 在多种 Linux 发行版上测试 +- 验证不同 Nginx 配置场景 +- 测试错误处理和恢复机制 + +## 📄 许可证 + +本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件 + +## 🙏 致谢 + +- [acme.sh](https://github.com/acmesh-official/acme.sh) - 优秀的 ACME 客户端 +- [Let's Encrypt](https://letsencrypt.org/) - 提供免费 SSL 证书 +- [Nginx](https://nginx.org/) - 高性能 Web 服务器 + +## 📞 支持 + +如有问题或建议,请通过以下方式联系: +- 提交 Issue +- 发送邮件至:[your-email@example.com] +- 访问项目主页:[项目地址] + +--- + +**⭐ 如果这个项目对您有帮助,请给我们一个 Star!** \ No newline at end of file diff --git a/acme_test.sh b/acme_test.sh new file mode 100644 index 0000000..40abb1b --- /dev/null +++ b/acme_test.sh @@ -0,0 +1,250 @@ +#!/bin/bash +# +# 脚本名称: acme_test.sh (Enhanced) +# 功能: 全面诊断 ACME 挑战环境(权限、配置、网络) +# 日期: 2026-03-03 + +# ================= 配置区域 ================= +# 默认配置,可被交互式输入覆盖 +DEFAULT_WEBROOT="/var/www/letsencrypt" +NGINX_CONF_DIR="/etc/nginx/conf.d" + +# 彩色输出 +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 + +# 日志函数 +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_step() { echo -e "\n${BLUE}[STEP]${NC} $1"; } +log_substep() { echo -e " ${CYAN}➜${NC} $1"; } + +# 检查 root 权限 +check_root() { + if [[ $EUID -ne 0 ]]; then + log_error "请使用 root 权限运行此脚本 (sudo)" + exit 1 + fi +} + +# 获取域名和 Webroot +get_input() { + log_step "配置检查参数" + + # 域名输入 + if [[ -z "$1" ]]; then + read -p "请输入要测试的域名 (例如: example.com): " DOMAIN + else + DOMAIN="$1" + fi + + if [[ -z "$DOMAIN" ]]; then + log_error "域名不能为空" + exit 1 + fi + log_info "目标域名: $DOMAIN" + + # Webroot 输入 + if [[ -z "$2" ]]; then + read -p "请输入 Webroot 路径 [默认: $DEFAULT_WEBROOT]: " INPUT_WEBROOT + WEBROOT="${INPUT_WEBROOT:-$DEFAULT_WEBROOT}" + else + WEBROOT="$2" + fi + log_info "Webroot: $WEBROOT" +} + +# 检查 DNS 解析 +check_dns() { + log_step "DNS 解析检查" + + # 获取本机公网 IP (尝试多个源) + LOCAL_IP=$(curl -s4 ifconfig.me || curl -s4 icanhazip.com) + log_substep "本机公网 IP: ${LOCAL_IP:-无法获取}" + + # 获取域名解析 IP + # 优先使用 dig,如果没有则尝试 ping + if command -v dig &> /dev/null; then + DOMAIN_IP=$(dig +short "$DOMAIN" | head -n1) + else + DOMAIN_IP=$(ping -c 1 "$DOMAIN" 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -n1) + fi + + log_substep "域名解析 IP: ${DOMAIN_IP:-无法获取}" + + if [[ -n "$LOCAL_IP" && -n "$DOMAIN_IP" ]]; then + if [[ "$LOCAL_IP" == "$DOMAIN_IP" ]]; then + log_info "✅ DNS 解析正确指向本机" + else + log_warn "⚠️ DNS 解析 IP ($DOMAIN_IP) 与本机 IP ($LOCAL_IP) 不一致" + log_warn " (如果是 CDN/负载均衡环境,请忽略此警告)" + fi + elif [[ -z "$DOMAIN_IP" ]]; then + log_warn "⚠️ 无法解析域名 $DOMAIN,请检查 DNS 设置" + fi +} + +# 检查 Webroot 权限与写入 +check_webroot_access() { + log_step "Webroot 写入测试" + + CHALLENGE_DIR="${WEBROOT}/.well-known/acme-challenge" + + # 检查 Webroot 是否存在 + if [[ ! -d "$WEBROOT" ]]; then + log_warn "Webroot 目录不存在: $WEBROOT" + read -p "是否创建该目录? [y/N] " create_choice + if [[ "$create_choice" =~ ^[Yy]$ ]]; then + mkdir -p "$WEBROOT" + log_info "✅ 已创建目录: $WEBROOT" + else + log_error "无法继续测试,目录不存在" + exit 1 + fi + fi + + # 创建挑战目录 + if [[ ! -d "$CHALLENGE_DIR" ]]; then + mkdir -p "$CHALLENGE_DIR" + log_substep "创建挑战目录: $CHALLENGE_DIR" + fi + + # 权限检查 (简单版) + TEST_FILE="${CHALLENGE_DIR}/test_token_$(date +%s)" + echo "acme_test_content" > "$TEST_FILE" + + if [[ -f "$TEST_FILE" ]]; then + log_info "✅ Root 用户写入成功: $TEST_FILE" + else + log_error "❌ Root 用户写入失败,请检查磁盘空间或文件系统只读状态" + return 1 + fi + + # 修正权限 (确保 Nginx 可读) + chown -R www-data:www-data "$WEBROOT" + chmod -R 755 "$WEBROOT" + log_info "✅ 已修正 Webroot 权限 (owner: www-data, mod: 755)" + + return 0 +} + +# Nginx 配置与端口检查 +check_nginx() { + log_step "Nginx 配置与服务检查" + + # 检查端口 + if ss -tlnp | grep -q ':80\b'; then + log_info "✅ 端口 80 正在监听" + else + log_error "❌ 端口 80 未被监听,Nginx 可能未启动" + log_info "尝试查看 Nginx 状态:" + systemctl status nginx --no-pager | head -n 5 + fi + + # 检查配置中是否有 location 规则 + CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.conf" + + # 如果找不到标准命名的 conf,尝试模糊搜索 + if [[ ! -f "$CONF_FILE" ]]; then + CONF_FILE=$(grep -l "server_name.*$DOMAIN" ${NGINX_CONF_DIR}/*.conf 2>/dev/null | head -n1) + fi + + if [[ -f "$CONF_FILE" ]]; then + log_substep "检查配置文件: $CONF_FILE" + if grep -q "location.*\.well-known/acme-challenge" "$CONF_FILE"; then + log_info "✅ 发现 ACME challenge location 规则" + else + log_warn "⚠️ 未在配置文件中发现显式的 .well-known/acme-challenge 规则" + log_warn " (如果使用了 include 或通用配置,可能也是正常的)" + fi + else + log_warn "⚠️ 未找到域名 $DOMAIN 的专属配置文件 (在 $NGINX_CONF_DIR)" + fi +} + +# 模拟 HTTP 请求 +perform_http_test() { + log_step "HTTP 访问测试 (模拟 ACME 验证)" + + TEST_FILENAME=$(basename "$TEST_FILE") + # 构造 URL + # 本地测试 URL (绕过 DNS) + TEST_URL="http://127.0.0.1/.well-known/acme-challenge/$TEST_FILENAME" + # 外部访问 URL + DOMAIN_URL="http://${DOMAIN}/.well-known/acme-challenge/$TEST_FILENAME" + + log_substep "1. 本地回环测试 (127.0.0.1)" + # 使用 Host 头强制指定域名 + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: $DOMAIN" "$TEST_URL") + + if [[ "$HTTP_CODE" == "200" ]]; then + log_info "✅ 本地访问成功 (HTTP 200)" + CONTENT=$(curl -s -H "Host: $DOMAIN" "$TEST_URL") + if [[ "$CONTENT" == "acme_test_content" ]]; then + log_info "✅ 内容匹配成功" + else + log_error "❌ 内容不匹配: 获取到了 '$CONTENT'" + fi + else + log_error "❌ 本地访问失败 (HTTP $HTTP_CODE)" + log_warn " 可能原因: Nginx root/alias 配置错误,或 location 匹配错误" + log_info " 尝试获取详细 Header:" + curl -I -H "Host: $DOMAIN" "$TEST_URL" 2>&1 | head -n 10 + fi + + log_substep "2. 域名解析访问测试 (公共网络模拟)" + echo " 尝试直接访问: $DOMAIN_URL" + EXTERNAL_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$DOMAIN_URL") + + if [[ "$EXTERNAL_CODE" == "200" ]]; then + log_info "✅ 域名访问成功 (HTTP 200)" + elif [[ "$EXTERNAL_CODE" == "301" ]] || [[ "$EXTERNAL_CODE" == "302" ]]; then + log_warn "⚠️ 检测到重定向 (HTTP $EXTERNAL_CODE)" + REDIRECT_URL=$(curl -s -o /dev/null -w "%{redirect_url}" "$DOMAIN_URL") + log_info " 重定向目标: $REDIRECT_URL" + log_info " ACME 协议通常支持重定向,但请确保最终目标可达 (HTTPS 配置是否正确?)" + elif [[ "$EXTERNAL_CODE" == "000" ]]; then + log_error "❌ 无法连接到服务器 (Timeout/Connection Refused)" + log_warn " 请检查防火墙 (ufw/iptables) 和安全组设置 (端口 80)" + else + log_error "❌ 域名访问失败 (HTTP $EXTERNAL_CODE)" + fi +} + +# 清理 +cleanup() { + log_step "清理测试文件" + if [[ -f "$TEST_FILE" ]]; then + rm -f "$TEST_FILE" + log_info "✅ 已删除测试文件: $TEST_FILE" + else + log_info "无文件需要清理" + fi +} + +# 主流程 +main() { + # 注册清理钩子,脚本退出时自动清理 + trap cleanup EXIT + + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE} 🔍 ACME 验证环境诊断工具 ${NC}" + echo -e "${BLUE}========================================${NC}" + + check_root + get_input "$1" "$2" + + check_dns + check_webroot_access + check_nginx + perform_http_test + + echo -e "\n${GREEN}=== 诊断结束 ===${NC}" +} + +main "$@" diff --git a/test_head.sh b/test_head.sh new file mode 100644 index 0000000..f2c4541 --- /dev/null +++ b/test_head.sh @@ -0,0 +1,243 @@ +#!/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 重载成功" + else + log_warn "⚠️ nginx 重载失败,请手动执行: service nginx force-reload" + fi +} + +# 主流程 +main() { diff --git a/test_head_clean.sh b/test_head_clean.sh new file mode 100644 index 0000000..22aed94 --- /dev/null +++ b/test_head_clean.sh @@ -0,0 +1,239 @@ +#!/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() { diff --git a/update_acme_cert.sh b/update_acme_cert.sh new file mode 100644 index 0000000..779de90 --- /dev/null +++ b/update_acme_cert.sh @@ -0,0 +1,310 @@ +#!/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 "$@" \ No newline at end of file