Private Docs

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.jsonAtRest.StepData.Enabled=false และ Payload.CreateUser.Enabled=false ทั้งหมด

ค่านี้ bind เข้า singleton ตอน startup ⇒ เปิด flag หรือย้าย LogOnlyEnforce คือการ 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 build0 error (multi-target net462/6/8/9/10)
lib testfail 0 ทุก TFM — net9/net10 1514 · net8 1308 · net462 687
UserService build0 error
UserService testFailed 2 / Total 2641 = baseline เดิม
migrationไม่ค้าง
boot จริงJWKS 200 kid kvdev20260827 · /health Healthy (postgres+redis) · DI error 0
Redis prefixSuperAppPlatform: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 401 SIG_KEY_NOT_FOUND)