zhizhi/server/setup/setup-ssl.sh

607 lines
22 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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应占用443Nginx 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 "$@"