Defense Hardening Roadmap (Master Plan)
Full gap register + remediation ~13 service — แหล่งความจริงของ posture
อัปเดต: 2026-07-02
แผนแม่บทความมั่นคงปลอดภัย SuperApp — Defense Hardening Roadmap
เอกสารนี้คืออะไร: แผนตั้งรับ (defensive) สำหรับเสริมเกราะความปลอดภัยให้ทั้ง platform ของ SuperApp — ไม่ใช่ เอกสารเจาะระบบ ทุกข้อค้นพบมาจากการ “อ่าน” โค้ดและ config ของเราเองเพื่อหาว่าเกราะป้องกันจุดไหนยังขาด แล้ววางแผนเติม
เขียนสำหรับใคร: ทีมพัฒนา SuperApp ที่ยังไม่มีผู้เชี่ยวชาญด้าน security โดยเฉพาะ เอกสารนี้ทำหน้าที่เป็น “ไกด์ไลน์” ให้ทีมเห็นภาพรวม เข้าใจว่าอะไรสำคัญก่อน-หลัง และเอาไปนำเสนอ/ลงมือได้จริง
วันที่: 2 กรกฎาคม 2026 · สถานะ: Draft — ผ่าน fact-check review 2 รอบ (multi-agent + adversarial verify ต่อ finding; รอบ 1 apply 6 จุด, รอบ 2 apply 13 confirmed + 5 nits, 2 ก.ค. 2026) · รอทีม review
โครงเอกสาร: ใช้ 7 หมวดมาตรฐาน API-GL-001 (แนวปฏิบัติ API Security ของ ธสน./EXIM) เป็น “โครงกระดูก” (เพื่อพูดภาษาเดียวกับกรรมการธนาคาร/ผู้ตรวจ) และเรียงลำดับการลงมือตาม “ความเสี่ยงจริง” (risk-first) โดยมี Phase 0 เป็นการห้ามเลือดก่อน
วิธีอ่านเอกสารนี้
| ถ้าคุณคือ… | อ่านส่วนไหน |
|---|---|
| ผู้บริหาร / หัวหน้าทีม (มีเวลา 5 นาที) | §0 Executive Summary — “แก้ 5 อย่างนี้ก่อน” |
| Dev ที่จะลงมือแก้ | §3 Gap Register (จุดที่ต้องแก้ + หลักฐาน) + §5 Roadmap (ลำดับงาน) |
| คนที่ต้องตอบผู้ตรวจ/ธนาคาร | §2 API-GL-001 Mapping (เราทำครบหมวดไหนแล้ว ขาดหมวดไหน) |
| คนที่อยากเข้าใจภาพรวมสถาปัตยกรรม | §1 Architecture & Threat Model |
คำเตือนสำคัญก่อนลงมือ: อย่าเพิ่งแก้ตาม §3 ตรงๆ ทุกข้อทันที — บางการแก้ (โดยเฉพาะ FallbackPolicy) ถ้าทำผิดลำดับจะทำให้ระบบ ล่มทั้ง platform อ่าน “ลำดับที่ปลอดภัย” ใน §4.2 และ §5 ก่อนเสมอ
§0 — Executive Summary: “แก้ 5 อย่างนี้ก่อน”
ทีมทำงานด้าน security มาได้ระดับหนึ่งแล้ว (มี WAF, APIM, Sentinel gateway, encryption library, security headers, และ แก้ finding เก่าไปหลายตัวแล้ว — ดู §3.4) แต่ยังมีช่องว่างเชิงระบบที่ต้องปิดก่อน เรียงตามความเสี่ยงจริง:
| # | ช่องว่าง | ความเสี่ยง | หลักฐาน (ยืนยันจากโค้ดจริง 2 ก.ค. 2026) | หมวด API-GL-001 |
|---|---|---|---|---|
| 1 | Hardcoded credential ในซอร์สโค้ด/config | 🔴 Critical | Backend_NotificationService/.../DiagnosticsController.cs:144 — username="admin@exim.go.th", password="Exim@2026!" อยู่ใน source (อยู่ใน git history ด้วย) endpoint เป็น [AllowAnonymous] · เพิ่มเติม: ThirdParty appsettings.Production.json:9-11 hardcode ConsumerKey/ConsumerSecret ค่า prod (ดู §3.1 #1) | §7(3) Data Confidentiality |
| 2 | JWT dev-bypass อยู่ในโค้ดจริง 9 service + ฝังใน Template + WorkflowService (bypass คนละ class) | 🔴 Critical (dev cluster ยืนยันแล้ว) | แบบ A if (isDev): ValidateIssuer=false, ...SignatureValidator=passthrough อยู่ใน Filter/Log/Consent/Centralized/Codex/FileManagement/Orchestrator/ThirdParty/Task + Backend_Template → ทุก service ใหม่ติดมาด้วย (UserService ปิดแล้ว = if (false)) · แบบ B scheme-replacement: Backend_WorkflowService ลงทะเบียน BypassAuthHandler เป็น auth scheme เดียว ทุก env ที่ไม่ใช่ Production (dev/sit/uat) → return success + claim Role=Admin เสมอโดยไม่ดู token (แรงกว่าแบบ A). ยืนยันแล้วว่า dev cluster ตั้ง ASPNETCORE_ENVIRONMENT=Development จริง (Dockerfile.DEV bake + IaC dev manifests explicit) → แบบ A active บน dev แน่นอน; SIT/UAT ใช้ Sit/Uat (แบบ A ไม่ active แต่ WF แบบ B active) (ดู §3.1 #2) | §7(1) Authentication |
| 3 | ไม่มี FallbackPolicy + [AllowAnonymous] explicit บน write endpoint | 🔴 Critical | grep FallbackPolicy ทั้ง backend = 0 hit → (ก) เคส “ลืมติด [Authorize]”: endpoint หลุดโดยไม่ต้อง login (เช่น Consent POST /receipt — end-state ตัดสินตอน audit: JWT หรือ API-key ดู §3.1 #3) — FallbackPolicy ปิดเคสนี้ได้ · (ข) เคส [AllowAnonymous] ตั้งใจ ระดับ class ทั้งที่มี write op: ThirdParty ~7 controllers, Notification maintenance toggle — FallbackPolicy ไม่ช่วย (AllowAnonymous override เสมอ) ต้อง audit ถอด/เปลี่ยนเป็น [Authorize]/[RequireApiKey] รายตัว (ดู §3.1 #3) | §7(2) Authorization |
| 4 | ความลับใน URL ขาออก (external) + integrity control ถูกปิด | 🟠 High | ThirdParty EGovTokenClient.cs:86 ส่ง ?ConsumerSecret=... query string ไป DGA external API (api.egov.go.th) — query param เป็น contract ของ DGA แก้ฝ่ายเดียวไม่ได้ (ความเสี่ยงจริง = access log ฝั่ง DGA/TLS-inspecting proxy ไม่ใช่ log เรา ดู §3.1 #4); HMAC signature (message-level integrity) ถูก comment ปิด (SecurityExtensions.cs:29-39) | §7(3) Conf. + 3.2 Integrity |
| 5 | ไม่มี Rate Limiting หลาย service + pattern ต้นแบบมีกับดัก | 🟠 High | ThirdParty, FileManagement ไม่มี rate limiter ใน pipeline เลย (ConsentService เพิ่มแล้วแต่เป็น non-partitioned = self-DoS ต้อง migrate — ดู §3.1 #5) → เสี่ยง resource exhaustion / DoS และเป็นช่องว่าง compliance | §7(7.1),(7.2) |
ข้อความสำหรับนำเสนอทีม (1 ประโยค): “เรามีเครื่องมือป้องกันครบเกือบหมดแล้ว แต่มี ‘สวิตช์นิรภัย’ 3 ตัวที่ยังไม่ได้เปิดพร้อมกันทั้ง platform (บังคับ login ทุก endpoint, ปิด dev-bypass, เอา secret ออกจากโค้ด) — งานเฟสแรกคือเปิดสวิตช์เหล่านี้อย่างเป็นระบบ ผ่าน library กลางที่เรามีอยู่แล้ว”
สิ่งที่ทีมทำถูกแล้วและควรให้เครดิต (ดูรายละเอียด §3.4): Sentinel แก้ cookie cross-tenant + fail-closed + token-refresh lock แล้ว · ThirdParty เลิก log secret แบบ plaintext แล้ว · FileManagement ปิด upload anonymous แล้ว · ConsentService เพิ่ม rate limiter แล้ว (แต่ยัง non-partitioned — ต้อง migrate ดู §3.1 #5)
§1 — สถาปัตยกรรมปัจจุบัน, Threat Model และเส้นแบ่งความเชื่อถือ (Trust Boundaries)
ก่อนวางแผนป้องกัน ต้องเห็น “แผนที่” ก่อนว่า request เดินทางผ่านอะไรบ้าง และแต่ละชั้นรับผิดชอบอะไร สำหรับทีมที่ไม่มีพื้น security — คิดง่ายๆ ว่าแต่ละชั้นคือ “ประตูรักษาความปลอดภัย” หนึ่งด่าน ยิ่งมีหลายด่านและแต่ละด่านทำงานถูกหน้าที่ = ยิ่งปลอดภัย (หลักการนี้เรียก Defense-in-Depth)
1.1 เส้นทางเดินของ request (จากภาพ Azure Landing Zone)
┌─── ผู้ใช้ (Browser / Mobile) ───┐
│ │
[1] Cloudflare (WAF/CDN) static HTML (Angular 21)
│ ← Azure Blob Static Website
▼
[2] Azure App Gateway (L7, WAF, Zone-redundant)
│
[3] Azure Firewall Premium
│
[4] Azure API Management (APIM) Premium
│ ├─ session façade: cookie → resolve → JWT
│ ├─ rate limit, JWT cache 30s
│ └─ strip Authorization ขาออก
▼
[5] Sentinel Gateway (AKS) ── OIDC ──► [IdP] Entra ID / Entra External ID
│ ├─ login, session store (Redis + Postgres) (PIM, Conditional
│ └─ /internal/resolve-session (InternalOnly) Access, MFA)
▼
[6] Backend Services (AKS pods) ── ผ่าน Backend_Package middleware
│ UserService, Notification, ThirdParty, ThirdPartyFX, Consent,
│ Filter, Codex, Log, Orchestrator, Task, Workflow,
│ FileManagement, Centralized ...
▼
[7] Data tier: Key Vault · PostgreSQL Flexible · Redis Premium ·
Cosmos DB · Service Bus Premium · Storage · Event Grid
CI/CD (แยกเส้น): Dev → Azure DevOps Repos → Pipelines → ACR → ArgoCD/Flux → AKS
Network: Hub-Spoke (Hub Connectivity + Spoke PROD/DEV/Sandbox), VNet peering,
VPN S2S ไป EXIM On-Premise
หมายเหตุความถูกต้อง: ภาพ Landing Zone ที่ให้มาเป็น reference architecture ของ Azure ซึ่งวาด CI/CD เป็น GitHub/GitHub Actions/ArgoCD แต่ของจริงทีมใช้ Azure DevOps (repos + pipelines) — เวลาสื่อสารกับผู้ตรวจให้ยึดของจริง
1.2 เส้นแบ่งความเชื่อถือ (Trust Boundaries) — จุดที่ต้องระวังที่สุด
“Trust boundary” = เส้นที่ข้อมูล/คำสั่งข้ามจาก “โซนที่เราคุมไม่ได้” ไป “โซนที่เราคุมได้” ทุกเส้นแบบนี้ต้องมีการตรวจสอบ:
| เส้นแบ่ง | จาก → ไป | ใครเฝ้า | ความเสี่ยงหลัก |
|---|---|---|---|
| B1 ขอบนอกสุด | Internet → Cloudflare/App GW | WAF | OWASP Top 10 ระดับ HTTP (injection, XSS payload), DDoS |
| B2 identity | ผู้ใช้ → Entra (IdP) | Entra + Conditional Access + MFA | credential theft, phishing |
| B3 session→token | Cookie → APIM → JWT | APIM + Sentinel | session hijack, cookie ปน tenant, token forgery |
| B4 gateway→service | APIM/Sentinel → backend | Backend_Package + JWT validation ต่อ service | จุดนี้คือที่ช่องว่างส่วนใหญ่อยู่ — dev-bypass, ไม่มี FallbackPolicy |
| B5 service→data | service → KV/DB/Redis | Managed Identity / KV / RBAC | secret leakage, over-privileged access |
| B6 external | service → EXIM/e-Gov/3rd party | ThirdParty service | ความลับหลุด (C-3), integrity ของข้อมูลขาออก |
| B7 supply chain | Dev → pipeline → image → cluster | CI security scans (กำลังวางแผน) | dependency เสี่ยง, secret ใน git, image เสี่ยง |
ข้อสังเกตสำคัญ: ประตูด่านนอก (B1-B3) ทีมทำได้ดีแล้ว — WAF, APIM session façade, Sentinel แก้ปัญหา cookie/session ไปมากแล้ว แต่ช่องว่างส่วนใหญ่กระจุกที่ B4 (ระหว่าง gateway กับ service) ซึ่งเป็น “ด่านใน” ที่คนมักคิดว่า “อยู่หลัง gateway แล้วปลอดภัย” — ความคิดนี้อันตราย เพราะถ้าใครทะลุด่านนอกมาได้ (หรือเป็น insider / service ข้างเคียงที่ถูก compromise) ด่านในที่เปิด anonymous จะไม่เหลืออะไรกั้นเลย นี่คือหลักการ Zero Trust: อย่าเชื่อว่า “อยู่ในเครือข่ายแล้วปลอดภัย”
1.3 Threat Model แบบย่อ (ใครคือภัย, อะไรคือของมีค่า)
- ของมีค่า (Assets): ข้อมูล PII ลูกค้า (onboarding, KYC), consent records, token/credential, ข้อมูลธุรกรรมการเงิน
- ผู้ก่อภัยที่ต้องกันจริง (ตามบริบทธนาคาร):
- External attacker — ยิงจากอินเทอร์เน็ต (กัน B1-B4)
- Malicious/compromised insider หรือ service — อยู่ในเครือข่ายแล้ว เรียก service อื่นตรงๆ (กัน B4-B5 = ที่เราอ่อน)
- Supply-chain — dependency/image ที่มีช่องโหว่ (กัน B7 = กำลังวางแผน CI scan)
- สิ่งที่ API-GL-001 บังคับให้กัน (มาตรา 5): Unauthorized Access, Excessive Data Exposure, Service Disruption, Improper Lifecycle, Security Misconfiguration, Inadequate 3rd-party security
§2 — กรอบมาตรฐาน API-GL-001 (7 หมวด) เทียบสถานะปัจจุบัน
นี่คือ “โครงกระดูก” ของแผน — API-GL-001 มาตรา 7 กำหนด 7 หมวดที่ API ของธนาคารต้องทำ เราจับแต่ละหมวดมาเทียบว่า “มาตรฐานสั่งอะไร / เรามีอะไร / ขาดอะไร” ตารางนี้คือสิ่งที่ใช้ตอบผู้ตรวจได้ตรงๆ
เกณฑ์สถานะ: ✅ ครบ · 🟡 มีบางส่วน/ไม่ทั่วถึง · 🔴 ขาด/ปิดอยู่
หมวด 1 — Authentication (การยืนยันตัวตน) · สถานะ: 🟡
- มาตรฐานสั่ง: ยืนยันตัวตนทั้งฝั่ง client และ server, token แบบ short-lived, ส่งผ่านช่องปลอดภัย (TLS), มีกลไกตรวจ revoke/expire, บริหาร session ให้สั้นเท่าที่จำเป็น อ้างอิง OpenID Connect
- เรามี: OIDC ผ่าน Entra (PKCE S256 + state), access/refresh token เก็บ server-side ใน Redis (ไม่หลุดไป browser), JWT validation ต่อ service (
ValidateIssuer/Audience/Lifetime/SigningKey+ JWKS จาก.well-known), multi-issuer (CIAM + Entra ID) - ขาด/เสี่ยง:
- 🔴 dev-bypass อยู่ในโค้ดจริง 9 service + Template + WorkflowService (คนละ class) (ดู §3.1 #2) — แบบ
if(isDev)active เมื่อ env=Development (dev cluster ยืนยันแล้วว่าเป็น Development), WF แบบ scheme-replacement active ทุก non-prod (dev/sit/uat) → JWT validation ถูกปิด = หมวดนี้เป็นโมฆะทั้ง platform บน dev; ส่วน SIT/UAT เป็นโมฆะเฉพาะ WorkflowService (แบบ B) — อีก 9+ service validation ทำงานจริงที่ SIT/UAT - 🔴 ThirdPartyFXService ไม่มี authentication scheme เลยทั้ง repo (grep AddAuthentication/AddJwtBearer = 0 — คือไม่มี auth เลย ไม่ใช่แค่ bypass) →
POST counterRates+ SignalR hub/hubs/fx-rateเปิด anonymous ทุก env รวม Production (ดู §4.2 Step 0 / §5 Phase 1) — service นี้เดิมไม่อยู่ใน inventory ของแผน; ทำให้ข้อ “เรามี JWT validation ต่อ service” ข้างบนมีข้อยกเว้น - 🟡 session TTL default 720 ชม. (30 วัน) เมื่อ RememberMe — ยาวเกินหลัก “สั้นเท่าที่จำเป็น” ของมาตรฐาน ควรทบทวน
- 🟡 SameSite ของ cookie ไม่ตรงกันในเอกสาร (Strict vs Lax/None) — ต้อง confirm ค่าจริงต่อ env
- 🔴 dev-bypass อยู่ในโค้ดจริง 9 service + Template + WorkflowService (คนละ class) (ดู §3.1 #2) — แบบ
หมวด 2 — Authorization (การตรวจสอบสิทธิ์) · สถานะ: 🔴
- มาตรฐานสั่ง: ตรวจสิทธิ์ก่อนเสมอ, เข้าถึงได้เฉพาะสิ่งที่มีสิทธิ์, แยก read-only ออกจาก admin function, ยืนยันตัวตน+ให้สิทธิ์ทุกครั้งก่อนเข้าถึง, กัน replay attack สำหรับ token-based auth (strong token generation, short-lived token, nonce — guideline ข้อ 7(2)) อ้างอิง OAuth 2.0
- เรามี: policy
RequireAdmin/RequireUser, role claim, IDOR check บาง endpoint (Sentinel session), Backend_Package มีApiKeyAuthFilter, security-tier attribute system - ขาด/เสี่ยง:
- 🔴 ไม่มี FallbackPolicy เลยทั้ง platform → default = anonymous (ดู §3.1 #3) นี่คือการละเมิดหลัก “ตรวจสิทธิ์ก่อนเสมอ” โดยตรง
- 🟡 replay-attack protection — ปัจจุบันพึ่ง JWT lifetime validation + APIM strip Authorization ขาออกเท่านั้น ยังไม่มี nonce/one-time token สำหรับ high-risk operation → ควรประเมินว่าครอบคลุมพอตามมาตรฐาน 7(2) ไหม
- 🔴 endpoint เปิด anonymous แยกเป็น 2 เคส (ดู §3.1 #3): (ก) ไม่มี attribute /
[Authorize]ถูก comment — Consent (ไม่มี attr เลย), UserServiceAdminOnboardingController([Authorize]comment) → FallbackPolicy ปิดได้ · (ข)[AllowAnonymous]explicit ระดับ class — ThirdParty 7 controllers, Notification maintenance, UserServiceOnboardingController+ Role/Permission controllers → FallbackPolicy ไม่ช่วย ต้อง audit รายตัว - 🟡 บาง endpoint มี
[Authorize]แต่ไม่มี role check (เดิม RoleController — ต้อง re-verify) → Broken Function Level Authorization ตามมาตรฐาน
หมวด 3 — Data Confidentiality & Integrity · สถานะ: 🔴
- มาตรฐานสั่ง: เข้ารหัสข้อมูล sensitive, key length เหมาะสม, ห้ามฝัง secret ใน source/spec, ตรวจ integrity, ส่งผ่าน HTTPS/TLS 1.2+, ข้อมูล high-risk พิจารณา JWS/JWE, ทำลายข้อมูลต้องส่งหลักฐาน (PDPA)
- เรามี:
AesEncryptionService(AES-256-CBC, random IV,enc:prefix),[Encrypt]field attribute + MediatREncryptionBehavior, HMAC signature service, Key Vault ทุก service, TLS ทุกชั้น - ขาด/เสี่ยง:
- 🔴 hardcoded credential ใน source/config (Notification
DiagnosticsController.cs:144+ ThirdPartyappsettings.Production.json:9-11ConsumerKey/Secret ค่า prod, ดู §3.1 #1) — ละเมิดข้อ “ห้ามฝัง secret ใน source” ตรงๆ - 🟠 HMAC / message-level integrity ถูกปิด (ThirdParty, ดู §3.1 #4) — guideline ข้อ 3.2 Data Integrity (ใต้ §7(3)) สั่งให้ข้อมูล high-risk / external write มี message-level integrity (ยก JWS/WS-Security เป็นตัวอย่าง) แต่ signature validation ถูก comment ปิด → คือ gap 🟠 High ที่ §0 #4 ชี้ (ย้ายมาจากหมวด 4 ให้ตรง guideline จริง)
- 🟠 ConsumerSecret อยู่ใน URL query ขาออกไป DGA (ThirdParty) — เป็น external contract ของ DGA (query param บังคับ แก้ฝ่ายเดียวไม่ได้) ความเสี่ยงจริง = access log ฝั่ง DGA/TLS-inspecting proxy ไม่ใช่ log เรา (ดู §3.1 #4)
- 🟡 PII เก็บ plaintext ใน Redis (ตาม middleware plan) — ควรพิจารณาเข้ารหัส at-rest
- 🟡 กระบวนการ รับ/ตรวจหลักฐานการทำลายข้อมูลตาม PDPA (ข้อ 3.1 guideline V3.0) — ทิศทางที่ถูก: ฝ่ายที่ทำลายข้อมูล (third party/ผู้ใช้บริการ API) ส่งหลักฐานมาให้ ธสน. งานของเรา = process เรียกเก็บ/ตรวจรับ ไม่ใช่ส่งออก — ยังไม่พบว่ามี process รองรับ
- 🔴 hardcoded credential ใน source/config (Notification
หมวด 4 — Secure Communication · สถานะ: ✅ (มี gap เล็ก)
- มาตรฐานสั่ง: บังคับใช้ TLS ร่วมกัน, ติดตั้ง certificate ที่ถูกต้อง/เป็นปัจจุบัน, บังคับ strong cipher suites, เลิกใช้ cipher อ่อน (ส่วนเกณฑ์ตรวจ cert ละเอียด 4 ข้อ — CA น่าเชื่อถือ/ไม่หมดอายุ/ไม่ถูกเพิกถอน/ชื่อตรง domain — guideline สั่งไว้ในหมวด 1 ข้อ 1.2 Server authentication)
- เรามี: TLS ทุก hop, App GW/APIM terminate TLS, Nginx ingress internal
- ขาด/เสี่ยง: 🟡 ควร audit cipher suite ที่ App GW/APIM ว่าปิดตัวอ่อนครบ · (หมายเหตุ: HMAC/message-level integrity ย้ายไปประเมินใต้หมวด 3 ข้อ 3.2 Data Integrity ตาม guideline แล้ว — ไม่ใช่หมวดนี้ที่ครอบเฉพาะ TLS/certificate/cipher)
หมวด 5 — Secure Coding & Configuration · สถานะ: 🟡
- มาตรฐานสั่ง: least functionality, Data Validation (request+response), Security Headers (CORS/HTTP verbs/Content-Type), Error Message ไม่เปิดเผยเกินจำเป็น, กัน injection
- เรามี:
AdvancedSecurityHeadersMiddleware(nosniff, X-Frame DENY, CSP nonce-based, COOP/COEP, strip Server header),AddStrictCors(บังคับ allowlist, throw ถ้าว่าง), FluentValidation, EF parameterized (กัน SQLi), Global Exception Handler RFC 9457 - ขาด/เสี่ยง:
- 🟡
IdentityModelEventSource.ShowPII = trueในทุก env ยกเว้น Production (รวม SIT/UAT) — ยืนยันครบทุก service หลัก ผ่าน grep patternif(!IsProduction) ShowPII=true(มาจาก template inheritance) → auth error log อาจเปิดเผย PII/token — ละเมิดหลัก “Error Message ไม่เกินจำเป็น” + “Security Misconfiguration” - 🟡 Swagger UI เปิดใน non-prod ทุก env — ควร confirm ว่าปิด/ป้องกันบน SIT/UAT ที่เข้าถึงจากภายนอกได้
- 🟡 whoami endpoint (Notification) คืน raw JWT header/payload/claims แบบ anonymous — reconnaissance surface
- 🟡
หมวด 6 — Audit Log & Monitoring · สถานะ: 🟡
- มาตรฐานสั่ง: เก็บ audit log ระบุตัวบุคคล (ใคร ทำอะไร ที่ไหน เมื่อไหร่), เก็บ ≥ 90 วัน, timestamp ถูกต้อง, log แก้ไขไม่ได้ (tamper-proof), เฝ้าระวัง auth ล้มเหลว/replay/session attack/error, ระวัง sensitive data ใน log
- เรามี: Serilog structured logging,
[Audit(true)]MediatR behavior, correlation ID middleware, มีแผน OpenTelemetry observability (IaC docs) - ขาด/เสี่ยง:
- 🟡 ยังไม่พบนโยบาย retention ≥ 90 วัน + tamper-proof (WORM/immutable) ที่ชัดเจน
- 🟡 sensitive data ใน log: ShowPII=true (หมวด 5) + เดิมมี log email/OTP — ต้อง audit log sink ว่าไม่มี PII
- 🟡 ยังไม่มี alerting เชิง security (brute-force auth, anomaly) เป็นระบบ — มาตรฐานสั่งให้ “ตรวจจับ แจ้งเตือน ตอบสนอง”
หมวด 7 — Resource Sufficiency (Rate Limit / DDoS / Payload size) · สถานะ: 🟡
- มาตรฐานสั่ง: (7.1) Rate limit + Circuit breaker, (7.2) DDoS protection (WAF ระดับ network + app), (7.3) จำกัดขนาด request/response (pagination)
- เรามี: Cloudflare + App GW WAF (network DDoS), APIM rate limit (Bearer 200/60s, Anon 30/60s), Backend_Package มี
RateLimitPenaltyMiddleware(penalty-box — มี code แต่ยังไม่มี enforcement จริง:IsBannedไม่มี caller = ยังไม่นับเป็น control ใช้งานได้ ดู §3.1 #5) +RequestSizeLimitMiddleware, Polly circuit breaker (Sentinel session read), ConsentService เพิ่มAddRateLimiter(non-partitioned = self-DoS ดู §3.1 #5) - ขาด/เสี่ยง:
- 🟠 rate limiter ระดับ service ไม่ทั่วถึง — ThirdParty, FileManagement ไม่มีเลย (ดู §3.1 #5) แม้ APIM จะ limit ที่ขอบ แต่ traffic ภายใน (service-to-service / bypass APIM) ไม่ถูกจำกัด
- 🟡 ควรยืนยันว่าทุก list endpoint มี pagination (จำกัด response size) ตามมาตรฐาน 7.3
สรุปคะแนนหมวด: Auth 🟡 · Authz 🔴 · Data 🔴 · Comm ✅ · Coding 🟡 · Audit 🟡 · Resource 🟡 → หมวดที่ต้องเร่งที่สุดคือ Data Confidentiality (3), Authorization (2) และ Authentication (1) ซึ่งตรงกับ 3 อันดับแรกของ Executive Summary (Comm ✅ = TLS/cipher เท่านั้น; message-level integrity/HMAC ประเมินใต้ Data หมวด 3 = 🟠 ยังเปิดค้าง)
§3 — Verified Gap Register (เรียงตามความเสี่ยง)
ข้อที่มี file:line กำกับ = ยืนยันจากโค้ดจริง ณ 2 ก.ค. 2026 (ไม่ใช่ลอกรายงานสแกนเก่าเดือน พ.ค.); ส่วนข้อที่ mark ⚠️ / “ควร confirm” / “ต้อง verify แยก” = ยังไม่ยืนยัน ถือเป็นสมมติฐานที่ต้องตรวจก่อนใช้ตอบผู้ตรวจ — สแกนเก่าหลายข้อถูกแก้ไปแล้ว (ดู §3.4) การใช้ข้อมูลเก่าจะทำให้ทีมเสียเวลาแก้สิ่งที่แก้แล้ว หรือมองข้ามของใหม่
3.1 Critical / High — ต้องเข้า Phase 0-1
#1 🔴 Hardcoded credential ใน source/config (NotificationService + ThirdParty)
- หลักฐาน (Notification):
Backend_NotificationService/src/Notification01.API/Controllers/v1/DiagnosticsController.cs:144—var payload = new { username = "admin@exim.go.th", password = "Exim@2026!" };· endpointConnectivityEximToken(:138) และConnectivityEximHeroSlides(:155) เป็น[AllowAnonymous] - หลักฐาน (ThirdParty — พบระหว่าง verify #4):
Backend_ThirdPartyService/.../appsettings.Production.json:9-11—ConsumerKey/ConsumerSecretค่า prod hardcode ในไฟล์ config ที่ commit เข้า repo → รั่วผ่าน image/git เช่นกัน - ทำไมอันตราย: credential จริงอยู่ในซอร์ส/config = อยู่ใน git history ตลอดไป ใครมี read access repo (หรือ image) เห็นได้หมด แม้ลบตอนนี้ก็ยังอยู่ใน history
- แก้: ถอด credential ทั้ง 2 จุดออก → ย้ายไป Key Vault → rotate ทันที (ถือว่ารั่วแล้ว) → ลบ/ปิด endpoint diagnostics หรือใส่
[Authorize]+[RequireApiKey]→ พิจารณา purge git history (BFG/filter-repo) - หมวด: §7(3)
#2 🔴 JWT dev-bypass อยู่ในโค้ดจริง 2 แบบ + ฝังใน Template
มี bypass 2 class ที่ต่างกัน — Phase 1 ต้องจัดการทั้งคู่ (verify criterion เดิม grep if(isDev) จับได้เฉพาะแบบ A):
- แบบ A —
if (isDev)guard (9 service + Template) (isDev = environment.IsDevelopment()):Backend_FilterService/.../AuthenticationExtensions.cs:28,45·Backend_LogService/...:28,45·Backend_ConsentService/...:27,44·Backend_Centralized/...:36,84·Backend_CodexService/...:28,45·Backend_FileManagementService/...:28,45·Backend_OrchestratorService/...:27,44·Backend_ThirdPartyService/...:28,45·Backend_TaskService/...:23,40Backend_Template/src/SuperApp01.API/.../AuthenticationExtensions.cs:27,44← ต้นตอ: service ใหม่ scaffold จากนี้ = ติด bypass มาฟรี- bypass ตั้ง
ValidateIssuer=false, ValidateAudience=false, ValidateLifetime=false, ValidateIssuerSigningKey=false, RequireSignedTokens=false, SignatureValidator=(t,_)=>new JsonWebToken(t)= token JWT-shaped อะไรก็ผ่าน - ตัวเปรียบเทียบที่ถูกต้อง:
Backend_UserService/.../AuthenticationExtensions.cs:38ใช้if (false)= ปิด bypass สนิท → validation จริงทำงานแม้บน dev - NotificationService: ใช้ pattern ต่าง (ไม่พบ
if (isDev)bypass block เดียวกัน) — ⚠️ ต้อง verify แยกว่ากันจริงไหม
- แบบ B — scheme-replacement (
Backend_WorkflowService) — ร้ายแรงกว่าแบบ A และเดิมหายจาก inventory ทั้งฉบับ:Backend_WorkflowService/src/WorkFlow01.API/Program.cs:73-81— guard เดียวคือthrow ถ้า IsProduction()แล้วลงทะเบียนBypassAuthHandlerเป็น authentication scheme เดียว (แทน JwtBearer) ในทุก env ที่ไม่ใช่ ProductionBypassAuthHandler.cs:20-42returnAuthenticateResult.Successเสมอ พร้อม claimRole="Admin"(:31) โดยไม่ดู token เลย → ทุก request นิรนามกลายเป็น Admin (แรงกว่าแบบ A ที่อย่างน้อยยังต้องส่ง JWT-shaped token)- active บน ทุก non-prod env (dev/sit/uat/local) โดยไม่ขึ้นกับชื่อ
ASPNETCORE_ENVIRONMENT— hedge เรื่อง “env=Development” ใช้กับ WF ไม่ได้ - comment ในโค้ดเอง (
Program.cs:69-72) ระบุว่าเป็น interim รอ “final phase” port real multi-tenant JWT validation (AzureAd:AllowedTenantsแบบ Notification/UserService)
- ทำไมอันตราย (S5 — ยืนยันแล้ว ไม่ใช่คำถามเปิดอีกต่อไป): dev cluster ตั้ง
ASPNETCORE_ENVIRONMENT=Developmentจริง — ยืนยัน 2 ชั้นอิสระ: (1)Dockerfile.DEVทุก service bakeENV ASPNETCORE_ENVIRONMENT=Development; (2) IaC dev deployment manifests (Backend_Iac/src/yamls/dev/superapp/deployments/*.yaml) ตั้งASPNETCORE_ENVIRONMENT: "Development"explicit ครบ (ยกเว้น sentinel-gateway ที่ไม่ตั้ง แต่ image ENV ชนะอยู่ดี) → แบบ A active บน dev cluster แน่นอน = ใครก็ปลอม token เรียก API บน dev ได้ (dev cluster มีข้อมูล/DB จริงที่ใช้ร่วมกัน). SIT/UAT ใช้ชื่อ envSit/Uat→IsDevelopment()=false→ แบบ A ไม่ active ที่นั่น แต่ WF แบบ B ยัง active (bypass ทุก non-Production) - แก้: แบบ A → เปลี่ยนทุก
if (isDev)→if (false)(ตาม UserService) หรือ ดีกว่า: helper กลางใน Backend_Package ผูก bypass กับ flag ที่ ไม่มีวันเป็น true บน cluster ใดๆ (เช่น localDebugbuild) → แก้ Template ก่อน. แบบ B (WF) → portAddAppAuthenticationจริง (JWT validation +AllowedTenants) แทนBypassAuthHandlerตามที่ comment ในโค้ดระบุ — ห้ามอาศัย verify แบบgrep if(isDev)เพราะจับ WF ไม่ได้ (ดู Phase 1) - หมวด: §7(1)
#3 🔴 การเข้าถึงแบบ anonymous — 2 ปัญหาที่ต้องแยกแก้ (FallbackPolicy แก้ได้แค่ครึ่งเดียว)
- ปัญหา (ก) — ไม่มี FallbackPolicy → “ลืมติด
[Authorize]” กลายเป็น anonymous เงียบๆ:- หลักฐาน: grep
FallbackPolicy/DefaultPolicy =(authorization) ทั้งAzureDevOps_SuperAPP/**/src= 0 hit (ที่เจอคือTimeoutPolicyExtensions.cs:29ซึ่งเป็น RequestTimeout คนละเรื่อง) · ทุกAddAuthorization(...)นิยามแค่ named policyRequireAdmin/RequireUserไม่ได้ setoptions.FallbackPolicy - ตัวอย่างที่หลุดเพราะ “ลืมติด”: ConsentService
ConsentControllerทั้ง class ไม่มี[Authorize]→POST /receipt(write),GET /user?identifier=(อ่าน consent ของ user ใดก็ได้) เปิด anonymous —ConsentController.cs:14-16· UserServiceAdminOnboardingController[Authorize]ถูก comment ไว้ (dev) — หมายเหตุ:OnboardingControllerเป็น[AllowAnonymous]explicit → อยู่เคส (ข) ไม่ใช่ (ก) - ⚠️ end-state ของ Consent
POST /receiptต้องตัดสินตอน audit (อย่าปล่อยกำกวม): ถ้า user-facing → ปล่อยให้ FallbackPolicy บังคับ JWT; ถ้า service-to-service →[AllowAnonymous]+[RequireApiKey](ต้อง registerApiKeyAuthFilterก่อน มิฉะนั้น attribute เงียบ — ดู §4.2 Step 2) — สองทางนี้ mutually exclusive เลือกทางเดียว - FallbackPolicy ปิดปัญหา (ก) ได้ (default-deny จับ endpoint ที่ลืมติด)
- หลักฐาน: grep
- ปัญหา (ข) —
[AllowAnonymous]explicit ระดับ class บน write endpoint → FallbackPolicy ช่วยไม่ได้:[AllowAnonymous]ระดับ class override FallbackPolicy เสมอ (พฤติกรรมมาตรฐาน ASP.NET Core) → endpoint พวกนี้ยังเปิด anonymous 100% แม้เปิด FallbackPolicy แล้ว- ThirdParty 7 controllers ติด
[AllowAnonymous]ระดับ class พร้อม write endpoints จริง:CustomerController.cs:29,CmsController.cs:29,FileManagementController.cs:26,EGovTokenController.cs:22(POST:56/DELETE:72),GeneralFinancialDetailController.cs:23,JuristicShareholderController.cs:28,GeneralFinancialFileController.cs:28— และCmsController.cs:53[Authorize(Roles="Admin")]ระดับ method ไม่มีผล เพราะ class-level[AllowAnonymous]ทำให้ authorization skip ทั้ง endpoint - NotificationService
MaintenanceController.cs:14,17[AllowAnonymous]→POST toggleเปิด/ปิด maintenance mode + broadcast ได้โดยไม่ auth ·OtpController.cs:29ทั้ง class[AllowAnonymous]+[SkipApiKey](บางส่วนตั้งใจ แต่ควร rate-limit เข้ม) - UserService
OnboardingController.cs:35[AllowAnonymous]ระดับ class (explicit) + Role/Permission/RoleMode/RolePermission controllers (RoleController.cs:23-24ฯลฯ มีทั้ง[Authorize]comment และ[AllowAnonymous]explicit) — เคส (ข) เช่นกัน (ต่างจากAdminOnboardingControllerที่เป็นเคส ก) - FallbackPolicy ช่วยไม่ได้ → ต้อง audit รายตัว แล้วถอด/เปลี่ยนเป็น
[Authorize]/[RequireApiKey](ดู Phase 1 งาน per-endpoint audit); CI gate ที่เช็คแค่ “มี attribute ใดก็ได้” จะปล่อยผ่านทั้งที่ยัง anonymous
- ทำไมอันตราย: “default-deny” คือหลักพื้นฐานที่สุด ระบบตอนนี้เป็น “default-allow” → พลาดติด
[Authorize]ครั้งเดียว = endpoint หลุด (ปัญหา ก) และ[AllowAnonymous]ตั้งใจบน write endpoint = เปิดโล่งถาวรจนกว่าจะ audit (ปัญหา ข) - ⚠️ วิธีแก้ต้องทำตามลำดับ (ถ้าทำผิด = ระบบล่มทั้ง platform): ดู §4.2 — สรุป: inventory endpoint ที่ต้อง anonymous จริงก่อน (OIDC callback, health/liveness probe, OTP public, antiforgery-token, endpoint ที่กันด้วย API key/InternalOnly เช่น
[RequireApiKey]+ Sentinel resolve-session, SignalR hubs) → decorate ให้ถูกกลไกตาม §4.2 Step 2 ([AllowAnonymous]สำหรับ controller,.AllowAnonymous()chain สำหรับ mapped endpoint เช่น health probe ที่ ติด attribute ไม่ได้) → แล้วค่อย เปิด FallbackPolicy → แยกงาน audit[AllowAnonymous]explicit (ปัญหา ข) ต่างหาก - ขนาดงานจริง (พูดตรงๆ): ไม่ใช่ “แก้ที่เดียวจบ” — เป็น 1 helper กลางใน Backend_Package + N การ wire-up ต่อ service + N การ audit endpoint anonymous เพราะแต่ละ service เรียก
AddAuthorizationของตัวเอง - หมวด: §7(2)
#4 🟠 ConsumerSecret ใน URL ขาออก (external DGA) + HMAC signature ถูกปิด (ThirdParty)
- หลักฐาน:
EGovTokenClient.cs:86—var url = $"{AuthUrl}?ConsumerSecret={ConsumerSecret}&AgentID={AgentId}";ส่งเป็น GET (:88) ไป DGA external API (api.egov.go.th/ws/auth/validate, ตั้งในappsettings*.jsonทุก env) · HMAC signature validation ปิด:SecurityExtensions.cs:29-39+MiddlewarePipelineExtensions.cs:80-81(// app.UseSignatureValidation();comment ไว้) - ข้อสำคัญ (แก้ความเข้าใจเดิม “ย้ายไป header”): query param
ConsumerSecret/AgentID+Consumer-Keyheader เป็น contract ของ DGA (ยืนยันจาก DGA API spec: query param บังคับ ไม่มีทางเลือก POST body) → “ย้าย ConsumerSecret ไป header/body” ฝ่ายเดียว = token acquisition พังทันที · ช่องรั่วฝั่ง log ของเราเองปิดอยู่แล้ว: ThirdParty target net10.0 → .NET 9+HttpClientFactoryredact query string เป็น default (ไม่มี opt-out ในโค้ด) และEGovTokenClient.cs:80-84log แค่AuthUrl(ไม่มี query) + mask ConsumerKey → ความเสี่ยงที่เหลือคือ access log ฝั่ง DGA / TLS-inspecting egress proxy เท่านั้น - แก้: (1) ยืนยัน DGA API spec ว่า query param บังคับจริง (ถ้ามีทางเลือก header ค่อยพิจารณา) (2) mitigation ที่คุมได้ = Serilog override category
System.Net.Http.HttpClientให้ ≥ Warning + ยืนยัน TLS-only และ egress proxy ไม่เก็บ URL (ไม่ใช่ “ย้ายไป header”) (3) เปิด HMAC signature validation กลับ (หรือระบุเหตุผล+timeline ถ้าจงใจปิดชั่วคราว) (4) ThirdParty prodConsumerKey/ConsumerSecretที่ hardcode ในappsettings.Production.json:9-11→ รวมเข้า scope Finding #1 (ถอด+rotate+KV) - หมวด: §7(3) + 3.2 Integrity
#5 🟠 Rate limiting ไม่ทั่วถึง — และ pattern ต้นแบบมีกับดัก self-DoS + penalty layer ไม่มี enforcement จริง
- หลักฐาน (ขาด limiter): ThirdParty (
Program.cs+MiddlewarePipelineExtensions.cs= 0 rate limiter) · FileManagement (เหมือนกัน, มีแค่ config flagEnableDistributedRateLimiting:false) · ConsentService = มีAddFixedWindowLimiter("GlobalPolicy", 100/min)(Program.cs:114-124) แต่ เป็น non-partitioned - ⚠️ อย่าลอก ConsentService
GlobalPolicyเป็นต้นแบบ (self-DoS):AddFixedWindowLimiter("GlobalPolicy")ใช้ partition key = ชื่อ policy (ค่าคงที่) = counter เดียวแชร์ทุก client ทั้ง service ไม่ใช่ per-client → client เดียวยิงครบ 100/min ก็กินโควตาหมด → user จริงทุกคนโดน 429 (self-DoS ง่ายกว่าเดิม; APIM ปล่อย Bearer 200/60s ต่อ caller ก็เกิน budget รวม backend แล้ว) - ⚠️
RateLimitPenaltyMiddlewareไม่มี enforcement จริง: grep ทั้ง platform พบPenaltyBox.IsBannedมีแค่ definition ไม่มี caller ที่ไหนเลย → wire แล้วได้ 0 การป้องกัน (ban ลง in-memory dict ที่ไม่มีใครอ่าน) · ซ้ำIdentityResolverเชื่อ headerX-Demo-Userที่ client ปลอมได้ (spoof เพื่อ ban เหยื่อ) + fallbackConnection.RemoteIpAddressซึ่งหลัง APIM/ingress = IP เดียวกันหมด (ban ครั้งเดียว = anonymous ทั้ง platform โดน) + state per-pod in-memory ไม่ consistent บน AKS หลาย replica · อันตรายผสม: ถ้าจับคู่ penalty (นับ 429 ต่อ identity) กับ global window ข้างบน → user บริสุทธิ์ที่โดน 429 เพราะคนอื่นทำโควตาหมด จะถูกสะสมจน ban เอง - แก้: helper กลางต้องใช้ partitioned limiter (
PartitionedRateLimiter/AddPolicyพร้อม partition key = user sub → API key → client IP จาก forwarded header) ไม่ใช่AddFixedWindowLimiterตัวเดียวแชร์ทั้ง service · exclude/healthจาก limiter (กัน probe โดน 429 → pod restart) · อย่าใส่RateLimitPenaltyMiddlewareใน helper v1 — primaryAddRateLimiter(partitioned) อย่างเดียวพอ; ถ้าจะใช้ escalation จริงต้องเพิ่ม (1) middleware เช็คIsBannedก่อนnext()(2) ตัดX-Demo-UserออกจากIdentityResolver(3) ใช้ IP จาก forwarded headers ที่ผ่านUseForwardedHeaders+KnownNetworksแล้วเท่านั้น (4) เก็บ state ใน Redis ไม่ใช่ in-memory · ยืนยัน APIM limit ครอบ path ที่ bypass ไม่ได้ - หมวด: §7(7.1),(7.2)
3.2 Medium — เข้า Phase 2
- 🟡
IdentityModelEventSource.ShowPII=truenon-prod — ยืนยันครบทุก service หลัก (Filter/Log/Consent/Centralized/Codex/FileMgmt/Orchestrator/Task/ThirdParty/Notification/UserService/Template) ผ่าน grep patternif(!IsProduction) ShowPII=true→ เปลี่ยนเป็นเปิดเฉพาะ local - 🟡 Swagger UI เปิด non-prod — confirm การเข้าถึงจากภายนอกบน SIT/UAT
- 🟡 whoami/diagnostics endpoints anonymous (Notification) → ใส่ auth หรือปิดใน prod
- 🟡 Sentinel
InternalOnlyAttributedefaultEnabled=false+ ไม่มี config ตั้งใน repo → ต้อง confirm IaC เปิด (InternalOnlyAttribute.cs:30-35,SecuritySettings.cs:44) และ no-op ถ้า ApiKey/CIDR ว่าง (:38,56) - 🟡 session TTL 720h — ทบทวนให้สั้นลง (มาตรฐาน §7(1))
- 🟡 PII plaintext ใน Redis user cache
- 🟡 APIM ไม่ validate JWT ก่อน passthrough (malformed/expired ทะลุถึง backend — backend validate เองอยู่แล้ว แต่ควร reject ที่ขอบ)
3.3 Low — Phase 2-3 / backlog
- 🔵 stale doc comment ชื่อ cookie ผิด (
AuthenticationExtensions.cs:19เขียน.SuperApp.Authแต่จริง.Exim.Auth.{tenant}) — ไม่ใช่ bug แต่ทำให้เข้าใจผิด - 🔵 TokenRefreshMiddleware fail-open by design (ต่างจาก SessionValidation ที่ fail-closed) — ความเสี่ยงต่ำ แต่ควร document เหตุผล
- 🔵 ZAP dev findings (500 error disclosure, X-Content-Type บน swagger) — dev-only
3.4 ✅ สิ่งที่แก้ไปแล้วตั้งแต่สแกนเดือน พ.ค. (ให้เครดิต + กันแก้ซ้ำ)
| Finding เก่า | สถานะปัจจุบัน | หลักฐาน |
|---|---|---|
| C-2 ThirdParty log ConsumerKey/Secret plaintext | ✅ แก้แล้ว (mask 4 ตัวแรก, ไม่ log secret) | EGovTokenClient.cs:82-84 |
| H-6b FileMgmt upload anonymous + ไม่มี size limit | ✅ แก้แล้ว ([Authorize] + [RequestSizeLimit(10MB)]) | FileManageController.cs:33,54-57 |
| ConsentService ไม่มี rate limit | ✅ เพิ่มแล้ว — ⚠️ แต่เป็น non-partitioned (GlobalPolicy counter เดียวแชร์ทั้ง service = self-DoS) ต้อง migrate → partitioned helper ใน Phase 1 (ดู §3.1 #5) | Consent/Program.cs:114-124,144-145 |
| V-001 Sentinel cookie ชื่อเดียวปน tenant | ✅ แก้แล้ว (per-tenant name + แยก DataProtection purpose) | AppConstants.cs:30, AuthenticationExtensions.cs:68-93 |
| V-005 Sentinel SessionValidation fail-open | ✅ แก้แล้ว (fail-closed 503, ADR-002) | SessionValidationMiddleware.cs:124-134 |
| V-007 Token refresh ไม่มี lock | ✅ แก้แล้ว (distributed lock 30s) | TokenRefreshService.cs:73-82 |
ข้อควรระวังตกค้าง: V-001 แก้ชื่อ cookie แล้ว แต่ การเลือก realm ตอนไม่มี Origin header ยังเลือกตาม cookie-presence/DisplayOrder (dual-cookie ไม่มี Origin → เลือก realm แรกเสมอ —
AuthenticationExtensions.cs:54-62) ตรงกับ finding SG-04/dual-cookie เดิม ควรตาม fix ที่ selector +?tenant=
§4 — หลักการป้องกันเชิงสถาปัตยกรรม (Defense-in-Depth ต่อชั้น)
ส่วนนี้คือ “ภาพเป้าหมาย” — แต่ละชั้นควรมี control อะไร และ หัวใจของแผน: ใช้ library กลาง (Backend_Package) เป็นจุดบังคับใช้ เพื่อให้ทีมที่ไม่มี security expert ไม่ต้องจำกฎเยอะ แค่ใช้ helper กลางให้ถูก
4.1 Control ที่ควรอยู่แต่ละชั้น
| ชั้น | Control ที่ควรมี | สถานะ |
|---|---|---|
| Cloudflare / App GW | WAF (OWASP ruleset), DDoS, TLS, rate limit ระดับ edge, bot mgmt | ✅ มี — ควร audit rule + cipher |
| APIM | JWT pre-validation, rate limit, subscription key, strip Authorization, session façade | 🟡 มี — เพิ่ม JWT validate ที่ขอบ, subscription key enforcement |
| Sentinel | OIDC, session store, InternalOnly, realm isolation | 🟡 ดีขึ้นมาก — ปิด InternalOnly default, fix realm selector |
| Backend_Package (⭐ จุดคานงัด) | FallbackPolicy helper, dev-bypass guard กลาง, rate-limit helper, security headers, encryption, signature, API key, current-user | 🟡 มีชิ้นส่วนเกือบครบ — ขาด “การบังคับใช้เป็น default” |
| แต่ละ Service | ใช้ helper กลาง, [Authorize] เป็น default, [AllowAnonymous] เฉพาะที่จำเป็น + มีเหตุผล | 🔴 wire-up ไม่ทั่วถึง |
| Data tier | Managed Identity, KV, RBAC least-privilege, encryption at-rest, no secret in config | 🟡 KV ครบ — ตรวจ least-privilege + PII at-rest |
| CI/CD (B7) | SAST, SCA/SBOM, secret scan, container scan, IaC scan | 🟡 ออกแบบแล้ว รอ implement (ดู §5 Phase 2) |
4.2 ⚠️ ลำดับที่ปลอดภัยสำหรับเปิด FallbackPolicy (สำคัญมาก)
การเปิด FallbackPolicy = RequireAuthenticatedUser คือ control ที่ leverage สูงสุด แต่ก็เสี่ยงทำระบบล่มมากที่สุด เพราะมันทำให้ ทุก endpoint ที่ไม่มี [Authorize]/[AllowAnonymous] กลายเป็น 401 ทันที รวมถึง endpoint ที่ต้อง anonymous จริงๆ ห้าม flip ทันที ต้องทำตามลำดับ:
Step 0 Prerequisite: ยืนยันว่า service มี "JWT authentication scheme จริง" ลงทะเบียนก่อน
(AddAppAuthentication / AddJwtBearer) — ถ้าไม่มี scheme เลย การตั้ง FallbackPolicy
จะโยน InvalidOperationException = 500 ทุก request (ไม่ใช่ 401)
· ThirdPartyFXService = ไม่มี auth เลยทั้ง repo (grep AddAuthentication/FallbackPolicy = 0)
→ ต้อง port auth ก่อนเข้า rollout (service นี้เดิมไม่อยู่ใน inventory ของแผน)
· WorkflowService = มี scheme แต่เป็น BypassAuthHandler (authenticate ทุกคน)
→ FallbackPolicy จะไม่ 500 แต่ "ผ่านหมดแบบไร้ความหมาย" ต้อง port JWT จริงก่อน (ดู §3.1 #2 แบบ B)
Step 1 Inventory: ไล่หา endpoint ที่ "ต้อง anonymous จริง" ให้ครบทุก service
(health/liveness/readiness probe, OIDC callback/login, OTP public,
antiforgery-token, webhook ที่ auth ด้วย signature แทน,
"ทุก endpoint ที่กันด้วย API key / InternalOnly" เช่น [RequireApiKey],
Sentinel /internal/resolve-session — filter พวกนี้แค่ตรวจ key
ไม่ได้ตั้ง authenticated user ให้ ASP.NET จึงโดน FallbackPolicy 401 เหมือนกัน,
และ SignalR hubs (MapHub) — โดน FallbackPolicy เหมือน MapHealthChecks
และ WS client ส่ง token ทาง query ?access_token= ไม่ใช่ Authorization header)
Step 2 Decorate ให้ถูกกลไก (มี 3 แบบ ไม่เหมือนกัน):
- Controller/action → ติด attribute [AllowAnonymous] "อย่างชัดเจน" (explicit)
endpoint ที่ใช้ API key ให้ติดคู่กัน [AllowAnonymous]+[RequireApiKey]
(pattern นี้มีใช้จริงแล้ว เช่น FilterService PublishController)
⚠️ prerequisite ก่อนใช้ [RequireApiKey] บน service ใด — ต้อง verify:
(1) options.Filters.Add<ApiKeyAuthFilter>() อยู่ใน AddControllers จริง
(ApiKeyAuthFilter เป็น MVC action filter — ถ้าไม่ register เข้า pipeline
attribute จะเงียบสนิท endpoint เปิด anonymous เต็มรูปแบบ)
ตัวอย่างที่พลาด: ConsentService เรียก AddApiKeyAuth แล้วแต่ไม่เคย Add filter
→ ติด [RequireApiKey] บน Consent วันนี้ = no-op (Consent POST /receipt คือเป้าของแผน)
(2) ApiKeyAuth:ApiKeys มีค่าใน config/KV ของ env นั้น
(dict ว่าง = ปฏิเสธทุก key = fail-closed พังของจริง)
→ ทางที่ดี: helper กลาง register filter อัตโนมัติ + fail-fast ตอน startup ถ้า ApiKeys ว่าง
- Health probe "ไม่ใช่ controller" — register ผ่าน app.MapHealthChecks("/health")
จึงไม่มี HealthController ให้ติด attribute ต้อง chain ที่ตัว endpoint แทน:
app.MapHealthChecks("/health").AllowAnonymous();
- SignalR hub ที่ต้อง [Authorize] — ต้องมี JwtBearerEvents.OnMessageReceived อ่าน
access_token จาก query string (NotificationService ทำแบบนี้อยู่แล้ว เป็นตัวอย่าง)
Step 3 Test บน 1 service นำร่อง (แนะนำ UserService — bypass ปิดอยู่แล้ว)
เปิด FallbackPolicy → รัน E2E → ยืนยัน health probe + login + flow ปกติ ไม่ 401
Step 4 Roll out ทีละ service (ไม่ใช่เปิดพร้อมกันทั้งหมด)
Step 5 ใส่ CI check กันถอยหลัง: fail build ถ้าเจอ controller ไม่มีทั้ง [Authorize]/[AllowAnonymous]
⚠️ ข้อจำกัด: check นี้ผ่านแม้ controller ติด [AllowAnonymous] ตั้งใจบน write endpoint
(เช่น ThirdParty 7 controllers) — ต้องมีงาน "audit [AllowAnonymous] รายตัว" แยกต่างหาก
(ดู Phase 1) เพื่อไม่ให้ verify เขียวทั้งที่ยังเปิด anonymous
เหตุผลที่ต้องมี Step 0-2 ก่อน: ถ้า flip เลย → (ก) service ที่ไม่มี auth scheme (ThirdPartyFX) จะ 500 ทุก request; (ข) health probe ของ Kubernetes จะได้ 401 → liveness fail → pod ถูก restart วนไป ทั้ง cluster ล่ม นี่คือกับดักที่คนทำตามเอกสารตรงๆ โดยไม่อ่านลำดับจะเจอ
4.3 ทำไมต้องบังคับที่ library กลาง (ไม่ใช่แก้ทีละ service ให้จำเอา)
ทีมไม่มี security expert → กลยุทธ์ที่ยั่งยืนคือ “pit of success”: ทำให้ทางที่ถูกต้องเป็นทางที่ง่ายที่สุด/default โดยไม่ต้องคิด
- helper กลาง 1 ตัว (
AddSuperAppAuthorizationที่ตั้ง FallbackPolicy + guard bypass ให้เสร็จ) → service เรียกบรรทัดเดียว - แก้ Template → service ใหม่ได้ของถูกฟรี
- CI gate → กันไม่ให้ถอยหลัง
- แต่ต้องพูดตรง: helper กลางยังต้องให้แต่ละ service “เรียกใช้” (opt-in) เพราะแต่ละ service มี
AddAuthorizationของตัวเอง → งานจริง = 1 helper + N wire-up + N audit
§5 — Roadmap 4 Phase + Governance
เรียงตาม risk-first: ห้ามเลือดก่อน (Phase 0) → วางรากฐาน (Phase 1) → ไล่เก็บเป็นระบบ (Phase 2) → ทำให้ยั่งยืน (Phase 3)
Phase 0 — ห้ามเลือด (วัน-สัปดาห์) 🔴
เป้า: ปิดสิ่งที่ “รั่วอยู่ตอนนี้” โดยไม่ต้องรื้อสถาปัตยกรรม
| งาน | verify ว่าเสร็จเมื่อ |
|---|---|
ถอด hardcoded credential #1: Notification DiagnosticsController.cs:144 + ThirdParty appsettings.Production.json:9-11 + rotate + ย้าย KV | grep credential ใน source/config = 0; endpoint ต้อง auth |
ยืนยัน ASPNETCORE_ENVIRONMENT ทุก cluster non-prod — dev = Development ยืนยันแล้วจาก Dockerfile.DEV + IaC dev manifests (§3.1 #2); SIT/UAT = Sit/Uat | มีตารางค่าต่อ cluster; เช็ค live cluster ไม่ drift จาก ArgoCD |
| ปิด dev-bypass ที่ Template ก่อน (กันของใหม่) | Template ใช้ if(false) หรือ guard กลาง |
| ปิด/auth endpoint anonymous ที่อันตราย (Notification maintenance toggle, whoami) | endpoint คืน 401/403 เมื่อไม่มี token |
แก้ #4 (external DGA): ยืนยัน DGA spec ว่า query param บังคับ + Serilog override System.Net.Http.HttpClient ≥ Warning + ยืนยัน egress proxy/TLS ไม่เก็บ URL (ห้ามย้าย ConsumerSecret ไป header ฝ่ายเดียว = token acquisition พัง) | DGA spec confirmed; HttpClient log ไม่มี query (net10 redact default ยืนยันแล้ว) |
Phase 1 — วางรากฐาน (2-6 สัปดาห์) 🔴→🟠
เป้า: ปิด root cause เชิงระบบ ผ่าน library กลาง
| งาน | verify |
|---|---|
สร้าง helper กลางใน Backend_Package: AddSuperAppAuthorization (FallbackPolicy) + dev-bypass guard | unit test helper; publish package |
Step 0 prereq: port JWT auth (AddAppAuthentication) ให้ service ที่ยังไม่มี — ThirdPartyFX (ไม่มี auth เลย → FallbackPolicy = 500) และ Workflow (แทน BypassAuthHandler แบบ B) | ThirdPartyFX/WF มี JWT scheme จริง; valid token 200 / no token 401 |
Inventory + decorate endpoint anonymous ทุก service (§4.2 Step 1-2) รวม SignalR hub + API-key prereq (Filters.Add<ApiKeyAuthFilter>()) | ทุก controller มี [Authorize]/[AllowAnonymous] ชัดเจน; endpoint API-key ยิงไม่มี key ได้ 401 |
| เปิด FallbackPolicy ทีละ service ตามลำดับ §4.2 (เริ่ม UserService) | E2E เขียว; health probe ไม่ 401 |
Audit [AllowAnonymous] explicit รายตัว บน write endpoint (ThirdParty 7 controllers, Notification maintenance) → ถอด/เปลี่ยนเป็น [Authorize]/[RequireApiKey] (#3 ปัญหา ข) | เรียก endpoint โดยไม่มี credential ได้ 401/403 ทุกตัว |
เปลี่ยน dev-bypass → guard กลาง: แบบ A if(isDev) (Filter/Log/Consent/Centralized/Codex/FileMgmt/Orchestrator/ThirdParty/Task) + แบบ B Workflow (BypassAuthHandler → JWT จริง) | (syntactic) grep if(isDev) bypass = 0 + WF ไม่มี BypassAuthHandler; (functional) ทีละ service — E2E เขียวบน dev/sit + valid token 200 / no token 401 |
Rate-limit helper กลาง = partitioned limiter (partition key = sub→API key→forwarded IP), exclude /health, ไม่รวม RateLimitPenaltyMiddleware (ยังไม่มี enforcement จริง) → wire ThirdParty, FileManagement + migrate Consent GlobalPolicy (non-partitioned) → partitioned helper (#5) | ทุก service (รวม Consent) มี partitioned AddRateLimiter; client เดียวยิงเกิน limit ได้ 429 แต่ client อื่นยังผ่าน (พิสูจน์ per-client) |
| เปิด HMAC signature ThirdParty กลับ (#4) | signature validation ทำงาน |
Phase 2 — ไล่เก็บเป็นระบบ (1-3 เดือน) 🟠→🟡
| งาน | verify |
|---|---|
| แก้ Medium ทั้งหมด §3.2 (ShowPII, Swagger, InternalOnly default, session TTL, PII at-rest) | แต่ละข้อมี evidence |
| CI security scanning (ตาม design เดิม security/01072026): SAST (SonarQube), SCA/SBOM (Dependency-Track+CycloneDX), secret scan (Gitleaks), container (Trivy), IaC (Checkov) | pipeline รันจริง เริ่ม non-blocking |
CI gate กัน regression: fail ถ้า controller ไม่มี auth attribute / เจอ if(isDev) bypass หรือ BypassAuthHandler scheme / เจอ secret | build fail เมื่อ inject test case |
| APIM: JWT pre-validation + subscription key enforcement | malformed token reject ที่ขอบ |
Phase 3 — ทำให้ยั่งยืน + compliance (ต่อเนื่อง) 🟡→✅
| งาน | verify |
|---|---|
| Audit log: retention ≥90 วัน + tamper-proof + security alerting (brute-force/anomaly) — หมวด 6 | นโยบาย + alert rule ใช้งานจริง |
| DAST (OWASP ZAP) เป็น pipeline ประจำ (ต่อจาก ZAP manual เดิม) | scan รายรอบ |
| PDPA: process เรียกเก็บ/ตรวจรับ หลักฐานการทำลายข้อมูล (ทิศทางถูก: ฝ่ายที่ทำลายข้อมูล — third party/ผู้ใช้บริการ API — ส่งหลักฐานมาให้ ธสน. ไม่ใช่เราส่งออก; guideline V3.0 ข้อ 3.1) | มี runbook รับ/ตรวจหลักฐาน |
| Threat modeling + security review เป็น gate ของ feature ใหม่ | checklist ใน PR template |
| Flip CI scanners จาก non-blocking → blocking (Gitleaks ก่อน) | build block เมื่อพบ critical |
5.1 Governance — ทำให้เดินต่อได้แม้ไม่มี security expert
หัวใจคือ ย้ายภาระจาก “คนต้องจำ” ไป “ระบบบังคับ”:
- Template + Library กลาง = source of truth — ของถูกต้องอยู่ที่เดียว service ใหม่ inherit
- CI gate = ตาข่ายกันถอยหลัง — สิ่งที่แก้แล้วต้องมี test/check กันกลับมา
- เอกสารนี้ = checklist — review รายไตรมาส เทียบ §2 (7 หมวด) ว่าเลื่อนขึ้นไหม
- ownership ชัด — กำหนดคน/ทีมดูแล Backend_Package security + APIM policy + IaC env
- external review เป็นระยะ — เมื่อ Phase 1-2 เสร็จ พิจารณา pentest/audit ภายนอกมายืนยัน (ตอนนี้ยังไม่คุ้ม เพราะช่องว่างพื้นฐานยังเปิด)
- IaC parity — ทุกการแก้ config security ต้องไป update ที่ IaC ทุก env ที่กระทบ (CI จะ override appsettings ด้วยค่าจาก IaC เสมอ)
ภาคผนวก A — Glossary สำหรับคนไม่มีพื้น Security
| คำ | ความหมายแบบง่าย |
|---|---|
| Defense-in-Depth | มีหลายด่านป้องกันซ้อนกัน ด่านหนึ่งพลาด ยังมีด่านอื่น |
| Zero Trust | ไม่เชื่อว่า “อยู่ในเครือข่ายแล้วปลอดภัย” ตรวจทุกครั้ง |
| Trust boundary | เส้นที่ข้อมูลข้ามจากโซนคุมไม่ได้ → คุมได้ ต้องตรวจ |
| FallbackPolicy | กฎ default ว่า endpoint ที่ไม่ได้ระบุอะไร ให้ “บังคับ login” (default-deny) |
| dev-bypass | โค้ดที่ปิดการตรวจ token ตอน dev เพื่อทดสอบง่าย — อันตรายถ้าติดไปถึง env จริง |
| Rate limiting | จำกัดจำนวนครั้งเรียก API กัน abuse / DoS |
| SAST / SCA / DAST | สแกนโค้ด / สแกน dependency / สแกนแอปที่รันอยู่ |
| WAF | กำแพงกรอง HTTP request อันตราย (injection, XSS) ที่ขอบ |
| anonymous endpoint | API ที่เรียกได้โดยไม่ต้อง login |
ภาคผนวก B — แหล่งอ้างอิง
- API-GL-001 แนวปฏิบัติการใช้ API ในการให้บริการ V3.0 (ธสน./EXIM, 4 พ.ย. 2568) — 7 หมวด API Security Standard
- เอกสารภายใน:
security/01072026/(CI scanning design),iac/27052026/SECURITY_SCAN_REPORT.md+SECURITY_VERIFICATION_AUDIT.md(สแกนเก่า — เทียบสถานะปัจจุบันแล้วใน §3),sentinel-gateway-service/27052026/(auth architecture),package/27052026/SECURITY_README.md(security tier system) - OWASP API Security Top 10, OWASP ASVS, NIST SP 800-53 (อ้างอิงเสริมได้ตอนทำ Phase 2-3)
เอกสารนี้ ณ 2 ก.ค. 2026 — ข้อที่มี file:line กำกับ = ยืนยันจากโค้ดแล้ว; ข้อที่ mark ⚠️/”ควร confirm”/“ต้อง verify แยก” = ยังไม่ verified (สมมติฐาน ต้องตรวจก่อนใช้ตอบผู้ตรวจ) · สแกนเก่าเดือน พ.ค. ถูกเทียบสถานะใหม่แล้ว (§3.4) · ก่อนลงมือ Phase ใด ให้อ่านลำดับความปลอดภัยใน §4.2 ก่อนเสมอ