#!/bin/bash # ═══════════════════════════════════════════════════════════ # 🔒 铸渊主权服务器 · SSL证书自动化配置 # ═══════════════════════════════════════════════════════════ # # 编号: ZY-SVR-SSL-001 # 守护: 铸渊 · ICE-GL-ZY001 # 版权: 国作登字-2026-A-00037559 # # 功能: # 使用 Let's Encrypt (certbot) 自动获取免费SSL证书 # 自动配置 Nginx HTTPS # 自动设置证书续期 # # 用法: # sudo bash setup-ssl.sh <域名> [邮箱] # sudo bash setup-ssl.sh guanghu.online admin@guanghu.online # sudo bash setup-ssl.sh --all # 配置所有已知域名 # # 前提条件: # 1. 域名已解析到本服务器IP # 2. Nginx已安装并运行 # 3. 80端口可从外网访问 # ═══════════════════════════════════════════════════════════ # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${GREEN}[铸渊SSL]${NC} $1"; } log_warn() { echo -e "${YELLOW}[铸渊SSL]${NC} ⚠️ $1"; } log_error() { echo -e "${RED}[铸渊SSL]${NC} ❌ $1"; } log_step() { echo -e "${BLUE}[铸渊SSL]${NC} 📌 $1"; } # ── 配置 ────────────────────────────────────── ZY_ROOT="/opt/zhuyuan" SSL_DIR="${ZY_ROOT}/config/ssl" NGINX_CONF_DIR="${ZY_ROOT}/config/nginx" NGINX_SITES_AVAILABLE="/etc/nginx/sites-available" NGINX_SITES_ENABLED="/etc/nginx/sites-enabled" LOG_FILE="${ZY_ROOT}/data/logs/ssl-setup.log" # ── 检查root权限 ───────────────────────────── if [ "$(id -u)" -ne 0 ]; then log_error "需要root权限运行此脚本" log_info "请使用: sudo bash $0 $*" exit 1 fi # ── 确保目录存在 ───────────────────────────── mkdir -p "$SSL_DIR" "$NGINX_CONF_DIR" "$(dirname $LOG_FILE)" # ── 记录日志 ───────────────────────────────── exec > >(tee -a "$LOG_FILE") 2>&1 echo "" echo "═══════════════════════════════════════════════" echo "🔒 SSL配置开始 · $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S CST')" echo "═══════════════════════════════════════════════" # ── §1 安装 certbot ────────────────────────── install_certbot() { log_step "§1 安装 certbot..." if command -v certbot &> /dev/null; then CERTBOT_VER=$(certbot --version 2>&1 | head -1) log_info "certbot 已安装: $CERTBOT_VER" return 0 fi log_info "安装 certbot 和 nginx 插件..." # 更新包列表 apt-get update -qq # 安装 certbot + nginx 插件 apt-get install -y -qq certbot python3-certbot-nginx if command -v certbot &> /dev/null; then log_info "✅ certbot 安装成功" else log_error "certbot 安装失败" exit 1 fi } # ── §2 验证域名解析 ────────────────────────── verify_domain() { local domain="$1" log_step "§2 验证域名解析: $domain" # 获取本机公网IP local server_ip server_ip=$(curl -sf --max-time 5 https://api.ipify.org 2>/dev/null || curl -sf --max-time 5 https://ifconfig.me 2>/dev/null || echo "unknown") log_info "本机公网IP: $server_ip" # 解析域名 local domain_ip domain_ip=$(dig +short "$domain" 2>/dev/null | tail -1) if [ -z "$domain_ip" ]; then log_error "域名 $domain 无法解析" log_warn "请确认域名DNS已配置指向本服务器IP: $server_ip" return 1 fi log_info "域名 $domain 解析到: $domain_ip" if [ "$domain_ip" = "$server_ip" ]; then log_info "✅ 域名解析正确 · 指向本机" return 0 else log_warn "域名解析IP ($domain_ip) 与本机IP ($server_ip) 不一致" log_warn "如果使用CDN或负载均衡,这可能是正常的" # 不阻断,让certbot验证决定 return 0 fi } # ── §3 获取SSL证书 ─────────────────────────── obtain_certificate() { local domain="$1" local email="${2:-admin@${domain}}" log_step "§3 获取SSL证书: $domain" # 检查是否已有有效证书 if [ -f "/etc/letsencrypt/live/${domain}/fullchain.pem" ]; then local expiry expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" 2>/dev/null | cut -d= -f2) log_info "已有证书,过期时间: $expiry" # 检查是否即将过期(30天内) if openssl x509 -checkend 2592000 -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" 2>/dev/null; then log_info "✅ 证书仍然有效,跳过获取" return 0 else log_warn "证书即将过期,续期中..." fi fi log_info "使用 Let's Encrypt 获取免费SSL证书..." log_info "域名: $domain" log_info "邮箱: $email" # 确保80端口的Nginx正在运行(certbot需要HTTP验证) if ! systemctl is-active --quiet nginx; then log_warn "Nginx未运行,启动中..." systemctl start nginx fi # 使用 certonly 模式 + nginx 插件进行验证 # certonly 只获取证书不修改Nginx配置,铸渊自己管理Nginx SSL配置 certbot certonly \ --nginx \ --non-interactive \ --agree-tos \ --email "$email" \ --domain "$domain" \ 2>&1 if [ $? -eq 0 ]; then log_info "✅ SSL证书获取成功: $domain" # 创建符号链接到铸渊标准路径 ln -sf "/etc/letsencrypt/live/${domain}/fullchain.pem" "${SSL_DIR}/${domain}-fullchain.pem" ln -sf "/etc/letsencrypt/live/${domain}/privkey.pem" "${SSL_DIR}/${domain}-privkey.pem" log_info "证书链接已创建: ${SSL_DIR}/" return 0 else log_error "SSL证书获取失败" log_warn "" log_warn "常见原因:" log_warn " 1. 域名未正确解析到本服务器" log_warn " 2. 服务器80端口未开放(检查防火墙/安全组)" log_warn " 3. Let's Encrypt 速率限制(每小时最多5次失败)" log_warn "" log_warn "排查步骤:" log_warn " ping $domain # 确认域名解析" log_warn " curl -I http://$domain # 确认HTTP可访问" log_warn " sudo ufw status # 检查防火墙" return 1 fi } # ── §4 配置Nginx SSL ───────────────────────── # ⚠️ 架构说明 (铸渊专线共存模式): # Xray 监听 443 (外部) · VLESS+Reality协议 # 非VLESS流量回落到 127.0.0.1:8443 (Xray的dest参数) # Nginx SSL 监听 127.0.0.1:8443 (内部) · 处理网站HTTPS请求 # 浏览器 → 443(Xray) → 8443(Nginx SSL) → 网站内容 # VPN客户端 → 443(Xray) → Reality认证 → 代理上网 configure_nginx_ssl() { local domain="$1" local cert_path="/etc/letsencrypt/live/${domain}" log_step "§4 配置Nginx HTTPS: $domain" if [ ! -f "${cert_path}/fullchain.pem" ]; then log_error "证书文件不存在: ${cert_path}/fullchain.pem" return 1 fi # 读取当前Nginx配置 local nginx_conf="${NGINX_CONF_DIR}/zhuyuan-sovereign.conf" if [ ! -f "$nginx_conf" ]; then nginx_conf="${NGINX_SITES_AVAILABLE}/zhuyuan.conf" fi if [ ! -f "$nginx_conf" ]; then log_error "Nginx配置文件未找到" return 1 fi # 确定这是哪个域名(主站还是预览站) local site_mode="preview" local api_port="3801" if echo "$domain" | grep -q "hololake"; then site_mode="production" api_port="3800" fi log_info "站点模式: $site_mode · API端口: $api_port" log_info "架构: Xray(443外部) → 回落 → Nginx(8443内部SSL)" # 生成SSL server block local ssl_conf="${NGINX_CONF_DIR}/ssl-${domain}.conf" cat > "$ssl_conf" << SSLCONF # ═══════════════════════════════════════════════ # 🔒 铸渊SSL配置 · ${domain} # 自动生成于: $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M CST') # 证书来源: Let's Encrypt (certbot) # 证书路径: ${cert_path}/ # # ⚠️ 架构 (Xray+Nginx共存): # 外部443 → Xray (VPN+Reality) → 非VLESS流量回落 → 127.0.0.1:8443 # Nginx SSL 监听 127.0.0.1:8443 · 不直接暴露给外部 # 浏览器访问 https://${domain} → 443(Xray回落) → 8443(这里) # ═══════════════════════════════════════════════ # ─── HTTPS 服务 (内部端口,接收Xray回落流量) ─── server { listen 127.0.0.1:8443 ssl http2; server_name ${domain}; # ─── SSL证书 (Let's Encrypt) ─── ssl_certificate ${cert_path}/fullchain.pem; ssl_certificate_key ${cert_path}/privkey.pem; # ─── SSL安全配置 ─── ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # ─── 安全头 ─── add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header X-Server-Identity "ZY-SVR-002" always; add_header X-Site-Mode "${site_mode}" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # ─── 静态文件 ─── root /opt/zhuyuan/sites/${site_mode}; index index.html; location / { try_files \$uri \$uri/ /index.html; } # ─── API反向代理 ─── location /api/ { proxy_pass http://127.0.0.1:${api_port}; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Site-Mode "${site_mode}"; proxy_cache_bypass \$http_upgrade; proxy_read_timeout 86400; } # ─── AI聊天API (SSE流式) ─── location /api/chat { proxy_pass http://127.0.0.1:3721/api/chat; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; chunked_transfer_encoding on; proxy_read_timeout 120s; proxy_send_timeout 60s; } # ─── Persona Studio API ─── location /api/ps/ { add_header Access-Control-Allow-Origin * always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; if (\$request_method = OPTIONS) { return 204; } proxy_pass http://127.0.0.1:3002/api/ps/; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_read_timeout 120s; } # ─── 铸渊专线订阅 ─── location /api/proxy-sub/ { proxy_pass http://127.0.0.1:3802/; proxy_http_version 1.1; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } # ─── 健康探针 ─── location = /health { proxy_pass http://127.0.0.1:${api_port}/api/health; proxy_set_header Host \$host; } # ─── 静态资源缓存 ─── location /static/ { alias /opt/zhuyuan/sites/${site_mode}/static/; expires 7d; add_header Cache-Control "public, immutable"; } # ─── 错误页面 ─── error_page 404 /404.html; error_page 500 502 503 504 /50x.html; # ─── 日志 ─── access_log /opt/zhuyuan/data/logs/nginx-${site_mode}-ssl.log; error_log /opt/zhuyuan/data/logs/nginx-${site_mode}-ssl-error.log; } # ─── HTTP → HTTPS 重定向 ─── # 浏览器 http://${domain} → 301 → https://${domain} → 443(Xray) → 8443(Nginx) server { listen 80; server_name ${domain}; return 301 https://\$host\$request_uri; } SSLCONF log_info "SSL配置已生成: $ssl_conf" # 安装到Nginx (使用00-前缀确保优先于主配置加载) cp "$ssl_conf" "${NGINX_SITES_AVAILABLE}/ssl-${domain}.conf" ln -sf "${NGINX_SITES_AVAILABLE}/ssl-${domain}.conf" "${NGINX_SITES_ENABLED}/ssl-${domain}.conf" log_info "SSL配置已安装到Nginx" log_info " HTTPS: 127.0.0.1:8443 (接收Xray回落流量)" log_info " HTTP重定向: 80 → https://${domain} → 443(Xray) → 8443(Nginx)" # 测试Nginx配置 if nginx -t 2>&1; then log_info "✅ Nginx配置测试通过" systemctl reload nginx log_info "✅ Nginx已重新加载" else log_error "Nginx配置测试失败" log_warn "检查是否有端口冲突 (Xray应占用443,Nginx SSL应在8443)" # 回滚 rm -f "${NGINX_SITES_ENABLED}/ssl-${domain}.conf" systemctl reload nginx return 1 fi # 确保Xray在443端口正常运行 (如果已安装) if command -v xray &>/dev/null && systemctl is-active --quiet xray; then log_info "✅ Xray服务运行中 (443端口,VPN+回落到8443)" elif command -v xray &>/dev/null; then log_warn "Xray已安装但未运行,尝试启动..." systemctl restart xray 2>/dev/null || true sleep 2 if systemctl is-active --quiet xray; then log_info "✅ Xray已启动" else log_warn "Xray启动失败,铸渊专线(VPN)可能不可用" log_warn "但HTTPS网站仍可正常工作" fi fi return 0 } # ── §5 设置自动续期 ─────────────────────────── setup_auto_renewal() { log_step "§5 配置证书自动续期" # certbot安装时通常已配置systemd timer if systemctl is-enabled certbot.timer 2>/dev/null; then log_info "✅ certbot自动续期已启用" else # 手动启用 systemctl enable certbot.timer 2>/dev/null || true systemctl start certbot.timer 2>/dev/null || true log_info "✅ certbot自动续期已配置" fi # 添加续期后自动reload nginx的hook local hook_dir="/etc/letsencrypt/renewal-hooks/post" mkdir -p "$hook_dir" cat > "${hook_dir}/reload-nginx.sh" << 'HOOK' #!/bin/bash # 铸渊SSL · 证书续期后自动重载Nginx systemctl reload nginx echo "[铸渊SSL] $(date) · 证书已续期 · Nginx已重载" >> /opt/zhuyuan/data/logs/ssl-renewal.log HOOK chmod +x "${hook_dir}/reload-nginx.sh" log_info "✅ Nginx reload hook 已配置" # 测试续期(dry-run) log_info "测试续期功能..." certbot renew --dry-run 2>&1 | tail -3 } # ── §6 验证HTTPS ───────────────────────────── verify_https() { local domain="$1" log_step "§6 验证HTTPS: https://$domain" sleep 2 # 等待Nginx完全重载 # 检查Nginx是否在8443内部端口监听 if ss -tlnp | grep -q ":8443 "; then log_info "✅ Nginx SSL端口 8443 监听中 (内部,接收Xray回落)" else log_warn "Nginx SSL端口 8443 未监听" fi # 检查Xray是否在443端口监听 if ss -tlnp | grep -q ":443 "; then log_info "✅ 外部端口 443 监听中" # 检查是Xray还是Nginx占用443 local port_443_proc port_443_proc=$(ss -tlnp | grep ":443 " | head -1) if echo "$port_443_proc" | grep -q "xray"; then log_info " 443端口由Xray占用 (正确 · VPN+HTTPS共存模式)" elif echo "$port_443_proc" | grep -q "nginx"; then log_warn " 443端口由Nginx占用 (需要启动Xray接管443端口)" log_warn " 如果铸渊专线(VPN)不工作,请先运行代理服务部署" fi else log_warn "外部端口 443 未监听 (Xray可能未运行)" fi # 使用curl测试HTTPS (通过443端口 → Xray回落 → Nginx 8443) local response response=$(curl -sf -o /dev/null -w "%{http_code}" "https://${domain}/" 2>/dev/null) if [ "$response" = "200" ] || [ "$response" = "301" ] || [ "$response" = "302" ]; then log_info "✅ HTTPS访问正常 · 状态码: $response" else log_warn "HTTPS访问状态码: ${response:-无响应}" log_warn "这可能是因为:" log_warn " - Xray未运行 (443端口未被Xray监听)" log_warn " - Xray配置中的dest未指向127.0.0.1:8443" log_warn " - DNS传播需要时间" log_warn " - 请确保铸渊专线(Xray)服务已部署并运行" log_warn "" log_warn "直接测试8443端口 (跳过Xray):" local direct_response direct_response=$(curl -sf -o /dev/null -w "%{http_code}" --resolve "${domain}:8443:127.0.0.1" "https://${domain}:8443/" 2>/dev/null) if [ "$direct_response" = "200" ] || [ "$direct_response" = "301" ] || [ "$direct_response" = "302" ]; then log_info "✅ 直连8443端口正常 · 状态码: $direct_response" log_info " → Nginx SSL配置正确,等Xray运行后HTTPS即可正常" else log_warn " 直连8443也失败 · 状态码: ${direct_response:-无响应}" fi fi # 检查证书信息 echo | openssl s_client -servername "$domain" -connect "${domain}:443" 2>/dev/null | \ openssl x509 -noout -subject -issuer -dates 2>/dev/null || true # 测试HTTP→HTTPS重定向 local redirect redirect=$(curl -sf -o /dev/null -w "%{redirect_url}" "http://${domain}/" 2>/dev/null) if echo "$redirect" | grep -q "https"; then log_info "✅ HTTP→HTTPS重定向正常" else log_warn "HTTP→HTTPS重定向未生效 (可能需要等待DNS)" fi } # ── 主逻辑 ──────────────────────────────────── main() { local domain="$1" local email="${2:-}" echo "" echo "═══════════════════════════════════════════════" echo "🔒 铸渊SSL证书自动化配置" echo "═══════════════════════════════════════════════" echo "" if [ "$domain" = "--all" ]; then # 配置所有已知域名 log_info "配置所有域名..." # 从Nginx配置中提取域名 local domains=() if [ -f "${NGINX_SITES_AVAILABLE}/zhuyuan.conf" ]; then while IFS= read -r d; do # 跳过占位符和IP if [[ "$d" != *"PLACEHOLDER"* ]] && [[ "$d" != *"_"* ]] && [[ "$d" =~ \. ]]; then domains+=("$d") fi done < <(grep "server_name" "${NGINX_SITES_AVAILABLE}/zhuyuan.conf" | awk '{for(i=2;i<=NF;i++) print $i}' | tr -d ';') fi if [ ${#domains[@]} -eq 0 ]; then log_error "未找到已配置的域名" log_warn "请指定域名: sudo bash $0 guanghu.online" exit 1 fi for d in "${domains[@]}"; do log_info "────────────────────────────" log_info "处理域名: $d" log_info "────────────────────────────" process_domain "$d" "$email" echo "" done elif [ -z "$domain" ]; then echo "用法:" echo " sudo bash $0 <域名> [邮箱]" echo "" echo "示例:" echo " sudo bash $0 guanghu.online" echo " sudo bash $0 guanghu.online admin@guanghu.online" echo " sudo bash $0 --all # 配置所有已知域名" echo "" echo "说明:" echo " 使用 Let's Encrypt 免费SSL证书" echo " 自动获取、配置、续期" echo " 证书有效期90天,自动续期" exit 0 else process_domain "$domain" "$email" fi echo "" echo "═══════════════════════════════════════════════" echo "🔒 SSL配置完成 · $(TZ=Asia/Shanghai date '+%Y-%m-%d %H:%M:%S CST')" echo "═══════════════════════════════════════════════" } process_domain() { local domain="$1" local email="${2:-admin@${domain}}" # §1 安装certbot install_certbot # §2 验证域名 verify_domain "$domain" || { log_error "域名验证失败,但仍尝试获取证书..." } # §3 获取证书 obtain_certificate "$domain" "$email" || { log_error "获取证书失败: $domain" return 1 } # §4 配置Nginx configure_nginx_ssl "$domain" || { log_error "Nginx SSL配置失败: $domain" return 1 } # §5 自动续期 setup_auto_renewal # §6 验证 verify_https "$domain" log_info "" log_info "🎉 $domain SSL配置完成!" log_info "" log_info "证书文件:" log_info " 完整链: /etc/letsencrypt/live/${domain}/fullchain.pem" log_info " 私钥: /etc/letsencrypt/live/${domain}/privkey.pem" log_info "" log_info "访问: https://${domain}" } main "$@"