Security Uplift — ของใหม่ทั้งหมดอยู่หลัง flag ที่ปิดทุก env (พิสูจน์ 3 จุด)
client-signature middleware, at-rest field encryption + EF value converter, JWKS/client-keys endpoint และ backfill — ทำไม flag ปิดแล้วพฤติกรรมเหมือนเดิมเป๊ะ พร้อมหลักฐานที่ตรวจจริง 3 จุดที่คนมักพลาด รวมถึงเหตุผลว่าทำไม value converter ต้อง unconditional
อัปเดต: 2026-08-31
← กลับหน้าภาพรวม · หน้านี้คือกลุ่ม 2 — ของใหม่ที่ไม่กระทบ business code เพราะ flag ปิด
ของใหม่ในรอบนี้: client-signature middleware · at-rest field encryption + value converter · JWKS + POST /client-keys · backfill
คำถามเดียวที่สำคัญคือ ปิด flag แล้วมั่นใจได้จริงไหมว่าไม่กระทบของเดิม — ตรวจ 3 จุดที่มักพลาด
จุดที่ 1 — flag default เป็น false ทุก env
ClientSignature อยู่ใน base appsettings.json
"ClientSignature": {
"Enabled": false,
"Mode": "LogOnly",
"DebugResponse": false,
"KeyRegistryPrefix": "SuperAppPlatform"
}
FieldEncryption อยู่ใน appsettings.Development/Sit/Uat.json — AtRest.StepData.Enabled=false และ Payload.CreateUser.Enabled=false ทั้งหมด
ค่านี้ bind เข้า singleton ตอน startup ⇒ เปิด flag หรือย้าย LogOnly → Enforce คือการ restart ด้วยค่าใหม่ ไม่ใช่ toggle สด
จุดที่ 2 — EF value converter ผูกแบบ unconditional แต่ fallback เป็น passthrough
จุดนี้ดูน่ากลัวที่สุดเวลาอ่าน diff เพราะ converter ถูกผูกกับ AuthDbContext โดยไม่มี if ครอบ
เหตุผลคือ EF Core cache model ด้วย key (context CLR type, design-time) ซึ่ง ไม่ดู ctor argument ⇒ ถ้าเขียน if (_fieldCipher != null) จะได้ model เดียวที่ compile ครั้งแรกแล้วถูกใช้กับทุก instance ในโปรเซส ไม่ว่า instance นั้นจะถูกสร้างมาพร้อม cipher ตัวไหน
ทางที่ใช้คือผูก converter เสมอ แต่ถ้าไม่มี cipher ให้ตกไปที่ PassthroughFieldCipher.Instance
var atRestConverter = new FieldCipherValueConverter(_fieldCipher ?? PassthroughFieldCipher.Instance);
⇒ flag ปิด = อ่าน/เขียนผ่าน identity cipher = ค่าเท่าเดิมทุกไบต์ และ model shape เหมือนกันทุกที่รวมถึง design-time factory ตอน generate migration ที่ไม่ส่ง cipher มาเลย
column ที่ผูกไว้: User.Phone · UserRestrictedProfile.IdentifierId · FlowInstanceStepData.DataJson · FlowInstanceHistoryStep.DataJson · UserStepRecord.DataJson
จุดที่ 3 — backfill ไม่ auto-run
StepDataFieldEncryptionBackfill และ AtRestFieldEncryptionBackfill เป็น static class ที่ไม่ได้ register ที่ไหนเลย
grep ทั้ง src/ = 0 call site · ไม่ใช่ IHostedService ไม่ใช่ BackgroundService ⇒ ไม่มีทางทำงานเองตอน boot ต้องเรียกเองเท่านั้น
ผลตรวจที่รันจริง
| ผล | |
|---|---|
| lib build | 0 error (multi-target net462/6/8/9/10) |
| lib test | fail 0 ทุก TFM — net9/net10 1514 · net8 1308 · net462 687 |
| UserService build | 0 error |
| UserService test | Failed 2 / Total 2641 = baseline เดิม |
| migration | ไม่ค้าง |
| boot จริง | JWKS 200 kid kvdev20260827 · /health Healthy (postgres+redis) · DI error 0 |
| Redis prefix | SuperAppPlatform:cache:client-key:<oid> — พิสูจน์ว่า KeyRegistryPrefix ทำงาน |
2 test ที่ fail เป็นของเดิมก่อนรอบนี้ — PipelineSelfWarmTest และ ProgramStartupTest
⚠️ ก่อนรัน test ต้อง
export USERSERVICE_COVERAGE_POSTGRES_CONNECTIONไม่งั้นได้ Failed 17 จาก AMLO integration test ที่ตั้งใจ fail เมื่อไม่มี Postgres จริง
กับดักที่ต้องจำตอนเอาไปใช้กับ service อื่น
AddScoped<IClientKeyStore, RedisClientKeyStore>()build ผ่านแต่พัง runtime — ต้องใช้AddClientSignatureStores()KeyRegistryPrefixของทุก service ต้องตรงกัน ไม่งั้น key ที่ register จาก service หนึ่งจะมองไม่เห็นจากอีก service (ทุก signed request 401SIG_KEY_NOT_FOUND)