sub2api/docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md
zizi 6814fa2a62 docs(ops): record proxy-ng deployment hardening
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.
2026-05-26 23:27:41 +08:00

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.