481 lines
24 KiB
YAML
481 lines
24 KiB
YAML
name: '🛡️ 语言保护罩 + Phase 1 · ZY-SVR-006 部署'
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 光湖智库节点 Phase 1 部署工作流
|
||
# 编号: ZY-WF-NOVEL-SHIELD
|
||
# 守护: 铸渊 · ICE-GL-ZY001
|
||
# 版权: 国作登字-2026-A-00037559
|
||
#
|
||
# 目标服务器: ZY-SVR-006 (43.153.203.105 · 新加坡 · 智库节点)
|
||
#
|
||
# 功能:
|
||
# 1. deploy — 部署 Phase 1 完整栈(Nginx保护罩+fail2ban+Node.js API+PM2+SSL)
|
||
# 2. setup-ssl — 单独为 novel.guanghuyaoming.com 申请/续签 SSL 证书
|
||
# 3. health — 检查保护罩 + API 运行状态
|
||
# 4. rollback — 回滚到上一份配置
|
||
#
|
||
# 所需 GitHub Secrets (全部7个):
|
||
# ZY_NOVEL_HOST — ZY-SVR-006 新加坡服务器 IP (43.153.203.105)
|
||
# ZY_NOVEL_KEY — ZY-SVR-006 SSH 私钥 (PEM格式)
|
||
# ZY_NOVEL_USER — ZY-SVR-006 SSH 用户名 (root)
|
||
# ZY_NOVEL_PATH — 部署路径 (/opt/novel-db)
|
||
# ZY_NOVEL_JWT_SECRET — JWT token 签名密钥 (64位随机)
|
||
# ZY_NOVEL_COS_BUCKET — COS 桶名 (zy-team-hub-sg-1317346199)
|
||
# ZY_NOVEL_COS_REGION — COS 区域 (ap-singapore)
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
- 'server/nginx/novel-mirror-shield.conf'
|
||
- 'server/novel-db/**'
|
||
workflow_dispatch:
|
||
inputs:
|
||
action:
|
||
description: '执行动作'
|
||
required: true
|
||
default: 'deploy'
|
||
type: choice
|
||
options:
|
||
- deploy
|
||
- setup-ssl
|
||
- health
|
||
- rollback
|
||
|
||
jobs:
|
||
|
||
# ═══════════════════════════════════════════════════════
|
||
# Job 1: Phase 1 完整部署
|
||
# ═══════════════════════════════════════════════════════
|
||
deploy:
|
||
name: '🚀 Phase 1 完整部署'
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: read
|
||
if: |
|
||
github.event_name == 'push' ||
|
||
(github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'deploy')
|
||
|
||
steps:
|
||
- name: '📥 检出代码'
|
||
uses: actions/checkout@v4
|
||
|
||
- name: '🔑 配置 SSH'
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key
|
||
chmod 600 ~/.ssh/novel_key
|
||
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
|
||
|
||
- name: '📦 安装服务器依赖 (nginx + fail2ban + certbot)'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
apt-get update -q
|
||
apt-get install -y nginx fail2ban certbot python3-certbot-nginx
|
||
mkdir -p ${{ secrets.ZY_NOVEL_PATH }}/app/public
|
||
mkdir -p ${{ secrets.ZY_NOVEL_PATH }}/logs
|
||
mkdir -p /var/www/certbot
|
||
touch /var/log/novel-shield-bans.log
|
||
chmod 644 /var/log/novel-shield-bans.log
|
||
echo "=== 服务器依赖安装完成 ==="
|
||
ENDSSH
|
||
|
||
- name: '📤 上传应用代码'
|
||
run: |
|
||
# 上传 app 目录 (index.js, package.json, public/)
|
||
scp -r -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/app/ \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/
|
||
# 上传 PM2 生态配置
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/ecosystem.config.js \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/ecosystem.config.js
|
||
|
||
- name: '⚙️ 生成 .env 并安装 Node.js 依赖'
|
||
env:
|
||
JWT_SECRET: ${{ secrets.ZY_NOVEL_JWT_SECRET }}
|
||
COS_BUCKET: ${{ secrets.ZY_NOVEL_COS_BUCKET }}
|
||
COS_REGION: ${{ secrets.ZY_NOVEL_COS_REGION }}
|
||
run: |
|
||
# 在 runner 上生成 .env 文件,再 scp 上传(避免 heredoc 展开 secrets)
|
||
cat > /tmp/novel-db.env << EOF
|
||
NODE_ENV=production
|
||
PORT=4000
|
||
JWT_SECRET=${JWT_SECRET}
|
||
COS_BUCKET=${COS_BUCKET}
|
||
COS_REGION=${COS_REGION}
|
||
BAN_LOG_PATH=/var/log/novel-shield-bans.log
|
||
NGINX_LOG=/var/log/nginx/novel-access.log
|
||
EOF
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
/tmp/novel-db.env \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:${{ secrets.ZY_NOVEL_PATH }}/.env
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} \
|
||
'chmod 600 ${{ secrets.ZY_NOVEL_PATH }}/.env && cd ${{ secrets.ZY_NOVEL_PATH }}/app && npm install --production --silent && echo "✓ Node.js 依赖安装完成"'
|
||
|
||
- name: '🚀 启动/重启 PM2 应用'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
cd ${{ secrets.ZY_NOVEL_PATH }}
|
||
# 如果 PM2 进程已存在则重启,否则启动
|
||
pm2 describe novel-api > /dev/null 2>&1 \
|
||
&& pm2 restart novel-api \
|
||
|| pm2 start ecosystem.config.js
|
||
pm2 save
|
||
# 确保开机自启(systemd,幂等)
|
||
pm2 startup systemd -u root --hp /root 2>/dev/null || true
|
||
systemctl enable pm2-root 2>/dev/null || true
|
||
echo "=== PM2 启动完成 ==="
|
||
pm2 list
|
||
ENDSSH
|
||
|
||
- name: '🗄️ 备份现有 Nginx 配置'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
BACKUP_TS=$(date +%Y%m%d_%H%M%S)
|
||
if [ -f /etc/nginx/sites-available/novel-mirror-shield.conf ]; then
|
||
cp /etc/nginx/sites-available/novel-mirror-shield.conf \
|
||
/etc/nginx/sites-available/novel-mirror-shield.conf.bak.$BACKUP_TS
|
||
echo "已备份 → novel-mirror-shield.conf.bak.$BACKUP_TS"
|
||
fi
|
||
ENDSSH
|
||
|
||
- name: '📤 上传 Nginx 语言保护罩配置'
|
||
run: |
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/nginx/novel-mirror-shield.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/nginx/sites-available/novel-mirror-shield.conf
|
||
|
||
- name: '🔧 配置 Nginx 全局速率限制'
|
||
run: |
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/security/nginx-rate-limit.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/nginx/conf.d/shield-rate-limit.conf
|
||
|
||
- name: '🔗 启用 Nginx 站点 + SSL 占位引导'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
ln -sf /etc/nginx/sites-available/novel-mirror-shield.conf \
|
||
/etc/nginx/sites-enabled/novel-mirror-shield.conf
|
||
rm -f /etc/nginx/sites-enabled/default
|
||
mkdir -p /var/www/certbot /etc/letsencrypt/live/novel.guanghuyaoming.com
|
||
# ─── SSL 占位文件引导(解决鸡蛋问题) ───
|
||
# novel-mirror-shield.conf 的 HTTPS 块引用 letsencrypt 文件
|
||
# 全新服务器上这些文件不存在 → nginx -t 失败 → certbot 也无法运行
|
||
# 修复方案: 先创建临时占位文件让 nginx -t 通过,certbot 申请成功后会替换为真实文件
|
||
if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then
|
||
printf '%s\n' \
|
||
'ssl_session_cache shared:le_nginx_SSL:10m;' \
|
||
'ssl_session_timeout 1440m;' \
|
||
'ssl_session_tickets off;' \
|
||
'ssl_protocols TLSv1.2 TLSv1.3;' \
|
||
'ssl_prefer_server_ciphers off;' \
|
||
'ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";' \
|
||
> /etc/letsencrypt/options-ssl-nginx.conf
|
||
echo "✓ 已创建 options-ssl-nginx.conf 占位文件"
|
||
fi
|
||
if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then
|
||
openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null
|
||
echo "✓ 已生成 ssl-dhparams.pem 占位文件 (2048-bit)"
|
||
fi
|
||
if [ ! -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then
|
||
openssl req -x509 -nodes -newkey rsa:2048 \
|
||
-keyout /etc/letsencrypt/live/novel.guanghuyaoming.com/privkey.pem \
|
||
-out /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem \
|
||
-days 30 -subj '/CN=novel.guanghuyaoming.com' 2>/dev/null
|
||
echo "✓ 已创建临时自签名证书占位(30天有效·certbot 申请后替换)"
|
||
fi
|
||
nginx -t
|
||
systemctl reload nginx || systemctl restart nginx
|
||
echo "✓ Nginx 重载成功"
|
||
ENDSSH
|
||
|
||
- name: '🛡️ 部署 fail2ban L3 保护层'
|
||
run: |
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/security/fail2ban-novel.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/jail.d/novel-shield.conf
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/security/fail2ban-filter-novel-scan.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/filter.d/novel-scan.conf
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/security/fail2ban-filter-novel-4xx.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/filter.d/novel-4xx.conf
|
||
scp -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
server/novel-db/security/fail2ban-action-ban-log.conf \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }}:/etc/fail2ban/action.d/novel-ban-log.conf
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
systemctl enable fail2ban
|
||
systemctl restart fail2ban
|
||
echo "✓ fail2ban 启动成功"
|
||
# 等待 fail2ban socket 就绪(异步启动,socket 需要数秒创建)
|
||
for i in 1 2 3 4 5; do
|
||
if [ -S /var/run/fail2ban/fail2ban.sock ]; then
|
||
echo "✓ fail2ban socket 已就绪 (第${i}次检测)"
|
||
break
|
||
fi
|
||
echo "⏳ 等待 fail2ban socket 就绪... (${i}/5)"
|
||
sleep 2
|
||
done
|
||
fail2ban-client status || echo "⚠ fail2ban-client status 查询失败,服务可能仍在初始化"
|
||
ENDSSH
|
||
|
||
- name: '🔒 配置 iptables 扫描端口引流 (L3 增强)'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
for PORT in 8080 8443 3000 3001 3002 8888 9090; do
|
||
iptables -t nat -D PREROUTING -p tcp --dport $PORT -j REDIRECT --to-port 80 2>/dev/null || true
|
||
iptables -t nat -A PREROUTING -p tcp --dport $PORT -j REDIRECT --to-port 80
|
||
done
|
||
apt-get install -y iptables-persistent -q || true
|
||
iptables-save > /etc/iptables/rules.v4 2>/dev/null || netfilter-persistent save 2>/dev/null || true
|
||
echo "✓ iptables 扫描端口引流配置完成"
|
||
ENDSSH
|
||
|
||
- name: "🔐 申请/续签 Let's Encrypt SSL 证书"
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
mkdir -p /var/www/certbot
|
||
# 检查是否是真实的 Let's Encrypt 证书,或只是上一步创建的占位自签名证书
|
||
CERT_ISSUER=$(openssl x509 -issuer -noout \
|
||
-in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || echo "none")
|
||
if echo "$CERT_ISSUER" | grep -qi "Let's Encrypt\|letsencrypt\|ISRG"; then
|
||
echo "✓ Let's Encrypt 证书已存在,执行续签检查..."
|
||
certbot renew --quiet || true
|
||
else
|
||
echo "=== 申请 Let's Encrypt 证书 (webroot 方式,不依赖 nginx 插件) ==="
|
||
# certonly --webroot: 不会尝试修改 nginx 配置,不会触发 nginx -t
|
||
# nginx 已在上一步启动(HTTP + 占位HTTPS),/.well-known/acme-challenge/ 可正常响应
|
||
certbot certonly --webroot \
|
||
-w /var/www/certbot \
|
||
-d novel.guanghuyaoming.com \
|
||
--non-interactive \
|
||
--agree-tos \
|
||
--email admin@guanghuyaoming.com \
|
||
--preferred-challenges http \
|
||
--quiet
|
||
echo "✓ Let's Encrypt 证书申请成功,重载 Nginx 激活真实证书..."
|
||
systemctl reload nginx
|
||
fi
|
||
certbot certificates 2>/dev/null | grep -E "Domains|Expiry|Certificate" || true
|
||
echo "✓ HTTPS 已激活 · novel.guanghuyaoming.com"
|
||
ENDSSH
|
||
|
||
- name: '✅ 完整部署验证'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
echo "═══ Phase 1 完整部署验证 · ZY-SVR-006 ═══"
|
||
echo ""
|
||
echo "─── Node.js API (PM2) ───"
|
||
pm2 describe novel-api | grep -E "status|pid|uptime|restarts" | head -5
|
||
HTTP_API=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1:4000/api/health 2>/dev/null || echo "超时")
|
||
echo "API /api/health: $HTTP_API"
|
||
echo ""
|
||
echo "─── Nginx 蜜罐 (L2) ───"
|
||
systemctl is-active nginx && echo "✓ Nginx 在线" || echo "✗ Nginx 离线"
|
||
HTTP_80=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1/ 2>/dev/null || echo "超时")
|
||
echo "蜜罐 HTTP 响应: $HTTP_80"
|
||
echo ""
|
||
echo "─── fail2ban (L3) ───"
|
||
systemctl is-active fail2ban && echo "✓ fail2ban 在线" || echo "✗ fail2ban 离线"
|
||
fail2ban-client status 2>/dev/null | grep "Jail list" || true
|
||
echo ""
|
||
echo "─── SSL 证书 ───"
|
||
if [ -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then
|
||
echo "✓ SSL 证书已安装"
|
||
openssl x509 -enddate -noout -in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || true
|
||
else
|
||
echo "⚠ SSL 证书待申请 (DNS 传播中?运行 setup-ssl 重试)"
|
||
fi
|
||
echo ""
|
||
echo "═══ Phase 1 部署完成 · 语言保护罩 L2+L3 已激活 ═══"
|
||
ENDSSH
|
||
|
||
# ═══════════════════════════════════════════════════════
|
||
# Job 2: 申请 SSL 证书
|
||
# ═══════════════════════════════════════════════════════
|
||
setup-ssl:
|
||
name: '🔐 申请 SSL 证书'
|
||
runs-on: ubuntu-latest
|
||
permissions: {}
|
||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'setup-ssl'
|
||
|
||
steps:
|
||
- name: '🔑 配置 SSH'
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key
|
||
chmod 600 ~/.ssh/novel_key
|
||
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
|
||
|
||
- name: "🔐 申请 Let's Encrypt SSL 证书 (webroot)"
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
# 安装 certbot (不安装 python3-certbot-nginx,避免 nginx 插件干扰 webroot 方式)
|
||
apt-get update -q
|
||
apt-get install -y certbot
|
||
mkdir -p /var/www/certbot /etc/letsencrypt/live/novel.guanghuyaoming.com
|
||
|
||
# ─── SSL 占位文件引导(与 deploy job 相同逻辑) ───
|
||
# nginx 配置引用 letsencrypt 文件,全新服务器上不存在 → nginx -t 失败
|
||
# 先创建临时占位文件让 nginx 可以运行
|
||
if [ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]; then
|
||
printf '%s\n' \
|
||
'ssl_session_cache shared:le_nginx_SSL:10m;' \
|
||
'ssl_session_timeout 1440m;' \
|
||
'ssl_session_tickets off;' \
|
||
'ssl_protocols TLSv1.2 TLSv1.3;' \
|
||
'ssl_prefer_server_ciphers off;' \
|
||
'ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256";' \
|
||
> /etc/letsencrypt/options-ssl-nginx.conf
|
||
echo "✓ 已创建 options-ssl-nginx.conf 占位文件"
|
||
fi
|
||
if [ ! -f /etc/letsencrypt/ssl-dhparams.pem ]; then
|
||
openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null
|
||
echo "✓ 已生成 ssl-dhparams.pem 占位文件 (2048-bit)"
|
||
fi
|
||
if [ ! -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then
|
||
openssl req -x509 -nodes -newkey rsa:2048 \
|
||
-keyout /etc/letsencrypt/live/novel.guanghuyaoming.com/privkey.pem \
|
||
-out /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem \
|
||
-days 30 -subj '/CN=novel.guanghuyaoming.com' 2>/dev/null
|
||
echo "✓ 已创建临时自签名证书占位(30天有效·certbot 申请后替换)"
|
||
fi
|
||
|
||
# 确保 nginx 能启动(占位文件就绪)
|
||
nginx -t && systemctl reload nginx || systemctl restart nginx
|
||
|
||
# 使用 webroot 方式申请证书
|
||
# --preferred-challenges http 确保只用 HTTP-01 验证,不触发 nginx 插件
|
||
certbot certonly --webroot \
|
||
-w /var/www/certbot \
|
||
-d novel.guanghuyaoming.com \
|
||
--non-interactive \
|
||
--agree-tos \
|
||
--email admin@guanghuyaoming.com \
|
||
--preferred-challenges http
|
||
echo "✓ Let's Encrypt 证书申请成功"
|
||
|
||
# 重载 nginx 使用真实证书
|
||
systemctl reload nginx
|
||
echo "✓ Nginx 重载 · HTTPS 已激活"
|
||
|
||
# 设置自动续期(幂等)
|
||
if ! crontab -l 2>/dev/null | grep -q "certbot renew"; then
|
||
(crontab -l 2>/dev/null; echo "0 3 1 * * certbot renew --quiet --post-hook 'systemctl reload nginx'") | crontab -
|
||
echo "✓ SSL 自动续期 cron 已配置"
|
||
fi
|
||
|
||
certbot certificates 2>/dev/null | grep -E "Domains|Expiry|Certificate" || true
|
||
echo "=== SSL 证书配置完成 ==="
|
||
ENDSSH
|
||
|
||
# ═══════════════════════════════════════════════════════
|
||
# Job 3: 健康检查
|
||
# ═══════════════════════════════════════════════════════
|
||
health:
|
||
name: '🔍 保护罩健康检查'
|
||
runs-on: ubuntu-latest
|
||
permissions: {}
|
||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'health'
|
||
|
||
steps:
|
||
- name: '🔑 配置 SSH'
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key
|
||
chmod 600 ~/.ssh/novel_key
|
||
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
|
||
|
||
- name: '🔍 检查保护罩状态'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
echo "═══ 语言保护罩健康检查 · ZY-SVR-006 ═══"
|
||
echo ""
|
||
echo "─── Nginx 状态 ───"
|
||
systemctl is-active nginx && echo "✓ Nginx 在线" || echo "✗ Nginx 离线"
|
||
nginx -t 2>&1 | tail -2
|
||
echo ""
|
||
echo "─── fail2ban 状态 ───"
|
||
systemctl is-active fail2ban && echo "✓ fail2ban 在线" || echo "✗ fail2ban 离线"
|
||
fail2ban-client status 2>/dev/null || echo "⚠ fail2ban-client 无法连接 socket"
|
||
echo ""
|
||
echo "─── 今日封禁记录 ───"
|
||
TODAY=$(date +%Y-%m-%d)
|
||
BANS=$(grep "$TODAY" /var/log/novel-shield-bans.log 2>/dev/null | wc -l)
|
||
echo "今日拦截: $BANS 次"
|
||
if [ "$BANS" -gt 0 ]; then
|
||
echo "最近10条:"
|
||
tail -10 /var/log/novel-shield-bans.log
|
||
fi
|
||
echo ""
|
||
echo "─── iptables 扫描引流规则 ───"
|
||
iptables -t nat -L PREROUTING --line-numbers 2>/dev/null | grep -E "REDIRECT|target" | head -15 || echo "无规则或无权限查看"
|
||
echo ""
|
||
echo "─── 蜜罐响应测试 ───"
|
||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 http://127.0.0.1/ 2>/dev/null || echo "超时")
|
||
echo "蜜罐 HTTP 响应码: $HTTP_CODE"
|
||
echo ""
|
||
echo "─── SSL 证书状态 ───"
|
||
if [ -f /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem ]; then
|
||
EXPIRY=$(openssl x509 -enddate -noout -in /etc/letsencrypt/live/novel.guanghuyaoming.com/fullchain.pem 2>/dev/null || echo "无法读取")
|
||
echo "SSL 证书到期: $EXPIRY"
|
||
else
|
||
echo "SSL 证书: 未申请 (运行 setup-ssl 动作)"
|
||
fi
|
||
echo ""
|
||
echo "═══ 检查完成 ═══"
|
||
ENDSSH
|
||
|
||
# ═══════════════════════════════════════════════════════
|
||
# Job 4: 回滚
|
||
# ═══════════════════════════════════════════════════════
|
||
rollback:
|
||
name: '⏪ 回滚配置'
|
||
runs-on: ubuntu-latest
|
||
permissions: {}
|
||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.action == 'rollback'
|
||
|
||
steps:
|
||
- name: '🔑 配置 SSH'
|
||
run: |
|
||
mkdir -p ~/.ssh
|
||
echo "${{ secrets.ZY_NOVEL_KEY }}" > ~/.ssh/novel_key
|
||
chmod 600 ~/.ssh/novel_key
|
||
ssh-keyscan -H ${{ secrets.ZY_NOVEL_HOST }} >> ~/.ssh/known_hosts
|
||
|
||
- name: '⏪ 回滚到上一份配置'
|
||
run: |
|
||
ssh -i ~/.ssh/novel_key -o StrictHostKeyChecking=no \
|
||
${{ secrets.ZY_NOVEL_USER }}@${{ secrets.ZY_NOVEL_HOST }} << 'ENDSSH'
|
||
set -e
|
||
LATEST_BAK=$(ls -t /etc/nginx/sites-available/novel-mirror-shield.conf.bak.* 2>/dev/null | head -1)
|
||
if [ -z "$LATEST_BAK" ]; then
|
||
echo "✗ 没有找到备份文件,无法回滚"
|
||
exit 1
|
||
fi
|
||
echo "回滚到: $LATEST_BAK"
|
||
cp "$LATEST_BAK" /etc/nginx/sites-available/novel-mirror-shield.conf
|
||
nginx -t
|
||
systemctl reload nginx
|
||
echo "✓ 回滚成功"
|
||
ENDSSH
|