diff --git a/create_new_domain_cert.sh b/create_new_domain_cert.sh new file mode 100644 index 0000000..6ffc346 --- /dev/null +++ b/create_new_domain_cert.sh @@ -0,0 +1,617 @@ +#!/bin/bash +# +# 脚本名称: create_new_domain_cert.sh +# 功能: 为新域名自动创建 Let's Encrypt SSL 证书 +# 作者: Assistant +# 日期: 2026-03-22 +# 用法: +# sudo ./create_new_domain_cert.sh +# +# 交互式输入: +# - 域名 (例如: example.com) +# - 后端服务端口 (例如: 3000) +# - 证书存放目录 (例如: /etc/ssl/certs/example.com) +# +# 前提条件: +# - 需要 root 权限运行 +# - 需要安装 nginx +# - 需要开放 80 和 443 端口 +# +# sudo chmod +x create_new_domain_cert.sh + +# ================= 配置区域 ================= +ACME_SH="/root/.acme.sh/acme.sh" +WEBROOT="/var/www/letsencrypt" +NGINX_CONF_DIR="/etc/nginx/conf.d" +NGINX_AVAILABLE="/etc/nginx/sites-available" +NGINX_ENABLED="/etc/nginx/sites-enabled" +EMAIL="admin@$(hostname -f)" +# =========================================== + +# 彩色输出(方便阅读) +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} $(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"; } +log_success() { echo -e "${CYAN}[完成]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"; } + +# 全局变量 +DOMAIN="" +BACKEND_PORT="" +CERT_DIR="" +LOG_FILE="" +HTTP_CONF_FILE="" +HTTPS_CONF_FILE="" + +# 检查是否 root 权限 +check_root() { + if [[ $EUID -ne 0 ]]; then + log_error "请使用 root 权限运行此脚本" + exit 1 + fi +} + +# 交互式收集参数 +input_parameters() { + echo "" + echo -e "${BLUE}========================================${NC}" + echo -e "${BLUE} 🆕 新域名 SSL 证书创建向导 ${NC}" + echo -e "${BLUE}========================================${NC}" + echo "" + + # 收集域名 + while true; do + read -p "$(echo -e "${CYAN}[输入]${NC} 请输入域名(例如:example.com): ")" DOMAIN + if [[ -z "$DOMAIN" ]]; then + log_error "域名不能为空" + continue + fi + # 简单的域名格式验证 + if [[ ! "$DOMAIN" =~ ^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$ ]]; then + log_error "域名格式不正确,请重新输入" + continue + fi + break + done + + # 收集后端端口 + while true; do + read -p "$(echo -e "${CYAN}[输入]${NC} 请输入后端服务端口(例如:3000): ")" BACKEND_PORT + if [[ -z "$BACKEND_PORT" ]]; then + log_error "端口不能为空" + continue + fi + # 端口必须是数字 + if [[ ! "$BACKEND_PORT" =~ ^[0-9]+$ ]] || [[ $BACKEND_PORT -lt 1 ]] || [[ $BACKEND_PORT -gt 65535 ]]; then + log_error "端口号必须在 1-65535 之间" + continue + fi + break + done + + # 收集证书目录 + read -p "$(echo -e "${CYAN}[输入]${NC} 请输入证书存放目录(默认:/etc/ssl/certs/${DOMAIN}): ")" INPUT_CERT_DIR + if [[ -z "$INPUT_CERT_DIR" ]]; then + CERT_DIR="/etc/ssl/certs/${DOMAIN}" + else + CERT_DIR="$INPUT_CERT_DIR" + fi + + # 设置日志文件 + LOG_FILE="/var/log/provision-${DOMAIN}.log" + + # 设置配置文件路径 + HTTP_CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.http.conf" + HTTPS_CONF_FILE="${NGINX_CONF_DIR}/${DOMAIN}.https.conf" + + echo "" + log_info "📋 配置汇总:" + echo " 域名: $DOMAIN" + echo " 端口: $BACKEND_PORT" + echo " 证书目录: $CERT_DIR" + echo " 日志文件: $LOG_FILE" + echo "" +} + +# 检查并安装 acme.sh +install_acme_sh() { + log_step "检查 acme.sh 安装状态..." + + if [[ -f "$ACME_SH" ]]; then + log_success "acme.sh 已安装: $ACME_SH" + + # 检查是否需要升级 + local version=$("$ACME_SH" --version 2>/dev/null | head -n1) + log_info "当前版本: $version" + + echo "" + read -p "$(echo -e "${YELLOW}[确认]${NC} 是否升级 acme.sh 到最新版本? [y/N]: ")" upgrade + if [[ "$upgrade" =~ ^[Yy]$ ]]; then + log_info "正在升级 acme.sh..." + if "$ACME_SH" --upgrade --auto-upgrade; then + log_success "acme.sh 升级成功" + else + log_warn "升级失败,继续使用当前版本" + fi + fi + else + log_warn "未检测到 acme.sh,开始自动安装..." + + # 自动安装 acme.sh + log_info "执行安装命令: curl https://get.acme.sh | sh -s email=$EMAIL" + if curl https://get.acme.sh | sh -s email=$EMAIL; then + log_success "acme.sh 安装成功" + + # 验证安装 + if [[ -f "$ACME_SH" ]]; then + log_info "验证安装: $ACME_SH" + else + log_error "acme.sh 安装验证失败" + exit 1 + fi + else + log_error "acme.sh 安装失败,请检查网络连接" + exit 1 + fi + fi + + # 设置默认 CA + log_info "设置默认 CA 为 Let's Encrypt..." + "$ACME_SH" --set-default-ca --server letsencrypt >/dev/null 2>&1 || true +} + +# 创建并授权 webroot 目录 +setup_webroot() { + log_step "创建并授权 webroot 目录..." + + if [[ ! -d "$WEBROOT" ]]; then + log_info "创建 webroot 目录: $WEBROOT" + mkdir -p "$WEBROOT" + else + log_success "webroot 目录已存在: $WEBROOT" + fi + + # 设置权限 + chown -R www-data:www-data "$WEBROOT" + chmod -R 755 "$WEBROOT" + + log_success "webroot 目录权限设置完成" + log_info "所有者: www-data:www-data" + log_info "权限: 755" +} + +# 备份旧配置(幂等性支持) +backup_old_configs() { + log_step "检查并备份旧配置..." + + local backup_count=0 + local timestamp=$(date +%s) + + # 备份 HTTP 配置 + if [[ -f "$HTTP_CONF_FILE" ]]; then + local backup_file="${HTTP_CONF_FILE}.bak-${timestamp}" + mv "$HTTP_CONF_FILE" "$backup_file" + log_info "已备份 HTTP 配置: $backup_file" + backup_count=$((backup_count + 1)) + fi + + # 备份 HTTPS 配置 + if [[ -f "$HTTPS_CONF_FILE" ]]; then + local backup_file="${HTTPS_CONF_FILE}.bak-${timestamp}" + mv "$HTTPS_CONF_FILE" "$backup_file" + log_info "已备份 HTTPS 配置: $backup_file" + backup_count=$((backup_count + 1)) + fi + + # 备份证书目录 + if [[ -d "$CERT_DIR" ]] && [[ -n "$(ls -A $CERT_DIR 2>/dev/null)" ]]; then + local backup_dir="${CERT_DIR}.bak-${timestamp}" + cp -r "$CERT_DIR" "$backup_dir" + log_info "已备份证书目录: $backup_dir" + backup_count=$((backup_count + 1)) + fi + + if [[ $backup_count -eq 0 ]]; then + log_info "未发现旧配置,无需备份" + else + log_success "共备份 $backup_count 个旧配置" + fi +} + +# 检查证书是否已存在且未过期 +check_existing_certificate() { + log_step "检查现有证书..." + + # 使用 acme.sh --list 查看已颁发的证书 + local cert_list=$("$ACME_SH" --list 2>/dev/null) + + if echo "$cert_list" | grep -q "$DOMAIN"; then + log_warn "发现已存在的证书: $DOMAIN" + + # 检查证书是否即将过期 + local cert_file="$("$ACME_SH" --list | grep "^$DOMAIN" | awk '{print $5}')" + if [[ -n "$cert_file" ]] && [[ -f "$cert_file" ]]; then + local expire_date=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null | cut -d= -f2) + local expire_timestamp=$(date -d "$expire_date" +%s 2>/dev/null) + local now_timestamp=$(date +%s) + local days_left=$(( (expire_timestamp - now_timestamp) / 86400 )) + + log_info "证书过期时间: $expire_date" + log_info "距离过期还有: ${days_left} 天" + + if [[ $days_left -gt 30 ]]; then + echo "" + echo -e "${YELLOW}现有证书仍在有效期内(>30天),请选择操作:${NC}" + echo "1) 跳过证书申请,直接安装现有证书" + echo "2) 强制重新申请证书 (Force Renew)" + echo "3) 退出脚本" + read -p "请输入选项 [1-3]: " choice + + case "$choice" in + 1) + log_info "用户选择: 跳过证书申请,使用现有证书" + return 0 + ;; + 2) + log_info "用户选择: 强制重新申请证书" + return 1 + ;; + *) + log_info "用户选择退出" + exit 0 + ;; + esac + else + log_warn "证书即将过期(<30天),将重新申请" + return 1 + fi + fi + fi + + log_info "未找到现有证书,将申请新证书" + return 1 +} + +# 生成临时 HTTP 配置用于 ACME 验证 +generate_http_config() { + log_step "生成临时 HTTP 配置..." + + cat > "$HTTP_CONF_FILE" </dev/null 2>&1; then + log_success "nginx 重载成功" + elif service nginx reload >/dev/null 2>&1; then + log_success "nginx 重载成功 (service 命令)" + elif nginx -s reload >/dev/null 2>&1; then + log_success "nginx 重载成功 (nginx -s)" + else + log_error "nginx 重载失败,请手动执行: systemctl reload nginx" + exit 1 + fi +} + +# 检测域名 80 端口可达性 +check_domain_reachability() { + log_step "检测域名 80 端口可达性..." + + local test_url="http://${DOMAIN}/.well-known/acme-challenge/test" + log_info "测试 URL: $test_url" + + local response + response=$(curl -I -s --connect-timeout 10 "$test_url" 2>&1) + local http_code=$(echo "$response" | grep -i "HTTP/" | head -n1 | awk '{print $2}') + + log_info "HTTP 响应码: ${http_code:-无响应}" + + # 返回 200 或 404 都视为通过(说明 nginx 已响应) + if [[ "$http_code" == "200" ]] || [[ "$http_code" == "404" ]]; then + log_success "域名 80 端口可达性检测通过" + return 0 + else + log_error "域名 80 端口不可达,请检查:" + echo " 1. 域名是否正确解析到此服务器" + echo " 2. 防火墙是否开放 80 端口" + echo " 3. nginx 是否正常启动" + echo "" + echo " 完整响应: $response" + return 1 + fi +} + +# 申请 Let's Encrypt 证书 +issue_certificate() { + log_step "申请 Let's Encrypt 证书..." + + log_info "执行命令: $ACME_SH --issue -d $DOMAIN --webroot $WEBROOT" + echo "" + + # 捕获输出用于分析 + local tmp_out=$(mktemp) + local tmp_err=$(mktemp) + + "$ACME_SH" --issue -d "$DOMAIN" --webroot "$WEBROOT" > >(tee "$tmp_out") 2> >(tee "$tmp_err" >&2) + local ret=$? + + # 分析结果 + if [[ $ret -eq 0 ]]; then + # 检查输出是否包含证书内容 + if grep -q "-----END CERTIFICATE-----" "$tmp_out" || grep -q "-----END CERTIFICATE-----" "$tmp_err"; then + log_success "✅ 证书申请成功!" + rm -f "$tmp_out" "$tmp_err" + return 0 + else + log_warn "证书申请命令返回成功,但未检测到证书内容" + log_info "输出内容:" + cat "$tmp_out" + cat "$tmp_err" + rm -f "$tmp_out" "$tmp_err" + return 0 + fi + else + log_error "❌ 证书申请失败 (返回码: $ret)" + echo "" + echo "========== 错误输出 ==========" + cat "$tmp_err" + echo "==============================" + echo "" + log_error "请检查日志: $LOG_FILE" + rm -f "$tmp_out" "$tmp_err" + exit 1 + fi +} + +# 安装证书到指定位置 +install_certificate() { + log_step "安装证书到指定目录..." + + # 确保目录存在 + mkdir -p "$CERT_DIR" + + log_info "证书安装目录: $CERT_DIR" + + # 使用 acme.sh 安装证书 + local install_cmd="$ACME_SH --install-cert -d $DOMAIN \ + --key-file ${CERT_DIR}/key.pem \ + --fullchain-file ${CERT_DIR}/cert.pem \ + --reloadcmd \"systemctl force-reload nginx\"" + + log_info "执行安装命令..." + echo "$install_cmd" | tee -a "$LOG_FILE" + + if eval "$install_cmd"; then + log_success "证书安装成功" + else + log_error "证书安装失败" + exit 1 + fi + + # 设置文件权限 + log_step "设置证书文件权限..." + + if chown www-data:www-data "${CERT_DIR}"/*.pem 2>/dev/null; then + log_success "文件所有者已设置为 www-data:www-data" + else + log_warn "chown 执行失败,请手动检查权限" + fi + + if chmod 600 "${CERT_DIR}"/*.pem 2>/dev/null; then + log_success "文件权限已设置为 600" + else + log_warn "chmod 执行失败,请手动检查权限" + fi + + # 验证证书文件 + log_step "验证证书文件..." + if [[ -f "${CERT_DIR}/cert.pem" ]]; then + log_success "证书文件存在: ${CERT_DIR}/cert.pem" + openssl x509 -in "${CERT_DIR}/cert.pem" -noout -subject -dates 2>/dev/null | tee -a "$LOG_FILE" + else + log_error "证书文件不存在: ${CERT_DIR}/cert.pem" + exit 1 + fi + + if [[ -f "${CERT_DIR}/key.pem" ]]; then + log_success "密钥文件存在: ${CERT_DIR}/key.pem" + else + log_error "密钥文件不存在: ${CERT_DIR}/key.pem" + exit 1 + fi +} + +# 生成最终 HTTPS 配置 +generate_https_config() { + log_step "生成 HTTPS 配置..." + + cat > "$HTTPS_CONF_FILE" </dev/null 2>&1 || service nginx reload >/dev/null 2>&1 || nginx -s reload; then + log_success "nginx 重载成功" + else + log_error "nginx 重载失败" + exit 1 + fi + + # 清理旧的符号链接(如果存在) + if [[ -L "${NGINX_ENABLED}/${DOMAIN}.conf" ]]; then + rm -f "${NGINX_ENABLED}/${DOMAIN}.conf" + fi + + echo "" + echo -e "${GREEN}========================================${NC}" + echo -e "${GREEN} ✅ SSL 证书创建成功! ${NC}" + echo -e "${GREEN}========================================${NC}" + echo "" + echo -e "${CYAN}📋 配置信息:${NC}" + echo " 域名: $DOMAIN" + echo " 证书文件: ${CERT_DIR}/cert.pem" + echo " 密钥文件: ${CERT_DIR}/key.pem" + echo " 后端端口: $BACKEND_PORT" + echo " Nginx 配置: $HTTPS_CONF_FILE" + echo " 日志文件: $LOG_FILE" + echo "" + echo -e "${CYAN}🧪 验证命令:${NC}" + echo " 1. 测试 HTTPS 访问:" + echo -e " ${GREEN}curl -I https://${DOMAIN}${NC}" + echo "" + echo " 2. 查看证书详情:" + echo -e " ${GREEN}openssl x509 -in ${CERT_DIR}/cert.pem -noout -text | head -20${NC}" + echo "" + echo " 3. 在线验证 SSL 配置:" + echo -e " ${GREEN}https://www.ssllabs.com/ssltest/analyze.html?d=${DOMAIN}${NC}" + echo "" + echo -e "${YELLOW}⚠️ 提示:${NC}" + echo " - HTTP 请求会自动 301 跳转到 HTTPS" + echo " - 证书将在 60 天后自动续期" + echo " - 如需手动续期: $ACME_SH --renew -d $DOMAIN" + echo "" +} + +# 主流程 +main() { + check_root + input_parameters + + echo "" + log_info "开始执行证书创建流程..." + echo "" + + install_acme_sh + setup_webroot + backup_old_configs + + # 检查是否需要申请新证书 + local skip_issue=false + check_existing_certificate || skip_issue=true + + generate_http_config + reload_nginx + + if [[ "$skip_issue" == "false" ]]; then + if check_domain_reachability; then + issue_certificate + else + log_error "域名可达性检测失败,无法继续" + exit 1 + fi + fi + + install_certificate + generate_https_config + final_verification + + echo "" + log_success "🎉 域名 [$DOMAIN] SSL 证书创建完成!" + echo "" +} + +# 执行主函数 +main "$@"