diff --git a/.gitignore b/.gitignore index cf251f07..a7525b96 100644 --- a/.gitignore +++ b/.gitignore @@ -119,10 +119,13 @@ tests CLAUDE.md .claude scripts +!proxy-ng/scripts/ +!proxy-ng/scripts/*.sh .code-review-state #openspec/ code-reviews/ AGENTS.md +!AGENTS.md backend/cmd/server/server deploy/docker-compose.override.yml .gocache/ @@ -131,6 +134,13 @@ docs/* !docs/PAYMENT.md !docs/PAYMENT_CN.md !docs/ADMIN_PAYMENT_INTEGRATION_API.md +!docs/agent-specs/ +!docs/agent-specs/*.md +!docs/memorys/ +!docs/memorys/*.md +!docs/superpowers/ +!docs/superpowers/plans/ +!docs/superpowers/plans/*.md .serena/ .codex/ frontend/coverage/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d9f2cfe5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,211 @@ +# AGENTS.md — Sub2API 部署与发版 + +## 项目概述 + +Sub2API — AI API 网关平台,Go + Vue 3 前后端一体,Ent ORM,支持多账户管理、API 密钥分发、Token 级计费、智能调度、支付集成。 + +- 上游: `https://github.com/Wei-Shaw/sub2api` +- 本地: `/Users/qingse/Sync/local-git/sub2api` +- 当前稳定版: `v0.1.126` +- 许可证: LGPL v3 + +## 部署留痕规则 + +所有远程部署、迁移、域名切换、证书、反代、Compose、数据库、Redis、Mihomo 或代理配置变化,都必须在当前仓库留痕。 + +- 根级 `AGENTS.md` 只记录规则、索引和通用流程。 +- 每台服务器必须在 `ops/remote/` 下单独一个子目录。 +- 服务器目录必须记录域名、IP、服务器名、Tabby profile、部署方式、部署架构、运行检查和操作记录。 +- 当前实际生效配置副本必须放在对应服务器目录的 `current/` 下,例如 `docker-compose.yml`、`nginx-sub2api.conf` 或 `Caddyfile`。 +- 计划但尚未部署的配置放在 `planned/` 下,不能写成当前事实。 +- `.env`、数据库密码、JWT/TOTP 密钥、Mihomo 订阅和真实代理配置只允许放在被 `.gitignore` 排除的 `secrets/` 或 `backups/` 中,禁止提交。 +- 通过 Tabby 操作远程后,必须同步更新对应服务器目录,避免远程真实状态和项目记录漂移。 + +当前远程部署索引见 `ops/remote/README.md`。每个服务器目录至少包含: + +```text +ops/remote/-sub2api/ +├── README.md # 服务器级部署记录 +├── current/ # 当前远程生效配置副本 +├── planned/ # 计划配置,未部署时使用 +├── secrets/README.md # 敏感配置保存说明,真实文件不提交 +├── backups/.gitkeep # 远程备份说明或占位,真实备份不提交 +└── .gitignore # 排除密钥、订阅和备份 +``` + +## 远程部署索引 + +| 目录 | Tabby 连接 | 公网入口 | 状态 | +|------|------------|----------|------| +| `ops/remote/jppro-sub2api/` | `个人-JPpro-akile-0504-1c1g` | `https://proxy.api.lilifamily.com` / `https://jp.proxy.api.lilifamily.com` | proxy-ng 公开入口节点,回源到 3c4g origin | +| `ops/remote/us-racknerd-0425-sub2api/` | `个人-US-racknerd-0425-1c1g` | `https://pai.cyan2000.uk` | 旧机,迁移来源和回滚来源 | +| `ops/remote/us-racknerd-0526-sub2api/` | `个人-US-racknerd-0526-3c4g` | `https://origin.proxy.api.lilifamily.com` / `https://catproxy.lilifamily.com` | Sub2API 受限 origin 和公开直连主站入口 | + +## JPpro 当前 proxy-ng 记录 + +| 项目 | 详情 | +|------|------| +| 服务器 | JP Pro VPS `151.242.164.72`,Debian Trixie 1C1G20G | +| 域名 | `proxy.api.lilifamily.com`、`jp.proxy.api.lilifamily.com` | +| Tabby 连接 | `个人-JPpro-akile-0504-1c1g` | +| 部署方式 | apt 安装 nginx + Certbot,作为 proxy-ng 入口中转 | +| 回源目标 | `https://origin.proxy.api.lilifamily.com` | +| 访问 | `https://proxy.api.lilifamily.com/`、`https://jp.proxy.api.lilifamily.com/` | +| 详细记录 | `ops/remote/jppro-sub2api/` | + +当前 JPpro 不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理;只运行 nginx 反向代理。当前已启用独立 origin 回源、3c4g 来源 IP 白名单和 `X-Proxy-Ng-Token` 校验。 + +## JPpro 历史部署记录 + +| 项目 | 详情 | +|------|------| +| 服务器 | JP Pro VPS `151.242.164.72`,Debian Trixie 1C1G20G | +| 域名 | `proxy.api.cyan2000.uk` | +| Tabby 连接 | `个人-JPpro-akile-0504-1c1g` | +| 部署目录 | `/opt/sub2api/`,已于 2026-05-26 清理 | +| 访问 | `https://proxy.api.cyan2000.uk/`,当前未在 JPpro 上提供服务 | +| 详细记录 | `ops/remote/jppro-sub2api/` | + +### 远程目录结构 + +``` +/opt/sub2api/ +├── docker-compose.yml # 官方 docker-compose.yml + Caddy +├── .env # 密钥配置 (chmod 600) +└── Caddyfile # Caddy 反向代理配置 +``` + +数据存储在 Docker 命名卷:`sub2api_data`、`postgres_data`、`redis_data`、`caddy_data`、`caddy_config`。 + +### 历史 Docker 服务架构 + +``` +Browser :443 (HTTPS) → Caddy → sub2api :8080 (内部) → postgres :5432 (内部) + :80 → 301 redirect → redis :6379 (内部) +``` + +| 容器 | 镜像 | 角色 | +|------|------|------| +| `sub2api-caddy` | `caddy:2-alpine` | HTTPS 自动证书 + 反向代理 | +| `sub2api` | `weishaw/sub2api:latest` | Go 后端 + 内嵌 Vue 前端 | +| `sub2api-postgres` | `postgres:18-alpine` | 主数据库 | +| `sub2api-redis` | `redis:8-alpine` | 缓存 | + +### Caddy 配置 + +``` +proxy.api.cyan2000.uk { + reverse_proxy sub2api:8080 + encode gzip +} +``` + +Caddy 自动从 Let's Encrypt 获取和管理 TLS 证书,HTTP(:80) 自动 301 跳转到 HTTPS(:443)。 + +## 发版流程 + +远程服务器可直接拉取 Docker Hub,常规升级只需 pull + restart。 + +### 常规升级(官方镜像) + +```bash +cd /opt/sub2api +docker compose pull sub2api +docker compose up -d +``` + +### 本地编译部署(二开修改时使用) + +当 GitHub 不可达需要国内 GOPROXY,或需要本地修改代码后部署时: + +```bash +# 1. 拉取代码 +cd /Users/qingse/Sync/local-git/sub2api +git fetch ea-ali +git checkout + +# 2. 构建前端 +cd frontend && pnpm install --frozen-lockfile && pnpm run build + +# 3. 交叉编译 Go +cd ../backend +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ + GOPROXY=https://goproxy.cn,direct \ + go build -tags embed -ldflags="-s -w -X main.Version=<版本>" -trimpath \ + -o /tmp/sub2api ./cmd/server + +# 4. 构建最小 Docker 镜像 +mkdir -p /tmp/sub2api-build/{deploy,backend/resources} +cp /tmp/sub2api /tmp/sub2api-build/ +cp -r ../backend/resources/* /tmp/sub2api-build/backend/resources/ +cp ../deploy/docker-entrypoint.sh /tmp/sub2api-build/deploy/ + +cat > /tmp/sub2api-build/Dockerfile << 'EOF' +FROM alpine:latest +RUN apk add --no-cache ca-certificates tzdata su-exec libpq wget +RUN addgroup -g 1000 sub2api && adduser -u 1000 -G sub2api -s /bin/sh -D sub2api +COPY sub2api /app/sub2api +COPY backend/resources /app/resources +COPY deploy/docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh && mkdir -p /app/data && chown sub2api:sub2api /app/data +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD wget -q -T 5 -O /dev/null http://localhost:8080/health || exit 1 +WORKDIR /app +ENTRYPOINT ["/app/docker-entrypoint.sh"] +CMD ["/app/sub2api"] +EOF + +docker build --platform linux/amd64 -t sub2api:<版本> /tmp/sub2api-build + +# 5. 导出上传 +docker save sub2api:<版本> | gzip > /tmp/sub2api-<版本>.tar.gz +# Tabby SFTP: open_profile(个人-JPpro-akile-0504-1c1g) → sftp_upload → /tmp/ + +# 6. 远程部署 +docker load < /tmp/sub2api-<版本>.tar.gz +cd /opt/sub2api +sed -i 's|image:.*|image: sub2api:<版本>|' docker-compose.yml +docker compose up -d + +# 7. 清理 +rm -rf /tmp/sub2api-build /tmp/sub2api /tmp/sub2api-*.tar.gz +``` + +## 运维命令 + +```bash +# 查看服务状态 +cd /opt/sub2api && docker compose ps + +# 查看日志 +docker logs sub2api --tail 50 -f + +# 查询数据库 +docker exec sub2api-postgres psql -U sub2api -d sub2api -c "SELECT ..." + +# 重启服务 +cd /opt/sub2api && docker compose up -d + +# 停止服务 +cd /opt/sub2api && docker compose down + +# 数据备份 +cd /opt/sub2api && tar czf /tmp/sub2api-backup-$(date +%Y%m%d).tar.gz data/ postgres_data/ redis_data/ +``` + +## Git 远程 + +| 远程 | URL | 角色 | +|------|-----|------| +| `origin` | `https://github.com/Wei-Shaw/sub2api.git` | 上游 | +| `ea-ali` | `ssh://git@101.200.34.71:2222/zizi-al/sub2api.git` | 可写开发远程 | + +## 注意事项 + +- Go 编译必须加 `-tags embed`,否则前端不会嵌入二进制 +- 前端 build 输出到 `backend/internal/web/dist`,Go 编译时通过 `//go:embed` 嵌入 +- GitHub 和 Docker Hub 在国内网络不可达时,Go proxy 用 `goproxy.cn`,Docker base image 用本地缓存 +- 远程服务器可访问 Docker Hub,postgres:18-alpine 和 redis:8-alpine 自动拉取 +- `.env` 文件权限必须是 600,包含数据库密码和 JWT 密钥 +- 首次启动会自动创建管理员账号(`AUTO_SETUP=true`) diff --git a/docs/agent-specs/2026-05-21-国际互联网络优化-审阅版.md b/docs/agent-specs/2026-05-21-国际互联网络优化-审阅版.md new file mode 100644 index 00000000..044f7f17 --- /dev/null +++ b/docs/agent-specs/2026-05-21-国际互联网络优化-审阅版.md @@ -0,0 +1,133 @@ +# 国际互联网络优化 — 审阅版 + +日期:2026-05-21 | 状态:待审阅 + +## 1. 背景与问题 + +sub2api 后端部署在高性能服务器(2C4G),计算资源充足,但国际入站网络质量不稳定。目标用户群体在中国大陆,需要利用轻量 VPS(1C/1G,国际网络优化线路)作为前置代理,提升国际用户访问质量。 + +**核心痛点**: +- 后端服务器国际线路质量差,大陆用户延迟高、丢包 +- 直接暴露后端到公网,攻击面大 +- 缺少 DDoS 防护和地理访问控制 + +## 2. 目标与非目标 + +### 目标 +- 利用 1C/1G 轻量 VPS 优化线路,作为 nginx 反向代理入口 +- 仅允许中国大陆 IP 访问(通过 Cloudflare + nginx 双层过滤) +- TLS 终止在边缘节点,回源链路加密 +- 1C/1G 被打爆后具备自动恢复和快速重建能力 +- 后端 sub2api 正确识别真实客户端 IP(限流、审计、ACL 不受代理影响) + +### 非目标 +- 不对 sub2api 业务代码做架构变更 +- 不做多地域 CDN 分发或 Anycast +- 不做后端高可用集群(当前单后端) +- 不支持非中国 IP 访问 + +## 3. 核心决策 + +### 决策 1:前端 VPS 规格选择 +**选择**:1C/1G,国际优化线路。 + +理由:nginx 纯反向代理占用内存 50-100MB,1G 余量足够。单核 CPU 处理 TLS 握手和转发,无需多核。 + +### 决策 2:中国 IP 访问控制方案 +**选择**:Cloudflare WAF 规则 + nginx `CF-IPCountry` 头校验,双层过滤。 + +理由: +- CF 免费计划自带 5 条 WAF 规则和自动添加 `CF-IPCountry` 头 +- WAF 层在边缘阻断,节省回源带宽 +- nginx 层防绕过 CF 直连源站的请求 +- 无需付费方案 + +### 决策 3:回源链路加密 +**选择**:自签证书 + HTTPS 回源。 + +理由:两台 VPS 走公网 IP 通信,明文 HTTP 会被同网段嗅探。自签证书零成本,nginx `proxy_ssl_verify off` 跳过验证。 + +### 决策 4:源站 IP 防护 +**选择**:iptables 白名单 Cloudflare IP 段,每天自动更新。 + +理由:即使 DNS 不公开源站 IP,攻击者可通过 IP 扫段发现。只允许 CF 回源 IP 入站,其余 443/80 流量到 iptables 就被 DROP。 + +### 决策 5:前端容灾策略 +**选择**:分层递进 — systemd 自动复活 → 内存超限自动降级 → fail2ban 自动封禁 → 一键重建脚本 → 备机 DNS 切换。 + +理由:1C/1G 被打爆是概率问题,必须做好多层自愈准备。 + +## 4. 架构 + +```mermaid +graph LR + A[中国大陆用户] -->|HTTPS 443| B[Cloudflare CDN] + B -->|"CF-IPCountry: CN
仅允许中国IP"| C[轻量VPS 1C/1G
nginx] + C -->|HTTPS 公网回源
自签证书| D[高性能服务器 2C4G
sub2api :8080] + D --> E[(PostgreSQL)] + D --> F[(Redis)] + + G["非中国IP"] -->|Block| B + + style B fill:#f90,stroke:#333,color:#000 + style C fill:#39f,stroke:#333,color:#fff + style D fill:#3c3,stroke:#333,color:#fff +``` + +**防御链**(四层): + +```mermaid +flowchart TD + A[用户请求] --> B{CF WAF
国家检查} + B -->|非中国 IP| C[Block 403] + B -->|中国 IP| D{iptables
CF IP 白名单} + D -->|非 CF IP| E[DROP] + D -->|CF IP| F{nginx
CF-IPCountry} + F -->|空/非 CN| G[403] + F -->|CN| H[sub2api
业务处理] +``` + +## 5. 影响范围 + +| 组件 | 变更类型 | 说明 | +|---|---|---| +| sub2api `config.yaml` | 配置变更 | 新增 `server.trusted_proxies` 含 nginx IP | +| sub2api `config.yaml` | 配置变更 | 如启用 IP ACL:`security.trust_forwarded_ip_for_api_key_acl: true` | +| 轻量 VPS | 新增部署 | nginx + fail2ban + iptables + 监控脚本 | +| 后端高性能服务器 | 新增 | 自签 TLS 证书,可选本地 nginx sidecar 做 TLS 终止 | +| DNS (`proxyapi.cyan2000.uk`) | 变更 | NS 切到 Cloudflare | +| sub2api 代码 | 无变更 | `ip.GetClientIP()` 已支持 `CF-Connecting-IP` / `X-Real-IP` / `X-Forwarded-For` | + +**兼容性**:sub2api 代码零变更。仅配置变化,回滚即删 `trusted_proxies` 项。 + +## 6. 风险与兼容性 + +| 风险 | 严重程度 | 缓解 | +|---|---|---| +| `trusted_proxies` 漏配导致限流器合并所有用户 IP | 高 | 验证清单必检项 | +| CF IP 段变更,iptables 未同步 → 正常用户被挡 | 中 | 每日 cron 自动更新 | +| 1C/1G 被 DDoS 打满连接 | 中 | CF 边缘 DDoS 防护 + 六层容灾 | +| 回源链路明文被嗅探 | 中 | 自签证书 HTTPS | +| 前端 VPS 成单点故障 | 低 | 备机 + DNS 快速切换 | +| CF 域名接入期间 DNS 切换导致短暂不可用 | 低 | TTL 提前降为 60s | +| nginx + sub2api 限流重复叠加 | 低 | nginx 限流是防攻击层(30r/s),sub2api 限流是业务层(auth 5-30/min),不冲突 | + +## 7. 验收标准 + +- [ ] 中国大陆 IP 通过 `https://proxyapi.cyan2000.uk/health` 返回 200 +- [ ] 非中国 IP(或 VPN 测试)返回 403 / CF Block +- [ ] sub2api 访问日志中 `client_ip` 是真实用户 IP,非 CF 或 nginx IP +- [ ] 登录/注册接口限流:同 IP 超频返回 429,不同 IP 独立计数 +- [ ] 流式 API 请求 SSE 无缓冲延迟 +- [ ] 源站 IP 直连被 iptables DROP(公网 curl 源站 IP:443 超时) +- [ ] `slowhttptest -c 50 -H` 后连接不耗尽,限流生效 +- [ ] nginx 进程被 kill 后 5s 内自动恢复(systemd) +- [ ] 前端 error_log 无异常 + +## 8. 待确认项 + +- [ ] 轻量 VPS 月流量配额是否足够?每请求进出各一份,需估算日请求量 +- [ ] 是否需要支持香港/澳门 IP?(当前白名单仅 CN) +- [ ] 后端防火墙是否支持按来源 IP 做入站限制?(除了 iptables,云厂商安全组也可做) +- [ ] CF Full (strict) 模式下,源站证书用 Let's Encrypt 还是自签?Full (strict) 要求有效证书 +- [ ] sub2api 是否启用了 API Key 的 IP 白名单功能?(决定是否需要开 `trust_forwarded_ip_for_api_key_acl`) \ No newline at end of file diff --git a/docs/agent-specs/2026-05-21-国际互联网络优化-执行版.md b/docs/agent-specs/2026-05-21-国际互联网络优化-执行版.md new file mode 100644 index 00000000..071c8001 --- /dev/null +++ b/docs/agent-specs/2026-05-21-国际互联网络优化-执行版.md @@ -0,0 +1,330 @@ +# 国际互联网络优化 — 执行版 + +日期:2026-05-21 | 前置:审阅版已通过 + +## 1. 目标与范围 + +在 1C/1G 轻量 VPS 上部署 nginx 反向代理,通过 Cloudflare CDN 实现中国 IP 白名单访问控制,将流量转发到后端 2C4G 高性能服务器的 sub2api 服务。 + +## 2. 前置假设 + +- 轻量 VPS 已就绪,OS Debian/Ubuntu,有公网 IP,已配置 SSH 密钥登录 +- 高性能服务器已部署 sub2api(Docker Compose),监听 8080,`config.yaml` 可编辑 +- 域名 `proxyapi.cyan2000.uk` 的 NS 管理权限在手 +- 两台 VPS 之间通过公网 IP 通信(无内网) +- 目标用户全部在中国大陆(+ 酌情香港/澳门) +- Cloudflare 免费计划账户已注册 + +## 3. 涉及文件与路径 + +### 轻量 VPS(新部署) + +| 路径 | 操作 | 说明 | +|---|---|---| +| `/etc/nginx/nginx.conf` | 写入 | 主配置,定义 upstream、限流区域 | +| `/etc/nginx/ssl/fullchain.pem` | certbot 签发 | Let's Encrypt 证书 | +| `/etc/nginx/ssl/privkey.pem` | certbot 签发 | 私钥 | +| `/etc/systemd/system/nginx.service.d/override.conf` | 创建 | MemoryMax + Restart | +| `/etc/fail2ban/jail.local` | 写入 | nginx-404 + nginx-limit-req 规则 | +| `/etc/fail2ban/filter.d/nginx-404.conf` | 写入 | 404 扫描过滤 | +| `/usr/local/bin/nginx-defense.sh` | 创建 | 内存压力自动降级 | +| `/usr/local/bin/cf-iptables.sh` | 创建 | CF IP 白名单更新 | +| `/usr/local/bin/rebuild-nginx.sh` | 创建 | 一键重建 | +| `/etc/cron.d/cf-iptables` | 写入 | 每日 3:00 执行 | +| `/etc/cron.d/nginx-defense` | 写入 | 每分钟执行 | + +### 高性能服务器(配置变更) + +| 路径 | 操作 | 说明 | +|---|---|---| +| `config.yaml` | 编辑 | `server.trusted_proxies` 加 nginx IP | +| `config.yaml` | 编辑(条件) | `security.trust_forwarded_ip_for_api_key_acl: true` | +| `/etc/ssl/private/sub2api-key.pem` | 生成 | 回源自签证书私钥 | +| `/etc/ssl/certs/sub2api-cert.pem` | 生成 | 回源自签证书 | +| `docker-compose.yml` | 编辑(可选) | nginx sidecar 做 TLS 终止 | + +## 4. 数据流与依赖 + +``` +用户浏览器 + │ HTTPS 443 (Let's Encrypt) + ▼ +Cloudflare CDN + │ 加头: CF-Connecting-IP, CF-IPCountry + │ HTTPS 443 (Let's Encrypt) + ▼ +轻量VPS nginx (1C/1G) + │ iptables: 仅接受 CF IP 段 + │ nginx: CF-IPCountry 非 CN → 403 + │ 加头: X-Real-IP, X-Forwarded-For + │ HTTPS 8443 (自签证书) + ▼ +后端 nginx sidecar (TLS 终止) + │ HTTP :8080 + ▼ +sub2api (Gin) + │ ip.GetClientIP() → CF-Connecting-IP → X-Real-IP → X-Forwarded-For + ▼ +PostgreSQL / Redis +``` + +依赖: +- 轻量 VPS → 后端公网 IP 可达(防火墙允许 8080 或 8443) +- 后端 → Redis / PostgreSQL(已有) +- 轻量 VPS → cloudflare.com(cron 拉取 IP 段) + +## 5. 接口契约与字段变化 + +### sub2api 配置变化 + +```yaml +# config.yaml 新增 +server: + trusted_proxies: + - "<轻量VPS公网IP>" + - "173.245.48.0/20" # Cloudflare IP 段(如需) + # ... 或信任所有 CF IP 段 + +# 如果启用了 API Key IP 白名单功能 +security: + trust_forwarded_ip_for_api_key_acl: true +``` + +### nginx 向下游传递的头 + +| 头名称 | 值来源 | 用途 | +|---|---|---| +| `X-Real-IP` | `$remote_addr`(CF 边缘 IP) | 传统兼容 | +| `X-Forwarded-For` | `$proxy_add_x_forwarded_for` | 代理链 | +| `X-Forwarded-Proto` | `$scheme`(https) | 协议感知 | +| `CF-Connecting-IP` | `$http_cf_connecting_ip`(CF 原始头) | sub2api 优先读取 | +| `Host` | `$host`(proxyapi.cyan2000.uk) | 虚拟主机 | + +### sub2api IP 解析优先级(`ip.go` 已有,不变) + +``` +CF-Connecting-IP → X-Real-IP → X-Forwarded-For(第一个公网IP) → c.ClientIP() +``` + +`c.ClientIP()`(限流器使用)仅在 `trusted_proxies` 含来源 IP 时才信任转发头。**必须配置**。 + +## 6. 边界情况与失败路径 + +| 场景 | 预期行为 | +|---|---| +| 用户绕过 CF 直连源站 IP | iptables DROP,连接超时 | +| 用户通过 CF 但伪造 CF-IPCountry 头 | CF 会覆盖此头或剥离,nginx 看到空 → 403 | +| 用户通过 CF 但来自非中国 IP | CF WAF Block(第一层),nginx `$http_cf_ipcountry != CN` → 403(第二层) | +| CF IP 段更新,iptables 未同步 | 受影响的新 CF IP 回源被 DROP,用户看到 502;每日 cron 最小化窗口 | +| 轻量 VPS OOM | systemd MemoryMax=800M 杀 nginx 而非全机 OOM;Restart=always 5s 恢复 | +| 轻量 VPS 彻底宕机 | 备机 DNS 切换;或一键重建脚本 | +| 后端 502/504 | nginx 返回 502 给用户;`proxy_next_upstream error timeout http_502` 有限重试 | +| sub2api `trusted_proxies` 未配 | 限流器把所有用户视为同一 IP(nginx IP),共享限流桶 | +| CF-Connecting-IP 传递失败(nginx 没透传) | sub2api 回退到 X-Forwarded-For 解析,仍能拿到真实 IP | +| Let's Encrypt 证书过期 | certbot renew 自动续期;需确保 80 端口可访问用于验证 | +| 前端磁盘满 | access_log 生产环境关掉;error_log logrotate maxsize 50M | + +## 7. 实施步骤 + +### 第一阶段:后端配置(零中断) + +1. 登录高性能服务器 +2. 确认 `config.yaml` 路径,备份后编辑: + ```yaml + server: + trusted_proxies: + - "<轻量VPS公网IP>" + ``` +3. 生成自签证书: + ```bash + openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ + -keyout /etc/ssl/private/sub2api-key.pem \ + -out /etc/ssl/certs/sub2api-cert.pem \ + -subj "/CN=<后端公网IP>" + ``` +4. 在 `docker-compose.yml` 中添加 nginx sidecar 做 TLS 终止(监听 8443 → 转发 8080),或直接用 sub2api 的 HTTPS 配置 +5. 重启 sub2api / Docker Compose +6. 验证:`curl --noproxy '*' -k https://<后端公网IP>:8443/health` 返回 200 + +### 第二阶段:Cloudflare DNS(可能中断 1-2 分钟) + +1. 在 Cloudflare 添加站点 `proxyapi.cyan2000.uk` +2. 将域名 NS 改为 Cloudflare 分配的 NS +3. 等 NS 生效后,添加 DNS 记录:A `@` → 轻量 VPS 公网 IP,橙色云朵开启 +4. SSL/TLS 模式设为 Full (strict) +5. WAF 规则:`(ip.geoip.country eq "CN")` → Allow,新建规则 +6. 验证:`curl -H "Host: proxyapi.cyan2000.uk" https://<轻量VPS_IP>/health --resolve proxyapi.cyan2000.uk:443:<轻量VPS_IP>` — 此时 nginx 还没配,先确认 CF 能到 VPS + +### 第三阶段:轻量 VPS 部署 + +按顺序执行: + +```bash +# 1. 基础软件 +apt update && apt install -y nginx certbot fail2ban iptables-persistent + +# 2. 获取证书(先关 nginx 80/443,或 certbot standalone 模式) +certbot certonly --standalone -d proxyapi.cyan2000.uk + +# 3. 写入 /etc/nginx/nginx.conf(见附录 A) + +# 4. 写入 systemd override(见附录 B) +mkdir -p /etc/systemd/system/nginx.service.d + +# 5. 写入 fail2ban 配置(见附录 C) + +# 6. 写入脚本(见附录 D) +# - /usr/local/bin/cf-iptables.sh +# - /usr/local/bin/nginx-defense.sh +# - /usr/local/bin/rebuild-nginx.sh + +# 7. 执行 iptables 白名单(先 accept 所有,确认 nginx 正常后再启用 DROP) +bash /usr/local/bin/cf-iptables.sh + +# 8. cron +echo "0 3 * * * root /usr/local/bin/cf-iptables.sh" > /etc/cron.d/cf-iptables +echo "* * * * * root /usr/local/bin/nginx-defense.sh" > /etc/cron.d/nginx-defense + +# 9. 关闭 access_log(生产环境) +# 编辑 nginx.conf: access_log off; +# 或保留但 logrotate: maxsize 50M rotate 3 + +# 10. 启动 +systemctl daemon-reload +systemctl enable --now nginx fail2ban +``` + +### 第四阶段:端到端验证 + +见第 8 节验证清单。 + +## 8. 测试与验证 + +### 自动化测试 + +```bash +# 1. 基本连通性 +curl -I https://proxyapi.cyan2000.uk/health +# 预期: 200 OK + +# 2. 中国 IP 访问控制 (CF-IPCountry 头) +curl -I https://proxyapi.cyan2000.uk/ -H "CF-IPCountry: CN" +# 预期: 正常转发,非 403 +curl -I https://proxyapi.cyan2000.uk/ -H "CF-IPCountry: US" +# 预期: 403 Forbidden +curl -I https://proxyapi.cyan2000.uk/ +# 预期: 403 (CF-IPCountry 为空 = 直连) + +# 3. 压力测试 — 限流 +ab -n 200 -c 50 https://proxyapi.cyan2000.uk/health +# 预期: 部分请求返回 429 / 503 + +# 4. Slowloris 抗性 +slowhttptest -c 50 -H -u https://proxyapi.cyan2000.uk/ +# 预期: 连接不耗尽,正常请求仍能响应 + +# 5. 源站 IP 直连 +curl -I --connect-timeout 5 https://<轻量VPS_IP>/ +# 预期: 超时或被拒绝 (iptables DROP) + +# 6. 限流验证 — 真实 IP +for i in $(seq 1 35); do + curl -s -o /dev/null -w "%{http_code}\n" https://proxyapi.cyan2000.uk/auth/login +done +# 预期: 前 20 个 200/401,后续 429 +``` + +### 验证清单 + +- [ ] `curl https://proxyapi.cyan2000.uk/health` → 200 +- [ ] CF 关闭橙色云朵(直连 VPS)→ `CF-IPCountry` 为空 → 403 +- [ ] `CF-IPCountry: US` → 403 +- [ ] `CF-IPCountry: CN` → 正常转发 +- [ ] sub2api 访问日志 `client_ip` = 真实用户 IP(查看日志验证) +- [ ] auth 限流:同 IP 超频返回 429,不同 IP 独立计数 +- [ ] 流式 API SSE 逐块到达 +- [ ] 源站 IP 直连超时 +- [ ] `ab` 压测限流生效 +- [ ] `slowhttptest` 连接不耗尽 +- [ ] `fail2ban-client status` 正常 +- [ ] `systemctl show nginx | grep Restart` → `Restart=always` +- [ ] `systemctl show nginx | grep MemoryMax` → `MemoryMax=838860800` +- [ ] nginx error_log 无异常 + +## 9. 回滚 / 降级 + +### 快速回滚 DNS +1. Cloudflare DNS 关闭 A 记录橙色云朵(变灰色 = DNS only) +2. 或将 A 记录直接指向后端服务器 IP(绕过轻量 VPS) + +### 回滚 sub2api 配置 +```bash +# 注释掉 trusted_proxies 行,恢复原状 +# 重启 sub2api +docker compose restart +``` + +### 轻量 VPS 紧急降级 +如果 nginx 出现异常但 iptables 正常: +```bash +# 紧急放行所有流量 +iptables -F CF-WHITELIST +iptables -A CF-WHITELIST -j ACCEPT +# 重启 nginx +systemctl restart nginx +``` + +--- + +## 附录 A:nginx 主配置 + +见 `NETWORK_OPTIMIZATION.md` 3.2 节完整配置。 + +## 附录 B:systemd override + +```ini +# /etc/systemd/system/nginx.service.d/override.conf +[Service] +Restart=always +RestartSec=5s +OOMScoreAdjust=-500 +MemoryMax=800M +MemoryHigh=700M +``` + +## 附录 C:fail2ban 配置 + +```ini +# /etc/fail2ban/jail.local +[nginx-404] +enabled = true +port = http,https +filter = nginx-404 +logpath = /var/log/nginx/access.log +maxretry = 30 +findtime = 60 +bantime = 600 + +[nginx-limit-req] +enabled = true +port = http,https +filter = nginx-limit-req +logpath = /var/log/nginx/error.log +maxretry = 5 +findtime = 60 +bantime = 600 +``` + +```ini +# /etc/fail2ban/filter.d/nginx-404.conf +[Definition] +failregex = ^ -.*"(GET|POST|HEAD).*" 404 +ignoreregex = +``` + +## 附录 D:脚本 + +见 `NETWORK_OPTIMIZATION.md` 3.4 节: +- `cf-iptables.sh` — CF IP 白名单(含 IPv6) +- `nginx-defense.sh` — 内存压力自动降级 +- `rebuild-nginx.sh` — 一键重建 \ No newline at end of file diff --git a/docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md b/docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md new file mode 100644 index 00000000..4f7492bf --- /dev/null +++ b/docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md @@ -0,0 +1,1028 @@ +# proxy-ng origin 加固方案 — 审阅版 + +日期:2026-05-26 +状态:已执行,2026-05-26 有边界调整 +适用范围:`proxy.api.lilifamily.com` 公开入口、`catproxy.lilifamily.com` 公开直连主站入口、`origin.proxy.api.lilifamily.com` 受限回源入口、`*.proxy.api.lilifamily.com` proxy-ng 节点扩展 + +> 2026-05-26 决策变更:`catproxy.lilifamily.com` 不再做 IP 白名单限制,作为 3c4g 公开直连主站域名和回滚入口;只有 `origin.proxy.api.lilifamily.com` 保持来源 IP 白名单和 `X-Proxy-Ng-Token` 校验。本文中早期关于 `catproxy` 受限访问的草案描述已被此决策覆盖,当前运行事实以 `ops/remote/us-racknerd-0526-sub2api/` 为准。 + +## 1. 结论 + +当前 JPpro 入口中转已经可以用于稳定性测试,但还不是生产加固架构。后续不依赖 Cloudflare Load Balancing;公开入口用普通 DNS 记录指向 proxy-ng 节点,防攻击和主站隐藏由 nginx、防火墙、origin 白名单和 token 完成。 + +当前已执行链路是: + +```mermaid +flowchart LR + U[用户] -->|HTTPS| P[公开入口
proxy.api.lilifamily.com] + P -->|HTTPS| J[JPpro nginx
jp.proxy.api.lilifamily.com] + J -->|HTTPS + 节点 Token| O[3c4g origin nginx
origin.proxy.api.lilifamily.com] + O -->|127.0.0.1:8080| S[Sub2API] + C[直连用户] -->|HTTPS| D[3c4g nginx
catproxy.lilifamily.com] + D -->|127.0.0.1:8080| S +``` + +推荐生产加固链路是: + +```mermaid +flowchart LR + U[用户] -->|HTTPS| D[公开入口
proxy.api.lilifamily.com
普通多 A / 手工切换] + D --> J[JPpro nginx
jp.proxy.api.lilifamily.com] + D --> N[其它 proxy-ng 节点
*.proxy.api.lilifamily.com] + J -->|HTTPS + 节点 Token| O[3c4g origin nginx
origin.proxy.api.lilifamily.com] + N -->|HTTPS + 节点 Token| O + O -->|本机回环| S[Sub2API :8080] + + C[普通公网用户] -.->|403 / 拒绝| O + X[未知服务器] -.->|IP 不在白名单| O +``` + +前置阶段先做域名迁移和入口角色调整: + +1. 公开服务入口保留为 `proxy.api.lilifamily.com`,但它不再指向 3c4g,而是指向 proxy-ng 节点。 +2. 3c4g 原直连入口从 `proxy.api.lilifamily.com` 改为 `catproxy.lilifamily.com`,作为公开直连主站域名和回滚入口。 +3. DNS 增加 `origin.proxy.api.lilifamily.com` A 记录,指向 3c4g,只给 proxy-ng 回源使用。 +4. 其它 proxy-ng 节点统一使用 `.proxy.api.lilifamily.com` 命名。 + +第一阶段加固只建议做三件事: + +1. 在 3c4g 增加独立 origin 入口:`origin.proxy.api.lilifamily.com`。 +2. origin 入口只允许 JPpro IP 访问,并校验 `X-Proxy-Ng-Token`。 +3. JPpro 从回源公网主域名改为回源 origin 入口。 +4. `proxy.api.lilifamily.com` 的 A 记录只指向 proxy-ng 节点,不再暴露 3c4g。 + +这三项完成后,proxy-ng 才从“能用的公网反代”升级成“信任边界清楚的入口中转”。 + +## 2. 当前已验证事实 + +| 项目 | 当前事实 | +|---|---| +| JPpro 服务器 | `个人-JPpro-akile-0504-1c1g` | +| JPpro 公网 IP | `151.242.164.72` | +| JPpro 入口域名 | `jp.proxy.api.lilifamily.com` | +| JPpro 当前职责 | 只运行 nginx proxy-ng,不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理 | +| JPpro 当前回源 | 测试期可临时回源 `https://catproxy.lilifamily.com`,加固后必须改为 `https://origin.proxy.api.lilifamily.com` | +| 3c4g 服务器 | `个人-US-racknerd-0526-3c4g` | +| 公开服务入口域名 | `proxy.api.lilifamily.com`,目标是普通多 A 指向 proxy-ng 节点 | +| 3c4g 公开直连主站域名 | `catproxy.lilifamily.com`,不做 IP 白名单限制 | +| 3c4g origin 域名 | `origin.proxy.api.lilifamily.com`,只给 proxy-ng 回源 | +| 3c4g 当前职责 | nginx 反代到本机 Sub2API `127.0.0.1:8080` | +| 当前风险边界 | 直接回源公网主域名,尚未做独立 origin、后端来源 IP 白名单、`X-Proxy-Ng-Token` 校验 | + +## 2.1 域名命名与 DNS 记录 + +本方案按以下域名边界规划: + +| 域名 | DNS 记录 | 角色 | 说明 | +|---|---|---|---| +| `proxy.api.lilifamily.com` | 多个 A -> proxy-ng 节点 IP | 公开服务入口 | 不用 Cloudflare LB;普通 DNS 多 A 或手工切换 | +| `catproxy.lilifamily.com` | A -> `216.45.59.243` | 3c4g 公开直连主站入口 | 可直接访问主站,也可作为 proxy-ng 故障时的回滚入口 | +| `origin.proxy.api.lilifamily.com` | A -> `216.45.59.243` | 3c4g 回源入口 | 只给 proxy-ng 节点访问,必须加 IP 白名单和 token | +| `jp.proxy.api.lilifamily.com` | A -> `151.242.164.72` | JPpro proxy-ng 节点 | 当前第一台入口中转节点 | +| `*.proxy.api.lilifamily.com` | 每个节点独立 A/AAAA | 后续 proxy-ng 节点命名空间 | 例如 `us.proxy.api.lilifamily.com`、`sg.proxy.api.lilifamily.com` | + +约束: + +- `proxy.api.lilifamily.com` 是用户看到的正式入口,但它只指向 proxy-ng 节点,不直接指向 3c4g。 +- `catproxy.lilifamily.com` 是独立公开直连入口;proxy-ng 回源不使用它,也不应在 `proxy.api.lilifamily.com` 链路中把它暴露到 `Location`、`Host`、页面链接或错误页中。 +- `*.proxy.api.lilifamily.com` 在本文表示节点命名规则,不等于必须使用一条 wildcard A 记录。 +- 多个节点如果 IP 不同,应为每个节点创建独立 A/AAAA 记录;`proxy.api.lilifamily.com` 可以配置多个 A 记录,但这是普通 DNS 轮询,不具备健康检查和按网络质量调度能力。 +- `origin.proxy.api.lilifamily.com` 即使有公网 DNS,也不应作为用户入口;访问控制必须在 nginx 和防火墙层生效。 +- `origin` 默认限制公网访问,证书签发不应依赖长期开放 HTTP-01;优先使用 DNS-01,或在签发/续期窗口临时开放并立即恢复限制。`catproxy` 公开访问,可使用常规 HTTP-01。 + +当前配置留痕: + +| 文件 | 说明 | +|---|---| +| `ops/remote/jppro-sub2api/README.md` | JPpro proxy-ng 部署记录 | +| `ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf` | JPpro 当前 nginx 配置副本 | +| `ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf` | 3c4g 当前 nginx 主入口配置副本 | +| `docs/memorys/2026-05-25-proxy-ng职责边界.md` | proxy-ng 不承担账号出口代理的边界记录 | +| `proxy-ng/README.md` | 多节点 proxy-ng 方案说明 | +| `proxy-ng/nginx/proxy-node.conf.template` | proxy-ng 节点 nginx 模板 | +| `proxy-ng/nginx/backend-gateway.conf.template` | 后端入口网关模板 | + +## 3. 目标和非目标 + +### 目标 + +- 把用户公开入口和 proxy-ng 回源入口分离。 +- 让 3c4g 后端能识别“这是受控 proxy-ng 节点发来的请求”。 +- 防止普通公网用户绕过 proxy-ng 直接访问 origin。 +- 防止客户端伪造 `X-Proxy-Ng-*` 或 `X-Forwarded-*` 头影响日志和安全判断。 +- 公开入口 `proxy.api.lilifamily.com` 只解析到 proxy-ng 节点,避免 3c4g 主站直接暴露。 +- 保留 `catproxy.lilifamily.com` 作为公开直连主站域名和紧急回滚入口。 +- 为后续新增 proxy-ng 节点提供标准流程。 + +### 非目标 + +- 不把 JPpro 改造成账号出口代理。 +- 不在 proxy-ng 节点部署 Clash、sing-box、Xray、SOCKS、mixed 等代理服务。 +- 不改 Sub2API 业务代码作为第一阶段方案。 +- 不在仓库提交真实 token、`.env`、API key、数据库密码。 +- 不立即强制所有用户流量都走 JPpro。 +- 不立即建设复杂的多地域调度系统。 + +## 4. 设计项解释和实施方案 + +### 4.1 独立 origin 入口 + +#### 作用 + +独立 origin 用来区分两类入口: + +| 入口 | 面向对象 | 是否公开给用户 | +|---|---|---| +| `proxy.api.lilifamily.com` | 用户公开访问,解析到 proxy-ng 节点 | 是 | +| `catproxy.lilifamily.com` | 运维直连、临时回滚 | 否 | +| `origin.proxy.api.lilifamily.com` | proxy-ng 节点回源 | 否 | + +如果 JPpro 继续回源 `https://catproxy.lilifamily.com`,那么 `catproxy` 就会被放进常规服务链路。它能用于短期迁移,但不能作为生产回源目标。生产回源必须使用 `origin.proxy.api.lilifamily.com`,并通过 IP 白名单和 token 限制访问。 + +独立 origin 的意义是:回源链路可以单独配置 IP 白名单、token 校验、日志格式和限流策略,不影响用户公开入口。 + +#### 实施方案 + +DNS 增加: + +```text +proxy.api.lilifamily.com -> 151.242.164.72 +proxy.api.lilifamily.com -> <其它 proxy-ng 节点 IP> +catproxy.lilifamily.com -> 216.45.59.243 +origin.proxy.api.lilifamily.com -> 216.45.59.243 +``` + +3c4g 新增 nginx server block: + +```nginx +server { + server_name origin.proxy.api.lilifamily.com; + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host proxy.api.lilifamily.com; + proxy_set_header X-Forwarded-Host $http_x_forwarded_host; + proxy_set_header X-Forwarded-Proto https; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +``` + +JPpro 后续改为: + +```nginx +proxy_pass https://origin.proxy.api.lilifamily.com; +proxy_ssl_server_name on; +proxy_ssl_name origin.proxy.api.lilifamily.com; +proxy_set_header Host origin.proxy.api.lilifamily.com; +proxy_set_header X-Forwarded-Host $host; +``` + +#### 风险 + +- 新增域名证书签发失败会导致 origin 不可用。 +- 如果直接切 JPpro 回源而未保留回滚,会造成 JPpro 入口不可用。 + +#### 缓解 + +- 先用 `curl --resolve` 验证 origin,再切 JPpro。 +- JPpro 原配置保留备份;失败时把 `proxy_pass` 改回 `https://catproxy.lilifamily.com`。 + +### 4.2 来源 IP 白名单 + +#### 作用 + +来源 IP 白名单保证 origin 只接受你控制的 proxy-ng 节点访问。 + +即使别人知道 `origin.proxy.api.lilifamily.com`,只要请求来源 IP 不是 JPpro 或后续受控节点,就会被拒绝。 + +#### 实施方案 + +3c4g origin nginx 增加: + +```nginx +location / { + allow 151.242.164.72; # JPpro + deny all; + + proxy_pass http://127.0.0.1:8080; +} +``` + +如果服务器防火墙可控,建议同时在系统防火墙层限制: + +```text +允许 151.242.164.72 访问 443 或 origin 专用端口 +拒绝其他公网来源访问 origin 入口 +``` + +后续新增节点时,只追加节点 IP: + +```nginx +allow 151.242.164.72; # JPpro +allow x.x.x.x; # 新 proxy-ng 节点 +deny all; +``` + +#### 风险 + +- 节点 IP 写错会导致 proxy-ng 回源 403。 +- VPS 如果更换 IP,origin 会继续拒绝旧配置之外的来源。 + +#### 缓解 + +- 每个节点目录必须记录公网 IP。 +- 新增或迁移节点时,先更新 origin allowlist,再切 DNS 或入口。 + +### 4.3 `X-Proxy-Ng-Token` + +#### 作用 + +IP 白名单证明“请求来自这台服务器 IP”,但不能证明“请求一定来自你配置的 nginx”。 + +`X-Proxy-Ng-Token` 是第二道校验: + +- 防止同一节点上其他进程误打 origin。 +- 防止节点配置被部分滥用。 +- 为后续多节点密钥轮换提供边界。 + +正确边界是: + +```text +来源 IP 白名单 + X-Proxy-Ng-Token + HTTPS 回源 +``` + +Token 不是防火墙替代品。 + +#### 实施方案 + +JPpro 注入 token: + +```nginx +proxy_set_header X-Proxy-Ng-Token "REDACTED_SECRET"; +proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; +``` + +3c4g origin 校验 token: + +```nginx +map $http_x_proxy_ng_token $proxy_ng_token_ok { + default 0; + "REDACTED_SECRET" 1; +} + +map $http_x_forwarded_host $sub2api_host { + default $http_x_forwarded_host; + "" "proxy.api.lilifamily.com"; +} + +server { + server_name origin.proxy.api.lilifamily.com; + + location / { + if ($proxy_ng_token_ok = 0) { + return 403; + } + + proxy_pass http://127.0.0.1:8080; + proxy_set_header X-Proxy-Ng-Token ""; + } +} +``` + +真实 token 保存建议: + +| 位置 | 规则 | +|---|---| +| 远程服务器 root-only 配置 | 可以保存真实 token,权限建议 `600` | +| 仓库 `ops/remote/**/current/` | 只保存脱敏副本 | +| 仓库 `secrets/` | 如需本机私有备份,必须被 `.gitignore` 排除 | + +#### 风险 + +- 所有节点共用一个 token 时,泄露后需要同步更新所有节点。 +- nginx 配置中使用明文 token,远程文件权限要控制好。 + +#### 缓解 + +- 第一阶段可以单 token。 +- 第二阶段支持双 token:先加新 token,再滚动节点,最后删旧 token。 +- 真实 token 不写入 Git。 + +### 4.4 覆盖客户端伪造 header + +#### 作用 + +客户端可以自己发送这些 header: + +```text +X-Proxy-Ng-Token +X-Proxy-Ng-Node +X-Forwarded-For +X-Real-IP +X-Forwarded-Host +``` + +如果 proxy-ng 不覆盖,后端日志和安全判断可能被伪造。 + +#### 实施方案 + +JPpro 对安全相关 header 使用 `proxy_set_header` 覆盖: + +```nginx +proxy_set_header X-Proxy-Ng-Token "REDACTED_SECRET"; +proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $remote_addr; +proxy_set_header X-Forwarded-Host $host; +``` + +3c4g origin 校验后清除 token,不继续传给 Sub2API: + +```nginx +proxy_set_header X-Proxy-Ng-Token ""; +proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; +``` + +#### 风险 + +- 如果 `X-Forwarded-For` 处理不当,Sub2API 可能拿不到真实客户端 IP。 +- 如果信任任意 `X-Forwarded-For`,日志和限流可能被客户端伪造。 + +#### 缓解 + +- 第一阶段先让 JPpro 覆盖 `X-Forwarded-For` 为 `$remote_addr`,保证不可伪造。 +- 后续如接入多级代理,再单独设计可信真实 IP 链路。 + +### 4.5 入口策略 + +#### 作用 + +入口策略决定用户应该访问哪条链路。 + +如果同时存在多个入口但没有规则,会出现: + +- 有的用户走 JPpro。 +- 有的用户绕过 JPpro 直连 3c4g。 +- 出问题时无法快速判断是哪条链路坏了。 + +#### 推荐阶段 + +| 阶段 | 入口策略 | 目的 | +|---|---|---| +| P0 迁移期 | `catproxy.lilifamily.com` 可短期直连,`jp.proxy.api.lilifamily.com` 用于测试中转 | 建立回滚入口 | +| P1 加固期 | JPpro 改走 origin + IP 白名单 + token | 验证安全边界 | +| P2 稳定期 | `proxy.api.lilifamily.com` 多 A 指向 proxy-ng 节点 | 常规用户不直连 3c4g | + +#### 实施方案 + +当前不依赖 Cloudflare LB。公开入口采用普通 DNS 多 A 或手工切换,接受“坏节点不会自动摘除”的限制。 + +先保持: + +```text +proxy.api.lilifamily.com -> 一个或多个 proxy-ng 节点 +jp.proxy.api.lilifamily.com -> JPpro proxy-ng +*.proxy.api.lilifamily.com -> 后续 proxy-ng 节点命名空间 +origin.proxy.api.lilifamily.com -> 3c4g origin,仅供 proxy-ng 回源 +catproxy.lilifamily.com -> 3c4g 受限直连/管理/回滚入口 +``` + +稳定期约束: + +```text +proxy.api.lilifamily.com 不能解析到 3c4g +proxy-ng 节点不能把 catproxy.lilifamily.com 暴露给用户 +catproxy.lilifamily.com 不能作为常规公开业务入口 +``` + +### 4.6 监控 + +#### 作用 + +proxy-ng 是多段链路。用户报慢或报错时,需要区分: + +- JPpro nginx 是否正常。 +- JPpro 到 3c4g origin 是否正常。 +- 3c4g nginx 是否正常。 +- Sub2API 是否正常。 +- 证书是否即将过期。 + +#### 实施方案 + +最小监控项: + +| 检查项 | 期望 | +|---|---| +| `https://jp.proxy.api.lilifamily.com/health` | `200` | +| `https://proxy.api.lilifamily.com/health` | `200`,且解析到 proxy-ng 节点 | +| `https://catproxy.lilifamily.com/health` 从管理 IP/Tailscale 访问 | `200` | +| `https://catproxy.lilifamily.com/health` 从普通公网访问 | `403` 或 `444` | +| `https://origin.proxy.api.lilifamily.com/health` 从非白名单来源访问 | `403` 或连接被拒绝 | +| `https://origin.proxy.api.lilifamily.com/health` 从 JPpro 访问 | `200` | +| `POST /v1/responses` 无授权访问 | `401 API_KEY_REQUIRED` | +| 证书剩余天数 | 小于 15 天告警 | +| nginx 服务状态 | 非 active 告警 | + +可以用 Uptime Kuma,或先用 cron + webhook。 + +#### 风险 + +- 只监控 `/health` 不足以发现 API 路由问题。 +- 从本机监控 origin 可能因为 IP 白名单而得到 403,这是预期,不是故障。 + +#### 缓解 + +- 同时监控 `/health` 和未授权 API 路由。 +- origin 监控要从 JPpro 节点执行。 + +### 4.7 日志可观测 + +#### 作用 + +响应慢时要知道慢在哪里。 + +关键区分: + +| 字段 | 含义 | +|---|---| +| `request_time` | 当前 nginx 从接收请求到响应完成的总耗时 | +| `upstream_response_time` | 当前 nginx 等上游响应的耗时 | +| `upstream_status` | 上游返回状态,例如 200、401、502、504 | +| `X-Proxy-Ng-Node` | 哪个 proxy-ng 节点处理了请求 | +| `request_id` | 一次请求的追踪 ID | + +#### 实施方案 + +JPpro 和 3c4g origin 都建议增加专用 log format: + +```nginx +log_format proxy_ng '$remote_addr "$request" status=$status ' + 'request_time=$request_time ' + 'upstream_status=$upstream_status ' + 'upstream_time=$upstream_response_time ' + 'node=$sent_http_x_proxy_ng_node ' + 'request_id=$request_id'; + +access_log /var/log/nginx/proxy-ng-access.log proxy_ng; +``` + +#### 风险 + +- access log 过大导致磁盘占满。 + +#### 缓解 + +- 配置 logrotate。 +- 稳定后可只保留错误日志和采样日志。 + +### 4.8 无 Cloudflare LB 的防攻击方案 + +#### 作用 + +不依赖 Cloudflare LB 后,公网攻击会直接打到 proxy-ng 节点。防攻击目标不是承诺抗大规模 DDoS,而是降低常见扫描、CC、小流量洪泛、慢连接和暴力破解对服务的影响,并保证攻击不会绕过 proxy-ng 打到 3c4g 主站。 + +API 网关类服务常见风险: + +- 大量并发连接。 +- 大请求体。 +- 慢速上传。 +- 登录和验证码接口被刷。 +- 流式响应占用长连接。 +- 路径扫描,例如 `/.env`、`/.git`、`/wp-login.php`。 +- 直接扫描 3c4g IP、`catproxy` 或 `origin`。 + +#### 实施方案 + +第一层:proxy-ng 节点系统防火墙。 + +```text +入站允许: +- 80/tcp、443/tcp:公网用户访问 proxy-ng +- 22/tcp:只允许管理 IP 或 Tailscale/内网管理入口 + +入站拒绝: +- 其它所有端口 +- 1080、7890、mixed、socks、xray、sing-box 等出口代理端口 +``` + +第二层:proxy-ng 节点 nginx 基础抗压。 + +```nginx +worker_processes auto; +worker_rlimit_nofile 65535; + +events { + worker_connections 4096; + multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + reset_timedout_connection on; + + keepalive_timeout 20s; + keepalive_requests 100; + client_header_timeout 10s; + client_body_timeout 30s; + send_timeout 30s; + + client_max_body_size 100m; + client_body_buffer_size 256k; + large_client_header_buffers 4 16k; + + limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m; + limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s; + limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m; +} +``` + +第三层:proxy-ng 节点 nginx 路由限流。 + +```nginx +location ~ /\.(git|svn|hg|env|aws|ssh) { + return 444; +} + +location ~* ^/(wp-admin|wp-login\.php|phpmyadmin|cgi-bin|vendor|boaform) { + return 444; +} + +location / { + limit_req zone=req_per_ip burst=30 nodelay; + limit_conn conn_per_ip 50; + + proxy_pass https://origin.proxy.api.lilifamily.com; +} + +location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) { + limit_req zone=auth_per_ip burst=3 nodelay; + limit_conn conn_per_ip 10; + + proxy_pass https://origin.proxy.api.lilifamily.com; +} +``` + +第四层:3c4g origin 防绕过。 + +```text +3c4g 防火墙: +- 只允许 proxy-ng 节点 IP 访问 origin 入口端口 +- 只允许管理 IP 或 Tailscale 访问 SSH +- Sub2API 8080 只监听 127.0.0.1 或 Docker 内网,不直接暴露公网 + +3c4g nginx: +- origin server 只 allow proxy-ng 节点 IP +- 必须校验 X-Proxy-Ng-Token +- default_server 对未知 Host 返回 444 +- catproxy 入口只用于受限直连/回滚,不作为公开业务入口 +``` + +第五层:fail2ban 和日志轮转。 + +```text +fail2ban 建议规则: +- nginx-404:短时间大量 404/444 扫描封禁 +- nginx-limit-req:短时间大量 429 封禁 +- sshd:SSH 暴力破解封禁 + +logrotate: +- nginx access/error 日志按大小轮转 +- 保留 3-7 份 +- 防止攻击流量把磁盘写满 +``` + +第六层:Sub2API 成本保护。 + +```text +- API Key 配额和余额限制 +- 单 key 并发限制 +- 单 key 速率限制 +- 异常 401/429/5xx 告警 +- 高额模型调用成本预警 +``` + +#### 风险 + +- 这些措施不能替代专业 DDoS 清洗;大流量打满 VPS 带宽时仍会不可用。 +- 太严格会误伤正常 API 客户端。 +- 流式响应连接时间长,连接数限制过低会误伤。 +- 多 A 普通 DNS 没有健康摘除;坏节点仍可能被解析到。 + +#### 缓解 + +- 先在 JPpro 测试入口启用。 +- 观察 429 和连接拒绝日志后再调参。 +- 对 `/v1/responses` 等长连接路径不要过度收紧。 +- `proxy.api.lilifamily.com` TTL 设低,例如 60-300 秒,故障时手工删除坏节点 A 记录。 +- 保留至少一个受限直连回滚入口,但不公开给普通用户。 + +### 4.9 备份和回滚 + +#### 作用 + +proxy-ng 节点无业务数据,真正不能丢的是 3c4g 的数据库、Redis、Sub2API 数据目录和运行配置。 + +入口改造前必须有回滚方案,否则一次 nginx 配置错误可能导致入口不可用。 + +#### 实施方案 + +改动前备份: + +```text +3c4g: +- /etc/nginx/sites-available/sub2api.conf +- /etc/nginx/sites-enabled/sub2api.conf +- /opt/sub2api/docker-compose.yml +- /opt/sub2api/.env +- PostgreSQL dump +- /opt/sub2api/data + +JPpro: +- /etc/nginx/sites-available/jp-proxy-sub2api.conf +- /etc/nginx/conf.d/proxy-ng-map.conf +``` + +回滚路径: + +| 故障 | 回滚方式 | +|---|---| +| origin 配置失败 | JPpro `proxy_pass` 改回 `https://catproxy.lilifamily.com` | +| token 校验误拦截 | 临时关闭 origin token 校验,恢复后重新加固 | +| IP 白名单误拦截 | 修正 `allow` 节点 IP 或临时回滚到公网主入口 | +| JPpro 故障 | 用户切回 `https://catproxy.lilifamily.com` | +| 3c4g 服务异常 | 使用 3c4g 备份和 Docker Compose 回滚 | + +### 4.10 多节点扩展 + +#### 作用 + +多节点用于提升入口稳定性,但不是简单“节点越多越好”。 + +不使用 Cloudflare LB 时,多 A 记录会把用户随机分配到节点。某个节点故障后,DNS 仍可能返回坏 IP,表现为部分用户随机失败。 + +#### 实施方案 + +新增节点标准流程: + +```text +1. 新建 ops/remote/-sub2api/ +2. 安装 nginx + certbot +3. 配置 proxy-ng node id +4. 回源 origin.proxy.api.lilifamily.com +5. 3c4g origin allow 新节点 IP +6. 给新节点配置 token +7. 验证 /health、/、/v1/responses +8. 加入 `proxy.api.lilifamily.com` 多 A 记录 +``` + +DNS 策略: + +| 阶段 | 方案 | 风险 | +|---|---|---| +| P0 | 单节点入口 | 简单,但节点故障需手工切换 | +| P1 | `proxy.api.lilifamily.com` 多 A 记录,TTL 60-300 秒 | 成本低,但坏节点不会自动摘除 | +| P2 | 自建监控 + 手工或脚本更新 DNS | 不依赖 Cloudflare LB,但需要自己处理摘除 | + +当前确认不采用 Cloudflare Load Balancing,因此方案不承诺按用户网络质量或节点健康自动调度。节点健康只能通过监控告警后手工移除 DNS 记录,或后续自建 DNS 更新脚本实现。 + +## 5. 推荐实施顺序 + +### 阶段 0:主站隐藏和入口域名迁移基线 + +先把公开入口和 3c4g 直连入口拆开,避免 proxy-ng 节点或公开 DNS 暴露主站。 + +动作: + +1. `proxy.api.lilifamily.com` 从 3c4g 移走,改为 A 到 proxy-ng 节点,例如 JPpro。 +2. DNS 增加或确认 `catproxy.lilifamily.com -> 216.45.59.243`,但它只作为受限直连、管理和回滚入口。 +3. 3c4g nginx 为 `catproxy.lilifamily.com` 签发证书,但访问限制为管理 IP、Tailscale 或临时回滚窗口。 +4. Sub2API 对外公开域名应保持为用户访问入口 `proxy.api.lilifamily.com`,避免页面、回调或错误信息暴露 `catproxy.lilifamily.com`。 +5. JPpro 临时回源可用 `https://catproxy.lilifamily.com`,但只作为切 origin 前的过渡。 + +目标基线链路: + +```text +proxy.api.lilifamily.com -> JPpro/proxy-ng -> catproxy.lilifamily.com(临时) -> 3c4g -> Sub2API +``` + +验收: + +- `proxy.api.lilifamily.com/health` 返回 200,且解析到 proxy-ng 节点 IP。 +- `jp.proxy.api.lilifamily.com/health` 返回 200。 +- `jp.proxy.api.lilifamily.com/` 返回 200。 +- 普通公网访问 `catproxy.lilifamily.com` 被拒绝,或仅在明确回滚窗口内允许。 +- 未授权 `POST /v1/responses` 返回 401。 + +### 阶段 1:3c4g 增加 origin + +动作: + +1. DNS 增加 `origin.proxy.api.lilifamily.com -> 216.45.59.243`。 +2. 3c4g nginx 新增 origin server block。 +3. certbot 为 origin 域名签发证书。 +4. 暂不切 JPpro,先验证 origin 本身。 + +验收: + +- 从 3c4g 本机访问 `127.0.0.1:8080/health` 正常。 +- 从 JPpro 访问 `https://origin.proxy.api.lilifamily.com/health` 正常。 +- 从非白名单来源访问 origin 被拒绝。 + +### 阶段 2:origin 加 IP 白名单和 token + +动作: + +1. 3c4g origin 允许 JPpro IP `151.242.164.72`。 +2. 3c4g origin 配置 `X-Proxy-Ng-Token` 校验。 +3. JPpro 配置注入 token。 +4. token 真实值只保存在远程 root-only 配置。 + +验收: + +- JPpro 带 token 访问 origin 返回 200。 +- JPpro 不带 token 访问 origin 返回 403。 +- 非 JPpro IP 访问 origin 返回 403 或连接拒绝。 +- origin 不把 `X-Proxy-Ng-Token` 继续传给 Sub2API。 + +### 阶段 3:JPpro 切换 origin 回源 + +动作: + +1. 备份 JPpro 当前 nginx 配置。 +2. JPpro `proxy_pass` 从 `https://catproxy.lilifamily.com` 改为 `https://origin.proxy.api.lilifamily.com`。 +3. `nginx -t` 后 reload。 +4. 公网验证 JPpro 入口。 + +验收: + +- `https://jp.proxy.api.lilifamily.com/health` 返回 200。 +- `https://jp.proxy.api.lilifamily.com/` 返回 200。 +- 未授权 `POST /v1/responses` 返回 401。 +- 3c4g origin access log 能看到 `X-Proxy-Ng-Node=jppro-akile-0504-1c1g`。 + +### 阶段 4:完善监控、日志、限流 + +动作: + +1. 增加 proxy-ng 专用 access log。 +2. 配置证书到期检查。 +3. 配置 JPpro 和 3c4g 健康检查。 +4. 先在 JPpro 入口启用保守限流、连接数限制和扫描路径拦截。 +5. 配置 fail2ban 和 logrotate。 +6. 3c4g 防火墙确认只允许 proxy-ng 节点访问 origin。 + +验收: + +- 能通过日志区分 JPpro 耗时和 3c4g origin 耗时。 +- 证书剩余天数低于阈值能告警。 +- 429 数量可观测,不误伤正常请求。 +- 访问 `/.env`、`/.git/config`、`/wp-login.php` 等扫描路径返回 444 或 403。 +- 3c4g `8080` 不对公网开放。 + +### 阶段 5:评估是否增加第二个 proxy-ng 节点 + +动作: + +1. 选择第二台节点。 +2. 按同样目录规范留痕。 +3. 先不进正式 DNS,用 `--resolve` 验证。 +4. 通过 `--resolve proxy.api.lilifamily.com:443:<节点IP>` 逐个验证。 +5. 验证通过后加入 `proxy.api.lilifamily.com` 多 A 记录。 + +验收: + +- 第二节点独立验证通过。 +- origin allowlist 已补第二节点 IP。 +- 节点配置副本已保存到项目 `ops/remote/-sub2api/`。 +- `proxy.api.lilifamily.com` 的多 A 记录不包含 3c4g IP。 + +## 6. 配置草案 + +### 6.1 3c4g origin nginx 草案 + +```nginx +map $http_x_proxy_ng_token $proxy_ng_token_ok { + default 0; + "REDACTED_SECRET" 1; +} + +map $http_x_forwarded_host $sub2api_host { + default $http_x_forwarded_host; + "" "proxy.api.lilifamily.com"; +} + +server { + server_name origin.proxy.api.lilifamily.com; + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + location / { + allow 151.242.164.72; + deny all; + + if ($proxy_ng_token_ok = 0) { + return 403; + } + + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $sub2api_host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $http_x_forwarded_host; + proxy_set_header X-Proxy-Ng-Token ""; + proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; + proxy_redirect https://origin.proxy.api.lilifamily.com/ https://$sub2api_host/; + proxy_redirect https://catproxy.lilifamily.com/ https://$sub2api_host/; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +``` + +待审阅点:`X-Real-IP` 和 `X-Forwarded-For` 是否按第一阶段“不可伪造优先”处理,还是按“保留真实客户端 IP 链路”处理。当前草案偏向保留链路,但需要结合 Sub2API 的 `trusted_proxies` 配置验证。 + +### 6.2 JPpro nginx 草案 + +```nginx +server { + server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + add_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g" always; + + location / { + proxy_pass https://origin.proxy.api.lilifamily.com; + proxy_ssl_server_name on; + proxy_ssl_name origin.proxy.api.lilifamily.com; + proxy_http_version 1.1; + + proxy_set_header Host origin.proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; + proxy_set_header X-Proxy-Ng-Token "REDACTED_SECRET"; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +``` + +待审阅点:当前 JPpro 配置已有 `client_max_body_size 256m`,草案写 `100m` 是为了和 3c4g 主入口一致。是否保留 `256m` 需要根据实际请求体规模确认。 + +### 6.3 3c4g catproxy 受限直连草案 + +`catproxy.lilifamily.com` 不作为常规用户入口。它的作用是运维验证和紧急回滚,因此默认应限制来源。 + +```nginx +server { + server_name catproxy.lilifamily.com; + + listen 443 ssl; + ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + # 管理 IP、Tailscale 出口或临时回滚窗口内的来源。 + allow ; + deny all; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host proxy.api.lilifamily.com; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} + +server { + listen 443 ssl default_server; + server_name _; + return 444; +} +``` + +如果管理 IP 不固定,优先使用 Tailscale、SSH 隧道或临时开放窗口,不建议长期把 `catproxy.lilifamily.com` 暴露给公网。 + +## 7. 验收清单 + +### 7.1 基础链路 + +- [ ] `https://proxy.api.lilifamily.com/health` 返回 200,且 DNS 解析结果只包含 proxy-ng 节点 IP。 +- [ ] 从管理 IP/Tailscale 访问 `https://catproxy.lilifamily.com/health` 返回 200。 +- [ ] `https://jp.proxy.api.lilifamily.com/health` 返回 200。 +- [ ] `https://jp.proxy.api.lilifamily.com/` 返回 200。 +- [ ] 未授权 `POST https://jp.proxy.api.lilifamily.com/v1/responses` 返回 `401 API_KEY_REQUIRED`。 +- [ ] 普通公网访问 `https://catproxy.lilifamily.com/` 返回 403/444,除非处于明确回滚窗口。 + +### 7.2 origin 边界 + +- [ ] JPpro 带 token 访问 `https://origin.proxy.api.lilifamily.com/health` 返回 200。 +- [ ] JPpro 不带 token 访问 origin 返回 403。 +- [ ] 非 JPpro IP 访问 origin 返回 403 或连接被拒绝。 +- [ ] origin 日志能看到 `X-Proxy-Ng-Node`。 +- [ ] origin 不把 `X-Proxy-Ng-Token` 继续传给 Sub2API。 + +### 7.3 回滚验证 + +- [ ] JPpro 配置可一条改回 `proxy_pass https://catproxy.lilifamily.com`。 +- [ ] 3c4g 受限入口 `catproxy.lilifamily.com` 对管理 IP/Tailscale 可访问,对普通公网不可访问。 +- [ ] 远程配置备份路径已记录。 + +### 7.4 运维验证 + +- [ ] `nginx -t` 通过。 +- [ ] `systemctl is-active nginx` 返回 `active`。 +- [ ] 证书到期时间已记录。 +- [ ] 访问日志可区分 `request_time` 和 `upstream_response_time`。 +- [ ] 限流启用后没有误伤正常 API 请求。 +- [ ] `/.env`、`/.git/config`、`/wp-login.php` 等扫描路径返回 444/403。 +- [ ] fail2ban 对高频 404/429 和 SSH 爆破生效。 +- [ ] logrotate 已限制 nginx 日志增长。 +- [ ] `proxy.api.lilifamily.com` 的任一 A 记录故障时,有手工摘除流程。 + +## 8. 风险和取舍 + +| 风险 | 严重性 | 原因 | 缓解 | +|---|---|---|---| +| origin 配置错误导致 JPpro 入口不可用 | 高 | JPpro 切到 origin 后依赖新链路 | 先验证 origin,再切 JPpro;保留回滚 | +| IP 白名单误配 | 高 | 节点 IP 写错或 VPS 换 IP | 操作前核对公网 IP;变更留痕 | +| token 泄露 | 中 | nginx 远程配置含明文 token | 权限 `600`;仓库只保存脱敏副本 | +| 真实客户端 IP 丢失 | 中 | 为防伪造而覆盖 `X-Forwarded-For` | 明确第一阶段优先级;后续再做可信 IP 链路 | +| 限流误伤正常请求 | 中 | API 客户端并发高、流式请求长 | 先保守配置并观察日志 | +| 大流量 DDoS 打满 proxy-ng 带宽 | 高 | 不使用 Cloudflare LB/清洗时,攻击流量直达 VPS | 接受该边界;多节点分散;必要时换高防/CDN | +| `catproxy` 被公网发现并直接访问 | 高 | 3c4g 直连入口暴露会绕过 proxy-ng | 默认 deny all,仅管理 IP/Tailscale/临时回滚允许 | +| 多 A 记录随机命中坏节点 | 中 | 普通 DNS 无健康检查 | TTL 60-300 秒;监控告警后手工摘除坏 A 记录 | +| 日志占满磁盘 | 低 | access log 增多 | 配置 logrotate 或采样 | + +## 9. 待确认项 + +- [x] 公开服务入口采用 `proxy.api.lilifamily.com`,但只解析到 proxy-ng 节点。 +- [x] `catproxy.lilifamily.com` 作为 3c4g 受限直连、管理和紧急回滚入口。 +- [x] origin 回源入口采用 `origin.proxy.api.lilifamily.com`。 +- [x] proxy-ng 节点统一使用 `.proxy.api.lilifamily.com` 命名。 +- [x] 不依赖 Cloudflare Load Balancing;`proxy.api.lilifamily.com` 使用普通 A 记录指向 proxy-ng 节点。 +- [ ] `catproxy.lilifamily.com` 的长期访问策略:仅 Tailscale/管理 IP,还是完全不开放公网。 +- [ ] JPpro 到 origin 是否继续使用 443,还是改用 origin 专用端口。 +- [ ] 第一阶段是否优先保证 `X-Forwarded-For` 不可伪造,接受真实客户端 IP 链路暂时简化。 +- [ ] token 采用单 token 还是一开始就做双 token 轮换。 +- [ ] 是否立即启用限流、fail2ban、扫描路径 444,还是先只增强 origin 安全边界。 +- [ ] 监控使用 Uptime Kuma、cron + webhook,还是暂时手工巡检。 +- [ ] 下一台 proxy-ng 节点是否已经确定。 + +## 10. 下一步 + +建议先审阅并确认以下决策: + +1. origin 域名和是否使用 443。 +2. JPpro 回源切换窗口和回滚策略。 +3. `X-Forwarded-For` 第一阶段处理策略。 +4. token 保存位置和轮换方式。 + +确认后再输出执行版文档,执行版应包含: + +- 远程配置文件路径。 +- 备份命令。 +- 变更步骤。 +- 验证命令。 +- 回滚命令。 +- 本地 `ops/remote/` 留痕更新清单。 diff --git a/docs/memorys/2026-05-25-proxy-ng职责边界.md b/docs/memorys/2026-05-25-proxy-ng职责边界.md new file mode 100644 index 00000000..7c0126ad --- /dev/null +++ b/docs/memorys/2026-05-25-proxy-ng职责边界.md @@ -0,0 +1,42 @@ +# proxy-ng 职责边界 + +日期:2026-05-25 + +## 背景 + +本次讨论 `proxy-ng` 多节点中转方案:一个域名可配置多个 nginx 节点 IP,用于优化用户到 sub2api 的入口线路。过程中曾讨论是否让这些节点同时作为 sub2api 账号的出口代理,最终确认不应这样做。 + +## 已确认边界 + +- `proxy-ng` 只负责入口中转和线路优化。 +- `proxy-ng` 节点只运行 nginx 反向代理相关能力。 +- `proxy-ng` 节点不承担 sub2api 账号出口代理职责。 +- sub2api 的账号代理应由独立代理池解决,不放进 `proxy-ng` 设计。 +- `proxy-ng` 节点不应暴露 `1080`、`7890`、`mixed`、`socks` 等代理端口。 +- `proxy-ng` 节点不部署 sing-box、Clash、Xray 等出口代理服务。 + +## 推荐架构 + +```text +用户 -> proxy-ng/nginx -> 后端入口网关 -> sub2api +sub2api 账号代理 -> 独立代理池 +``` + +## 安全原因 + +- 避免中转节点变成公网开放代理。 +- 避免入口中转流量和账号出口流量互相抢带宽、连接数和 CPU。 +- 避免把“线路优化”和“账号代理池”两个生命周期不同的系统绑在一起。 +- 避免后续排障时无法区分入口链路问题、上游代理池问题和 sub2api 业务问题。 + +## 当前项目资产 + +- `proxy-ng/README.md`:记录多节点中转架构、职责边界、风险和落地顺序。 +- `proxy-ng/env.example`:只保留入口中转所需变量。 +- `proxy-ng/nginx/proxy-node.conf.template`:proxy-ng 节点 nginx 模板。 +- `proxy-ng/nginx/backend-gateway.conf.template`:后端入口网关模板。 +- `proxy-ng/scripts/install-proxy-node.sh`:proxy-ng 节点安装脚本骨架。 + +## 后续执行约束 + +以后修改 `proxy-ng/` 时,先检查本文件和 `proxy-ng/README.md`。除非用户明确重新改变架构边界,否则不要把账号出口代理、代理池、sing-box、Clash、Xray 或 SOCKS 端口重新放回 `proxy-ng`。 diff --git a/docs/memorys/2026-05-26-catproxy访问边界变更.md b/docs/memorys/2026-05-26-catproxy访问边界变更.md new file mode 100644 index 00000000..bd0f07e9 --- /dev/null +++ b/docs/memorys/2026-05-26-catproxy访问边界变更.md @@ -0,0 +1,29 @@ +# catproxy 访问边界变更 + +日期:2026-05-26 + +## 当前结论 + +- `catproxy.lilifamily.com` 不做 IP 白名单限制,是 3c4g 上可直接访问的主站域名和回滚入口。 +- `origin.proxy.api.lilifamily.com` 才是回源专用域名,继续限制 proxy-ng 节点来源 IP,并校验 `X-Proxy-Ng-Token`。 +- `proxy.api.lilifamily.com` 仍作为 proxy-ng 公开入口,目前指向 JPpro 节点。 + +## 已执行变更 + +- 服务器:`个人-US-racknerd-0526-3c4g` +- 远程配置:`/etc/nginx/sites-available/catproxy-sub2api.conf` +- 备份路径:`/etc/nginx/sites-available/catproxy-sub2api.conf.bak-20260526043650` +- 删除内容:`allow 106.37.220.142` 与 `deny all` +- 保留内容:`origin.proxy.api.lilifamily.com` 的 `allow 151.242.164.72`、`deny all` 和 token 校验 + +## 验证结果 + +- `nginx -t` 成功。 +- `systemctl reload nginx` 成功。 +- `https://catproxy.lilifamily.com/health` 返回 `200`。 +- 非白名单来源访问 `https://origin.proxy.api.lilifamily.com/health` 返回 `403`。 +- `https://proxy.api.lilifamily.com/health` 经 JPpro 返回 `200`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。 + +## 后续注意 + +不要再把 `catproxy.lilifamily.com` 配成管理 IP 白名单入口。后续新增 proxy-ng 节点时,只需要把新节点公网 IP 补到 `origin.proxy.api.lilifamily.com` 的 allowlist,并给节点配置同一个受控 token 文件。 diff --git a/docs/memorys/2026-05-26-主站测速与限速核对.md b/docs/memorys/2026-05-26-主站测速与限速核对.md new file mode 100644 index 00000000..22e347fb --- /dev/null +++ b/docs/memorys/2026-05-26-主站测速与限速核对.md @@ -0,0 +1,81 @@ +# 主站测速与限速核对 + +日期:2026-05-26 + +## 本机链路测速结论 + +从当前本机网络直连测试,`proxy.api.lilifamily.com` 经 JPpro 的 HTTPS 轻量请求比 `catproxy.lilifamily.com` 直连 3c4g 更快、更稳。 + +| 测试项 | 直连 `catproxy.lilifamily.com` | 经 JPpro `proxy.api.lilifamily.com` | +|---|---:|---:| +| `/health` 中位总耗时 | `0.763s` | `0.559s` | +| `/health` 成功率 | `5/6`,1 次 8s 超时 | `6/6` | +| 首页 `HEAD /` 中位耗时 | `1.817s` | `1.315s` | +| ICMP 平均延迟 | `238ms`,10% 丢包 | `57ms`,30% 丢包 | + +测试命令使用 `curl --noproxy '*' -4`,本机未检测到 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 环境变量。ICMP 结果只作参考,HTTPS curl 结果优先。 + +## nginx 限速边界 + +- `catproxy.lilifamily.com`:3c4g 主站直连入口,当前没有 nginx `limit_req` / `limit_conn`,也没有 IP 白名单。 +- `origin.proxy.api.lilifamily.com`:3c4g 回源入口,当前没有 nginx 速率限制;只允许 JPpro IP `151.242.164.72`,并校验 `X-Proxy-Ng-Token`。 +- `proxy.api.lilifamily.com` / `jp.proxy.api.lilifamily.com`:JPpro 入口节点,有 nginx 按客户端 IP 的速率和并发限制。 + +JPpro 当前入口限制: + +```nginx +limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s; +limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m; +limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m; + +limit_conn conn_per_ip 50; +limit_req zone=req_per_ip burst=30 nodelay; +limit_req_status 429; +``` + +认证相关路径更严格: + +```nginx +limit_req zone=auth_per_ip burst=3 nodelay; +limit_conn conn_per_ip 10; +``` + +## Sub2API 业务请求限速维度 + +Sub2API 的模型/API 网关业务请求不是按 IP 限速,而是按用户、用户+分组、API Key 额度做限制。 + +业务 RPM 计数 Redis key: + +```text +rpm:u:{userID}:{minute} +rpm:ug:{userID}:{groupID}:{minute} +``` + +含义: + +- 用户级 `rpm_limit`:同一用户全局每分钟请求数上限,`0` 表示不限。 +- 分组级 `rpm_limit`:同一用户在某个分组内每分钟请求数上限,`0` 表示不限。 +- 用户-分组 `rpm_override`:管理员可给某个用户在某个分组设置专属 RPM。 +- API Key 的 `rate_limit_5h` / `rate_limit_1d` / `rate_limit_7d`:按费用消耗窗口限制,不是请求次数。 + +登录、注册、验证码等公开认证接口另有应用内 Redis 限速,维度是 `c.ClientIP()`。 + +## 生产快照 + +通过 Tabby profile `个人-US-racknerd-0526-3c4g` 查询 PostgreSQL 的非敏感限速字段: + +- 用户 `zizi2000email@gmail.com`:`rpm_limit = 0`。 +- 分组 `briqt`:`rpm_limit = 40`。 +- 分组 `omm`:`rpm_limit = 100`。 +- 分组 `free日卡-20刀`、`plus号池-使用余额`、`free周卡-每天50刀`、`free月卡-每天60刀`:`rpm_limit = 20`。 +- 分组 `pro号池-使用余额`:`rpm_limit = 30`。 +- API Key `briqt`:`rate_limit_5h = 300`,`rate_limit_1d = 500`。 +- API Key `free`:`rate_limit_1d = 50`。 +- API Key `omm`:窗口额度均为 `0`,表示不限。 + +后续如果用户反馈“主站请求被限速”,先判断入口: + +1. 走 `proxy.api.lilifamily.com` 时,先看 JPpro nginx 是否返回 `429`。 +2. 走 `catproxy.lilifamily.com` 时,跳过 nginx IP 限速排查,直接看 Sub2API 业务层错误。 +3. 如果错误是 `rate_limit_exceeded`,优先查用户/分组 RPM 或 API Key 窗口额度。 +4. 如果错误来自上游模型账号 `rate_limit_error`,那是账号池或上游限流,不是用户/IP 限流。 diff --git a/docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md b/docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md new file mode 100644 index 00000000..e4728527 --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md @@ -0,0 +1,1041 @@ +# proxy-ng Origin Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Deploy a non-Cloudflare-LB proxy-ng architecture where `proxy.api.lilifamily.com` only resolves to nginx proxy nodes, `origin.proxy.api.lilifamily.com` is restricted to proxy-ng回源, and `catproxy.lilifamily.com` remains a public direct main-site and rollback entry on 3c4g. + +**Architecture:** Users enter through `proxy.api.lilifamily.com`, which uses ordinary DNS A records pointing only at proxy-ng nodes. Each proxy-ng node terminates HTTPS, applies basic abuse protection, injects `X-Proxy-Ng-Token`, and sends traffic to `origin.proxy.api.lilifamily.com`. The origin host only accepts traffic from allowlisted proxy-ng IPs with a valid token; `catproxy.lilifamily.com` directly serves the same Sub2API主站 without an nginx IP allowlist. + +> 2026-05-26 decision update: `catproxy.lilifamily.com` is now a public direct main-site and rollback entry on 3c4g. Only `origin.proxy.api.lilifamily.com` remains protected by proxy-ng source IP allowlist and `X-Proxy-Ng-Token`. This plan contains historical implementation steps; current runtime facts are recorded under `ops/remote/us-racknerd-0526-sub2api/`. + +**Tech Stack:** Debian/Ubuntu, nginx, certbot or DNS-01 certificate issuance, system firewall, fail2ban, logrotate, Docker Compose Sub2API, project docs under `ops/remote/` and `docs/`. + +--- + +## Source Spec + +- `docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md` +- `ops/remote/jppro-sub2api/README.md` +- `ops/remote/us-racknerd-0526-sub2api/README.md` +- `docs/memorys/2026-05-25-proxy-ng职责边界.md` + +## File Structure + +- Modify: `.gitignore` + - Responsibility: allow project planning docs under `docs/superpowers/plans/` to be tracked. +- Create: `docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md` + - Responsibility: task-by-task execution plan. +- Modify after remote execution: `ops/remote/jppro-sub2api/README.md` + - Responsibility: record JPpro proxy-ng runtime changes, DNS role, security controls, and validation evidence. +- Modify after remote execution: `ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf` + - Responsibility: sanitized copy of JPpro active nginx site config. +- Create after remote execution: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` + - Responsibility: record fail2ban/logrotate/firewall decisions and test results. +- Modify after remote execution: `ops/remote/us-racknerd-0526-sub2api/README.md` + - Responsibility: record 3c4g origin/catproxy setup, allowlist, token boundary, and validation evidence. +- Modify after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf` + - Responsibility: sanitized copy of active public/limited nginx config on 3c4g. +- Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf` + - Responsibility: sanitized copy of active origin nginx config on 3c4g. +- Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf` + - Responsibility: sanitized copy of active catproxy public direct config on 3c4g. +- Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` + - Responsibility: record firewall policy and verification outputs without secrets. + +## Execution Preconditions + +- Tabby profile `个人-JPpro-akile-0504-1c1g` is available for JPpro. +- Tabby profile `个人-US-racknerd-0526-3c4g` is available for 3c4g. +- DNS control for `lilifamily.com` is available. +- A secret `PROXY_NG_TOKEN` is generated outside Git and saved only in root-readable remote files. +- If using HTTP-01 certificates for restricted hosts, the operator accepts a temporary public issuance window; otherwise use DNS-01. + +## Task 1: Track Plan Documents + +**Files:** +- Modify: `.gitignore:130-140` +- Test: `git check-ignore` and `git status` + +- [ ] **Step 1: Verify docs plans are currently ignored or missing** + +Run: + +```bash +git check-ignore -v docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md || true +``` + +Expected before this task is complete: either no file exists yet or the path is ignored by `docs/*`. + +- [ ] **Step 2: Add docs/superpowers/plans gitignore exceptions** + +Edit `.gitignore` so the docs section contains: + +```gitignore +docs/* +!docs/PAYMENT.md +!docs/PAYMENT_CN.md +!docs/ADMIN_PAYMENT_INTEGRATION_API.md +!docs/agent-specs/ +!docs/agent-specs/*.md +!docs/memorys/ +!docs/memorys/*.md +!docs/superpowers/ +!docs/superpowers/plans/ +!docs/superpowers/plans/*.md +``` + +- [ ] **Step 3: Verify plan path is trackable** + +Run: + +```bash +git check-ignore -v docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md || true +git status --short --untracked-files=all | rg 'docs/superpowers/plans|.gitignore' +``` + +Expected: + +```text + M .gitignore +?? docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md +``` + +- [ ] **Step 4: Commit tracking change with the plan when implementation docs are ready** + +Run only after Task 8 documentation updates are complete: + +```bash +git add .gitignore docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md +git commit -m "docs: add proxy-ng origin hardening plan" +``` + +Expected: commit succeeds. If other unrelated files are present, do not include them unless the user asks. + +## Task 2: DNS Preflight and Cutover Plan + +**Files:** +- Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` +- Modify after verification: `ops/remote/jppro-sub2api/README.md` +- Test: DNS lookups from local machine and from both servers + +- [ ] **Step 1: Record intended DNS state** + +Use this target state: + +```text +proxy.api.lilifamily.com A -> 151.242.164.72 +proxy.api.lilifamily.com A -> +jp.proxy.api.lilifamily.com A -> 151.242.164.72 +catproxy.lilifamily.com A -> 216.45.59.243 +origin.proxy.api.lilifamily.com A -> 216.45.59.243 +``` + +Important constraint: + +```text +proxy.api.lilifamily.com must not contain 216.45.59.243 after cutover. +``` + +- [ ] **Step 2: Check current DNS from local machine** + +Run: + +```bash +dig +short A proxy.api.lilifamily.com +dig +short A jp.proxy.api.lilifamily.com +dig +short A catproxy.lilifamily.com +dig +short A origin.proxy.api.lilifamily.com +``` + +Expected before DNS changes may vary. Record actual output in the remote README update. + +- [ ] **Step 3: Lower TTL before cutover** + +In DNS provider UI/API, set TTL for these names to 60-300 seconds: + +```text +proxy.api.lilifamily.com +jp.proxy.api.lilifamily.com +catproxy.lilifamily.com +origin.proxy.api.lilifamily.com +``` + +Expected: DNS provider accepts low TTL. + +- [ ] **Step 4: Do not cut public DNS until origin and JPpro tests pass** + +Hold this exact rule: + +```text +Do not move proxy.api.lilifamily.com away from the known-good path until: +1. catproxy health works from management path. +2. origin health works from JPpro with token. +3. JPpro serves proxy.api.lilifamily.com with a valid certificate. +4. /v1/responses unauthenticated returns 401 through JPpro. +``` + +- [ ] **Step 5: Document DNS state after final cutover** + +After DNS changes are made and verified, append this table to both remote READMEs: + +```markdown +## DNS state after proxy-ng hardening + +| Name | A records | Role | +|---|---|---| +| `proxy.api.lilifamily.com` | `` | Public entry | +| `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node | +| `catproxy.lilifamily.com` | `216.45.59.243` | Restricted direct management/rollback entry | +| `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only | +``` + +## Task 3: Prepare 3c4g Backups and Secret + +**Files:** +- Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` +- Create after verification: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` +- Test: backup files exist on 3c4g + +- [ ] **Step 1: Open 3c4g Tabby session** + +Use Tabby profile: + +```text +个人-US-racknerd-0526-3c4g +``` + +Expected: shell prompt on the 3c4g server. + +- [ ] **Step 2: Create a dated backup directory** + +Run on 3c4g: + +```bash +set -euo pipefail +backup_dir="/root/sub2api-hardening-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$backup_dir" +cp -a /etc/nginx "$backup_dir/nginx" +cp -a /opt/sub2api/docker-compose.yml "$backup_dir/docker-compose.yml" +cp -a /opt/sub2api/.env "$backup_dir/sub2api.env" +docker exec sub2api-postgres pg_dump -U sub2api -d sub2api > "$backup_dir/sub2api.sql" +chmod -R go-rwx "$backup_dir" +printf '%s\n' "$backup_dir" +``` + +Expected: prints backup path like `/root/sub2api-hardening-backup-20260526-170000`. + +- [ ] **Step 3: Generate or install proxy-ng token outside Git** + +Run on 3c4g: + +```bash +set -euo pipefail +install -d -m 700 /etc/nginx/proxy-ng +if [ ! -f /etc/nginx/proxy-ng/token.conf ]; then + token="$(openssl rand -hex 32)" + printf 'set $proxy_ng_expected_token "%s";\n' "$token" > /etc/nginx/proxy-ng/token.conf + chmod 600 /etc/nginx/proxy-ng/token.conf +fi +ls -l /etc/nginx/proxy-ng/token.conf +``` + +Expected: + +```text +-rw------- ... /etc/nginx/proxy-ng/token.conf +``` + +Do not print the token into chat, logs, docs, or Git. + +- [ ] **Step 4: Copy token securely to JPpro** + +Use Tabby SFTP or an SSH copy method approved by the operator. Target path on JPpro: + +```text +/etc/nginx/proxy-ng/token.conf +``` + +Expected on JPpro: + +```text +-rw------- root root /etc/nginx/proxy-ng/token.conf +``` + +- [ ] **Step 5: Record backup path without secrets** + +Append to `ops/remote/us-racknerd-0526-sub2api/README.md`: + +```markdown +### 2026-05-26 proxy-ng origin hardening backup + +- Backup path: `/root/sub2api-hardening-backup-` +- Includes nginx config, docker-compose.yml, `.env`, and PostgreSQL dump. +- Secret token exists only on remote root-readable nginx config and is not stored in Git. +``` + +## Task 4: Configure 3c4g Origin and Restricted Catproxy + +**Files:** +- Create after remote verification: `ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf` +- Create after remote verification: `ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf` +- Modify after remote verification: `ops/remote/us-racknerd-0526-sub2api/README.md` +- Test: `nginx -t`, origin 403/200 behavior, catproxy restricted behavior + +- [ ] **Step 1: Confirm Sub2API local health on 3c4g** + +Run on 3c4g: + +```bash +curl -sS http://127.0.0.1:8080/health +``` + +Expected: + +```json +{"status":"ok"} +``` + +- [ ] **Step 2: Create nginx token map file on 3c4g** + +Run on 3c4g: + +```bash +set -euo pipefail +cat > /etc/nginx/conf.d/proxy-ng-token-map.conf <<'EOF' +include /etc/nginx/proxy-ng/token.conf; + +map $http_x_proxy_ng_token $proxy_ng_token_ok { + default 0; + $proxy_ng_expected_token 1; +} + +map $http_x_forwarded_host $sub2api_host { + default $http_x_forwarded_host; + "" "proxy.api.lilifamily.com"; +} +EOF +nginx -t +``` + +Expected: + +```text +nginx: configuration file /etc/nginx/nginx.conf test is successful +``` + +- [ ] **Step 3: Create origin site config** + +Run on 3c4g, replacing certificate paths if certbot uses different paths: + +```bash +set -euo pipefail +cat > /etc/nginx/sites-available/origin-sub2api.conf <<'EOF' +server { + server_name origin.proxy.api.lilifamily.com; + + listen 443 ssl; + listen [::]:443 ssl; + ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + location / { + allow 151.242.164.72; + deny all; + + if ($proxy_ng_token_ok = 0) { + return 403; + } + + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $sub2api_host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $sub2api_host; + proxy_set_header X-Proxy-Ng-Token ""; + proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; + proxy_redirect https://origin.proxy.api.lilifamily.com/ https://$sub2api_host/; + proxy_redirect https://catproxy.lilifamily.com/ https://$sub2api_host/; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +EOF +ln -sf /etc/nginx/sites-available/origin-sub2api.conf /etc/nginx/sites-enabled/origin-sub2api.conf +nginx -t +``` + +Expected: nginx test succeeds. + +- [ ] **Step 4: Create restricted catproxy site config** + +Run on 3c4g. Replace `` before enabling: + +```bash +set -euo pipefail +cat > /etc/nginx/sites-available/catproxy-sub2api.conf <<'EOF' +server { + server_name catproxy.lilifamily.com; + + listen 443 ssl; + listen [::]:443 ssl; + ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + allow ; + deny all; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host proxy.api.lilifamily.com; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +EOF +test "$(grep -c '' /etc/nginx/sites-available/catproxy-sub2api.conf)" = "0" +ln -sf /etc/nginx/sites-available/catproxy-sub2api.conf /etc/nginx/sites-enabled/catproxy-sub2api.conf +nginx -t +``` + +Expected after replacing the IP: nginx test succeeds. If the placeholder remains, the `test` command fails and the task must stop. + +- [ ] **Step 5: Add unknown host default deny** + +Run on 3c4g: + +```bash +set -euo pipefail +cat > /etc/nginx/sites-available/00-default-deny.conf <<'EOF' +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + return 444; +} + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name _; + ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; + return 444; +} +EOF +ln -sf /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/00-default-deny.conf +nginx -t +systemctl reload nginx +``` + +Expected: nginx reload succeeds. + +- [ ] **Step 6: Verify origin denies non-token requests** + +Run from local or any non-JPpro network: + +```bash +curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health +``` + +Expected: + +```text +HTTP/1.1 403 Forbidden +``` + +or connection refused/closed if firewall blocks before nginx. + +- [ ] **Step 7: Verify catproxy restriction** + +Run from non-management network: + +```bash +curl --noproxy '*' -i https://catproxy.lilifamily.com/health +``` + +Expected: + +```text +HTTP/1.1 403 Forbidden +``` + +or `curl: (52) Empty reply from server` if nginx returns 444. + +- [ ] **Step 8: Save sanitized config copies locally** + +From local machine, use Tabby SFTP or copy commands to update: + +```text +ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf +ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf +``` + +Before saving, replace any secret token value with: + +```text +REDACTED_SECRET +``` + +Expected: files contain no real token. + +## Task 5: Configure JPpro proxy-ng for Public Host and Origin + +**Files:** +- Modify after remote verification: `ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf` +- Create after remote verification: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` +- Modify after remote verification: `ops/remote/jppro-sub2api/README.md` +- Test: nginx syntax, public host health, unauthorized API returns 401 + +- [ ] **Step 1: Open JPpro Tabby session** + +Use Tabby profile: + +```text +个人-JPpro-akile-0504-1c1g +``` + +Expected: shell prompt on JPpro. + +- [ ] **Step 2: Backup current JPpro nginx config** + +Run on JPpro: + +```bash +set -euo pipefail +backup_dir="/root/proxy-ng-hardening-backup-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$backup_dir" +cp -a /etc/nginx "$backup_dir/nginx" +chmod -R go-rwx "$backup_dir" +printf '%s\n' "$backup_dir" +``` + +Expected: prints backup path. + +- [ ] **Step 3: Confirm token file exists on JPpro** + +Run on JPpro: + +```bash +test -f /etc/nginx/proxy-ng/token.conf +stat -c '%a %U %G %n' /etc/nginx/proxy-ng/token.conf +``` + +Expected: + +```text +600 root root /etc/nginx/proxy-ng/token.conf +``` + +- [ ] **Step 4: Create proxy-ng nginx config** + +Run on JPpro: + +```bash +set -euo pipefail +cat > /etc/nginx/conf.d/proxy-ng-map.conf <<'EOF' +map $http_upgrade $connection_upgrade { + default upgrade; + "" close; +} + +limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m; +limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s; +limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m; +EOF + +cat > /etc/nginx/sites-available/jp-proxy-sub2api.conf <<'EOF' +include /etc/nginx/proxy-ng/token.conf; + +server { + listen 80; + listen [::]:80; + server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; + + ssl_certificate /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/privkey.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + client_header_timeout 10s; + client_body_timeout 30s; + send_timeout 30s; + keepalive_timeout 20s; + keepalive_requests 100; + + limit_conn conn_per_ip 50; + limit_req zone=req_per_ip burst=30 nodelay; + limit_req_status 429; + + add_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g" always; + add_header X-Content-Type-Options nosniff always; + + location ~ /\.(git|svn|hg|env|aws|ssh) { + return 444; + } + + location ~* ^/(wp-admin|wp-login\.php|phpmyadmin|cgi-bin|vendor|boaform) { + return 444; + } + + location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) { + limit_req zone=auth_per_ip burst=3 nodelay; + limit_conn conn_per_ip 10; + + proxy_pass https://origin.proxy.api.lilifamily.com; + proxy_ssl_server_name on; + proxy_ssl_name origin.proxy.api.lilifamily.com; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host origin.proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_buffering off; + proxy_request_buffering off; + } + + location / { + proxy_pass https://origin.proxy.api.lilifamily.com; + proxy_ssl_server_name on; + proxy_ssl_name origin.proxy.api.lilifamily.com; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host origin.proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} +EOF + +ln -sf /etc/nginx/sites-available/jp-proxy-sub2api.conf /etc/nginx/sites-enabled/jp-proxy-sub2api.conf +nginx -t +``` + +Expected: nginx test succeeds. + +- [ ] **Step 5: Resolve certificate coverage before reload** + +Run on JPpro: + +```bash +openssl x509 -in /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem -noout -text | grep -E 'DNS:proxy.api.lilifamily.com|DNS:jp.proxy.api.lilifamily.com' || true +``` + +Expected: certificate includes both names. If it only includes `jp.proxy.api.lilifamily.com`, issue or install a certificate that covers: + +```text +proxy.api.lilifamily.com +jp.proxy.api.lilifamily.com +``` + +Do not reload public DNS to JPpro until certificate coverage is correct. + +- [ ] **Step 6: Reload JPpro nginx** + +Run on JPpro: + +```bash +nginx -t +systemctl reload nginx +systemctl is-active nginx +``` + +Expected: + +```text +active +``` + +- [ ] **Step 7: Verify JPpro node domain** + +Run from local: + +```bash +curl --noproxy '*' -sS -i https://jp.proxy.api.lilifamily.com/health +curl --noproxy '*' -i -X POST https://jp.proxy.api.lilifamily.com/v1/responses \ + -H 'Content-Type: application/json' \ + --data '{"model":"gpt-5.5","input":"ping"}' +``` + +Expected: + +```text +HTTP/1.1 200 OK +{"status":"ok"} +HTTP/1.1 401 Unauthorized +``` + +The 401 response body should include `API_KEY_REQUIRED`. + +- [ ] **Step 8: Verify public host against JPpro before DNS cutover** + +Run from local: + +```bash +curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -sS -i https://proxy.api.lilifamily.com/health +curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i -X POST https://proxy.api.lilifamily.com/v1/responses \ + -H 'Content-Type: application/json' \ + --data '{"model":"gpt-5.5","input":"ping"}' +``` + +Expected: + +```text +HTTP/1.1 200 OK +{"status":"ok"} +HTTP/1.1 401 Unauthorized +``` + +- [ ] **Step 9: Save sanitized JPpro config locally** + +Copy `/etc/nginx/sites-available/jp-proxy-sub2api.conf` to: + +```text +ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf +``` + +Replace token references with: + +```text +REDACTED_SECRET +``` + +Expected: local config contains no real token. + +## Task 6: Add Basic Abuse Protection Services + +**Files:** +- Create after verification: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` +- Create after verification: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` +- Test: scan paths return 444/403, fail2ban active, logrotate config present + +- [ ] **Step 1: Install fail2ban on JPpro** + +Run on JPpro: + +```bash +apt-get update +apt-get install -y fail2ban +systemctl enable --now fail2ban +systemctl is-active fail2ban +``` + +Expected: + +```text +active +``` + +- [ ] **Step 2: Configure JPpro fail2ban nginx filters** + +Run on JPpro: + +```bash +cat > /etc/fail2ban/filter.d/nginx-proxy-ng-scan.conf <<'EOF' +[Definition] +failregex = ^ - .* "(GET|POST|HEAD) /(\.env|\.git|wp-login\.php|wp-admin|phpmyadmin|cgi-bin|vendor|boaform).*" (403|404|444|429) +ignoreregex = +EOF + +cat > /etc/fail2ban/jail.d/nginx-proxy-ng.conf <<'EOF' +[nginx-proxy-ng-scan] +enabled = true +filter = nginx-proxy-ng-scan +logpath = /var/log/nginx/*access*.log +maxretry = 10 +findtime = 600 +bantime = 3600 +EOF + +systemctl restart fail2ban +fail2ban-client status nginx-proxy-ng-scan +``` + +Expected: jail exists and is enabled. + +- [ ] **Step 3: Configure logrotate for nginx hardening logs** + +Run on JPpro and 3c4g: + +```bash +cat > /etc/logrotate.d/nginx-proxy-ng <<'EOF' +/var/log/nginx/*.log { + daily + rotate 7 + missingok + notifempty + compress + delaycompress + sharedscripts + postrotate + [ -s /run/nginx.pid ] && kill -USR1 "$(cat /run/nginx.pid)" + endscript +} +EOF +logrotate -d /etc/logrotate.d/nginx-proxy-ng +``` + +Expected: dry run shows no syntax error. + +- [ ] **Step 4: Verify scan path blocking on JPpro** + +Run from local: + +```bash +curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i https://proxy.api.lilifamily.com/.env || true +curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i https://proxy.api.lilifamily.com/wp-login.php || true +``` + +Expected: `444` as empty reply or a `403/404` if nginx build/environment does not surface 444 through curl. The request must not reach Sub2API. + +- [ ] **Step 5: Verify 3c4g direct exposure is closed** + +Run from local: + +```bash +curl --noproxy '*' -i http://216.45.59.243:8080/health || true +curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health || true +curl --noproxy '*' -i https://catproxy.lilifamily.com/health || true +``` + +Expected: + +```text +8080: connection refused, timeout, or no route +origin: 403 or connection refused/closed +catproxy: 403/444 from non-management network +``` + +- [ ] **Step 6: Record hardening notes** + +Create `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md`: + +```markdown +# JPpro proxy-ng hardening notes + +- Public host served: `proxy.api.lilifamily.com`, `jp.proxy.api.lilifamily.com` +- Origin: `https://origin.proxy.api.lilifamily.com` +- Token: stored on remote only, redacted from Git +- Scan-path blocking: enabled for dotfiles, wp-login, phpmyadmin, cgi-bin, vendor, boaform +- Rate limit: general `10r/s`, auth `10r/m` +- fail2ban: `nginx-proxy-ng-scan` +- logrotate: `/etc/logrotate.d/nginx-proxy-ng` +- Validation date: `YYYY-MM-DD` +``` + +Create `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md`: + +```markdown +# 3c4g firewall and origin boundary notes + +- `origin.proxy.api.lilifamily.com`: only proxy-ng node IPs allowed +- `catproxy.lilifamily.com`: restricted to management/Tailscale or explicit rollback window +- `127.0.0.1:8080`: Sub2API local upstream only +- `X-Proxy-Ng-Token`: required at origin and redacted from Git +- Validation date: `YYYY-MM-DD` +``` + +## Task 7: Cut Public DNS to proxy-ng Nodes + +**Files:** +- Modify after verification: `ops/remote/jppro-sub2api/README.md` +- Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` +- Test: DNS answer and end-to-end public host + +- [ ] **Step 1: Confirm final pre-cutover checks** + +Run from local: + +```bash +curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -sS https://proxy.api.lilifamily.com/health +curl --noproxy '*' -sS https://jp.proxy.api.lilifamily.com/health +curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health || true +``` + +Expected: + +```text +{"status":"ok"} +{"status":"ok"} +origin from local is 403 or blocked +``` + +- [ ] **Step 2: Update DNS A records** + +In DNS provider: + +```text +Remove 216.45.59.243 from proxy.api.lilifamily.com. +Add 151.242.164.72 to proxy.api.lilifamily.com. +Keep TTL 60-300 seconds. +``` + +Expected: `proxy.api.lilifamily.com` no longer has a 3c4g A record. + +- [ ] **Step 3: Verify public DNS** + +Run: + +```bash +dig +short A proxy.api.lilifamily.com +``` + +Expected: + +```text +151.242.164.72 +``` + +If multiple proxy-ng nodes exist, expected output includes only proxy-ng IPs and never `216.45.59.243`. + +- [ ] **Step 4: Verify public entry** + +Run: + +```bash +curl --noproxy '*' -sS -i https://proxy.api.lilifamily.com/health +curl --noproxy '*' -i -X POST https://proxy.api.lilifamily.com/v1/responses \ + -H 'Content-Type: application/json' \ + --data '{"model":"gpt-5.5","input":"ping"}' +``` + +Expected: + +```text +HTTP/1.1 200 OK +{"status":"ok"} +HTTP/1.1 401 Unauthorized +``` + +- [ ] **Step 5: Verify no catproxy leakage** + +Run: + +```bash +curl --noproxy '*' -sS -D /tmp/proxy-headers.txt -o /tmp/proxy-body.txt https://proxy.api.lilifamily.com/ +rg -n 'catproxy|origin\\.proxy' /tmp/proxy-headers.txt /tmp/proxy-body.txt || true +``` + +Expected: no `catproxy` or `origin.proxy` appears in headers or body. + +- [ ] **Step 6: Record DNS cutover** + +Append to both remote READMEs: + +```markdown +### 2026-05-26 proxy.api.lilifamily.com DNS cutover + +- `proxy.api.lilifamily.com` now resolves only to proxy-ng node IPs. +- `216.45.59.243` is no longer exposed through the public service hostname. +- Public health check returned `200`. +- Unauthenticated `/v1/responses` returned `401 API_KEY_REQUIRED`. +``` + +## Task 8: Final Documentation and Repository Verification + +**Files:** +- Modify: `ops/remote/jppro-sub2api/README.md` +- Modify: `ops/remote/us-racknerd-0526-sub2api/README.md` +- Modify or create: `ops/remote/**/current/*.conf` +- Modify: `docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md` only if execution discovers a plan correction +- Test: secret scan and git status + +- [ ] **Step 1: Verify no secrets in docs** + +Run: + +```bash +rg -n 'set \\$proxy_ng_expected_token|[A-Fa-f0-9]{64}|PROXY_NG_TOKEN|BEGIN PRIVATE KEY|PASSWORD=' \ + docs ops proxy-ng .gitignore || true +``` + +Expected: no real token, private key, or password. Mentions of placeholder names are acceptable only when they are clearly examples. + +- [ ] **Step 2: Verify remote current config copies exist** + +Run: + +```bash +test -f ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf +test -f ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md +test -f ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf +test -f ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf +test -f ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md +``` + +Expected: all tests exit 0. + +- [ ] **Step 3: Verify git sees intended docs** + +Run: + +```bash +git status --short --untracked-files=all | rg 'docs/superpowers/plans|docs/agent-specs|docs/memorys|ops/remote|proxy-ng|.gitignore' +``` + +Expected: changed docs and ops files are visible. If unrelated files appear, do not stage them unless they are part of this deployment record. + +- [ ] **Step 4: Commit deployment documentation only after user approval** + +Run after user explicitly asks to commit: + +```bash +git add .gitignore \ + docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md \ + docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md \ + docs/memorys/2026-05-25-proxy-ng职责边界.md \ + ops/remote \ + proxy-ng +git commit -m "docs: record proxy-ng origin hardening deployment plan" +``` + +Expected: commit succeeds and includes only deployment docs/templates/records, not real secrets. + +## Self-Review + +**Spec coverage:** The plan covers domain role changes, hiding 3c4g from `proxy.api.lilifamily.com`, origin allowlist/token enforcement, JPpro public host support, no Cloudflare LB assumptions, basic anti-abuse controls, DNS cutover, and project documentation. + +**Placeholder scan:** The plan contains explicit placeholders only where execution must insert environment-specific values that cannot be safely known in Git: ``, ``, and timestamp values. Each placeholder has a concrete validation step that fails if left unresolved in active remote config. + +**Type consistency:** Domain names, paths, and service roles are consistent: `proxy.api.lilifamily.com` is public and points only to proxy-ng nodes; `catproxy.lilifamily.com` is restricted direct/rollback; `origin.proxy.api.lilifamily.com` is proxy-ng-only origin; `jp.proxy.api.lilifamily.com` is the JPpro node. diff --git a/ops/remote/README.md b/ops/remote/README.md new file mode 100644 index 00000000..79540c37 --- /dev/null +++ b/ops/remote/README.md @@ -0,0 +1,31 @@ +# Remote Deployment Inventory + +本目录记录 Sub2API 远程部署事实。每台服务器必须单独一个子目录,目录名建议使用: + +```text +ops/remote/--sub2api/ +``` + +## 当前目录 + +| 目录 | Tabby profile | 公网入口 | 状态 | +|------|---------------|----------|------| +| `jppro-sub2api/` | `个人-JPpro-akile-0504-1c1g` | `https://proxy.api.lilifamily.com` / `https://jp.proxy.api.lilifamily.com` | proxy-ng 公开入口节点,回源到 3c4g origin | +| `us-racknerd-0425-sub2api/` | `个人-US-racknerd-0425-1c1g` | `https://pai.cyan2000.uk` | 旧机,迁移来源和回滚来源 | +| `us-racknerd-0526-sub2api/` | `个人-US-racknerd-0526-3c4g` | `https://origin.proxy.api.lilifamily.com` / `https://catproxy.lilifamily.com` | Sub2API 受限 origin 和公开直连主站入口 | + +## 留痕规则 + +每个服务器目录至少包含: + +- `README.md`:域名、IP、服务器名、Tabby profile、部署方式、部署架构、运行检查和操作记录。 +- `current/`:当前远程实际生效配置副本,例如 `docker-compose.yml`、`nginx-sub2api.conf` 或 `Caddyfile`。 +- `secrets/README.md`:说明哪些真实密钥可在本机私有保存,但不得提交。 +- `.gitignore`:排除 `.env`、密钥、订阅、临时备份和数据库备份。 +- `backups/`:只保留 `.gitkeep`、非敏感说明或 `*.SHA256SUMS` 校验记录;真实备份文件不得提交。 + +## 变更要求 + +- 迁移、部署、域名切换、证书、反代、Compose、数据库、Redis、Mihomo 或代理配置变化后,必须同步更新对应服务器目录。 +- 根 `AGENTS.md` 只保留规则和索引;具体运行事实沉到本目录的服务器子目录。 +- 未验证的信息必须标注为“目标”“计划”或“待确认”,不能写成当前事实。 diff --git a/ops/remote/jppro-sub2api/.gitignore b/ops/remote/jppro-sub2api/.gitignore new file mode 100644 index 00000000..a3750e85 --- /dev/null +++ b/ops/remote/jppro-sub2api/.gitignore @@ -0,0 +1,10 @@ +# Remote runtime secrets and ad-hoc backups. +secrets/* +!secrets/README.md +backups/* +!backups/.gitkeep + +# Never commit copied environment files. +*.env +.env +.env.* diff --git a/ops/remote/jppro-sub2api/README.md b/ops/remote/jppro-sub2api/README.md new file mode 100644 index 00000000..b3001a82 --- /dev/null +++ b/ops/remote/jppro-sub2api/README.md @@ -0,0 +1,147 @@ +# JPpro proxy-ng Remote Inventory + +最后核对时间:2026-05-26 19:10 Asia/Shanghai + +## 远程位置 + +- Tabby profile:`个人-JPpro-akile-0504-1c1g` +- Hostname:`jppro2-20260504201455dc6fca` +- 公网 IP:`151.242.164.72` +- 公开入口:`https://proxy.api.lilifamily.com` +- 节点入口:`https://jp.proxy.api.lilifamily.com` +- DNS:`proxy.api.lilifamily.com -> 151.242.164.72`,`jp.proxy.api.lilifamily.com -> 151.242.164.72` +- 回源目标:`https://origin.proxy.api.lilifamily.com` +- 当前状态:proxy-ng 公开入口节点;不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理 + +## 本地留痕 + +- 当前远程状态说明:`current/README.md` +- 当前 nginx 站点配置副本:`current/nginx-jp-proxy-sub2api.conf` +- 当前 nginx map 配置副本:`current/proxy-ng-map.conf` +- 加固记录:`current/proxy-ng-hardening-notes.md` + +`secrets/` 目录允许在本机私有保存历史 `.env`、临时 admin 密码或其他敏感材料,但这些文件已被 `.gitignore` 排除,不能提交,也不能视为当前远程生效配置。 + +## 当前部署架构 + +```text +Client + ↓ HTTPS :443 proxy.api.lilifamily.com / jp.proxy.api.lilifamily.com +JPpro nginx proxy-ng + ↓ HTTPS + X-Proxy-Ng-Token +3c4g nginx origin.proxy.api.lilifamily.com + ↓ 127.0.0.1:8080 +Sub2API +``` + +JPpro 节点职责边界: + +- 只做入口反向代理和线路中转。 +- 不保存 Sub2API 业务状态。 +- 不持有 Sub2API 数据库密码、Redis 密码或业务 API Key。 +- 不部署 Clash、sing-box、Xray、SOCKS、mixed 等账号出口代理能力。 +- 持有 TLS 证书私钥和 proxy-ng 回源 token;token 仅在远程 root-only 文件中保存,不进 Git。 + +## 当前部署方式 + +- 安装方式:Debian apt 安装 nginx、certbot、python3-certbot-nginx、curl、ca-certificates、fail2ban、logrotate。 +- nginx 版本:`nginx/1.26.3` +- Certbot 版本:`certbot 4.0.0` +- 证书路径:`/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem` +- 私钥路径:`/etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem` +- 证书覆盖域名:`proxy.api.lilifamily.com`、`jp.proxy.api.lilifamily.com` +- 证书到期时间:`2026-08-24 10:01:14 UTC` +- 远程站点配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf` +- 远程站点启用链接:`/etc/nginx/sites-enabled/jp-proxy-sub2api.conf` +- 远程 map 配置:`/etc/nginx/conf.d/proxy-ng-map.conf` +- 远程 token 配置:`/etc/nginx/proxy-ng/token.conf`,权限 `600 root root` + +关键代理配置: + +```text +proxy_pass https://origin.proxy.api.lilifamily.com; +proxy_ssl_server_name on; +proxy_ssl_name origin.proxy.api.lilifamily.com; +proxy_set_header Host origin.proxy.api.lilifamily.com; +proxy_set_header X-Forwarded-Host $host; +proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; +proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; +proxy_read_timeout 3600s; +proxy_send_timeout 3600s; +proxy_buffering off; +proxy_request_buffering off; +``` + +## DNS state after proxy-ng hardening + +| Name | A records | Role | +|---|---|---| +| `proxy.api.lilifamily.com` | `151.242.164.72` | Public entry | +| `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node | +| `catproxy.lilifamily.com` | `216.45.59.243` | Public direct main-site and rollback entry | +| `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only | + +## 运行检查 + +```bash +nginx -t +systemctl is-active nginx +systemctl is-active fail2ban +fail2ban-client status nginx-proxy-ng-scan +openssl x509 -noout -enddate -in /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem +ss -tulpn +curl --noproxy '*' -sS https://proxy.api.lilifamily.com/health +curl --noproxy '*' -sS https://jp.proxy.api.lilifamily.com/health +curl --noproxy '*' -i -X POST https://proxy.api.lilifamily.com/v1/responses \ + -H 'Content-Type: application/json' \ + --data '{"model":"gpt-5.5","input":"ping"}' +``` + +期望结果: + +- `nginx -t` 返回配置语法正确。 +- nginx 服务状态为 `active`。 +- fail2ban 服务状态为 `active`,启用 `nginx-proxy-ng-scan` 和 `sshd` jail。 +- 监听端口包含 `22`、`80`、`443`。 +- `/health` 返回 `200 {"status":"ok"}`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。 +- 未授权 `/v1/responses` 返回 `401 API_KEY_REQUIRED`,证明请求已到达后端 API 层。 + +## 操作记录 + +### 2026-05-24 admin 密码重置 + +- 账号:`zizi2000email@gmail.com` +- 结果:登录接口已验证成功。 +- 本地临时密码留痕:`secrets/admin-reset-20260524-165937.txt` +- 远程旧密码 hash 恢复 SQL:`/opt/sub2api/backups/password-reset-20260524-170007/restore-admin-password.sql` + +临时密码文件被 `.gitignore` 排除,且应保持 `600` 权限。登录后应尽快在后台修改为正式密码。 + +### 2026-05-26 清理 JPpro 部署 + +- 执行范围:只清理 `个人-JPpro-akile-0504-1c1g` 上的 Sub2API Compose 栈,不动 RackNerd 两台服务器。 +- 远程确认:hostname 为 `jppro2-20260504201455dc6fca`,清理前存在 `/opt/sub2api`,运行 `sub2api`、`sub2api-caddy`、`sub2api-postgres`、`sub2api-redis` 四个容器。 +- 已执行:`docker compose down --volumes --remove-orphans --rmi all --timeout 30`。 +- 已删除:四个容器、`sub2api_sub2api_data`、`sub2api_postgres_data`、`sub2api_redis_data`、`sub2api_caddy_data`、`sub2api_caddy_config`、`sub2api_sub2api-network`、四个服务镜像和 `/opt/sub2api`。 +- 验证结果:`docker ps -a` 无容器,匹配 `sub2api/postgres_data/redis_data/caddy_data/caddy_config` 的卷为空,匹配 Sub2API/Caddy/PostgreSQL/Redis 的镜像为空,宿主机监听端口只剩 `22`。 + +### 2026-05-26 部署 JPpro proxy-ng 入口中转 + +- 执行范围:只在 `个人-JPpro-akile-0504-1c1g` 上部署 nginx 入口中转,不在该节点恢复 Sub2API Compose 栈。 +- 域名:`jp.proxy.api.lilifamily.com` +- DNS:`jp.proxy.api.lilifamily.com -> 151.242.164.72` +- 初始回源:`https://proxy.api.lilifamily.com` +- 已安装:`nginx/1.26.3`、`certbot 4.0.0`、`python3-certbot-nginx`、`curl`、`ca-certificates`。 +- 已配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf`、`/etc/nginx/conf.d/proxy-ng-map.conf`。 +- 已签发初始证书:`/etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem`,到期时间 `2026-08-24 06:22:03 UTC`。 +- 公网验证:`https://jp.proxy.api.lilifamily.com/health` 返回 `200 {"status":"ok"}`,根页面返回 `200`,未授权 `POST /v1/responses` 返回 `401 API_KEY_REQUIRED`。 + +### 2026-05-26 proxy-ng origin hardening + +- 3c4g 生成 proxy-ng token,JPpro 通过 root-only 文件 `/etc/nginx/proxy-ng/token.conf` 引用。 +- JPpro 回源从 `https://proxy.api.lilifamily.com` 改为 `https://origin.proxy.api.lilifamily.com`。 +- `proxy.api.lilifamily.com` DNS 切换到 `151.242.164.72`。 +- JPpro 使用 HTTP-01 签发覆盖 `proxy.api.lilifamily.com` 和 `jp.proxy.api.lilifamily.com` 的证书。 +- 远程备份:`/root/proxy-ng-hardening-backup-20260526-183749`。 +- 验证结果:`https://proxy.api.lilifamily.com/health` 返回 `200 {"status":"ok"}`,未授权 `POST /v1/responses` 返回 `401 API_KEY_REQUIRED`,响应头包含 `X-Proxy-Ng-Node: jppro-akile-0504-1c1g`。 +- 防滥用:启用 nginx 连接数/请求速率限制、扫描路径 444、fail2ban `nginx-proxy-ng-scan`、nginx logrotate。 diff --git a/ops/remote/jppro-sub2api/backups/.gitkeep b/ops/remote/jppro-sub2api/backups/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/ops/remote/jppro-sub2api/backups/.gitkeep @@ -0,0 +1 @@ + diff --git a/ops/remote/jppro-sub2api/current/README.md b/ops/remote/jppro-sub2api/current/README.md new file mode 100644 index 00000000..4fdddfd7 --- /dev/null +++ b/ops/remote/jppro-sub2api/current/README.md @@ -0,0 +1,15 @@ +# JPpro Current State + +最后核对时间:2026-05-26 19:10 Asia/Shanghai + +JPpro 当前作为 `proxy-ng` 公开入口节点使用,只运行 nginx 反向代理,不运行 Sub2API、PostgreSQL、Redis、Caddy 或账号出口代理。 + +- 公开入口:`https://proxy.api.lilifamily.com` +- 节点入口:`https://jp.proxy.api.lilifamily.com` +- 回源目标:`https://origin.proxy.api.lilifamily.com` +- nginx 站点配置:`/etc/nginx/sites-available/jp-proxy-sub2api.conf` +- nginx map 配置:`/etc/nginx/conf.d/proxy-ng-map.conf` +- token 文件:`/etc/nginx/proxy-ng/token.conf`,远程 root-only,不入 Git +- 本地配置副本:`current/nginx-jp-proxy-sub2api.conf`、`current/proxy-ng-map.conf` +- 监听端口:SSH `22`、HTTP `80`、HTTPS `443` +- 证书:`/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem`,覆盖 `proxy.api.lilifamily.com` 和 `jp.proxy.api.lilifamily.com` diff --git a/ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf b/ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf new file mode 100644 index 00000000..c88b5c2f --- /dev/null +++ b/ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf @@ -0,0 +1,93 @@ +server { + listen 80; + listen [::]:80; + server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/html; + default_type text/plain; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; + + ssl_certificate /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + include /etc/nginx/proxy-ng/token.conf; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + client_header_timeout 10s; + client_body_timeout 30s; + send_timeout 30s; + keepalive_timeout 20s; + keepalive_requests 100; + + limit_conn conn_per_ip 50; + limit_req zone=req_per_ip burst=30 nodelay; + limit_req_status 429; + + add_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g" always; + add_header X-Content-Type-Options nosniff always; + + location ~ /\.(git|svn|hg|env|aws|ssh) { + return 444; + } + + location ~* ^/(wp-admin|wp-login\.php|phpmyadmin|cgi-bin|vendor|boaform) { + return 444; + } + + location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) { + limit_req zone=auth_per_ip burst=3 nodelay; + limit_conn conn_per_ip 10; + + proxy_pass https://origin.proxy.api.lilifamily.com; + proxy_ssl_server_name on; + proxy_ssl_name origin.proxy.api.lilifamily.com; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host origin.proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_buffering off; + proxy_request_buffering off; + } + + location / { + proxy_pass https://origin.proxy.api.lilifamily.com; + proxy_ssl_server_name on; + proxy_ssl_name origin.proxy.api.lilifamily.com; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host origin.proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} diff --git a/ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md b/ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md new file mode 100644 index 00000000..e92dd045 --- /dev/null +++ b/ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md @@ -0,0 +1,13 @@ +# JPpro proxy-ng hardening notes + +日期:2026-05-26 + +- Public host served: `proxy.api.lilifamily.com`, `jp.proxy.api.lilifamily.com` +- Origin: `https://origin.proxy.api.lilifamily.com` +- Token: stored on remote only at `/etc/nginx/proxy-ng/token.conf`, redacted from Git +- Certificate: `/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem`, SAN includes both public and node hostnames +- Scan-path blocking: enabled for dotfiles, wp-login, phpmyadmin, cgi-bin, vendor, boaform +- Rate limit: general `10r/s`, auth `10r/m`, connection limit `50` per IP on public server +- fail2ban: `nginx-proxy-ng-scan` and `sshd` active +- logrotate: `/etc/logrotate.d/nginx-proxy-ng` +- Validation: public `/health` returned `200`, unauthenticated `/v1/responses` returned `401 API_KEY_REQUIRED`, scan paths returned closed connection/444 behavior diff --git a/ops/remote/jppro-sub2api/current/proxy-ng-map.conf b/ops/remote/jppro-sub2api/current/proxy-ng-map.conf new file mode 100644 index 00000000..80db98d9 --- /dev/null +++ b/ops/remote/jppro-sub2api/current/proxy-ng-map.conf @@ -0,0 +1,8 @@ +map $http_upgrade $connection_upgrade { + default upgrade; + "" close; +} + +limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m; +limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s; +limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m; diff --git a/ops/remote/jppro-sub2api/secrets/README.md b/ops/remote/jppro-sub2api/secrets/README.md new file mode 100644 index 00000000..08390494 --- /dev/null +++ b/ops/remote/jppro-sub2api/secrets/README.md @@ -0,0 +1,7 @@ +# Secrets Directory + +此目录用于本机保存远程运行所需密钥文件。 + +- `sub2api.env`:远程 `/opt/sub2api/.env` 的本地备份,包含真实密钥。 +- 该文件被上级 `.gitignore` 排除,且应保持 `600` 权限。 +- 不要提交真实 `.env`,不要把密钥写入 README 或 issue。 diff --git a/ops/remote/us-racknerd-0425-sub2api/.gitignore b/ops/remote/us-racknerd-0425-sub2api/.gitignore new file mode 100644 index 00000000..b59c1250 --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/.gitignore @@ -0,0 +1,13 @@ +# Remote runtime secrets and ad-hoc backups. +secrets/* +!secrets/README.md +backups/* +!backups/.gitkeep +!backups/*.SHA256SUMS + +# Never commit copied environment files or proxy subscriptions. +*.env +.env +.env.* +mihomo-config.yaml +airport.yaml diff --git a/ops/remote/us-racknerd-0425-sub2api/README.md b/ops/remote/us-racknerd-0425-sub2api/README.md new file mode 100644 index 00000000..e6b75081 --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/README.md @@ -0,0 +1,114 @@ +# US RackNerd 0425 Sub2API Remote Inventory + +最后核对时间:2026-05-26 12:10 Asia/Shanghai + +## 远程位置 + +- Tabby profile:`个人-US-racknerd-0425-1c1g` +- Hostname:`racknerd-042910c` +- 公网 IP:`107.173.86.201` +- 当前公网入口:`https://pai.cyan2000.uk` +- 迁移后角色:旧机,短期回滚来源 +- 部署目录:`/opt/sub2api` +- Compose project:`sub2api` +- 系统:已验证为 Linux 服务器;本次未读取完整 `/etc/os-release` + +## 本地留痕 + +- 当前远程 Compose 副本:`current/docker-compose.yml` +- 当前远程 nginx 站点副本:`current/nginx-sub2api.conf` +- 迁移包 SHA256 记录:`backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS` +- 密钥目录说明:`secrets/README.md` + +真实 `.env`、Mihomo 配置、订阅文件和临时备份不得提交。需要本地留存时放入 `secrets/` 或 `backups/`,并确认 `.gitignore` 已排除。 + +## 当前远程配置状态 + +- nginx 管理公网域名 `pai.cyan2000.uk`,证书由 Certbot 管理。 +- nginx 配置路径:`/etc/nginx/sites-available/sub2api.conf` +- nginx 启用路径:`/etc/nginx/sites-enabled/sub2api.conf -> /etc/nginx/sites-available/sub2api.conf` +- Sub2API 只绑定宿主机 `127.0.0.1:8080`,由 nginx 反代公网 HTTPS。 +- Docker Compose 使用本地目录持久化,不使用 Docker 命名卷。 +- 运行目录包含: + - `/opt/sub2api/data` + - `/opt/sub2api/postgres_data` + - `/opt/sub2api/redis_data` + - `/opt/sub2api/mihomo` +- 当前容器: + - `sub2api`:`weishaw/sub2api:latest`,healthy,`127.0.0.1:8080->8080` + - `sub2api-postgres`:`postgres:18-alpine`,healthy + - `sub2api-redis`:`redis:8-alpine`,healthy + - `sub2api-mihomo`:`metacubex/mihomo:latest` +- 最新运行数据规模快照: + - `data`:约 70M + - `postgres_data`:约 201M + - `redis_data`:约 16M + - `mihomo`:约 44K + +## 部署架构 + +```text +Browser :443 HTTPS -> nginx -> 127.0.0.1:8080 -> sub2api container + -> postgres container + -> redis container + -> mihomo container (account proxy) +``` + +公网只应暴露 `80/443/22`。`8080` 只监听 `127.0.0.1`,PostgreSQL、Redis、Mihomo 不映射到公网。 + +## 迁移关系 + +这台服务器是已完成迁移的数据来源,并保留为短期回滚来源。目标服务器记录见: + +```text +ops/remote/us-racknerd-0526-sub2api/ +``` + +迁移时已优先迁移: + +- PostgreSQL 数据:`/opt/sub2api/postgres_data` 或通过 `pg_dump` 导出。 +- Redis 数据:`/opt/sub2api/redis_data`。 +- Sub2API 数据目录:`/opt/sub2api/data`。 +- Mihomo 配置:`/opt/sub2api/mihomo`。 +- 运行配置:`/opt/sub2api/docker-compose.yml`、`/opt/sub2api/.env`。 +- nginx 配置:`/etc/nginx/sites-available/sub2api.conf`,目标域名需改为 `proxy.api.lilifamily.com`。 + +迁移包路径: + +```text +/opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/ +``` + +## 运行检查 + +```bash +cd /opt/sub2api +docker compose ps +curl -sS http://127.0.0.1:8080/health +curl --noproxy '*' -sS https://pai.cyan2000.uk/health +systemctl status nginx --no-pager +``` + +期望结果: + +- `sub2api`、`sub2api-postgres`、`sub2api-redis` 为 healthy。 +- 本机 `/health` 返回 `{"status":"ok"}`。 +- 公网入口 `/health` 返回 `{"status":"ok"}`。 + +## 操作记录 + +### 2026-05-26 迁移前盘点 + +- 通过 Tabby profile `个人-US-racknerd-0425-1c1g` 只读核对。 +- 确认 `/opt/sub2api` 存在且 Docker Compose 服务健康。 +- 确认 nginx 站点配置由 Certbot 管理 `pai.cyan2000.uk`。 +- 已将可提交的 Compose/nginx 配置副本写入 `current/`。 + +### 2026-05-26 迁移备份与回滚状态 + +- 旧机生成迁移包:`/opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/`。 +- 迁移包包含 PostgreSQL custom dump、运行配置和非日志运行数据归档、旧 nginx 配置、SHA256 校验文件。 +- 旧机本地 `sha256sum -c SHA256SUMS` 校验通过。 +- 迁移完成后只读复核:`sub2api`、`sub2api-postgres`、`sub2api-redis` 仍为 healthy,`sub2api-mihomo` 仍为 running。 +- 旧机本机 `/health` 返回 `{"status":"ok"}`。 +- 未执行停机或数据删除操作;如新机回滚失败,可继续使用旧域名 `https://pai.cyan2000.uk`。 diff --git a/ops/remote/us-racknerd-0425-sub2api/backups/.gitkeep b/ops/remote/us-racknerd-0425-sub2api/backups/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/backups/.gitkeep @@ -0,0 +1 @@ + diff --git a/ops/remote/us-racknerd-0425-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS b/ops/remote/us-racknerd-0425-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS new file mode 100644 index 00000000..6f2dbea6 --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS @@ -0,0 +1,3 @@ +4fa5f72c3bc3c68b95bd5846cee5114516435c1dcf1e7623c377a1911e8b6cfe /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/nginx-sub2api.conf +c3890902498a69f5bb4f2d2632a852bbba075744043bf88cc08b4116ee8c9599 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api-config-data-mihomo-redis.tar.gz +808f4790296bb13525c6cdc4ae3e372e083cd3f220c9bf6e41988d4886474b61 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api.dump diff --git a/ops/remote/us-racknerd-0425-sub2api/current/docker-compose.yml b/ops/remote/us-racknerd-0425-sub2api/current/docker-compose.yml new file mode 100644 index 00000000..5bdb0e69 --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/current/docker-compose.yml @@ -0,0 +1,152 @@ +x-logging: &default-logging + driver: json-file + options: + max-size: ${LOG_MAX_SIZE:-20m} + max-file: "${LOG_MAX_FILE:-5}" + +services: + sub2api: + image: ${SUB2API_IMAGE:-weishaw/sub2api:latest} + container_name: sub2api + restart: unless-stopped + logging: *default-logging + ulimits: + nofile: + soft: 100000 + hard: 100000 + ports: + - "${BIND_HOST:-127.0.0.1}:${SERVER_PORT:-8080}:8080" + volumes: + - ./data:/app/data + environment: + - AUTO_SETUP=true + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=8080 + - SERVER_MODE=${SERVER_MODE:-release} + - RUN_MODE=${RUN_MODE:-standard} + - TZ=${TZ:-Asia/Shanghai} + + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USER=${POSTGRES_USER:-sub2api} + - DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - DATABASE_DBNAME=${POSTGRES_DB:-sub2api} + - DATABASE_SSLMODE=disable + - DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-20} + - DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-5} + - DATABASE_CONN_MAX_LIFETIME_MINUTES=30 + - DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5 + + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_DB=${REDIS_DB:-0} + - REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-128} + - REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-4} + - REDIS_ENABLE_TLS=false + + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD is required} + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} + - JWT_EXPIRE_HOUR=24 + - TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required} + + - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} + - SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false} + - SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false} + - SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + mihomo: + condition: service_started + networks: + - sub2api-network + healthcheck: + test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + postgres: + image: ${POSTGRES_IMAGE:-postgres:18-alpine} + container_name: sub2api-postgres + restart: unless-stopped + logging: *default-logging + shm_size: 64m + volumes: + - ./postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER:-sub2api} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - POSTGRES_DB=${POSTGRES_DB:-sub2api} + - PGDATA=/var/lib/postgresql/data + - TZ=${TZ:-Asia/Shanghai} + command: + - postgres + - -c + - max_connections=30 + - -c + - shared_buffers=64MB + - -c + - effective_cache_size=256MB + - -c + - work_mem=2MB + - -c + - maintenance_work_mem=32MB + networks: + - sub2api-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + redis: + image: ${REDIS_IMAGE:-redis:8-alpine} + container_name: sub2api-redis + restart: unless-stopped + logging: *default-logging + volumes: + - ./redis_data:/data + command: > + sh -c 'redis-server + --save 60 1 + --appendonly yes + --appendfsync everysec + $${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"} + $${REDIS_MAXMEMORY:+--maxmemory "$$REDIS_MAXMEMORY" --maxmemory-policy allkeys-lru}' + environment: + - TZ=${TZ:-Asia/Shanghai} + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_MAXMEMORY=${REDIS_MAXMEMORY:-128mb} + - REDISCLI_AUTH=${REDIS_PASSWORD:-} + networks: + - sub2api-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + mihomo: + image: ${MIHOMO_IMAGE:-metacubex/mihomo:latest} + container_name: sub2api-mihomo + restart: unless-stopped + logging: *default-logging + volumes: + - ./mihomo:/root/.config/mihomo + environment: + - TZ=${TZ:-Asia/Shanghai} + networks: + - sub2api-network + +networks: + sub2api-network: + name: sub2api-network + driver: bridge diff --git a/ops/remote/us-racknerd-0425-sub2api/current/nginx-sub2api.conf b/ops/remote/us-racknerd-0425-sub2api/current/nginx-sub2api.conf new file mode 100644 index 00000000..22afea5f --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/current/nginx-sub2api.conf @@ -0,0 +1,43 @@ +server { + server_name pai.cyan2000.uk; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 50m; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + proxy_pass http://127.0.0.1:8080; + 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; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/pai.cyan2000.uk/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/pai.cyan2000.uk/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot +} + +server { + if ($host = pai.cyan2000.uk) { + return 301 https://$host$request_uri; + } # managed by Certbot + + listen 80; + listen [::]:80; + server_name pai.cyan2000.uk; + return 404; # managed by Certbot +} diff --git a/ops/remote/us-racknerd-0425-sub2api/secrets/README.md b/ops/remote/us-racknerd-0425-sub2api/secrets/README.md new file mode 100644 index 00000000..96bb15ec --- /dev/null +++ b/ops/remote/us-racknerd-0425-sub2api/secrets/README.md @@ -0,0 +1,8 @@ +# Secrets Directory + +此目录用于本机保存 `个人-US-racknerd-0425-1c1g` 上 Sub2API 运行所需密钥备份。 + +- 远程 `.env` 路径:`/opt/sub2api/.env` +- 远程 Mihomo 配置路径:`/opt/sub2api/mihomo/config.yaml` +- 这些文件包含数据库密码、JWT 密钥、TOTP 密钥、订阅地址或代理凭据,默认被上级 `.gitignore` 排除。 +- 不要提交真实密钥,不要把密钥写入 README、issue 或 PR。 diff --git a/ops/remote/us-racknerd-0526-sub2api/.gitignore b/ops/remote/us-racknerd-0526-sub2api/.gitignore new file mode 100644 index 00000000..b59c1250 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/.gitignore @@ -0,0 +1,13 @@ +# Remote runtime secrets and ad-hoc backups. +secrets/* +!secrets/README.md +backups/* +!backups/.gitkeep +!backups/*.SHA256SUMS + +# Never commit copied environment files or proxy subscriptions. +*.env +.env +.env.* +mihomo-config.yaml +airport.yaml diff --git a/ops/remote/us-racknerd-0526-sub2api/README.md b/ops/remote/us-racknerd-0526-sub2api/README.md new file mode 100644 index 00000000..36a0a5a3 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/README.md @@ -0,0 +1,168 @@ +# US RackNerd 0526 Sub2API Remote Inventory + +最后核对时间:2026-05-26 19:37 Asia/Shanghai + +## 远程位置 + +- Tabby profile:`个人-US-racknerd-0526-3c4g` +- Hostname:`racknerd-0a1c758` +- 公网 IP:`216.45.59.243` +- origin 入口:`https://origin.proxy.api.lilifamily.com` +- 公开直连主站入口:`https://catproxy.lilifamily.com` +- 部署目录:`/opt/sub2api` +- Compose project:`sub2api` +- 系统:Debian GNU/Linux 13 (trixie) + +## 当前状态 + +- 已部署并验证。 +- Docker:`Docker version 29.5.2, build 79eb04c` +- Docker Compose:`Docker Compose version v5.1.4` +- nginx:`nginx/1.26.3` +- DNS:`origin.proxy.api.lilifamily.com -> 216.45.59.243`,`catproxy.lilifamily.com -> 216.45.59.243` +- `proxy.api.lilifamily.com` 已切到 JPpro `151.242.164.72`,3c4g 上旧 `proxy.api` nginx 站点已禁用。 +- Sub2API 只绑定宿主机 `127.0.0.1:8080`,公网只能经 nginx `catproxy` 或受限 `origin` 转发。 +- PostgreSQL、Redis、Mihomo 不映射公网端口。 +- 从旧机迁移的业务计数已核对:`users=1`、`api_keys=3`、`accounts=133`。 + +## 本地留痕 + +- 当前远程 Compose 副本:`current/docker-compose.yml` +- 旧主域 nginx 配置副本:`current/nginx-sub2api.conf`,当前远程已禁用,仅作历史/回滚参考 +- 当前 origin 配置副本:`current/nginx-origin-sub2api.conf` +- 当前 catproxy 配置副本:`current/nginx-catproxy-sub2api.conf` +- 当前默认拒绝配置副本:`current/nginx-default-deny.conf` +- 当前 token map 配置副本:`current/proxy-ng-token-map.conf`,真实 token 已脱敏 +- 防火墙与 origin 边界记录:`current/firewall-notes.md` +- 迁移包 SHA256 记录:`backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS` +- 密钥目录说明:`secrets/README.md` + +真实 `.env`、Mihomo 配置、订阅文件、proxy-ng token、证书私钥和临时备份不得提交。需要本地留存时放入 `secrets/` 或 `backups/`,并确认 `.gitignore` 已排除。 + +## 部署架构 + +```text +Client + ↓ HTTPS :443 proxy.api.lilifamily.com +JPpro nginx proxy-ng + ↓ HTTPS + X-Proxy-Ng-Token +3c4g nginx origin.proxy.api.lilifamily.com + ↓ 127.0.0.1:8080 +sub2api container + ↓ +postgres / redis / mihomo containers +``` + +公开直连主站路径: + +```text +Client + ↓ HTTPS :443 catproxy.lilifamily.com +3c4g nginx + ↓ 127.0.0.1:8080 +Sub2API +``` + +部署安全边界: + +- `origin.proxy.api.lilifamily.com` 只允许 JPpro IP `151.242.164.72` 访问,并校验 `X-Proxy-Ng-Token`。 +- `catproxy.lilifamily.com` 不做 IP 白名单限制,是 3c4g 上可直接访问的主站域名和回滚入口。 +- `proxy.api.lilifamily.com` 不再解析到 3c4g,也不再由 3c4g nginx 站点提供服务。 +- Sub2API 容器端口只绑定 `127.0.0.1:8080`。 + +## 当前配置要点 + +- origin 域名:`origin.proxy.api.lilifamily.com` +- catproxy 域名:`catproxy.lilifamily.com` +- TLS:Certbot 管理 nginx 证书。 +- origin 证书路径:`/etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem` +- catproxy 证书路径:`/etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem` +- token 配置:`/etc/nginx/proxy-ng/token.conf`,权限 `600 root root` +- token map 配置:`/etc/nginx/conf.d/proxy-ng-token-map.conf`,本地副本已脱敏 +- 远程 `.env`:`PUBLIC_DOMAIN=proxy.api.lilifamily.com` +- URL allowlist 当前关闭:`SECURITY_URL_ALLOWLIST_ENABLED=false` + +## DNS state after proxy-ng hardening + +| Name | A records | Role | +|---|---|---| +| `proxy.api.lilifamily.com` | `151.242.164.72` | Public entry | +| `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node | +| `catproxy.lilifamily.com` | `216.45.59.243` | Public direct main-site and rollback entry | +| `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only | + +## 运行检查 + +```bash +cd /opt/sub2api +docker compose ps +curl -sS http://127.0.0.1:8080/health +nginx -t +systemctl is-active nginx +curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health +curl --noproxy '*' -i https://catproxy.lilifamily.com/health +``` + +期望结果: + +- `sub2api`、`sub2api-postgres`、`sub2api-redis` 为 healthy。 +- 本机 `/health` 返回 `{"status":"ok"}`。 +- 非 JPpro 来源访问 `origin.proxy.api.lilifamily.com/health` 返回 `403`。 +- JPpro 带 `X-Proxy-Ng-Token` 访问 origin 返回 `200`。 +- 公网访问 `catproxy.lilifamily.com/health` 返回 `200`。 +- 直连 `216.45.59.243:8080` 不对公网开放。 + +## 操作记录 + +### 2026-05-26 迁移目标盘点 + +- 通过 Tabby profile `个人-US-racknerd-0526-3c4g` 只读核对。 +- 确认公网 IP:`216.45.59.243`。 +- 确认系统:Debian GNU/Linux 13 (trixie)。 +- 确认尚未部署 `/opt/sub2api`,尚未安装 Docker。 + +### 2026-05-26 部署与迁移完成 + +- 安装 Docker CE、Docker Compose plugin、nginx、certbot、`python3-certbot-nginx`、`rsync`。 +- 从旧机迁移包恢复 `/opt/sub2api`,迁移包 SHA256 在新机按 basename 重新校验通过。 +- 使用 `pg_restore -U sub2api -d sub2api --clean --if-exists` 导入 PostgreSQL dump。 +- 业务数据计数核对通过:`users=1`、`api_keys=3`、`accounts=133`。 +- 启动 Compose 服务,`sub2api`、`sub2api-postgres`、`sub2api-redis` 均为 healthy,`sub2api-mihomo` 为 running。 +- 配置 nginx 站点 `/etc/nginx/sites-available/sub2api.conf`,启用 `/etc/nginx/sites-enabled/sub2api.conf`。 +- Certbot 已为 `proxy.api.lilifamily.com` 签发并部署 HTTPS 证书。 +- 公网验证通过:`https://proxy.api.lilifamily.com/health` 返回 `{"status":"ok"}`。 +- API 路由验证通过:未授权 `POST https://proxy.api.lilifamily.com/v1/responses` 返回 `401 API_KEY_REQUIRED`。 +- 旧机未停止,仍作为短期回滚来源。 + +### 2026-05-26 proxy-ng origin hardening backup + +- Backup path: `/root/sub2api-hardening-backup-20260526-033010` +- Includes nginx config, docker-compose.yml, `.env`, and PostgreSQL dump. +- Secret token exists only on remote root-readable nginx config and is not stored in Git. + +### 2026-05-26 origin/catproxy hardening + +- 为 `origin.proxy.api.lilifamily.com` 和 `catproxy.lilifamily.com` 签发 Let's Encrypt 证书,到期时间均为 `2026-08-24`。 +- 新增 `/etc/nginx/sites-available/origin-sub2api.conf`,只允许 `151.242.164.72` 并校验 `X-Proxy-Ng-Token`。 +- 新增 `/etc/nginx/sites-available/catproxy-sub2api.conf`;初始版本限制管理来源,后续已按当前决策改为公开直连主站入口。 +- 新增 `/etc/nginx/sites-available/00-default-deny.conf`,未知 Host 默认返回 `444`。 +- 为 nginx 增加 `map_hash_bucket_size 128`,修复长 hostname map 导致的 `nginx -t` 失败。 +- 旧 `proxy.api.lilifamily.com` 站点已从 `sites-enabled` 禁用,备份路径:`/root/sub2api-disable-public-proxy-backup-20260526-040529`。 +- 验证结果:非白名单访问 origin 返回 `403`,JPpro 带 token 访问 origin 返回 `200`,公网主入口 `proxy.api.lilifamily.com` 返回 JPpro 节点响应头。 +- 配置 logrotate:`/etc/logrotate.d/nginx-proxy-ng`,dry-run 无语法错误。 + +### 2026-05-26 catproxy 访问边界调整 + +- 按当前决策,`catproxy.lilifamily.com` 不再限制访问来源,作为 3c4g 公开直连主站域名和回滚入口。 +- 远程备份:`/etc/nginx/sites-available/catproxy-sub2api.conf.bak-20260526043650`。 +- 从 `/etc/nginx/sites-available/catproxy-sub2api.conf` 删除 `allow 106.37.220.142` 和 `deny all`。 +- 保留 `origin.proxy.api.lilifamily.com` 的来源 IP 白名单和 `X-Proxy-Ng-Token` 校验。 +- 验证结果:`nginx -t` 成功,`systemctl reload nginx` 成功,`catproxy.lilifamily.com/health` 返回 `200`,非白名单访问 `origin.proxy.api.lilifamily.com/health` 返回 `403`。 + +### 2026-05-26 主站限速核对 + +- `catproxy.lilifamily.com` 当前无 nginx `limit_req` / `limit_conn`,主站直连不按 IP 限速。 +- `origin.proxy.api.lilifamily.com` 当前只做 JPpro IP 白名单和 `X-Proxy-Ng-Token` 校验,不做 nginx 速率限制。 +- Sub2API 业务请求限速维度是用户、用户+分组、API Key 窗口额度;不是按 IP 限速。 +- 生产库非敏感快照:当前用户全局 `rpm_limit=0`;部分分组配置 `20/30/40/100 RPM`;API Key `briqt` 配置 `5h=300`、`1d=500`,API Key `free` 配置 `1d=50`。 +- 本机测速与限速维度详见 `docs/memorys/2026-05-26-主站测速与限速核对.md`。 diff --git a/ops/remote/us-racknerd-0526-sub2api/backups/.gitkeep b/ops/remote/us-racknerd-0526-sub2api/backups/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/backups/.gitkeep @@ -0,0 +1 @@ + diff --git a/ops/remote/us-racknerd-0526-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS b/ops/remote/us-racknerd-0526-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS new file mode 100644 index 00000000..6f2dbea6 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/backups/migrate-to-racknerd-0526-20260525-205103.SHA256SUMS @@ -0,0 +1,3 @@ +4fa5f72c3bc3c68b95bd5846cee5114516435c1dcf1e7623c377a1911e8b6cfe /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/nginx-sub2api.conf +c3890902498a69f5bb4f2d2632a852bbba075744043bf88cc08b4116ee8c9599 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api-config-data-mihomo-redis.tar.gz +808f4790296bb13525c6cdc4ae3e372e083cd3f220c9bf6e41988d4886474b61 /opt/sub2api/backups/migrate-to-racknerd-0526-20260525-205103/sub2api.dump diff --git a/ops/remote/us-racknerd-0526-sub2api/current/docker-compose.yml b/ops/remote/us-racknerd-0526-sub2api/current/docker-compose.yml new file mode 100644 index 00000000..5bdb0e69 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/docker-compose.yml @@ -0,0 +1,152 @@ +x-logging: &default-logging + driver: json-file + options: + max-size: ${LOG_MAX_SIZE:-20m} + max-file: "${LOG_MAX_FILE:-5}" + +services: + sub2api: + image: ${SUB2API_IMAGE:-weishaw/sub2api:latest} + container_name: sub2api + restart: unless-stopped + logging: *default-logging + ulimits: + nofile: + soft: 100000 + hard: 100000 + ports: + - "${BIND_HOST:-127.0.0.1}:${SERVER_PORT:-8080}:8080" + volumes: + - ./data:/app/data + environment: + - AUTO_SETUP=true + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=8080 + - SERVER_MODE=${SERVER_MODE:-release} + - RUN_MODE=${RUN_MODE:-standard} + - TZ=${TZ:-Asia/Shanghai} + + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USER=${POSTGRES_USER:-sub2api} + - DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - DATABASE_DBNAME=${POSTGRES_DB:-sub2api} + - DATABASE_SSLMODE=disable + - DATABASE_MAX_OPEN_CONNS=${DATABASE_MAX_OPEN_CONNS:-20} + - DATABASE_MAX_IDLE_CONNS=${DATABASE_MAX_IDLE_CONNS:-5} + - DATABASE_CONN_MAX_LIFETIME_MINUTES=30 + - DATABASE_CONN_MAX_IDLE_TIME_MINUTES=5 + + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_DB=${REDIS_DB:-0} + - REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-128} + - REDIS_MIN_IDLE_CONNS=${REDIS_MIN_IDLE_CONNS:-4} + - REDIS_ENABLE_TLS=false + + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD is required} + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} + - JWT_EXPIRE_HOUR=24 + - TOTP_ENCRYPTION_KEY=${TOTP_ENCRYPTION_KEY:?TOTP_ENCRYPTION_KEY is required} + + - UPDATE_PROXY_URL=${UPDATE_PROXY_URL:-} + - SECURITY_URL_ALLOWLIST_ENABLED=${SECURITY_URL_ALLOWLIST_ENABLED:-false} + - SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP=${SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP:-false} + - SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS=${SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS:-false} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + mihomo: + condition: service_started + networks: + - sub2api-network + healthcheck: + test: ["CMD", "wget", "-q", "-T", "5", "-O", "/dev/null", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + postgres: + image: ${POSTGRES_IMAGE:-postgres:18-alpine} + container_name: sub2api-postgres + restart: unless-stopped + logging: *default-logging + shm_size: 64m + volumes: + - ./postgres_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER:-sub2api} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} + - POSTGRES_DB=${POSTGRES_DB:-sub2api} + - PGDATA=/var/lib/postgresql/data + - TZ=${TZ:-Asia/Shanghai} + command: + - postgres + - -c + - max_connections=30 + - -c + - shared_buffers=64MB + - -c + - effective_cache_size=256MB + - -c + - work_mem=2MB + - -c + - maintenance_work_mem=32MB + networks: + - sub2api-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + redis: + image: ${REDIS_IMAGE:-redis:8-alpine} + container_name: sub2api-redis + restart: unless-stopped + logging: *default-logging + volumes: + - ./redis_data:/data + command: > + sh -c 'redis-server + --save 60 1 + --appendonly yes + --appendfsync everysec + $${REDIS_PASSWORD:+--requirepass "$$REDIS_PASSWORD"} + $${REDIS_MAXMEMORY:+--maxmemory "$$REDIS_MAXMEMORY" --maxmemory-policy allkeys-lru}' + environment: + - TZ=${TZ:-Asia/Shanghai} + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_MAXMEMORY=${REDIS_MAXMEMORY:-128mb} + - REDISCLI_AUTH=${REDIS_PASSWORD:-} + networks: + - sub2api-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + mihomo: + image: ${MIHOMO_IMAGE:-metacubex/mihomo:latest} + container_name: sub2api-mihomo + restart: unless-stopped + logging: *default-logging + volumes: + - ./mihomo:/root/.config/mihomo + environment: + - TZ=${TZ:-Asia/Shanghai} + networks: + - sub2api-network + +networks: + sub2api-network: + name: sub2api-network + driver: bridge diff --git a/ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md b/ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md new file mode 100644 index 00000000..422c40d2 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md @@ -0,0 +1,14 @@ +# 3c4g firewall and origin boundary notes + +日期:2026-05-26 + +- `origin.proxy.api.lilifamily.com`: nginx allowlist only permits JPpro `151.242.164.72`. +- `origin.proxy.api.lilifamily.com`: `X-Proxy-Ng-Token` is required and redacted from Git. +- `catproxy.lilifamily.com`: public direct main-site entry on 3c4g; no nginx IP allowlist is applied. +- `proxy.api.lilifamily.com`: DNS points to JPpro `151.242.164.72`; 3c4g old `sub2api.conf` site is disabled from `sites-enabled`. +- `127.0.0.1:8080`: Sub2API local upstream only; Docker publishes it on loopback, not public interfaces. +- Default unknown Host behavior: nginx `00-default-deny.conf` returns `444` for unknown HTTP/HTTPS hosts. +- logrotate: `/etc/logrotate.d/nginx-proxy-ng` dry-run succeeded. +- Validation: local/public origin without token returned `403`; JPpro with token returned `200`; `catproxy.lilifamily.com/health` returned `200`; direct 3c4g `proxy.api` SNI no longer serves the application. + +Known limitation: origin enforcement is nginx-layer allow/deny, not a system firewall rule. If future origin is moved to a dedicated port, also add nftables/cloud firewall rules for proxy-ng node IPs only. diff --git a/ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf b/ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf new file mode 100644 index 00000000..ba96b34c --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf @@ -0,0 +1,28 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name catproxy.lilifamily.com; + + ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host proxy.api.lilifamily.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host proxy.api.lilifamily.com; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} diff --git a/ops/remote/us-racknerd-0526-sub2api/current/nginx-default-deny.conf b/ops/remote/us-racknerd-0526-sub2api/current/nginx-default-deny.conf new file mode 100644 index 00000000..8623f0a6 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/nginx-default-deny.conf @@ -0,0 +1,38 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/html; + default_type text/plain; + } + + location / { + return 444; + } +} + +server { + listen 80; + listen [::]:80; + server_name origin.proxy.api.lilifamily.com catproxy.lilifamily.com; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/html; + default_type text/plain; + } + + location / { + return 444; + } +} + +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + server_name _; + ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; + return 444; +} diff --git a/ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf b/ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf new file mode 100644 index 00000000..4d6aca59 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf @@ -0,0 +1,39 @@ +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name origin.proxy.api.lilifamily.com; + + ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + server_tokens off; + underscores_in_headers on; + client_max_body_size 100m; + + location / { + allow 151.242.164.72; + deny all; + + if ($proxy_ng_token_ok = 0) { + return 403; + } + + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $sub2api_host; + proxy_set_header X-Real-IP $http_x_real_ip; + proxy_set_header X-Forwarded-For $http_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Host $sub2api_host; + proxy_set_header X-Proxy-Ng-Token ""; + proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; + proxy_redirect https://origin.proxy.api.lilifamily.com/ https://$sub2api_host/; + proxy_redirect https://catproxy.lilifamily.com/ https://$sub2api_host/; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} diff --git a/ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf b/ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf new file mode 100644 index 00000000..f4c8ed75 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf @@ -0,0 +1,51 @@ +# HISTORICAL / DISABLED CONFIG. +# 2026-05-26 起,3c4g 不再作为 `proxy.api.lilifamily.com` 公开入口。 +# 远程 `/etc/nginx/sites-enabled/sub2api.conf` 已删除;此文件仅保留作迁移历史和紧急回滚参考。 + +server { + server_name proxy.api.lilifamily.com; + + server_tokens off; + # Sub2API/Codex clients use headers with underscores, so nginx must keep them. + underscores_in_headers on; + client_max_body_size 100m; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + proxy_pass http://127.0.0.1:8080; + 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; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } + + listen [::]:443 ssl ipv6only=on; # managed by Certbot + listen 443 ssl; # managed by Certbot + ssl_certificate /etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + +} +server { + if ($host = proxy.api.lilifamily.com) { + return 301 https://$host$request_uri; + } # managed by Certbot + + + listen 80; + listen [::]:80; + server_name proxy.api.lilifamily.com; + return 404; # managed by Certbot + + +} diff --git a/ops/remote/us-racknerd-0526-sub2api/current/proxy-ng-token-map.conf b/ops/remote/us-racknerd-0526-sub2api/current/proxy-ng-token-map.conf new file mode 100644 index 00000000..fc2a386e --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/current/proxy-ng-token-map.conf @@ -0,0 +1,10 @@ +map $http_x_proxy_ng_token $proxy_ng_token_ok { + default 0; + "REDACTED_SECRET" 1; +} + +map $http_x_forwarded_host $sub2api_host { + default "proxy.api.lilifamily.com"; + "proxy.api.lilifamily.com" "proxy.api.lilifamily.com"; + "jp.proxy.api.lilifamily.com" "jp.proxy.api.lilifamily.com"; +} diff --git a/ops/remote/us-racknerd-0526-sub2api/planned/nginx-sub2api.conf b/ops/remote/us-racknerd-0526-sub2api/planned/nginx-sub2api.conf new file mode 100644 index 00000000..a3ecfb5a --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/planned/nginx-sub2api.conf @@ -0,0 +1,28 @@ +server { + listen 80; + listen [::]:80; + server_name proxy.api.lilifamily.com; + + server_tokens off; + # Sub2API/Codex clients use headers with underscores, so nginx must keep them. + underscores_in_headers on; + client_max_body_size 100m; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "DENY" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + location / { + proxy_pass http://127.0.0.1:8080; + 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; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + } +} diff --git a/ops/remote/us-racknerd-0526-sub2api/secrets/README.md b/ops/remote/us-racknerd-0526-sub2api/secrets/README.md new file mode 100644 index 00000000..1596f557 --- /dev/null +++ b/ops/remote/us-racknerd-0526-sub2api/secrets/README.md @@ -0,0 +1,8 @@ +# Secrets Directory + +此目录用于本机保存 `个人-US-racknerd-0526-3c4g` 上 Sub2API 运行所需密钥备份。 + +- 目标远程 `.env` 路径:`/opt/sub2api/.env` +- 目标远程 Mihomo 配置路径:`/opt/sub2api/mihomo/config.yaml` +- 这些文件包含数据库密码、JWT 密钥、TOTP 密钥、订阅地址或代理凭据,默认被上级 `.gitignore` 排除。 +- 不要提交真实密钥,不要把密钥写入 README、issue 或 PR。 diff --git a/proxy-ng/README.md b/proxy-ng/README.md new file mode 100644 index 00000000..7ef4a839 --- /dev/null +++ b/proxy-ng/README.md @@ -0,0 +1,207 @@ +# proxy-ng 多节点中转方案 + +日期:2026-05-25 + +## 结论 + +方案可行:一个域名可以解析到多个 proxy-ng 节点,每个节点运行 nginx 反向代理,再把请求转发到同一个 sub2api 后端。sub2api 前面需要有一个后端入口网关,只允许这些 proxy-ng 节点访问,并校验共享密钥,避免后端公网端口被扫到后被直接利用。 + +最小推荐拓扑: + +```mermaid +flowchart LR + U[用户] --> D[同一域名
多 A/AAAA 记录] + D --> N1[proxy-ng 节点 A
nginx] + D --> N2[proxy-ng 节点 B
nginx] + D --> N3[proxy-ng 节点 C
nginx] + N1 --> B[后端入口网关
校验节点 IP + 共享密钥] + N2 --> B + N3 --> B + B --> S[sub2api :8080] + S --> P[(PostgreSQL)] + S --> R[(Redis)] +``` + +## 关键设计 + +### 1. 一个域名配置多个 IP + +可行,但有边界: + +- 普通 DNS 多 A 记录只能做粗粒度轮询,不能保证按节点健康状态实时摘除故障节点。 +- 如果需要健康检查、故障摘除、权重、地区路由,必须使用 Cloudflare Load Balancing、DNS 服务商健康检查,或自建调度。 +- 只配置多个 A 记录时,某个 proxy-ng 节点宕机后,仍可能有用户解析到坏 IP,直到 DNS 缓存过期。 + +推荐阶段: + +| 阶段 | DNS 方案 | 适用场景 | +|---|---|---| +| P0 | 多个 A 记录,TTL 60-300 秒 | 节点少、可接受短暂失败 | +| P1 | Cloudflare Load Balancing 或同类健康检查 | 要求自动故障摘除 | +| P2 | 按地区/运营商调度 | 有明确区域访问质量目标 | + +### 2. proxy-ng 节点职责 + +proxy-ng 节点只做入口和转发,不保存业务状态: + +- TLS 终止或透传到后端入口网关。 +- 限流、连接数控制、请求体大小限制。 +- 透传真实客户端 IP 链路。 +- 给后端入口网关注入节点共享密钥。 +- 健康检查自身 nginx 和后端可达性。 + +proxy-ng 节点不应该持有 sub2api 管理员账号、数据库密码、Redis 密码、业务 API Key,也不提供账号出口代理能力。 + +### 3. 后端入口网关职责 + +后端入口网关可以是后端服务器上的 Caddy 或 nginx,放在 sub2api 前面: + +- 只允许 proxy-ng 节点公网 IP 访问后端入口端口。 +- 校验 `X-Proxy-Ng-Token` 共享密钥。 +- 校验通过后删除密钥头,再转发给 sub2api。 +- 统一写入 `X-Forwarded-For` / `X-Real-IP`。 +- sub2api `server.trusted_proxies` 只信任后端入口网关或最后一跳代理。 + +密钥不是替代防火墙的安全边界。正确做法是“来源 IP 白名单 + 密钥校验 + TLS 回源”同时存在。 + +### 4. sub2api 内置节点管理 + +如果要求“在 sub2api 应用内管理 proxy-ng 节点和密钥”,也可行,但这是代码改造,不是单纯运维配置: + +- 新增 inbound proxy node 配置或数据模型,至少包含节点名、节点公网 IP/CIDR、密钥哈希、启停状态、备注。 +- 在 Gin 全局中间件里校验来源 IP 和 `X-Proxy-Ng-Token`,校验失败直接 403。 +- 支持双 token 轮换,避免所有节点同时停机换密钥。 +- 管理端需要提供节点增删改查和密钥重置入口。 + +第一版不建议直接改 sub2api 业务代码。先用后端入口网关完成 IP 白名单和密钥校验,复杂度更低,回滚也更干净。 + +### 5. 账号出口代理池独立 + +sub2api 的账号代理不属于 proxy-ng 节点职责。账号出口代理池应独立建设和管理: + +- proxy-ng 只优化用户到 sub2api 的入口线路。 +- sub2api 账号访问上游模型/API 时,继续使用现有代理管理里的 `proxy_id`。 +- 这些账号代理可以来自独立代理池、第三方代理、专用出口节点或其他合规网络资源。 +- proxy-ng 节点不暴露 `1080`、`7890`、`mixed`、`socks` 等出口代理端口。 +- 不在 proxy-ng 节点上部署 sing-box、Clash、Xray 等出口代理服务。 + +这个边界可以避免 proxy-ng 节点变成开放代理风险点,也避免入口中转带宽和账号出口流量互相抢资源。 + +## 会出问题的步骤 + +### 高风险 + +1. **只做多 A 记录但没有健康检查** + + 节点宕机时 DNS 仍可能返回坏 IP。用户表现为随机失败,不是全部失败。 + + 处理方式:P0 接受这个限制;P1 引入 Cloudflare Load Balancing 或 DNS 健康检查。 + +2. **后端公网端口只靠隐藏 IP** + + 后端 IP 可能被扫到。只要后端入口没有 IP 白名单和密钥校验,攻击者可以绕过 proxy-ng 直接打 sub2api。 + + 处理方式:后端防火墙只放行 proxy-ng 节点 IP;入口网关校验 `X-Proxy-Ng-Token`;sub2api 容器端口不直接暴露公网。 + +3. **真实客户端 IP 链路配置错误** + + 当前 sub2api 限流器使用 Gin `c.ClientIP()`,它依赖 `server.trusted_proxies`。配置错会导致所有用户被当成同一个节点 IP,登录/注册/API Key ACL 都可能异常。 + + 处理方式:只信任最后一跳后端入口网关,端到端验证登录限流和 API Key IP ACL。 + +4. **共享密钥轮换没有计划** + + 如果所有 proxy-ng 节点共用一个密钥,泄露后必须同步更新所有节点和后端入口网关。 + + 处理方式:后端入口支持至少两个有效 token,轮换时先加新 token,再滚动节点,最后删旧 token。 + +### 中风险 + +1. **回源证书和 TLS 模式混乱** + + proxy-ng 到后端如果走公网,HTTP 明文会暴露请求内容和 API Key。 + + 处理方式:回源使用 HTTPS。自签证书可以先落地,但生产建议固定 CA 或证书指纹,避免 `proxy_ssl_verify off` 长期存在。 + +2. **SSE / 流式请求被 nginx 缓冲** + + sub2api 代理模型 API 时存在流式响应。nginx 默认缓冲可能让流式变成攒包。 + + 处理方式:代理路径关闭 `proxy_buffering`,并验证 SSE 逐块到达。 + +3. **节点流量配额耗尽** + + 多节点会把每个请求复制成“用户到节点 + 节点到后端”两段公网流量。 + + 处理方式:上线前按日请求量、平均响应大小、流式比例估算月流量。 + +4. **多个节点证书签发和 DNS 切换顺序** + + 多 A 记录生效前,节点需要先能签发或加载证书。否则部分用户会命中 TLS 错误节点。 + + 处理方式:先部署节点,逐个本地 `--resolve` 验证,再加入 DNS。 + +5. **把入口中转节点误用为账号代理节点** + + 如果把 proxy-ng 节点同时拿来做账号出口代理,入口线路优化、账号代理池、上游访问风险会混在一起,后续很难排障和扩容。 + + 处理方式:proxy-ng 节点只开放 80/443 和必要回源链路;账号代理池另建,不纳入 `proxy-ng/`。 + +## 我可以完全完成的部分 + +在本仓库内可以完整交付: + +- `proxy-ng/` 目录结构、架构说明和部署模板。 +- proxy-ng nginx 配置模板。 +- proxy-ng `.env` 模板。 +- proxy-ng 节点安装脚本骨架。 +- 后端入口网关 nginx/Caddy 配置模板。 +- sub2api 配置变更说明:`server.trusted_proxies`、API Key ACL 可信转发开关。 +- 账号代理池和 proxy-ng 的边界说明。 +- 本地静态检查:nginx 配置语法只能在目标机器上最终验证,但模板结构可以先审阅。 + +如果授权连接目标服务器,还可以完成: + +- 在每台 proxy-ng VPS 安装 nginx、写入配置、启动服务。 +- 在后端服务器配置入口网关、密钥校验、防火墙白名单。 +- 端到端验证 `/health`、真实 IP、登录限流、SSE。 +- 输出实际节点清单和回滚命令。 + +## 必须人工配置或确认的部分 + +- 域名 DNS 托管平台账号操作,尤其是添加多 A 记录、切 Cloudflare、开健康检查或 Load Balancing。 +- proxy-ng 节点机器购买、系统初始化、SSH 权限授权。 +- 后端服务器防火墙/云安全组放行规则确认。 +- 生产密钥生成和保存位置确认。仓库只放模板,不提交真实 token。 +- 是否只允许中国大陆访问,以及是否包含香港、澳门、台湾。 +- 是否接受普通多 A 记录的故障窗口,还是一开始就购买/启用健康检查调度。 +- 入口中转流量规模和节点带宽预算。 +- 账号代理池使用哪个外部方案。该问题不由 proxy-ng 节点承担。 + +## 官方依据 + +- Cloudflare DNS 支持同名多个 A/AAAA 记录,但普通 DNS 记录不是健康检查调度: +- Cloudflare 同名多个 A/AAAA 记录可用于简单负载均衡,但 CNAME 与 A/AAAA 不能同名混用: +- Cloudflare Load Balancing 的 DNS 记录支持 A/AAAA/CNAME,并按池健康状态和策略分发: +- Cloudflare Health Checks / Load Balancing 可以探测源站健康,用于故障判断和摘除: + +## 推荐落地顺序 + +1. 先部署一个 proxy-ng 节点,后端入口网关开启 IP 白名单和密钥校验。 +2. 验证单节点链路:`/health`、真实 IP、登录限流、SSE、后端直连拒绝。 +3. 增加第二个 proxy-ng 节点,但先不加入正式 DNS,用 `--resolve` 指定节点 IP 验证入口。 +4. 把多个节点加入 DNS,多 A 记录先设低 TTL。 +5. 如果出现随机失败或节点经常波动,再升级为带健康检查的 DNS/负载均衡。 + +## 目录说明 + +```text +proxy-ng/ +├── README.md +├── env.example +├── nginx/ +│ ├── proxy-node.conf.template +│ └── backend-gateway.conf.template +└── scripts/ + └── install-proxy-node.sh +``` diff --git a/proxy-ng/env.example b/proxy-ng/env.example new file mode 100644 index 00000000..c55b42e0 --- /dev/null +++ b/proxy-ng/env.example @@ -0,0 +1,19 @@ +# proxy-ng 节点配置模板。复制到目标机器后填写真实值,不要把真实密钥提交到仓库。 + +PROXY_NG_DOMAIN=proxy.api.lilifamily.com +PROXY_NG_NODE_NAME=jppro-akile-0504-1c1g + +# 后端入口网关地址,建议使用 HTTPS。 +SUB2API_BACKEND_ORIGIN=https://origin.proxy.api.lilifamily.com +SUB2API_BACKEND_ORIGIN_HOST=origin.proxy.api.lilifamily.com + +# proxy-ng 节点发给后端入口网关的共享密钥只应保存在远程 root-only 文件。 +# 当前约定路径:/etc/nginx/proxy-ng/token.conf,权限 600 root root。 +PROXY_NG_TOKEN_FILE=/etc/nginx/proxy-ng/token.conf + +# 可选:中国大陆访问控制。需要 Cloudflare 或其他入口注入可信国家头时才启用。 +ALLOW_COUNTRY_CODES=CN + +# TLS 证书路径。可以由 certbot 写入,也可以由运维系统分发。 +TLS_CERT_PATH=/etc/letsencrypt/live/proxy.api.lilifamily.com/fullchain.pem +TLS_KEY_PATH=/etc/letsencrypt/live/proxy.api.lilifamily.com/privkey.pem diff --git a/proxy-ng/nginx/backend-gateway.conf.template b/proxy-ng/nginx/backend-gateway.conf.template new file mode 100644 index 00000000..c8cb5ed6 --- /dev/null +++ b/proxy-ng/nginx/backend-gateway.conf.template @@ -0,0 +1,53 @@ +# 后端入口网关 nginx 模板,部署在 sub2api 所在服务器。 +# 适合放到 Debian/Ubuntu 默认 nginx 的 /etc/nginx/conf.d/ 下; +# 该目录通常被包含在 http {} 作用域内,因此可以使用 map 指令。 +# 目标:后端公网入口即使被扫到,也必须同时满足来源 IP 白名单和共享密钥。 +# 真实 token 不写入模板产物;后端通过 root-only map 条目文件 include。 +# 文件内容示例: "real-token" 1; + +map $http_x_proxy_ng_token $proxy_ng_token_valid { + default 0; + include {{PROXY_NG_TOKEN_MAP_FILE}}; +} + +map $http_x_real_ip $proxy_ng_real_ip { + default $http_x_real_ip; + "" $remote_addr; +} + +server { + listen 8443 ssl http2; + server_name {{BACKEND_GATEWAY_SERVER_NAME}}; + + ssl_certificate {{BACKEND_TLS_CERT_PATH}}; + ssl_certificate_key {{BACKEND_TLS_KEY_PATH}}; + ssl_protocols TLSv1.2 TLSv1.3; + + # 目标机器上应同时配置系统防火墙或云安全组,只允许 proxy-ng 节点 IP 访问 8443。 + allow {{PROXY_NG_NODE_IP_1}}; + allow {{PROXY_NG_NODE_IP_2}}; + deny all; + + if ($proxy_ng_token_valid = 0) { + return 403; + } + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $proxy_ng_real_ip; + proxy_set_header X-Forwarded-For $http_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + + # 密钥只用于入口网关校验,不能继续传给 sub2api。 + proxy_set_header X-Proxy-Ng-Token ""; + proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; + + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } +} diff --git a/proxy-ng/nginx/proxy-node.conf.template b/proxy-ng/nginx/proxy-node.conf.template new file mode 100644 index 00000000..3987a58f --- /dev/null +++ b/proxy-ng/nginx/proxy-node.conf.template @@ -0,0 +1,117 @@ +# proxy-ng 节点 nginx 模板。 +# 部署时替换 {{DOMAIN}}、{{BACKEND_ORIGIN}}、{{BACKEND_ORIGIN_HOST}}、{{NODE_NAME}}、{{TLS_CERT_PATH}}、{{TLS_KEY_PATH}}。 +# 真实 token 不写入模板产物;节点通过 root-only 文件 include,例如 /etc/nginx/proxy-ng/token.conf。 + +worker_processes auto; +pid /run/nginx.pid; + +events { + worker_connections 1024; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + tcp_nopush on; + keepalive_timeout 30s; + keepalive_requests 100; + + client_header_timeout 10s; + client_body_timeout 30s; + client_max_body_size 256m; + + limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m; + limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=30r/s; + limit_req_zone $binary_remote_addr zone=req_auth:10m rate=10r/m; + + map $http_upgrade $connection_upgrade { + default upgrade; + "" close; + } + + server { + listen 80; + server_name {{DOMAIN}}; + return 301 https://$host$request_uri; + } + + server { + listen 443 ssl http2; + server_name {{DOMAIN}}; + + ssl_certificate {{TLS_CERT_PATH}}; + ssl_certificate_key {{TLS_KEY_PATH}}; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_cache shared:SSL:20m; + ssl_session_timeout 10m; + + include {{PROXY_NG_TOKEN_FILE}}; + + limit_conn conn_per_ip 20; + limit_req zone=req_per_ip burst=30 nodelay; + limit_req_status 429; + + add_header X-Proxy-Ng-Node "{{NODE_NAME}}" always; + add_header X-Content-Type-Options nosniff always; + + location = /health { + proxy_pass {{BACKEND_ORIGIN}}/health; + proxy_ssl_server_name on; + proxy_ssl_name {{BACKEND_ORIGIN_HOST}}; + proxy_set_header Host {{BACKEND_ORIGIN_HOST}}; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}"; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + access_log off; + } + + location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) { + limit_conn conn_per_ip 5; + limit_req zone=req_auth burst=3 nodelay; + limit_req_status 429; + + proxy_pass {{BACKEND_ORIGIN}}; + proxy_ssl_server_name on; + proxy_ssl_name {{BACKEND_ORIGIN_HOST}}; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host {{BACKEND_ORIGIN_HOST}}; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}"; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + proxy_send_timeout 60s; + proxy_buffering off; + } + + location / { + proxy_pass {{BACKEND_ORIGIN}}; + proxy_ssl_server_name on; + proxy_ssl_name {{BACKEND_ORIGIN_HOST}}; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Host {{BACKEND_ORIGIN_HOST}}; + proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; + proxy_set_header X-Proxy-Ng-Node "{{NODE_NAME}}"; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_buffering off; + proxy_request_buffering off; + } + } +} diff --git a/proxy-ng/scripts/install-proxy-node.sh b/proxy-ng/scripts/install-proxy-node.sh new file mode 100755 index 00000000..c81886cc --- /dev/null +++ b/proxy-ng/scripts/install-proxy-node.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 在 proxy-ng 节点上执行的安装骨架。 +# 该脚本只处理 nginx 基础安装和配置落位;DNS、证书签发、真实密钥分发仍由人工或运维系统完成。 + +if [[ "${EUID}" -ne 0 ]]; then + echo "must run as root" >&2 + exit 1 +fi + +ENV_FILE="${1:-/etc/proxy-ng/proxy-ng.env}" +if [[ ! -f "${ENV_FILE}" ]]; then + echo "env file not found: ${ENV_FILE}" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +source "${ENV_FILE}" + +required_vars=( + PROXY_NG_DOMAIN + PROXY_NG_NODE_NAME + SUB2API_BACKEND_ORIGIN + SUB2API_BACKEND_ORIGIN_HOST + PROXY_NG_TOKEN_FILE + TLS_CERT_PATH + TLS_KEY_PATH +) + +for name in "${required_vars[@]}"; do + if [[ -z "${!name:-}" ]]; then + echo "missing required env: ${name}" >&2 + exit 1 + fi +done + +apt-get update +apt-get install -y nginx openssl ca-certificates curl + +install -d -m 0755 /etc/proxy-ng +install -d -m 0700 /etc/nginx/proxy-ng + +if [[ ! -f "${PROXY_NG_TOKEN_FILE}" ]]; then + echo "token file not found: ${PROXY_NG_TOKEN_FILE}" >&2 + echo "create it as root-only, for example: set \\$proxy_ng_expected_token \"\";" >&2 + exit 1 +fi +chmod 600 "${PROXY_NG_TOKEN_FILE}" +chown root:root "${PROXY_NG_TOKEN_FILE}" + +TEMPLATE_PATH="${PROXY_NG_TEMPLATE_PATH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/nginx/proxy-node.conf.template}" +if [[ ! -f "${TEMPLATE_PATH}" ]]; then + echo "template not found: ${TEMPLATE_PATH}" >&2 + exit 1 +fi + +sed \ + -e "s|{{DOMAIN}}|${PROXY_NG_DOMAIN}|g" \ + -e "s|{{NODE_NAME}}|${PROXY_NG_NODE_NAME}|g" \ + -e "s|{{BACKEND_ORIGIN}}|${SUB2API_BACKEND_ORIGIN}|g" \ + -e "s|{{BACKEND_ORIGIN_HOST}}|${SUB2API_BACKEND_ORIGIN_HOST}|g" \ + -e "s|{{PROXY_NG_TOKEN_FILE}}|${PROXY_NG_TOKEN_FILE}|g" \ + -e "s|{{TLS_CERT_PATH}}|${TLS_CERT_PATH}|g" \ + -e "s|{{TLS_KEY_PATH}}|${TLS_KEY_PATH}|g" \ + "${TEMPLATE_PATH}" > /etc/nginx/nginx.conf + +nginx -t +systemctl enable --now nginx +systemctl reload nginx + +echo "proxy-ng node installed: ${PROXY_NG_NODE_NAME} (${PROXY_NG_DOMAIN})"