Monitor Log + Payload Encryption — ความคืบหน้าถึง 03/09/2026
งานรอบล่าสุดของ security uplift: ข้อ 3 whole-body payload encryption ขึ้น lib แล้ว, ข้อ 5 observe mode merge เข้า development แล้ว, ข้อ 6 Monitor Log เขียนครบทุกชิ้นและ build/test ผ่านแต่ยังไม่ commit — พร้อมบั๊กข้อมูลหายเงียบ 6 ทางที่ review จับได้และแก้แล้ว และคำตัดสินที่ยังค้างอยู่ที่ Owner
อัปเดต: 2026-09-03
Monitor Log + Payload Encryption — ความคืบหน้าถึง 03/09/2026
เอกสารนี้ตอบคำถามเดียว: ตอนนี้ไปถึงไหนแล้ว และอะไรเสร็จจริง — “เสร็จ” ในเอกสารนี้แปลว่า build ผ่าน test ผ่าน และมี exit code เป็นหลักฐาน ไม่ใช่ “เขียนโค้ดแล้ว”
สรุปสั้นที่สุด
โปรแกรม security แบ่งเป็น 6 เรื่อง ตอนนี้:
- ข้อ 1 body signature · ข้อ 2 encryption field · ข้อ 4 at-rest — Owner ประกาศผ่านแล้วตั้งแต่ 02/09 พร้อม apply กับ service อื่น
- ข้อ 3 whole-body payload encryption — ฝั่ง backend merge เข้า
developmentแล้ว (lib 10.25.0) · ฝั่งหน้าบ้านเขียนเสร็จแล้วแต่ยังไม่ commit - ข้อ 5 API restriction — P0 ปิดช่องถอนสิทธิ์แล้ว (PR ยังไม่ merge) · observe mode merge เข้า
developmentแล้ว (lib 10.24.0) · ส่วนไล่ติด attribute แล้วเปิด Enforce Owner สั่งพักไว้ - ข้อ 6 Monitor Log — เขียนครบทุกชิ้นแล้วและ build/test ผ่านทุกก้อน แต่ ยังไม่ commit และยังมี 1 ชิ้นที่ทำไม่ได้เพราะต้องรอ Owner ตัดสิน (read API)
ยังไปไม่ถึงขั้น “พร้อม apply เข้า Notification Service” เพราะยังเหลือ merge, publish lib, และคำตัดสินอีกหลายข้อ
ทำอะไรเสร็จบ้างในรอบนี้ พร้อมหลักฐาน
lib SupApp_util_lib — 10.24.0 และ 10.25.0 เข้า development แล้ว
Backend_Package development ตอนนี้อยู่ที่ d113e24 ซึ่งกิน PR #2875 (observe mode, 10.24.0) และ PR #2886 (whole-body payload encryption, 10.25.0) เข้าไปแล้วทั้งคู่ นี่คือสองก้อนที่ Owner อนุมัติให้ merge
whole-body ที่เข้าไปมีสาระสำคัญคือ กุญแจของผู้รับมาจาก registry ของ Sentinel ไม่ใช่ header ต่อ request (ทางที่ POC ทำแล้วถูกปฏิเสธ) และ แปะ annotation แล้วต้องเข้ารหัสได้เสมอ ล้มเหลว = 500 ห้ามส่ง plaintext ทั้ง whole-body และ field-level ยังได้เพดานขนาด body ที่ปฏิเสธด้วย 413 ก่อน buffer ซึ่งเดิมไม่มีทั้งสองเส้น
ข้อ 6 Monitor Log — ตาราง, consumer, producer, ops, config ครบทุกชิ้น
| ชิ้นงาน | อยู่ที่ | สถานะ verify |
|---|---|---|
ตาราง RequestLogs + migration | Backend_LogService branch MIGRATION 2665a4f | build exit 0 · has-pending-model-changes = none |
| consumer + batch writer | Backend_LogService branch feature/log-request-audit-trail | build exit 0 · test exit 0 · 207 passed / 0 failed |
| partition pre-create + backlog/DLQ metric + archive job | Backend_LogService branch feature/log-request-audit-ops | build exit 0 |
| producer: capture middleware + masking + publisher (10.26.0) | Backend_Package branch feature/pkg-request-log-capture | build exit 0 · test exit 0 · 1627 net10 · 1627 net9 · 1392 net8 · 1329 net6 · 687 net462 |
| config ทุก service ทุก env | Backend_Iac branch feature/iac-request-log-topic | 66 ไฟล์ (22 service × dev/sit/uat) · flag OFF ทุกที่ |
| ฝั่งหน้าบ้าน (ข้อ 3) | Frontend_SuperAppLibraryUi + Frontend_HostAppSuperApp | ยังไม่ผ่าน test เต็ม — ดูหัวข้อ “ยังค้าง” |
⚠️ ทุกก้อนในตารางนี้ยังไม่ commit ยกเว้น MIGRATION กับส่วน consumer ที่ commit ไว้แล้ว ไฟล์อยู่ใน working tree ของแต่ละ worktree ครบถ้วน ไม่หายไปไหน แต่ยังไม่มีอะไรถูก push และไม่มีอะไรถูก merge เข้า development โดยเอเจนต์
นโยบายเก็บ log ที่ Owner เปลี่ยนกลางทาง
เดิมแผน 02/09 เขียนไว้ว่าเก็บ body เฉพาะ field ที่อยู่ใน allow-list ระหว่างทาง Owner เปลี่ยนเป็น:
- เก็บทุก action ทุกเส้น ไม่มี allow-list ต่อ field แล้ว
- ยกเว้นเป็นราย API — route ที่อยู่ในรายการยกเว้นจะเก็บแค่ metadata ไม่เก็บ body เลย
- file upload ไม่เก็บอยู่แล้ว — multipart, octet-stream, response แบบ stream และอะไรที่ไม่ใช่ข้อความ เก็บแค่ metadata กับขนาด
- field อ่อนไหว mask เป็น
***เฉย ๆ ไม่เข้ารหัส — Owner เลือกเองโดยรับผลว่าถอดกลับไม่ได้
โค้ดที่ส่งมอบทำตามนี้ทั้งหมดแล้ว โดยที่มาของ “field ไหนอ่อนไหว” คือ metadata ของ [EncryptedField] / EncryptedResponseFieldMap ที่ lib อ่านอยู่แล้ว บวกรายการ JSON path เพิ่มเติมที่ service ตั้งเองได้ใน config และมี รายการที่ตัดทิ้งเสมอไม่ว่าใครตั้งค่าอะไร คือ Authorization, Cookie, Set-Cookie, X-API-Key และค่าที่หน้าตาเป็น token ทุกความลึก · query string และค่า route parameter ก็ถูก redact ด้วย ไม่ใช่กรองแต่ body
บั๊กที่ review จับได้และแก้ไปแล้ว — ทั้งหมดคือ “ข้อมูลหายเงียบ”
รอบนี้มี review อิสระ 3 รอบและ skeptic 1 รอบ สิ่งที่จับได้ไม่ใช่เรื่องสไตล์ แต่เป็นทางที่ข้อมูลหายโดยไม่มีใครรู้:
- dedup ด้วย
CorrelationId— correlation id ถูก propagate ข้าม hop และ record มีSourceServiceอยู่แล้ว แปลว่า 1 correlation id ต่อได้หลาย record คนละ service การทำให้มัน unique ทำให้ record ของ hop ที่สองถูกยุบทิ้งแล้ว complete message ที่หายไปด้วย แก้โดยเพิ่มEventIdต่อ record และย้าย unique index ไปเป็น(EventId, ReceivedAt)ส่วน index บนCorrelationIdเก็บไว้แบบไม่ unique เพราะการสืบสวนค้นทั้ง trace ด้วยมัน - body ที่เป็น blob pointer ถูก dead-letter ทิ้ง — ซึ่งทิ้งพอดีกับ body ใหญ่ที่ claim-check ตั้งใจจะเก็บ แก้โดยเพิ่มคอลัมน์
RequestBodyBlobUri/ResponseBodyBlobUriแล้วเขียนแถวพร้อม pointer Modeที่อยู่นอก enum — deserializer รับค่าตัวเลขที่ไม่รู้จักได้ และInlineเป็นค่า 0 ของ enum ⇒ producer รุ่นใหม่ที่ส่ง mode ที่สามจะรอดทุกด่านแล้วลงเป็นแถวที่ไม่มีทั้ง byte และ pointer แก้โดยตีกลับเป็น invalid payload- timestamp ที่ไม่มี offset ถูกอ่านด้วยเวลาท้องถิ่นของ pod —
10:00กลายเป็น03:00Zบนเครื่องไทย แปลว่าไทม์ไลน์ของการสืบสวนเลื่อนตาม timezone ของ pod ที่บังเอิญหยิบ message แก้โดยอ่านค่าที่ไม่มี offset เป็น UTC - partition bound เป็น bare date — PostgreSQL resolve ครั้งเดียวตอนรัน DDL ด้วย TimeZone ของ session ⇒ migration ที่ขยาย window ทีหลังถ้ารันคนละ timezone จะเหลือช่องโหว่ และทุกแถวที่ตกในช่องนั้น insert ไม่ผ่าน แก้เป็น
TIMESTAMPTZ '... 00:00:00+00'ทั้ง 12 ก้อน - 1 แถวเสียลาก batch ตายทั้งชุด — batch เป็น INSERT ก้อนเดียว ถ้าแถวเดียวผิด constraint หรือ
ReceivedAtเลย partition สุดท้าย ทั้ง 50 message จะ abandon แล้วเวียนกลับมาพังซ้ำจน dead-letter ยกชุด แก้โดยเมื่อ batch ล้ม ให้ retry ทีละแถว แถวที่ผ่านก็ complete เหลือแถวที่ผิดจริงเท่านั้นที่ dead-letter
นอกจากนี้ skeptic ยังชี้ว่า schema เดิม เก็บสิ่งที่การสืบสวนต้องใช้ไม่ครบ — ไม่มี query string, ไม่มีค่า route parameter, ไม่มี Content-Type, และไม่มีเวอร์ชันของกติกา masking ⇒ /companies/{id} มาถึงโดยไม่มี id และ ?accountNo= หายทั้งอัน จึงเพิ่ม 4 คอลัมน์ QueryStringRedacted, RouteValuesRedacted, ContentType, RedactionProfileVersion รวมตารางเป็น 22 คอลัมน์
ยังค้าง — และค้างเพราะอะไร
ต้องให้ Owner ตัดสินก่อน ไม่มีใครทำแทนได้
- read API ของ request log — ตอนนี้ ยังไม่มีเลย
AuditLogControllerมีแค่ endpoint ของ audit-logs การจะกั้นด้วย role จึงยังทำไม่ได้ เพราะ policy ที่ไม่มี endpoint คือ config ที่ตายแล้ว การสร้าง read API แปลว่าต้อง mint contract สาธารณะใหม่: route, filter ที่รองรับ, คืน body มาในลิสต์ด้วยไหม, และเวลาเจอEventIdซ้ำจะเลือกแถวไหน — ทั้งหมดนี้เป็นการตัดสินใจของ Owner ไม่ใช่ของเอเจนต์ - PDPA / การลบรายบุคคล — partition เป็น append-only และ blob ที่ archive ไปแล้วลบรายคนไม่ได้ ถ้ามีคนใช้สิทธิ์ขอลบข้อมูลส่วนบุคคล ดีไซน์ปัจจุบันตอบไม่ได้ ต้องเลือกระหว่าง retention สั้นพอที่จะยอมรับได้ กับทำเส้น delete-by-UserOid
- log DB แยก instance หรือไม่ —
RequestLogRepositoryใช้AuthDbContextตัวเดียวกับAuditLogsซึ่งขัดกับที่แผนเขียนไว้ว่า log DB ต้องเป็น instance แยกไม่ปนกับ non-prod ที่ dev/SIT/UAT ใช้ร่วมกัน - ตัวเลขที่ยังไม่มีใครวัด — request/วัน, ขนาด body p50/p99, tier ของ Service Bus (Standard 256 KB หรือ Premium 1 MB), PostgreSQL major version และ IOPS tier · สองตัวหลังเป็นตัวชี้ว่า
DETACH PARTITION CONCURRENTLYใช้ได้ไหม (ต้อง PG 14+) และเกณฑ์ claim-check ควรเป็นเท่าไร
ต้องทำก่อน deploy ไม่งั้นพังเงียบ
- 🔴 topic
request-log-eventและ subscriptionLog-RequestLog-Subยังไม่มีจริง — ไม่อยู่ในscripts/provision-servicebus-topics.ps1และไม่อยู่ใน inventory ⇒ deploy ไปตอนนี้ producer ส่งลง topic ที่ไม่มี subscription และ ข้อความหายเงียบ · และ subscription ต้องสร้างด้วยrequiresSession=falseซึ่งแก้ทีหลังไม่ได้ ต้องลบสร้างใหม่ - 🔴
/healthของ LogService รวมทุก check ไว้ที่ endpoint เดียว (MiddlewarePipelineExtensions.cs:115) ⇒ backlog พุ่งหรือ partition หาย จะทำให้/healthตอบ 503 ซึ่งถ้า k8s probe ชี้มาที่นี่ pod จะถูก restart หรือถูกถอดออกจาก service ต้องกรอง 2 check ใหม่ออกจาก probe ก่อน - archive job ยังไม่มี implementation ของ blob store —
IArchiveBlobStoreเป็น port เปล่า และยังไม่ได้เพิ่มAzure.Storage.Blobsเข้า project เพราะการเพิ่ม dependency เข้า repo นี้ต้องขออนุมัติ · job ปิดอยู่เป็นค่าตั้งต้น ถ้าเปิดโดยไม่มี store มันจะ error log แล้วไม่ทำอะไร - ฝั่งหน้าบ้านยังไม่ผ่าน test เต็ม —
nx test shellบน branch ของเราได้ 6 suites / 28 tests fail เทียบกับ baseline บนdevelopmentที่ 5 suites / 21 tests fail (คือ ของเดิมพังอยู่ก่อนแล้ว แต่ของเราเพิ่มมา 1 suite) ต้นเหตุคือlogin.state.spec.tsขาด DI mock ของ service ตัวใหม่ ผมแก้ไฟล์ไปแล้วแต่ยังรัน test ซ้ำไม่จบเพราะถูกสั่งหยุดงาน · ส่วนnx test auth-sdkเทียบ baseline แล้วเท่ากันเป๊ะ (21 fail เหมือนกันทั้งคู่ ของเราเพิ่ม 6 เทสต์ที่ผ่านหมด)
สิ่งที่ค้นพบระหว่างทางและเปลี่ยนขนาดงานฝั่งหน้าบ้าน
เอกสารเก่าบอกว่าฝั่งหน้าบ้าน “ยังไม่มี call site จริง” ซึ่งถูก แต่ของจริงคือ:
@exim/auth-sdkมีของครบเกือบหมดแล้ว — ถอด JWE ขาตอบด้วย ECDH-ES+A256KW/A256GCM ได้ · มีกุญแจ EC ดอกที่สองแยกจากกุญแจลายเซ็นจริง เก็บแบบ non-extractable ใน IndexedDB · interceptor ที่เดิน object หาค่าที่ถูก seal ก็มีอยู่แล้ว- backend ส่ง whole-body มาเป็น
{data:"<JWE>", meta}ไม่ใช่ JWE เปล่า ๆ ⇒ interceptor เดิมเปิดได้อยู่แล้ว งานฝั่ง lib จึงแทบไม่ต้องแก้ production code เลย - 🔴 ช่องที่ตายจริงคือ กุญแจไม่เคยถูกลงทะเบียนกับ Sentinel —
signedUrlPrefixesเป็น[]⇒ ไม่มี request ไหนถูก sign ⇒ 401 ที่เป็น trigger ของregister()ไม่มีทางเกิด ⇒ backend ที่ไปถาม registry หากุญแจผู้รับจะไม่เจอแน่นอน แก้โดยทำให้การลงทะเบียนเกิดขึ้นด้วยตัวเอง ไม่ผูกกับการ sign และไม่บล็อกการเปิดแอป
โค้ดอยู่ที่ไหน
| repo | branch | head | สถานะ |
|---|---|---|---|
Backend_Package | development | d113e24 | 10.24.0 + 10.25.0 merge แล้ว |
Backend_Package | feature/pkg-request-log-capture | ยังไม่ commit | producer 10.26.0 · build/test เขียวทั้ง 5 TFM |
Backend_LogService | MIGRATION | 2665a4f | ตาราง 22 คอลัมน์ · ยัง push ไม่ครบ |
Backend_LogService | feature/log-request-audit-trail | db43d4f + ยังไม่ commit | consumer · 207 tests ผ่าน |
Backend_LogService | feature/log-request-audit-ops | ยังไม่ commit | partition/metric/archive · build เขียว |
Backend_Iac | feature/iac-request-log-topic | ยังไม่ commit | 66 ไฟล์ config |
Frontend_SuperAppLibraryUi | feature/authsdk-whole-body-response | ยังไม่ commit | เพิ่มเฉพาะ spec |
Frontend_HostAppSuperApp | feature/hostapp-whole-body-response | ยังไม่ commit | ลงทะเบียนกุญแจกับ Sentinel |
PR ที่เปิดค้างและ ห้าม merge เอง: Backend_UserService #2871 (ข้อ 5 P0) · Backend_LogService #2885 (Monitor Log — ตอนนี้ PR ตามหลัง branch อยู่ เพราะ commit ใหม่ยังไม่ push)
ลำดับที่ห้ามสลับ ตอนจะเอาขึ้นจริง
- migration ขึ้นก่อน — ตารางต้องมีก่อนที่ consumer จะเขียนอะไรได้
- topic + subscription ต้องถูก provision ก่อน consumer และ ก่อน producer ตัวแรก — topic ที่ไม่มี subscription ทิ้งข้อความเงียบ ๆ
- consumer ขึ้นก่อน publisher
- metric backlog + alert ต้องมีก่อนเปิดใช้จริง — วันนี้ health check เดิมตอบ Healthy แค่ peek ได้ ถ้า consumer ล่มข้อมูลจะหายเงียบเมื่อชน TTL
- blob container + สิทธิ์ของ identity ต้องพร้อมก่อนเปิด capture — ถ้า RBAC ไม่พร้อม body ก้อนแรกที่ใหญ่เกินต้องตกไปเป็น metadata อย่างเดียว ไม่ใช่ทำ request ของผู้ใช้พัง
- ค่อยเปิด flag ของ producer ทีละ service
ที่ยังพิสูจน์ไม่ได้ อย่าอ้างว่าทดสอบแล้ว
- migration ไม่เคยรันกับ PostgreSQL จริง — Docker บนเครื่องที่ทำงานไม่ได้รัน และ repo นี้มีแต่ EF InMemory ⇒ partition,
ON CONFLICTและ BRIN ยังไม่ verify ด้วยการรันจริง ที่ทำได้คือเทียบชื่อ table/column/index ที่ EF generate กับ raw SQL ซึ่งตรงกันทุกตัว และhas-pending-model-changes= none - ยังไม่มีใครวัด performance ของ whole-body encryption จริง ตัวเลขในแผนมาจากการอ่านโค้ด
UseOutputCacheของ Centralized เก็บ plaintext ข้ามผู้ใช้ได้หรือไม่ ยังไม่ verify- การ dedup ข้าม batch ทำไม่ได้โดยธรรมชาติของ schema — unique index ต้องมี partition key อยู่ด้วย และ
ReceivedAtตั้งต่อ batch ⇒ message ที่ถูกส่งซ้ำข้าม batch จะได้แถวที่สอง อันนี้เป็นผลที่ต้องยอมรับ ไม่ใช่บั๊ก แต่ read API ต้อง distinct ตามEventId