IMPL PLAN 03 — Client → API Edge (APIM Hardening)
เปิด control ที่ APIM ให้ครบ 4 ตัว: subscription key, rate limit, JWT pre-validation, payload size
อัปเดต: 2026-07-07
IMPL PLAN 03 — Client → API Edge (APIM Hardening)
วันที่: 07/07/2026 · สถานะ: ⏳ รอ verify · Priority: 🟠 Phase 2 อ้างอิง: DISCUSSION หมวด 3 · SG-20 จาก COMPLIANCE
สรุป
เปิด control ที่ APIM ให้ครบ 4 ตัว: subscription key enforcement · rate limit (ตอนนี้ comment out อยู่ — SG-20) · JWT pre-validation · payload size — ทั้งหมดเป็น policy XML ฝั่ง APIM ไม่แตะโค้ด backend
Sequence Diagram — Request ผ่านขอบที่ harden แล้ว
sequenceDiagram
autonumber
participant SPA as Client (Angular/Mobile)
participant CF as Cloudflare WAF
participant GW as App Gateway (WAF)
participant APIM as APIM
participant SG as Sentinel
participant BE as Backend
SPA->>CF: HTTPS request + Cookie + Ocp-Apim-Subscription-Key
CF->>GW: ผ่าน WAF rules (OWASP CRS)
GW->>APIM: forward
rect rgb(230, 240, 255)
Note over APIM: ลำดับ policy ใหม่ (inbound)
APIM->>APIM: ① validate subscription key → ไม่มี/ผิด = 401 ทันที
APIM->>APIM: ② check request size ≤ limit → เกิน = 413
APIM->>APIM: ③ rate-limit-by-key (per subscription/session) → เกิน = 429
APIM->>SG: ④ resolve-session (cookie → JWT) — flow เดิม
SG-->>APIM: JWT
APIM->>APIM: ⑤ validate-jwt (issuer/audience/expiry/signature ผ่าน OpenID config)<br/>→ malformed/expired = 401 ที่ขอบ ไม่ทะลุถึง backend
end
APIM->>BE: forward + JWT (strip Authorization ขาออกตามเดิม)
BE->>BE: validate JWT ซ้ำ (defense-in-depth — ไม่ยกเลิก)
BE-->>SPA: response
ขั้นตอน Implement
APIM policy อยู่ 2 ที่: git-tracked XML ใน
Backend_Iac(ถ้ามี) + ตัว resource บน Azure Portal — ทุกการแก้ต้อง commit XML เข้า git ด้วย ไม่แก้ portal อย่างเดียว
3.1 Subscription Key Enforcement
| # | จุด | Action |
|---|---|---|
| 1 | APIM Products | สร้าง product ต่อ consumer group: superapp-web, superapp-mobile, partner-{name} — แต่ละตัว require subscription |
| 2 | APIM APIs | ตั้ง subscriptionRequired: true ทุก API (ตอนนี้บาง API เปิด open) |
| 3 | Frontend | gateway-credentials.interceptor.ts (มีอยู่แล้วใน apps/shell/src/app/interceptors/) — เพิ่มแนบ Ocp-Apim-Subscription-Key จาก environment config |
| 4 | Key storage ฝั่ง FE | subscription key เป็น app identity ไม่ใช่ user secret (rate-limit/analytics per app) — ฝังใน env config ได้ แต่ต้องเข้าใจว่าใครเปิด devtools ก็เห็น → ห้ามใช้แทน authentication |
คำตอบเรื่อง key จากไหน (FE): subscription key ≠ ความลับระดับ credential — มันคือตัวระบุ app สำหรับ rate limit/quota/analytics · การป้องกันจริงยังคงเป็น JWT (user) + WAF · ถ้าต้องการผูกแน่นกว่านั้นสำหรับ partner ใช้ client certificate (mTLS ที่ APIM รองรับ)
3.2 เปิด Rate Limit กลับ (SG-20)
<!-- inbound policy — ตัวเลขเป็น starting point ปรับตาม load test -->
<choose>
<when condition="@(context.Request.Headers.ContainsKey("Authorization"))">
<rate-limit-by-key calls="200" renewal-period="60"
counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","anon").GetHashCode().ToString())" />
</when>
<otherwise>
<rate-limit-by-key calls="30" renewal-period="60"
counter-key="@(context.Request.IpAddress)" />
</otherwise>
</choose>
- uncomment/เพิ่ม policy ที่ global scope + ตรวจว่าครอบทุก route (per-API policy อาจ override — audit ทีละ API)
- ตัวเลข 200/30 ต่อ 60s = design intent เดิม — ยืนยันกับ load จริงก่อน production
3.3 JWT Pre-validation ที่ขอบ
<validate-jwt header-name="Authorization" failed-validation-httpcode="401"
failed-validation-error-message="Unauthorized">
<openid-config url="https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration" />
<audiences><audience>{api-audience}</audience></audiences>
<issuers><issuer>{issuer-ciam}</issuer><issuer>{issuer-entra}</issuer></issuers>
</validate-jwt>
- ใส่หลัง session façade (ตำแหน่ง ⑤ ใน diagram — validate JWT ที่ façade ออกมา)
- multi-issuer ต้องครบทั้ง CIAM + Entra ID (ตาม backend ปัจจุบัน)
- ⚠️ backend ยังต้อง validate เอง — APIM validation คือชั้นกรอง ไม่ใช่ชั้นแทน (Zero Trust B4)
3.4 Payload Size ที่ขอบ
<check-header name="Content-Length" failed-check-httpcode="413" ... />
<!-- หรือใช้ policy expression ตรวจ context.Request.Body ขนาด > 10MB → return-response 413 -->
ตั้งให้สอดคล้อง RequestSizeLimitMiddleware ฝั่ง backend (ขอบ ≥ backend เสมอ เพื่อไม่ให้ backend เจอ request ที่ขอบปล่อยแต่ตัวเองรับไม่ได้)
3.5 Verify WAF จริง (จาก ◇ HLD-asserted → ✅ verified)
- export Cloudflare WAF rules + App GW WAF policy → เก็บ snapshot ใน
Backend_Iac/docs/เป็นหลักฐานต่อผู้ตรวจ - ยืนยัน OWASP CRS version + mode (Prevention ไม่ใช่ Detection)
Verify
- เรียก API โดยไม่มี subscription key → 401 ที่ APIM (ไม่ถึง backend — ดูจาก App Insights dependency)
- ยิงเกิน rate → 429 + header
Retry-After - ส่ง JWT expired → 401 จาก APIM (backend log ไม่มี request เข้า)
- POST 20MB → 413 ที่ขอบ
- Regression: flow ปกติ login→ใช้งาน ไม่กระทบ (E2E เขียว)
⚠️ ข้อควรระวัง
- เปิด subscription enforcement ก่อนแจก key ให้ทุก client = client เดิมพังหมด — ลำดับ: สร้าง product/key → แจกทุก app (web/mobile/partner) → monitor ว่าทุก traffic มี key → ค่อยบังคับ
- rate limit ที่ APIM เป็น per-gateway-instance ใน consumption tier — Premium (ที่ใช้อยู่) แชร์ counter ได้ แต่ตรวจ config
counter-keyให้ per-client จริง ไม่ใช่ global (ซ้ำบทเรียน self-DoS ของ Consent) validate-jwtเพิ่ม latency ~ms + ต้อง reach OpenID config endpoint — ตั้ง cache (default มี) + fail behavior ชัดเจน- อย่าลืม SIT/UAT — policy ต้องเหมือน prod (ต่างแค่ค่า) มิฉะนั้นเทสไม่เจอปัญหาก่อนขึ้นจริง
🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)
- ลำดับ: rate limit (3.2) ก่อน subscription key (3.1) — rate limit แค่ uncomment policy ที่มีอยู่ ไม่ต้อง coordinate กับ client ใดๆ · subscription key ต้องสร้าง product/แจก key/monitor ก่อน enforce = งานหลายสัปดาห์
- D7 = per frontend app + per partner (superapp-web, superapp-mobile, partner-{name}) — per-tenant ละเอียดเกินไปสำหรับเป้าหมาย (identity ของ tenant อยู่ใน JWT อยู่แล้ว)
- enforce แบบ 2 จังหวะ: จังหวะแรก policy แค่ log request ที่ไม่มี key (observe mode) 1-2 สัปดาห์ → ดู traffic จริงว่าครบทุก client → ค่อย 401 — กัน client ที่ลืมนึกไม่ถึง (LIFF, WebView, partner เก่า)
- validate-jwt ที่ขอบทำท้ายสุด — มี fail-mode ซับซ้อนสุด (OpenID config unreachable, multi-issuer) และ backend validate อยู่แล้ว — คุณค่า marginal ต่ำกว่า 3 ตัวแรก
- 3.5 (verify WAF) ทำคู่ขนานได้เลย — เป็นงาน read-only ไม่เสี่ยง แต่ได้หลักฐานตอบผู้ตรวจทันที — ถ้าให้เลือกงานแรกของหมวดนี้ผมเลือกตัวนี้
- ทุก policy XML ต้องเข้า git ก่อนแตะ portal — ถ้าตอนนี้ policy อยู่แค่บน portal ให้ export เข้า
Backend_Iacเป็น step 0 — ไม่งั้นการ “uncomment” จะไม่มี history ว่าใครเปิด/ปิดอะไรเมื่อไหร่
📚 อ้างอิงมาตรฐาน (สำหรับใช้ในที่ประชุม):
| หลักการ | แหล่งอ้างอิง |
|---|---|
| Subscription key = app identity ไม่ใช่ authentication (official position) | Microsoft Learn — Subscriptions in Azure API Management |
validate-jwt policy reference | Microsoft Learn — Validate JWT policy |
rate-limit-by-key policy reference | Microsoft Learn — rate-limit-by-key policy |
| เหตุผลที่ต้องมี rate limit / payload limit ที่ขอบ | OWASP API4 — Unrestricted Resource Consumption |
| Defense-in-depth: ขอบ validate แล้ว backend ต้อง validate ซ้ำ (verify explicitly) | Microsoft — Zero Trust security model |