diff --git a/.claude/skills/iis-deploy-runbook/SKILL.md b/.claude/skills/iis-deploy-runbook/SKILL.md index b206f1e..0145c43 100644 --- a/.claude/skills/iis-deploy-runbook/SKILL.md +++ b/.claude/skills/iis-deploy-runbook/SKILL.md @@ -52,8 +52,8 @@ Internet **SPA web.config:** 2 FE có `URL Rewrite` rule: 1. HTTP → HTTPS redirect (bắt buộc, CORS whitelist chỉ https) -2. `/api/* → http://localhost:5443/api/*` (ARR reverse proxy) -3. `/hubs/* → http://localhost:5443/hubs/*` (SignalR) +2. `/api/* → http://127.0.0.1:5443/api/*` (ARR reverse proxy) +3. `/hubs/* → http://127.0.0.1:5443/hubs/*` (SignalR) 4. React Router fallback: `/*` → `/index.html` ## Quick commands @@ -90,8 +90,8 @@ Get-Content "C:\inetpub\apps\SolutionErp\Api\Logs\stdout_*.log" -Tail 30 ```powershell # Từ server -curl http://localhost:5443/health/live -curl http://localhost:5443/health/ready +curl http://127.0.0.1:5443/health/live +curl http://127.0.0.1:5443/health/ready # Từ ngoài curl https://api.huypham.vn/health/ready @@ -215,12 +215,12 @@ Xem gotcha #25 (docs/gotchas.md): ### HTTP 502 Bad Gateway (Admin/User → API) ``` -1. Check API up: curl http://localhost:5443/health/live +1. Check API up: curl http://127.0.0.1:5443/health/live - Down → restart API site + check stdout log 2. Check ARR enabled: IIS Manager > server level > Application Request Routing - "Enable proxy" phải tick 3. Check URL Rewrite rule fe web.config - - action type="Rewrite" url="http://localhost:5443/{R:0}" + - action type="Rewrite" url="http://127.0.0.1:5443/{R:0}" ``` ### SignalR 401 (WebSocket connect fail) @@ -296,7 +296,7 @@ scp -r .\fe-user\dist\* user@server:C:/inetpub/apps/SolutionErp/User/ # Trên server: Restart-WebAppPool -Name "SolutionErp-Api" -curl http://localhost:5443/health/ready +curl http://127.0.0.1:5443/health/ready ``` ## Backup + recovery @@ -322,11 +322,42 @@ Restore: xem `docs/guides/runbook.md`. - [ ] Gitea runner registration token (re-register service) - [ ] Admin default `Admin@123456` (đổi qua `/system/users` admin UI ngay sau deploy) +## Hardening — IPv4/IPv6 port hijack (G-084 VietReport incident) + +**Bài học từ VPS shared với VIETREPORT (2026-04-23):** VietReport team +deploy Next.js app chiếm port 3000 (0.0.0.0 bind) khiến Gitea bị đẩy +sang IPv6-only `[::]:3000` → IIS ARR `localhost:3000` resolve IPv4 +first → hit Next.js thay vì Gitea → `git.baocaogiaoduc.vn` trả homepage +VietReport. + +**3 rules áp dụng cho mọi service trên VPS shared:** + +1. **Reverse-proxy luôn dùng IP literal `127.0.0.1`**, không dùng `localhost` + - IIS ARR rewrite rule: `http://127.0.0.1:5443/{R:0}` ✓ + - Health check curl: `curl http://127.0.0.1:5443/health/live` ✓ + - Windows DNS resolver có thể cache IPv6 first → fail nếu service bind IPv4-only + +2. **Backend services bind loopback IPv4 explicit**, không `0.0.0.0` + - ASP.NET Core Kestrel (standalone): `UseUrls("http://127.0.0.1:5443")` hoặc env `ASPNETCORE_URLS=http://127.0.0.1:5443` + - IIS ASP.NET Core Module out-of-process: ANCM tự inject port ephemeral → KHÔNG cần manual (OK) + - Nếu deploy Kestrel standalone qua NSSM (tương lai): hardcode 127.0.0.1 trong appsettings.Production.json + +3. **Service dependency cho boot order** khi nhiều services cùng port family + - NSSM: `nssm set DependOnService ` + - Không cần cho SOLUTION_ERP hiện tại (API in IIS app pool, không NSSM service) + +**Hiện trạng SOLUTION_ERP — risk THẤP:** + +- API host trong IIS app pool out-of-process → ANCM quản lý port Kestrel ephemeral +- FE gọi trực tiếp `https://api.huypham.vn` qua CORS (không ARR proxy) +- Không có standalone Kestrel service trên port cố định +- **Nhưng** tương lai nếu thêm reverse proxy (fe-admin/user → `/api` → api.huypham.vn, hoặc /hubs for SignalR) → PHẢI dùng 127.0.0.1 không localhost + ## Related - `docs/guides/deployment-iis.md` — first-time setup - `docs/guides/runbook.md` — operations guide chi tiết - `docs/guides/cicd.md` — CI/CD pipeline -- `docs/gotchas.md` — #25 webSocket lock, #26 SignalR, #28 LibreOffice 404, #29 PS 5.1 UTF-16 +- `docs/gotchas.md` — #25 webSocket lock, #26 SignalR, #28 LibreOffice 404, #29 PS 5.1 UTF-16, **#33 IPv4/IPv6 port hijack (G-084)** - `scripts/deploy-iis.ps1` · `scripts/backup-sql.ps1` · `scripts/install-libreoffice.ps1` - `.gitea/workflows/deploy.yml` — CI/CD definition diff --git a/docs/gotchas.md b/docs/gotchas.md index 86639c6..da2120a 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -300,6 +300,39 @@ Write-Host "Setup IIS sites done" # thay vi "Hoan tat" ``` +## IIS / Windows Server (continued) + +### 33. IPv4/IPv6 port hijack trên VPS shared (G-084) + +**Triệu chứng:** `git.baocaogiaoduc.vn` trả về homepage Next.js của VietReport +thay vì Gitea UI. Headers lộ `x-nextjs-cache: HIT` + `X-Powered-By: ARR/3.0` +(request đã qua IIS ARR proxy rồi mới hit Next.js). + +**Root cause:** Next.js app (NSSM service) được deploy lên VPS shared với +Gitea, ignore env `PORT=3001 HOSTNAME=127.0.0.1` và bind `0.0.0.0:3000`. +Gitea bind `0.0.0.0:3000` trước đó bị Windows fallback xuống IPv6-only +`[::]:3000` (default `IPV6_V6ONLY=1`). IIS ARR rewrite `http://localhost:3000` +→ Windows DNS resolve IPv4 first → hit Next.js → leak homepage cho TẤT CẢ +subdomain có ARR proxy về `:3000`. + +**Fix (VietReport applied):** +1. Next.js NSSM env `PORT=3001 HOSTNAME=127.0.0.1` — bind loopback IPv4 +2. Gitea `HTTP_ADDR=127.0.0.1` — bind loopback IPv4 explicit +3. IIS `web.config` rewrite URL dùng `127.0.0.1` thay `localhost` +4. NSSM `DependOnService=gitea` — boot order tránh race + +**3 rules rút ra — áp dụng mọi service trên VPS shared:** +- Reverse-proxy luôn **IP literal `127.0.0.1`**, KHÔNG dùng `localhost` +- Backend services bind **loopback IPv4 explicit**, KHÔNG `0.0.0.0` +- Service dependency cho boot order khi nhiều service cùng port family + +**SOLUTION_ERP relevance:** +- API host trong IIS app pool out-of-process (ANCM tự quản lý port Kestrel ephemeral) → risk THẤP +- FE gọi trực tiếp `https://api.huypham.vn` (không ARR proxy) → risk THẤP +- **NHƯNG** nếu tương lai thêm ARR reverse proxy (fe-admin/user `/api` proxy) hoặc + deploy Kestrel standalone qua NSSM → PHẢI apply 3 rules trên +- Scripts + skill doc đã update `localhost` → `127.0.0.1` để đồng bộ + ## Checklist debug bug mới 1. Build pass không? → fail → check using + package version compat @@ -313,3 +346,4 @@ Write-Host "Setup IIS sites done" # thay vi "Hoan tat" 9. Nếu workflow 403 → check FE `workflow.nextPhases` sync từ BE pinned policy 10. Nếu SignalR 401 → dùng `accessTokenFactory` + BE OnMessageReceived hook (#26) 11. Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30) +12. Nếu subdomain trả sai content / bị hijack → check IPv4/IPv6 port collision trên VPS shared (#33)