Add deployment traces for the JPpro proxy node, RackNerd origin hosts, catproxy boundary changes, local speed checks, and Sub2API rate-limit behavior. Keep remote secrets excluded while tracking sanitized nginx, compose, plan, and operational memory documents.
1042 lines
31 KiB
Markdown
1042 lines
31 KiB
Markdown
# 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 -> <future proxy-ng node IPs only>
|
|
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` | `<proxy-ng node IPs only>` | 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-<timestamp>`
|
|
- 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 `<ADMIN_OR_TAILSCALE_IP>` 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 <ADMIN_OR_TAILSCALE_IP>;
|
|
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 '<ADMIN_OR_TAILSCALE_IP>' /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 = ^<HOST> - .* "(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: `<future proxy-ng node IPs only>`, `<ADMIN_OR_TAILSCALE_IP>`, 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.
|