first commit

This commit is contained in:
jeremygan2021
2026-03-03 15:01:07 +08:00
commit 5d12be811e
5 changed files with 1347 additions and 0 deletions

305
README.md Normal file
View File

@@ -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-dataNginx 运行用户)
- **目录权限**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**

250
acme_test.sh Normal file
View File

@@ -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 "$@"

243
test_head.sh Normal file
View File

@@ -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() {

239
test_head_clean.sh Normal file
View File

@@ -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() {

310
update_acme_cert.sh Normal file
View File

@@ -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 "$@"