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

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.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:

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加固-审阅版.md only 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.