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.
31 KiB
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.comis now a public direct main-site and rollback entry on 3c4g. Onlyorigin.proxy.api.lilifamily.comremains protected by proxy-ng source IP allowlist andX-Proxy-Ng-Token. This plan contains historical implementation steps; current runtime facts are recorded underops/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加固-审阅版.mdops/remote/jppro-sub2api/README.mdops/remote/us-racknerd-0526-sub2api/README.mddocs/memorys/2026-05-25-proxy-ng职责边界.md
File Structure
- Modify:
.gitignore- Responsibility: allow project planning docs under
docs/superpowers/plans/to be tracked.
- Responsibility: allow project planning docs under
- 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-1c1gis available for JPpro. - Tabby profile
个人-US-racknerd-0526-3c4gis available for 3c4g. - DNS control for
lilifamily.comis available. - A secret
PROXY_NG_TOKENis 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-ignoreandgit status -
Step 1: Verify docs plans are currently ignored or missing
Run:
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:
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:
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:
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:
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:
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:
proxy.api.lilifamily.com must not contain 216.45.59.243 after cutover.
- Step 2: Check current DNS from local machine
Run:
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:
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:
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:
## 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:
个人-US-racknerd-0526-3c4g
Expected: shell prompt on the 3c4g server.
- Step 2: Create a dated backup directory
Run on 3c4g:
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:
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:
-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:
/etc/nginx/proxy-ng/token.conf
Expected on JPpro:
-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:
### 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:
curl -sS http://127.0.0.1:8080/health
Expected:
{"status":"ok"}
- Step 2: Create nginx token map file on 3c4g
Run on 3c4g:
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:
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:
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:
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:
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:
curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health
Expected:
HTTP/1.1 403 Forbidden
or connection refused/closed if firewall blocks before nginx.
- Step 7: Verify catproxy restriction
Run from non-management network:
curl --noproxy '*' -i https://catproxy.lilifamily.com/health
Expected:
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:
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:
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:
个人-JPpro-akile-0504-1c1g
Expected: shell prompt on JPpro.
- Step 2: Backup current JPpro nginx config
Run on JPpro:
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:
test -f /etc/nginx/proxy-ng/token.conf
stat -c '%a %U %G %n' /etc/nginx/proxy-ng/token.conf
Expected:
600 root root /etc/nginx/proxy-ng/token.conf
- Step 4: Create proxy-ng nginx config
Run on JPpro:
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:
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:
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:
nginx -t
systemctl reload nginx
systemctl is-active nginx
Expected:
active
- Step 7: Verify JPpro node domain
Run from local:
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:
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:
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:
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:
ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf
Replace token references with:
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:
apt-get update
apt-get install -y fail2ban
systemctl enable --now fail2ban
systemctl is-active fail2ban
Expected:
active
- Step 2: Configure JPpro fail2ban nginx filters
Run on JPpro:
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:
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:
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:
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:
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:
# 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:
# 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:
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:
{"status":"ok"}
{"status":"ok"}
origin from local is 403 or blocked
- Step 2: Update DNS A records
In DNS provider:
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:
dig +short A proxy.api.lilifamily.com
Expected:
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:
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:
HTTP/1.1 200 OK
{"status":"ok"}
HTTP/1.1 401 Unauthorized
- Step 5: Verify no catproxy leakage
Run:
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:
### 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加固-审阅版.mdonly if execution discovers a plan correction -
Test: secret scan and git status
-
Step 1: Verify no secrets in docs
Run:
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:
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:
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:
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.