Private Docs

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 + migrationBackend_LogService branch MIGRATION 2665a4fbuild exit 0 · has-pending-model-changes = none
consumer + batch writerBackend_LogService branch feature/log-request-audit-trailbuild exit 0 · test exit 0 · 207 passed / 0 failed
partition pre-create + backlog/DLQ metric + archive jobBackend_LogService branch feature/log-request-audit-opsbuild exit 0
producer: capture middleware + masking + publisher (10.26.0)Backend_Package branch feature/pkg-request-log-capturebuild exit 0 · test exit 0 · 1627 net10 · 1627 net9 · 1392 net8 · 1329 net6 · 687 net462
config ทุก service ทุก envBackend_Iac branch feature/iac-request-log-topic66 ไฟล์ (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 รอบ สิ่งที่จับได้ไม่ใช่เรื่องสไตล์ แต่เป็นทางที่ข้อมูลหายโดยไม่มีใครรู้:

  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 ด้วยมัน
  2. body ที่เป็น blob pointer ถูก dead-letter ทิ้ง — ซึ่งทิ้งพอดีกับ body ใหญ่ที่ claim-check ตั้งใจจะเก็บ แก้โดยเพิ่มคอลัมน์ RequestBodyBlobUri / ResponseBodyBlobUri แล้วเขียนแถวพร้อม pointer
  3. Mode ที่อยู่นอก enum — deserializer รับค่าตัวเลขที่ไม่รู้จักได้ และ Inline เป็นค่า 0 ของ enum ⇒ producer รุ่นใหม่ที่ส่ง mode ที่สามจะรอดทุกด่านแล้วลงเป็นแถวที่ไม่มีทั้ง byte และ pointer แก้โดยตีกลับเป็น invalid payload
  4. timestamp ที่ไม่มี offset ถูกอ่านด้วยเวลาท้องถิ่นของ pod — 10:00 กลายเป็น 03:00Z บนเครื่องไทย แปลว่าไทม์ไลน์ของการสืบสวนเลื่อนตาม timezone ของ pod ที่บังเอิญหยิบ message แก้โดยอ่านค่าที่ไม่มี offset เป็น UTC
  5. partition bound เป็น bare date — PostgreSQL resolve ครั้งเดียวตอนรัน DDL ด้วย TimeZone ของ session ⇒ migration ที่ขยาย window ทีหลังถ้ารันคนละ timezone จะเหลือช่องโหว่ และทุกแถวที่ตกในช่องนั้น insert ไม่ผ่าน แก้เป็น TIMESTAMPTZ '... 00:00:00+00' ทั้ง 12 ก้อน
  6. 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 ตัดสินก่อน ไม่มีใครทำแทนได้

  1. read API ของ request log — ตอนนี้ ยังไม่มีเลย AuditLogController มีแค่ endpoint ของ audit-logs การจะกั้นด้วย role จึงยังทำไม่ได้ เพราะ policy ที่ไม่มี endpoint คือ config ที่ตายแล้ว การสร้าง read API แปลว่าต้อง mint contract สาธารณะใหม่: route, filter ที่รองรับ, คืน body มาในลิสต์ด้วยไหม, และเวลาเจอ EventId ซ้ำจะเลือกแถวไหน — ทั้งหมดนี้เป็นการตัดสินใจของ Owner ไม่ใช่ของเอเจนต์
  2. PDPA / การลบรายบุคคล — partition เป็น append-only และ blob ที่ archive ไปแล้วลบรายคนไม่ได้ ถ้ามีคนใช้สิทธิ์ขอลบข้อมูลส่วนบุคคล ดีไซน์ปัจจุบันตอบไม่ได้ ต้องเลือกระหว่าง retention สั้นพอที่จะยอมรับได้ กับทำเส้น delete-by-UserOid
  3. log DB แยก instance หรือไม่RequestLogRepository ใช้ AuthDbContext ตัวเดียวกับ AuditLogs ซึ่งขัดกับที่แผนเขียนไว้ว่า log DB ต้องเป็น instance แยกไม่ปนกับ non-prod ที่ dev/SIT/UAT ใช้ร่วมกัน
  4. ตัวเลขที่ยังไม่มีใครวัด — 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 และ subscription Log-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 storeIArchiveBlobStore เป็น 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 เลย
  • 🔴 ช่องที่ตายจริงคือ กุญแจไม่เคยถูกลงทะเบียนกับ SentinelsignedUrlPrefixes เป็น [] ⇒ ไม่มี request ไหนถูก sign ⇒ 401 ที่เป็น trigger ของ register() ไม่มีทางเกิด ⇒ backend ที่ไปถาม registry หากุญแจผู้รับจะไม่เจอแน่นอน แก้โดยทำให้การลงทะเบียนเกิดขึ้นด้วยตัวเอง ไม่ผูกกับการ sign และไม่บล็อกการเปิดแอป

โค้ดอยู่ที่ไหน

repobranchheadสถานะ
Backend_Packagedevelopmentd113e2410.24.0 + 10.25.0 merge แล้ว
Backend_Packagefeature/pkg-request-log-captureยังไม่ commitproducer 10.26.0 · build/test เขียวทั้ง 5 TFM
Backend_LogServiceMIGRATION2665a4fตาราง 22 คอลัมน์ · ยัง push ไม่ครบ
Backend_LogServicefeature/log-request-audit-traildb43d4f + ยังไม่ commitconsumer · 207 tests ผ่าน
Backend_LogServicefeature/log-request-audit-opsยังไม่ commitpartition/metric/archive · build เขียว
Backend_Iacfeature/iac-request-log-topicยังไม่ commit66 ไฟล์ config
Frontend_SuperAppLibraryUifeature/authsdk-whole-body-responseยังไม่ commitเพิ่มเฉพาะ spec
Frontend_HostAppSuperAppfeature/hostapp-whole-body-responseยังไม่ commitลงทะเบียนกุญแจกับ Sentinel

PR ที่เปิดค้างและ ห้าม merge เอง: Backend_UserService #2871 (ข้อ 5 P0) · Backend_LogService #2885 (Monitor Log — ตอนนี้ PR ตามหลัง branch อยู่ เพราะ commit ใหม่ยังไม่ push)

ลำดับที่ห้ามสลับ ตอนจะเอาขึ้นจริง

  1. migration ขึ้นก่อน — ตารางต้องมีก่อนที่ consumer จะเขียนอะไรได้
  2. topic + subscription ต้องถูก provision ก่อน consumer และ ก่อน producer ตัวแรก — topic ที่ไม่มี subscription ทิ้งข้อความเงียบ ๆ
  3. consumer ขึ้นก่อน publisher
  4. metric backlog + alert ต้องมีก่อนเปิดใช้จริง — วันนี้ health check เดิมตอบ Healthy แค่ peek ได้ ถ้า consumer ล่มข้อมูลจะหายเงียบเมื่อชน TTL
  5. blob container + สิทธิ์ของ identity ต้องพร้อมก่อนเปิด capture — ถ้า RBAC ไม่พร้อม body ก้อนแรกที่ใหญ่เกินต้องตกไปเป็น metadata อย่างเดียว ไม่ใช่ทำ request ของผู้ใช้พัง
  6. ค่อยเปิด 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