# proxy-ng Origin Hardening Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Deploy a non-Cloudflare-LB proxy-ng architecture where `proxy.api.lilifamily.com` only resolves to nginx proxy nodes, `origin.proxy.api.lilifamily.com` is restricted to proxy-ng回源, and `catproxy.lilifamily.com` remains a public direct main-site and rollback entry on 3c4g. **Architecture:** Users enter through `proxy.api.lilifamily.com`, which uses ordinary DNS A records pointing only at proxy-ng nodes. Each proxy-ng node terminates HTTPS, applies basic abuse protection, injects `X-Proxy-Ng-Token`, and sends traffic to `origin.proxy.api.lilifamily.com`. The origin host only accepts traffic from allowlisted proxy-ng IPs with a valid token; `catproxy.lilifamily.com` directly serves the same Sub2API主站 without an nginx IP allowlist. > 2026-05-26 decision update: `catproxy.lilifamily.com` is now a public direct main-site and rollback entry on 3c4g. Only `origin.proxy.api.lilifamily.com` remains protected by proxy-ng source IP allowlist and `X-Proxy-Ng-Token`. This plan contains historical implementation steps; current runtime facts are recorded under `ops/remote/us-racknerd-0526-sub2api/`. **Tech Stack:** Debian/Ubuntu, nginx, certbot or DNS-01 certificate issuance, system firewall, fail2ban, logrotate, Docker Compose Sub2API, project docs under `ops/remote/` and `docs/`. --- ## Source Spec - `docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md` - `ops/remote/jppro-sub2api/README.md` - `ops/remote/us-racknerd-0526-sub2api/README.md` - `docs/memorys/2026-05-25-proxy-ng职责边界.md` ## File Structure - Modify: `.gitignore` - Responsibility: allow project planning docs under `docs/superpowers/plans/` to be tracked. - Create: `docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md` - Responsibility: task-by-task execution plan. - Modify after remote execution: `ops/remote/jppro-sub2api/README.md` - Responsibility: record JPpro proxy-ng runtime changes, DNS role, security controls, and validation evidence. - Modify after remote execution: `ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf` - Responsibility: sanitized copy of JPpro active nginx site config. - Create after remote execution: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` - Responsibility: record fail2ban/logrotate/firewall decisions and test results. - Modify after remote execution: `ops/remote/us-racknerd-0526-sub2api/README.md` - Responsibility: record 3c4g origin/catproxy setup, allowlist, token boundary, and validation evidence. - Modify after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-sub2api.conf` - Responsibility: sanitized copy of active public/limited nginx config on 3c4g. - Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf` - Responsibility: sanitized copy of active origin nginx config on 3c4g. - Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf` - Responsibility: sanitized copy of active catproxy public direct config on 3c4g. - Create after remote execution: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` - Responsibility: record firewall policy and verification outputs without secrets. ## Execution Preconditions - Tabby profile `个人-JPpro-akile-0504-1c1g` is available for JPpro. - Tabby profile `个人-US-racknerd-0526-3c4g` is available for 3c4g. - DNS control for `lilifamily.com` is available. - A secret `PROXY_NG_TOKEN` is generated outside Git and saved only in root-readable remote files. - If using HTTP-01 certificates for restricted hosts, the operator accepts a temporary public issuance window; otherwise use DNS-01. ## Task 1: Track Plan Documents **Files:** - Modify: `.gitignore:130-140` - Test: `git check-ignore` and `git status` - [ ] **Step 1: Verify docs plans are currently ignored or missing** Run: ```bash git check-ignore -v docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md || true ``` Expected before this task is complete: either no file exists yet or the path is ignored by `docs/*`. - [ ] **Step 2: Add docs/superpowers/plans gitignore exceptions** Edit `.gitignore` so the docs section contains: ```gitignore docs/* !docs/PAYMENT.md !docs/PAYMENT_CN.md !docs/ADMIN_PAYMENT_INTEGRATION_API.md !docs/agent-specs/ !docs/agent-specs/*.md !docs/memorys/ !docs/memorys/*.md !docs/superpowers/ !docs/superpowers/plans/ !docs/superpowers/plans/*.md ``` - [ ] **Step 3: Verify plan path is trackable** Run: ```bash git check-ignore -v docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md || true git status --short --untracked-files=all | rg 'docs/superpowers/plans|.gitignore' ``` Expected: ```text M .gitignore ?? docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md ``` - [ ] **Step 4: Commit tracking change with the plan when implementation docs are ready** Run only after Task 8 documentation updates are complete: ```bash git add .gitignore docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md git commit -m "docs: add proxy-ng origin hardening plan" ``` Expected: commit succeeds. If other unrelated files are present, do not include them unless the user asks. ## Task 2: DNS Preflight and Cutover Plan **Files:** - Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` - Modify after verification: `ops/remote/jppro-sub2api/README.md` - Test: DNS lookups from local machine and from both servers - [ ] **Step 1: Record intended DNS state** Use this target state: ```text proxy.api.lilifamily.com A -> 151.242.164.72 proxy.api.lilifamily.com A -> jp.proxy.api.lilifamily.com A -> 151.242.164.72 catproxy.lilifamily.com A -> 216.45.59.243 origin.proxy.api.lilifamily.com A -> 216.45.59.243 ``` Important constraint: ```text proxy.api.lilifamily.com must not contain 216.45.59.243 after cutover. ``` - [ ] **Step 2: Check current DNS from local machine** Run: ```bash dig +short A proxy.api.lilifamily.com dig +short A jp.proxy.api.lilifamily.com dig +short A catproxy.lilifamily.com dig +short A origin.proxy.api.lilifamily.com ``` Expected before DNS changes may vary. Record actual output in the remote README update. - [ ] **Step 3: Lower TTL before cutover** In DNS provider UI/API, set TTL for these names to 60-300 seconds: ```text proxy.api.lilifamily.com jp.proxy.api.lilifamily.com catproxy.lilifamily.com origin.proxy.api.lilifamily.com ``` Expected: DNS provider accepts low TTL. - [ ] **Step 4: Do not cut public DNS until origin and JPpro tests pass** Hold this exact rule: ```text Do not move proxy.api.lilifamily.com away from the known-good path until: 1. catproxy health works from management path. 2. origin health works from JPpro with token. 3. JPpro serves proxy.api.lilifamily.com with a valid certificate. 4. /v1/responses unauthenticated returns 401 through JPpro. ``` - [ ] **Step 5: Document DNS state after final cutover** After DNS changes are made and verified, append this table to both remote READMEs: ```markdown ## DNS state after proxy-ng hardening | Name | A records | Role | |---|---|---| | `proxy.api.lilifamily.com` | `` | Public entry | | `jp.proxy.api.lilifamily.com` | `151.242.164.72` | JPpro proxy-ng node | | `catproxy.lilifamily.com` | `216.45.59.243` | Restricted direct management/rollback entry | | `origin.proxy.api.lilifamily.com` | `216.45.59.243` | Proxy-ng origin only | ``` ## Task 3: Prepare 3c4g Backups and Secret **Files:** - Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` - Create after verification: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` - Test: backup files exist on 3c4g - [ ] **Step 1: Open 3c4g Tabby session** Use Tabby profile: ```text 个人-US-racknerd-0526-3c4g ``` Expected: shell prompt on the 3c4g server. - [ ] **Step 2: Create a dated backup directory** Run on 3c4g: ```bash set -euo pipefail backup_dir="/root/sub2api-hardening-backup-$(date +%Y%m%d-%H%M%S)" mkdir -p "$backup_dir" cp -a /etc/nginx "$backup_dir/nginx" cp -a /opt/sub2api/docker-compose.yml "$backup_dir/docker-compose.yml" cp -a /opt/sub2api/.env "$backup_dir/sub2api.env" docker exec sub2api-postgres pg_dump -U sub2api -d sub2api > "$backup_dir/sub2api.sql" chmod -R go-rwx "$backup_dir" printf '%s\n' "$backup_dir" ``` Expected: prints backup path like `/root/sub2api-hardening-backup-20260526-170000`. - [ ] **Step 3: Generate or install proxy-ng token outside Git** Run on 3c4g: ```bash set -euo pipefail install -d -m 700 /etc/nginx/proxy-ng if [ ! -f /etc/nginx/proxy-ng/token.conf ]; then token="$(openssl rand -hex 32)" printf 'set $proxy_ng_expected_token "%s";\n' "$token" > /etc/nginx/proxy-ng/token.conf chmod 600 /etc/nginx/proxy-ng/token.conf fi ls -l /etc/nginx/proxy-ng/token.conf ``` Expected: ```text -rw------- ... /etc/nginx/proxy-ng/token.conf ``` Do not print the token into chat, logs, docs, or Git. - [ ] **Step 4: Copy token securely to JPpro** Use Tabby SFTP or an SSH copy method approved by the operator. Target path on JPpro: ```text /etc/nginx/proxy-ng/token.conf ``` Expected on JPpro: ```text -rw------- root root /etc/nginx/proxy-ng/token.conf ``` - [ ] **Step 5: Record backup path without secrets** Append to `ops/remote/us-racknerd-0526-sub2api/README.md`: ```markdown ### 2026-05-26 proxy-ng origin hardening backup - Backup path: `/root/sub2api-hardening-backup-` - Includes nginx config, docker-compose.yml, `.env`, and PostgreSQL dump. - Secret token exists only on remote root-readable nginx config and is not stored in Git. ``` ## Task 4: Configure 3c4g Origin and Restricted Catproxy **Files:** - Create after remote verification: `ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf` - Create after remote verification: `ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf` - Modify after remote verification: `ops/remote/us-racknerd-0526-sub2api/README.md` - Test: `nginx -t`, origin 403/200 behavior, catproxy restricted behavior - [ ] **Step 1: Confirm Sub2API local health on 3c4g** Run on 3c4g: ```bash curl -sS http://127.0.0.1:8080/health ``` Expected: ```json {"status":"ok"} ``` - [ ] **Step 2: Create nginx token map file on 3c4g** Run on 3c4g: ```bash set -euo pipefail cat > /etc/nginx/conf.d/proxy-ng-token-map.conf <<'EOF' include /etc/nginx/proxy-ng/token.conf; map $http_x_proxy_ng_token $proxy_ng_token_ok { default 0; $proxy_ng_expected_token 1; } map $http_x_forwarded_host $sub2api_host { default $http_x_forwarded_host; "" "proxy.api.lilifamily.com"; } EOF nginx -t ``` Expected: ```text nginx: configuration file /etc/nginx/nginx.conf test is successful ``` - [ ] **Step 3: Create origin site config** Run on 3c4g, replacing certificate paths if certbot uses different paths: ```bash set -euo pipefail cat > /etc/nginx/sites-available/origin-sub2api.conf <<'EOF' server { server_name origin.proxy.api.lilifamily.com; listen 443 ssl; listen [::]:443 ssl; ssl_certificate /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/origin.proxy.api.lilifamily.com/privkey.pem; server_tokens off; underscores_in_headers on; client_max_body_size 100m; location / { allow 151.242.164.72; deny all; if ($proxy_ng_token_ok = 0) { return 403; } proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $sub2api_host; proxy_set_header X-Real-IP $http_x_real_ip; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Host $sub2api_host; proxy_set_header X-Proxy-Ng-Token ""; proxy_set_header X-Proxy-Ng-Node $http_x_proxy_ng_node; proxy_redirect https://origin.proxy.api.lilifamily.com/ https://$sub2api_host/; proxy_redirect https://catproxy.lilifamily.com/ https://$sub2api_host/; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } } EOF ln -sf /etc/nginx/sites-available/origin-sub2api.conf /etc/nginx/sites-enabled/origin-sub2api.conf nginx -t ``` Expected: nginx test succeeds. - [ ] **Step 4: Create restricted catproxy site config** Run on 3c4g. Replace `` before enabling: ```bash set -euo pipefail cat > /etc/nginx/sites-available/catproxy-sub2api.conf <<'EOF' server { server_name catproxy.lilifamily.com; listen 443 ssl; listen [::]:443 ssl; ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; server_tokens off; underscores_in_headers on; client_max_body_size 100m; allow ; deny all; location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host proxy.api.lilifamily.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-Host proxy.api.lilifamily.com; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } } EOF test "$(grep -c '' /etc/nginx/sites-available/catproxy-sub2api.conf)" = "0" ln -sf /etc/nginx/sites-available/catproxy-sub2api.conf /etc/nginx/sites-enabled/catproxy-sub2api.conf nginx -t ``` Expected after replacing the IP: nginx test succeeds. If the placeholder remains, the `test` command fails and the task must stop. - [ ] **Step 5: Add unknown host default deny** Run on 3c4g: ```bash set -euo pipefail cat > /etc/nginx/sites-available/00-default-deny.conf <<'EOF' server { listen 80 default_server; listen [::]:80 default_server; server_name _; return 444; } server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name _; ssl_certificate /etc/letsencrypt/live/catproxy.lilifamily.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/catproxy.lilifamily.com/privkey.pem; return 444; } EOF ln -sf /etc/nginx/sites-available/00-default-deny.conf /etc/nginx/sites-enabled/00-default-deny.conf nginx -t systemctl reload nginx ``` Expected: nginx reload succeeds. - [ ] **Step 6: Verify origin denies non-token requests** Run from local or any non-JPpro network: ```bash curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health ``` Expected: ```text HTTP/1.1 403 Forbidden ``` or connection refused/closed if firewall blocks before nginx. - [ ] **Step 7: Verify catproxy restriction** Run from non-management network: ```bash curl --noproxy '*' -i https://catproxy.lilifamily.com/health ``` Expected: ```text HTTP/1.1 403 Forbidden ``` or `curl: (52) Empty reply from server` if nginx returns 444. - [ ] **Step 8: Save sanitized config copies locally** From local machine, use Tabby SFTP or copy commands to update: ```text ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf ``` Before saving, replace any secret token value with: ```text REDACTED_SECRET ``` Expected: files contain no real token. ## Task 5: Configure JPpro proxy-ng for Public Host and Origin **Files:** - Modify after remote verification: `ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf` - Create after remote verification: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` - Modify after remote verification: `ops/remote/jppro-sub2api/README.md` - Test: nginx syntax, public host health, unauthorized API returns 401 - [ ] **Step 1: Open JPpro Tabby session** Use Tabby profile: ```text 个人-JPpro-akile-0504-1c1g ``` Expected: shell prompt on JPpro. - [ ] **Step 2: Backup current JPpro nginx config** Run on JPpro: ```bash set -euo pipefail backup_dir="/root/proxy-ng-hardening-backup-$(date +%Y%m%d-%H%M%S)" mkdir -p "$backup_dir" cp -a /etc/nginx "$backup_dir/nginx" chmod -R go-rwx "$backup_dir" printf '%s\n' "$backup_dir" ``` Expected: prints backup path. - [ ] **Step 3: Confirm token file exists on JPpro** Run on JPpro: ```bash test -f /etc/nginx/proxy-ng/token.conf stat -c '%a %U %G %n' /etc/nginx/proxy-ng/token.conf ``` Expected: ```text 600 root root /etc/nginx/proxy-ng/token.conf ``` - [ ] **Step 4: Create proxy-ng nginx config** Run on JPpro: ```bash set -euo pipefail cat > /etc/nginx/conf.d/proxy-ng-map.conf <<'EOF' map $http_upgrade $connection_upgrade { default upgrade; "" close; } limit_conn_zone $binary_remote_addr zone=conn_per_ip:20m; limit_req_zone $binary_remote_addr zone=req_per_ip:20m rate=10r/s; limit_req_zone $binary_remote_addr zone=auth_per_ip:10m rate=10r/m; EOF cat > /etc/nginx/sites-available/jp-proxy-sub2api.conf <<'EOF' include /etc/nginx/proxy-ng/token.conf; server { listen 80; listen [::]:80; server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; server_name proxy.api.lilifamily.com jp.proxy.api.lilifamily.com; ssl_certificate /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/privkey.pem; server_tokens off; underscores_in_headers on; client_max_body_size 100m; client_header_timeout 10s; client_body_timeout 30s; send_timeout 30s; keepalive_timeout 20s; keepalive_requests 100; limit_conn conn_per_ip 50; limit_req zone=req_per_ip burst=30 nodelay; limit_req_status 429; add_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g" always; add_header X-Content-Type-Options nosniff always; location ~ /\.(git|svn|hg|env|aws|ssh) { return 444; } location ~* ^/(wp-admin|wp-login\.php|phpmyadmin|cgi-bin|vendor|boaform) { return 444; } location ~ ^/api/v1/auth/(login|login/2fa|register|send-verify-code|forgot-password|reset-password) { limit_req zone=auth_per_ip burst=3 nodelay; limit_conn conn_per_ip 10; proxy_pass https://origin.proxy.api.lilifamily.com; proxy_ssl_server_name on; proxy_ssl_name origin.proxy.api.lilifamily.com; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host origin.proxy.api.lilifamily.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; proxy_read_timeout 60s; proxy_send_timeout 60s; proxy_buffering off; proxy_request_buffering off; } location / { proxy_pass https://origin.proxy.api.lilifamily.com; proxy_ssl_server_name on; proxy_ssl_name origin.proxy.api.lilifamily.com; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host origin.proxy.api.lilifamily.com; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Proxy-Ng-Node "jppro-akile-0504-1c1g"; proxy_set_header X-Proxy-Ng-Token $proxy_ng_expected_token; proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_request_buffering off; } } EOF ln -sf /etc/nginx/sites-available/jp-proxy-sub2api.conf /etc/nginx/sites-enabled/jp-proxy-sub2api.conf nginx -t ``` Expected: nginx test succeeds. - [ ] **Step 5: Resolve certificate coverage before reload** Run on JPpro: ```bash openssl x509 -in /etc/letsencrypt/live/jp.proxy.api.lilifamily.com/fullchain.pem -noout -text | grep -E 'DNS:proxy.api.lilifamily.com|DNS:jp.proxy.api.lilifamily.com' || true ``` Expected: certificate includes both names. If it only includes `jp.proxy.api.lilifamily.com`, issue or install a certificate that covers: ```text proxy.api.lilifamily.com jp.proxy.api.lilifamily.com ``` Do not reload public DNS to JPpro until certificate coverage is correct. - [ ] **Step 6: Reload JPpro nginx** Run on JPpro: ```bash nginx -t systemctl reload nginx systemctl is-active nginx ``` Expected: ```text active ``` - [ ] **Step 7: Verify JPpro node domain** Run from local: ```bash curl --noproxy '*' -sS -i https://jp.proxy.api.lilifamily.com/health curl --noproxy '*' -i -X POST https://jp.proxy.api.lilifamily.com/v1/responses \ -H 'Content-Type: application/json' \ --data '{"model":"gpt-5.5","input":"ping"}' ``` Expected: ```text HTTP/1.1 200 OK {"status":"ok"} HTTP/1.1 401 Unauthorized ``` The 401 response body should include `API_KEY_REQUIRED`. - [ ] **Step 8: Verify public host against JPpro before DNS cutover** Run from local: ```bash curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -sS -i https://proxy.api.lilifamily.com/health curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i -X POST https://proxy.api.lilifamily.com/v1/responses \ -H 'Content-Type: application/json' \ --data '{"model":"gpt-5.5","input":"ping"}' ``` Expected: ```text HTTP/1.1 200 OK {"status":"ok"} HTTP/1.1 401 Unauthorized ``` - [ ] **Step 9: Save sanitized JPpro config locally** Copy `/etc/nginx/sites-available/jp-proxy-sub2api.conf` to: ```text ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf ``` Replace token references with: ```text REDACTED_SECRET ``` Expected: local config contains no real token. ## Task 6: Add Basic Abuse Protection Services **Files:** - Create after verification: `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md` - Create after verification: `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md` - Test: scan paths return 444/403, fail2ban active, logrotate config present - [ ] **Step 1: Install fail2ban on JPpro** Run on JPpro: ```bash apt-get update apt-get install -y fail2ban systemctl enable --now fail2ban systemctl is-active fail2ban ``` Expected: ```text active ``` - [ ] **Step 2: Configure JPpro fail2ban nginx filters** Run on JPpro: ```bash cat > /etc/fail2ban/filter.d/nginx-proxy-ng-scan.conf <<'EOF' [Definition] failregex = ^ - .* "(GET|POST|HEAD) /(\.env|\.git|wp-login\.php|wp-admin|phpmyadmin|cgi-bin|vendor|boaform).*" (403|404|444|429) ignoreregex = EOF cat > /etc/fail2ban/jail.d/nginx-proxy-ng.conf <<'EOF' [nginx-proxy-ng-scan] enabled = true filter = nginx-proxy-ng-scan logpath = /var/log/nginx/*access*.log maxretry = 10 findtime = 600 bantime = 3600 EOF systemctl restart fail2ban fail2ban-client status nginx-proxy-ng-scan ``` Expected: jail exists and is enabled. - [ ] **Step 3: Configure logrotate for nginx hardening logs** Run on JPpro and 3c4g: ```bash cat > /etc/logrotate.d/nginx-proxy-ng <<'EOF' /var/log/nginx/*.log { daily rotate 7 missingok notifempty compress delaycompress sharedscripts postrotate [ -s /run/nginx.pid ] && kill -USR1 "$(cat /run/nginx.pid)" endscript } EOF logrotate -d /etc/logrotate.d/nginx-proxy-ng ``` Expected: dry run shows no syntax error. - [ ] **Step 4: Verify scan path blocking on JPpro** Run from local: ```bash curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i https://proxy.api.lilifamily.com/.env || true curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -i https://proxy.api.lilifamily.com/wp-login.php || true ``` Expected: `444` as empty reply or a `403/404` if nginx build/environment does not surface 444 through curl. The request must not reach Sub2API. - [ ] **Step 5: Verify 3c4g direct exposure is closed** Run from local: ```bash curl --noproxy '*' -i http://216.45.59.243:8080/health || true curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health || true curl --noproxy '*' -i https://catproxy.lilifamily.com/health || true ``` Expected: ```text 8080: connection refused, timeout, or no route origin: 403 or connection refused/closed catproxy: 403/444 from non-management network ``` - [ ] **Step 6: Record hardening notes** Create `ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md`: ```markdown # JPpro proxy-ng hardening notes - Public host served: `proxy.api.lilifamily.com`, `jp.proxy.api.lilifamily.com` - Origin: `https://origin.proxy.api.lilifamily.com` - Token: stored on remote only, redacted from Git - Scan-path blocking: enabled for dotfiles, wp-login, phpmyadmin, cgi-bin, vendor, boaform - Rate limit: general `10r/s`, auth `10r/m` - fail2ban: `nginx-proxy-ng-scan` - logrotate: `/etc/logrotate.d/nginx-proxy-ng` - Validation date: `YYYY-MM-DD` ``` Create `ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md`: ```markdown # 3c4g firewall and origin boundary notes - `origin.proxy.api.lilifamily.com`: only proxy-ng node IPs allowed - `catproxy.lilifamily.com`: restricted to management/Tailscale or explicit rollback window - `127.0.0.1:8080`: Sub2API local upstream only - `X-Proxy-Ng-Token`: required at origin and redacted from Git - Validation date: `YYYY-MM-DD` ``` ## Task 7: Cut Public DNS to proxy-ng Nodes **Files:** - Modify after verification: `ops/remote/jppro-sub2api/README.md` - Modify after verification: `ops/remote/us-racknerd-0526-sub2api/README.md` - Test: DNS answer and end-to-end public host - [ ] **Step 1: Confirm final pre-cutover checks** Run from local: ```bash curl --noproxy '*' --resolve proxy.api.lilifamily.com:443:151.242.164.72 -sS https://proxy.api.lilifamily.com/health curl --noproxy '*' -sS https://jp.proxy.api.lilifamily.com/health curl --noproxy '*' -i https://origin.proxy.api.lilifamily.com/health || true ``` Expected: ```text {"status":"ok"} {"status":"ok"} origin from local is 403 or blocked ``` - [ ] **Step 2: Update DNS A records** In DNS provider: ```text Remove 216.45.59.243 from proxy.api.lilifamily.com. Add 151.242.164.72 to proxy.api.lilifamily.com. Keep TTL 60-300 seconds. ``` Expected: `proxy.api.lilifamily.com` no longer has a 3c4g A record. - [ ] **Step 3: Verify public DNS** Run: ```bash dig +short A proxy.api.lilifamily.com ``` Expected: ```text 151.242.164.72 ``` If multiple proxy-ng nodes exist, expected output includes only proxy-ng IPs and never `216.45.59.243`. - [ ] **Step 4: Verify public entry** Run: ```bash curl --noproxy '*' -sS -i https://proxy.api.lilifamily.com/health curl --noproxy '*' -i -X POST https://proxy.api.lilifamily.com/v1/responses \ -H 'Content-Type: application/json' \ --data '{"model":"gpt-5.5","input":"ping"}' ``` Expected: ```text HTTP/1.1 200 OK {"status":"ok"} HTTP/1.1 401 Unauthorized ``` - [ ] **Step 5: Verify no catproxy leakage** Run: ```bash curl --noproxy '*' -sS -D /tmp/proxy-headers.txt -o /tmp/proxy-body.txt https://proxy.api.lilifamily.com/ rg -n 'catproxy|origin\\.proxy' /tmp/proxy-headers.txt /tmp/proxy-body.txt || true ``` Expected: no `catproxy` or `origin.proxy` appears in headers or body. - [ ] **Step 6: Record DNS cutover** Append to both remote READMEs: ```markdown ### 2026-05-26 proxy.api.lilifamily.com DNS cutover - `proxy.api.lilifamily.com` now resolves only to proxy-ng node IPs. - `216.45.59.243` is no longer exposed through the public service hostname. - Public health check returned `200`. - Unauthenticated `/v1/responses` returned `401 API_KEY_REQUIRED`. ``` ## Task 8: Final Documentation and Repository Verification **Files:** - Modify: `ops/remote/jppro-sub2api/README.md` - Modify: `ops/remote/us-racknerd-0526-sub2api/README.md` - Modify or create: `ops/remote/**/current/*.conf` - Modify: `docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md` only if execution discovers a plan correction - Test: secret scan and git status - [ ] **Step 1: Verify no secrets in docs** Run: ```bash rg -n 'set \\$proxy_ng_expected_token|[A-Fa-f0-9]{64}|PROXY_NG_TOKEN|BEGIN PRIVATE KEY|PASSWORD=' \ docs ops proxy-ng .gitignore || true ``` Expected: no real token, private key, or password. Mentions of placeholder names are acceptable only when they are clearly examples. - [ ] **Step 2: Verify remote current config copies exist** Run: ```bash test -f ops/remote/jppro-sub2api/current/nginx-jp-proxy-sub2api.conf test -f ops/remote/jppro-sub2api/current/proxy-ng-hardening-notes.md test -f ops/remote/us-racknerd-0526-sub2api/current/nginx-origin-sub2api.conf test -f ops/remote/us-racknerd-0526-sub2api/current/nginx-catproxy-sub2api.conf test -f ops/remote/us-racknerd-0526-sub2api/current/firewall-notes.md ``` Expected: all tests exit 0. - [ ] **Step 3: Verify git sees intended docs** Run: ```bash git status --short --untracked-files=all | rg 'docs/superpowers/plans|docs/agent-specs|docs/memorys|ops/remote|proxy-ng|.gitignore' ``` Expected: changed docs and ops files are visible. If unrelated files appear, do not stage them unless they are part of this deployment record. - [ ] **Step 4: Commit deployment documentation only after user approval** Run after user explicitly asks to commit: ```bash git add .gitignore \ docs/superpowers/plans/2026-05-26-proxy-ng-origin-hardening.md \ docs/agent-specs/2026-05-26-proxy-ng-origin加固-审阅版.md \ docs/memorys/2026-05-25-proxy-ng职责边界.md \ ops/remote \ proxy-ng git commit -m "docs: record proxy-ng origin hardening deployment plan" ``` Expected: commit succeeds and includes only deployment docs/templates/records, not real secrets. ## Self-Review **Spec coverage:** The plan covers domain role changes, hiding 3c4g from `proxy.api.lilifamily.com`, origin allowlist/token enforcement, JPpro public host support, no Cloudflare LB assumptions, basic anti-abuse controls, DNS cutover, and project documentation. **Placeholder scan:** The plan contains explicit placeholders only where execution must insert environment-specific values that cannot be safely known in Git: ``, ``, and timestamp values. Each placeholder has a concrete validation step that fails if left unresolved in active remote config. **Type consistency:** Domain names, paths, and service roles are consistent: `proxy.api.lilifamily.com` is public and points only to proxy-ng nodes; `catproxy.lilifamily.com` is restricted direct/rollback; `origin.proxy.api.lilifamily.com` is proxy-ng-only origin; `jp.proxy.api.lilifamily.com` is the JPpro node.