first commit
This commit is contained in:
305
README.md
Normal file
305
README.md
Normal 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-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!**
|
||||||
250
acme_test.sh
Normal file
250
acme_test.sh
Normal 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
243
test_head.sh
Normal 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
239
test_head_clean.sh
Normal 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
310
update_acme_cert.sh
Normal 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 "$@"
|
||||||
Reference in New Issue
Block a user