IMPL PLAN 05 — API Service Hardening
Helper กลาง 5 ตัวใน Backend_Package + ลำดับ rollout FallbackPolicy ที่ปลอดภัย — หัวใจของทั้งแผน
อัปเดต: 2026-07-07
IMPL PLAN 05 — API Service Hardening (Helper กลาง 5 ตัว + FallbackPolicy Rollout)
วันที่: 07/07/2026 · สถานะ: ⏳ รอ verify · Priority: 🔴 Phase 1 (หัวใจของทั้งแผน) อ้างอิง: DISCUSSION หมวด 5 · ลำดับปลอดภัยจาก ROADMAP §4.2
สรุป
สร้าง helper กลาง 5 ตัวใน Backend_Package (จุดคานงัด — service เรียกบรรทัดเดียวได้ของถูก) แล้ว rollout FallbackPolicy ทีละ service ตามลำดับปลอดภัย — นี่คืองานที่ปิด root cause ของ “default-allow ทั้ง platform”
หลักการ Pit of Success: ทางที่ถูกต้องเป็นทางที่ง่ายที่สุด — ทีมไม่ต้องจำกฎ security แค่เรียก helper + Template ให้ของถูกมาแต่แรก + CI gate กันถอยหลัง
§1 Helper กลาง 5 ตัว — สร้างใน Backend_Package
ไฟล์ที่ต้องสร้าง/แก้
| # | ไฟล์ (Backend_Package/src/) | เนื้อหา |
|---|---|---|
| 1 | Extension/AuthorizationExtensions.cs (ใหม่) | AddSuperAppAuthorization() — ดู code sketch ด้านล่าง |
| 2 | Extension/AuthenticationGuardExtensions.cs (ใหม่) | dev-bypass guard กลาง — bypass ผูกกับ #if DEBUG + env var explicit 2 ชั้น |
| 3 | Extension/RateLimitingExtensions.cs (ใหม่) | AddSuperAppRateLimiter() — partitioned per-client |
| 4 | Extension/SecurityFilterExtensions.cs (แก้) | ให้ AddAppSecurity register ApiKeyAuthFilter อัตโนมัติ + fail-fast ถ้า ApiKeyAuth:ApiKeys ว่างทั้งที่มี [RequireApiKey] ใช้ |
| 5 | Extension/LoggingExtensions.cs (แก้) | ShowPII default = เปิดเฉพาะเมื่อ DOTNET_RUNNING_LOCAL=true (ไม่ใช่ !IsProduction()) |
| 6 | README.md + CHANGELOG.md | document + bump minor version |
Code Sketch — AddSuperAppAuthorization
public static IServiceCollection AddSuperAppAuthorization(
this IServiceCollection services, Action<AuthorizationOptions>? configure = null)
{
services.AddAuthorization(options =>
{
// default-deny: ทุก endpoint ที่ไม่มี [Authorize]/[AllowAnonymous] → ต้อง authenticated
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.AddPolicy("RequireAdmin", p => p.RequireRole("Admin"));
options.AddPolicy("RequireUser", p => p.RequireAuthenticatedUser());
configure?.Invoke(options); // service เพิ่ม policy เฉพาะทางได้
});
return services;
}
Code Sketch — Dev-Bypass Guard กลาง (แทน if(isDev) รายตัว)
// AuthenticationGuardExtensions.cs — เงื่อนไข bypass ต้องผ่าน "2 ชั้น" ที่ไม่มีวัน true บน cluster:
// ชั้น 1: compile-time — image CI build เป็น Release เสมอ → #if DEBUG ตัดโค้ด bypass ออกจาก binary
// ชั้น 2: explicit opt-in — dev ต้องตั้งเองบนเครื่อง local เท่านั้น
public static bool IsLocalAuthBypassAllowed(this IWebHostEnvironment env)
{
#if DEBUG
return Environment.GetEnvironmentVariable("SUPERAPP_LOCAL_AUTH_BYPASS") == "true"
&& env.IsDevelopment();
#else
return false; // Release build = bypass เป็นไปไม่ได้ ไม่ว่า env ชื่ออะไร
#endif
}
Code Sketch — Partitioned Rate Limiter (อย่าลอก Consent GlobalPolicy)
public static IServiceCollection AddSuperAppRateLimiter(
this IServiceCollection services, IConfiguration config)
{
services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(ctx =>
{
// partition ต่อ client: user sub → API key → forwarded IP (ผ่าน UseForwardedHeaders แล้ว)
var key = ctx.User.FindFirst("sub")?.Value
?? ctx.Request.Headers["X-Api-Key"].FirstOrDefault()
?? ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown";
return RateLimitPartition.GetFixedWindowLimiter(key, _ => new()
{
PermitLimit = config.GetValue("RateLimiting:PermitLimit", 100),
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0
});
});
});
return services;
}
// การ wire: app.UseRateLimiter() + MapHealthChecks(...).DisableRateLimiting()
// ❌ ห้ามใส่ RateLimitPenaltyMiddleware — IsBanned ไม่มี caller = 0 enforcement (ROADMAP #5/M4)
§2 FallbackPolicy Rollout — ลำดับปลอดภัย (ห้ามสลับขั้น)
⚠️ เปิด FallbackPolicy ผิดลำดับ = health probe 401 → liveness fail → pod restart วน → ล่มทั้ง cluster
Activity Diagram
flowchart TD
S0["Step 0: Prerequisite ต่อ service<br/>มี JWT scheme จริงไหม?"] -->|ThirdPartyFX: ไม่มี auth เลย| S0a["port AddAppAuthentication ก่อน<br/>(ไม่งั้น FallbackPolicy = 500 ทุก request)"]
S0 -->|Workflow: BypassAuthHandler| S0b["port JWT จริงก่อน<br/>(ไม่งั้นผ่านหมดแบบไร้ความหมาย)"]
S0 -->|service อื่น: มี JWT แล้ว| S1
S0a --> S1
S0b --> S1
S1["Step 1: Inventory endpoint ที่ต้อง anonymous จริง<br/>health probe · OIDC callback · OTP public ·<br/>antiforgery-token · webhook · API-key endpoints · SignalR hubs"] --> S2
S2["Step 2: Decorate ให้ถูกกลไก (3 แบบ)"] --> S2a["Controller → attribute [AllowAnonymous]<br/>(API-key คู่ [AllowAnonymous]+[RequireApiKey]<br/>⚠️ ต้อง Filters.Add<ApiKeyAuthFilter> แล้วเท่านั้น)"]
S2 --> S2b["Health probe ไม่ใช่ controller →<br/>app.MapHealthChecks('/health').AllowAnonymous()"]
S2 --> S2c["SignalR hub → [Authorize] +<br/>OnMessageReceived อ่าน ?access_token=<br/>(ลอกจาก NotificationService)"]
S2a --> S3
S2b --> S3
S2c --> S3
S3["Step 3: เปิดที่ UserService นำร่อง<br/>(bypass ปิดแล้ว + ApiKeyAuthFilter register แล้ว)"] --> S3v{"E2E เขียว?<br/>probe ไม่ 401?"}
S3v -->|ไม่| FIX[แก้ inventory ที่หลุด] --> S3
S3v -->|ใช่| S4["Step 4: Rollout ทีละ service<br/>(ห้ามพร้อมกันทั้งหมด)"]
S4 --> S5["Step 5: CI regression gate<br/>(IMPL_PLAN_01 A9)"]
S5 --> S6["งานแยก: audit [AllowAnonymous] explicit รายตัว<br/>ThirdParty 7 controllers · Notification maintenance ·<br/>UserService Onboarding/Role controllers<br/>→ ถอด/เปลี่ยน [Authorize]/[RequireApiKey]"]
Sequence Diagram — พฤติกรรมหลังเปิด (default-deny)
sequenceDiagram
autonumber
participant C as Client
participant BE as Service (FallbackPolicy เปิดแล้ว)
participant K8s as Kubelet (probe)
rect rgb(230,255,230)
Note over C,BE: Controller ที่ "ลืม" ติด attribute
C->>BE: GET /api/v1/forgotten-endpoint (ไม่มี token)
BE->>BE: ไม่มี [Authorize]/[AllowAnonymous] → FallbackPolicy จับ
BE-->>C: 401 ✅ (เดิม: 200 หลุด anonymous)
end
rect rgb(230,240,255)
Note over K8s,BE: Health probe (decorate แล้วใน Step 2)
K8s->>BE: GET /health
BE->>BE: MapHealthChecks(...).AllowAnonymous() → ข้าม policy
BE-->>K8s: 200 ✅ pod ไม่ restart
end
rect rgb(255,245,230)
Note over C,BE: Endpoint API-key (service-to-service)
C->>BE: POST /internal/x + X-Api-Key
BE->>BE: [AllowAnonymous] ข้าม FallbackPolicy →<br/>[RequireApiKey] → ApiKeyAuthFilter ตรวจ key
BE-->>C: 200 / 401 ตาม key ✅
end
§3 ลำดับงานต่อ Service (Wire-up Checklist)
Per-service PR — ใช้ checklist เดียวกันทุกตัว:
[ ] 1. อัปเดต Backend_Package เวอร์ชันใหม่ใน csproj
[ ] 2. Program.cs: AddAuthorization(...) เดิม → AddSuperAppAuthorization(...)
[ ] 3. AuthenticationExtensions.cs: ลบ if(isDev) block → ใช้ guard กลาง
[ ] 4. Program.cs: เพิ่ม AddSuperAppRateLimiter + app.UseRateLimiter()
[ ] 5. Inventory + decorate anonymous endpoints (Step 1-2)
[ ] 6. ตรวจ Filters.Add<ApiKeyAuthFilter>() ถ้าใช้ [RequireApiKey]
[ ] 7. รัน local: probe /health = 200, endpoint ไม่มี token = 401
[ ] 8. Deploy dev → E2E → SIT
ลำดับ service: ① UserService (นำร่อง — สะอาดสุด) → ② Backend_Template (service ใหม่ได้ของถูก) → ③ Sentinel → ④ Notification, Consent, Filter, Log → ⑤ Centralized, Codex, FileManagement, Orchestrator, Task → ⑥ ThirdParty (+audit AllowAnonymous 7 controllers) → ⑦ Workflow, ThirdPartyFX (ต้อง port auth ก่อน — Step 0)
Verify
- Unit tests ใน Package: FallbackPolicy บังคับ 401 กับ endpoint เปล่า · rate limiter partition per-client (client A โดน 429, client B ผ่าน) · guard คืน false บน Release build
- ต่อ service: 8-point checklist ผ่าน + E2E เขียว
- ทั้ง platform:
grep FallbackPolicyพบทุก service ·grep "if (isDev)"= 0 - CI regression gates active (IMPL_PLAN_01 A9)
⚠️ ข้อควรระวัง
- FileManagement layer สลับ (02=Application, 03=Infrastructure) — path ต่างจาก service อื่นตอน wire
- Package เป็น NuGet แชร์ — bump version + CHANGELOG ทุกครั้ง service ค่อยๆ upgrade ได้ ไม่บังคับพร้อมกัน
[AllowAnonymous]ระดับ class override ทุกอย่างรวม[Authorize]ระดับ method (เคสCmsController.cs:53) — audit Step 6 ห้ามข้าม- อย่าใส่ mitigation ที่ยังไม่มี enforcement (
RateLimitPenaltyMiddleware) เข้า helper — จะสร้างความมั่นใจปลอม
🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)
- นี่คือ plan ที่ผมจะทุ่ม effort มากที่สุดใน 10 เล่ม — Backend_Package คือจุดคานงัดจริง: แก้ 1 ที่ ได้ 14 service · ถ้างบ/คนจำกัด ตัดหมวดอื่นได้ก่อน หมวดนี้ห้ามตัด
- helper 5 ตัวทำครบใน Package version เดียว แต่ rollout ทีละ service — อย่าทยอย helper หลาย version (ทุก service จะต้อง bump หลายรอบ) · opt-in ผ่านการเรียก method ไม่ใช่ผ่าน version
- Step 1 (inventory anonymous endpoints) คือจุดที่คนจะพลาด — ผมจะทำเป็นตารางกลาง (service × endpoint × เหตุผลที่ต้อง anonymous) ให้ review ได้ ไม่ใช่ต่างคนต่างไล่ใน code ของตัวเอง — ตารางนี้กลายเป็น AUTHORIZATION_MATRIX ตั้งต้น (S2 ของ SUPPLEMENTAL) ฟรี
- UserService นำร่อง + ตั้ง deadline ชัดต่อ service — rollout 14 service ที่ไม่มี deadline จะลากเป็นปี · สัปดาห์ละ 2-3 service หลัง pilot เขียว = เสร็จใน ~6 สัปดาห์
- สิ่งที่ผมจะเพิ่มจาก plan นี้: unit test ใน Package ที่ assert ตัว helper เอง (FallbackPolicy บังคับ 401, guard คืน false บน Release) — เพราะ helper กลางที่พัง = พังทั้ง platform — test ต้องแน่นกว่าโค้ดปกติ
📚 อ้างอิงมาตรฐาน (สำหรับใช้ในที่ประชุม):
| หลักการ | แหล่งอ้างอิง |
|---|---|
| Deny by default (FallbackPolicy = หลัก fail-safe defaults) | OWASP Authorization Cheat Sheet — Enforce Least Privilege / Deny by Default |
| วิธีบังคับ authenticated user ทั้ง app ใน ASP.NET Core (official) | Microsoft Learn — Require authenticated users (FallbackPolicy) |
| Built-in rate limiting middleware (.NET) | Microsoft Learn — Rate limiting middleware in ASP.NET Core |
| Missing function-level authorization | OWASP API5 — Broken Function Level Authorization |
| Secure defaults / “pit of success” เป็น practice มาตรฐาน | Microsoft Security Development Lifecycle (SDL) Practices |