เข้ารหัสรายฟิลด์ หรือ เข้ารหัสทั้ง payload — เลือกทางไหน
เทียบ 2 ทางเลือกของชั้นเข้ารหัสระหว่างทาง พร้อมผลกระทบรายมิติ ความเห็นของ advisor รายมิติ และเคสที่ต้องคิดเผื่อ (service อื่นมาเรียก · partner ภายนอก · mobile · batch · debug production) พร้อมทางออกของแต่ละเคส — หน้าสำหรับระดมความคิดก่อนตัดสิน
อัปเดต: 2026-09-01
หน้านี้ไว้ ระดมความคิดก่อนตัดสิน ไม่ใช่ข้อกำหนด — ข้อสรุปกับคำถามที่ยังเปิดอยู่ท้ายหน้า
1. กำลังเลือกอะไร
ข้อมูลอ่อนไหว (วันเกิด · สัญชาติ · ที่อยู่ · เบอร์โทร) วิ่งจากหน้าเว็บมาหลังบ้าน ระหว่างทางมีคนกลางที่ ถอด TLS ได้ อยู่หลายจุด (APIM · ingress · proxy ขององค์กร) เราจะเข้ารหัสทับอีกชั้นเพื่อไม่ให้จุดพวกนั้นอ่านออก — คำถามคือเข้ารหัสแค่ไหน
ทาง A — เข้ารหัสรายฟิลด์
JSON ยังเป็น JSON ปกติ แค่ บางค่า กลายเป็นก้อนเข้ารหัส
{
"email": "somchai@example.com",
"firstNameTH": "สมชาย",
"lastNameTH": "ใจดี",
"birthDate": "eyJhbGciOiJSU0EtT0FFUC0yNTYi...StUvWx",
"nationality": "eyJhbGciOiJSU0EtT0FFUC0yNTYi...KpQrSt",
"mailingAddress": "eyJhbGciOiJSU0EtT0FFUC0yNTYi...MnOpQr"
}
ทาง B — เข้ารหัสทั้งก้อน
body ทั้งอันกลายเป็นข้อความเข้ารหัสเส้นเดียว ไม่เหลือรูป JSON ให้เห็น
eyJhbGciOiJSU0EtT0FFUC0yNTYi....AbCdEf.GhIjKl.MnOpQr.StUvWx
ทั้ง 2 ทาง หลังบ้านถอดให้เรียบร้อยก่อนถึงโค้ด business ⇒ คนเขียน handler ไม่ต้องรู้เรื่อง และค่าที่บันทึกลงฐานข้อมูลเหมือนกันทุกประการ
2. เทียบรายมิติ + ผลกระทบ + ความเห็น advisor
✓ = ทางที่ advisor เลือกในมิตินั้น
| # | มิติ | ทาง A — รายฟิลด์ | ทาง B — ทั้งก้อน | ผลกระทบ — ใครเจ็บ เจ็บตอนไหน | advisor (FABLE) เลือก |
|---|---|---|---|---|---|
| 1 | คนกลางเห็นอะไร | เห็นโครงทั้งหมด: ชื่อ field, field ไหนมี/ไม่มี, ความยาวของค่าที่เข้ารหัส และ ค่าที่ไม่ได้ mark ทั้งหมด (อีเมล ชื่อ) · ซ่อนเฉพาะค่าที่ mark | ✓ เห็นแค่ URL กับก้อนทึบ | A: ถ้าจำแนกข้อมูลถูก = พอ · แต่ถ้ามี field อ่อนไหวที่ลืม mark = รั่วโดยไม่มีใครรู้ B: ปิดหมดโดยไม่ต้องตัดสินใจอะไรเลย | ทาง B ปิดสนิทโดยไม่ต้องพึ่งว่าจำแนกข้อมูลถูก — A ปลอดภัยเท่าที่ list ถูกเท่านั้น |
| 2 | field ใหม่ที่เพิ่มทีหลัง | ค่าตั้งต้นคือ ไม่เข้ารหัส — ต้องมีคนไป mark ทุกครั้ง ลืม = plaintext เงียบ ๆ ไม่มี error | ✓ ครอบให้เองอัตโนมัติ ลืมไม่ได้ | A: ความปลอดภัยเสื่อมลงตามเวลาถ้าไม่มี process กำกับ · เจ็บตอน 6 เดือนถัดไป ไม่ใช่วันนี้ B: ไม่มีปัญหานี้เลย | ทาง B โครงสร้างชนะวินัย — ความเสี่ยง “ลืม” ของ A โตขึ้นตามเวลา |
| 3 | คนเรียกที่ยังไม่พร้อม | ✓ ส่ง plaintext มาได้ ระบบรับทั้ง 2 แบบ ⇒ ของเดิมไม่พัง | ต้องเข้ารหัสทั้งก้อนหรือไม่ก็ไม่ต้องเลย — ครึ่ง ๆ ไม่ได้ | A: เปิดใช้ได้โดยไม่ต้องรอใคร B: ต้องนัดวันเปลี่ยนพร้อมกันทุกคนที่เรียก endpoint นั้น | ทาง A รับทั้ง 2 แบบรายฟิลด์ คือทางเดียวที่ไม่หักของเดิม |
| 4 | การทยอยเปลี่ยน | ✓ ทีละ field ได้ — เปิดวันเกิดก่อน แล้วค่อยตามด้วยที่อยู่ | ทั้ง endpoint พร้อมกัน | A: ย้อนกลับง่าย เสี่ยงต่อรอบต่ำ B: ทุกรอบคือ big bang | ทาง A เปิดทีละ field ย้อนกลับทีละ field — B คือ big bang ทุกรอบ |
| 5 | งานฝั่งคนเรียก | ต้องรู้ว่า field ไหนบ้าง ที่ต้องเข้ารหัส และต้องตามให้ทันเมื่อรายการเปลี่ยน | ✓ เข้ารหัสทั้งก้อนจบ ไม่ต้องรู้ว่าข้างในมีอะไร | A: รายการ field กลายเป็น contract ที่ต้อง sync กัน 2 ฝั่ง — จุดที่หลุดง่ายเวลามีทีมใหม่มาต่อ B: ฝั่งคนเรียก ง่ายกว่า | ทาง B ก้อนเดียวจบ ไม่ต้อง sync รายการ field ข้ามทีม |
| 6 | ขนาดที่บวมขึ้น | ทุก field ที่เข้ารหัสพกกุญแจของตัวเองมาด้วย ⇒ บวม ~0.5 KB ต่อ field · 5 field ≈ +2.5 KB | ✓ บวมครั้งเดียว ~0.5 KB ไม่ว่าข้างในจะมีกี่ field | A: endpoint ที่ mark หลาย field payload โตขึ้นชัด · จะหนักกับมือถือ/เน็ตช้า B: คงที่ | ทาง B overhead คงที่ — ของ A โตเป็นเส้นตรงตามจำนวน field |
| 7 | debug ตอนเกิดเหตุจริง | ✓ ยังเห็น field ที่ไม่อ่อนไหวใน log/trace ⇒ ตามรอยได้ว่า request ไหนของใคร | มืดสนิท — เห็นแค่ว่ามี request เข้ามา | A: ทีม support ยังทำงานได้ B: 🔴 เกิด incident แล้วสืบไม่ได้เลย ต้องวางเครื่องมืออื่นทดแทนก่อนใช้จริง | ทาง A ความมืดของ B คือค่า ops ที่ต้องจ่ายทุกวัน ไม่ใช่จ่ายครั้งเดียว |
| 8 | สัญญาของ API (Swagger) ⚠️ Swagger สร้างจากการอ่านโครง DTO ตอนแอปสตาร์ท ไม่ได้วิ่งผ่าน middleware ⇒ ทั้ง 2 ทางโชว์ schema ได้เหมือนกัน ประเด็นจริงคือ เอกสารโกหกแค่ไหนเทียบกับของที่ส่งจริง | ✓ โครงถูกทั้งหมด · โกหกเฉพาะ ชนิดของ field ที่ mark (เช่นโชว์ birthDate เป็นวันที่ แต่ของจริงส่งเป็นข้อความเข้ารหัส) · ปุ่ม Try it out ยังใช้ได้จริงเพราะระบบรับ plaintext ด้วย | โชว์โครงข้างในได้ แต่ของที่ส่งจริงเป็นข้อความก้อนเดียว ⇒ ไม่ตรงกับความจริงทั้งอัน · ปุ่ม Try it out ส่ง JSON ธรรมดา ซึ่งจะผ่านหรือไม่ผ่านขึ้นกับว่าเปิดรับ plaintext อยู่ไหม | A: คนมาต่อใหม่อ่านเองรู้เรื่องเกือบหมด เหลือ 3 field ที่ต้องกำกับ B: ต้องเขียนทับคำอธิบาย body ทั้งอัน ไม่งั้นคนอ่านทำตามแล้วยิงไม่ผ่าน ทั้งคู่: ต้องแต่ง Swagger เพิ่มถึงจะตรงความจริง 100% แต่ของ A แต่งน้อยกว่ามาก | ทาง A เอกสารเพี้ยนน้อยกว่า (เฉพาะ 3 field ไม่ใช่ทั้ง body) และ Try it out ยังใช้งานได้ — ชนะห่างน้อยกว่าที่เคยเขียนไว้ |
| 9 | gateway ที่ต้องอ่าน body (validate · route · เก็บสถิติ) | ✓ ทำได้กับ field ที่ไม่ได้เข้ารหัส | ทำไม่ได้เลย | A: ของเดิมที่อ่าน body อยู่ยังรอด B: ต้องไล่ดูว่ามี policy ไหนพึ่ง body อยู่บ้าง ก่อนเปิด | ทาง A policy เดิมที่พึ่ง body ยังรอด |
| 10 | ความเสียหายเมื่อถอดไม่ได้ | ปฏิเสธทั้ง request (ปลอดภัยไว้ก่อน) | เหมือนกัน | เท่ากัน | เสมอ ปลอดภัยไว้ก่อนเท่ากันทั้งคู่ |
| 11 | การสลับค่าข้ามคำขอ | แต่ละ field เป็นก้อนอิสระ ⇒ คนที่เซ็นคำขอได้ เอาก้อนที่ดักได้จากคำขออื่นมาแปะได้ | ✓ ทั้งก้อนผูกกันอยู่แล้ว สลับเฉพาะบางค่าไม่ได้ | A: ต้องพึ่งชั้นลายเซ็นเป็นตัวกัน · ถ้าวันหน้าเอาไปใช้กับ endpoint ที่ไม่มีลายเซ็น ช่องนี้จะเปิด B: ปิดโดยโครงสร้าง | ทาง B ปิดโดยโครงสร้าง — A ต้องพึ่งชั้นลายเซ็นซึ่งเป็นเงื่อนไขภายนอก |
| 12 | ข้อมูลขากลับ (response) | ✓ ทำได้ตามปกติ เข้ารหัสเฉพาะ field ที่อ่อนไหว | 🔴 ทำไม่ได้ตรง ๆ — ถ้าเข้ารหัสทั้งก้อน จะทับ error message กับ status ที่ระบบอื่นต้องอ่านด้วย | B: ขากลับต้องออกแบบใหม่ทั้งชุด ไม่ใช่แค่กลับด้าน | ทาง A ทำได้จริงวันนี้ — ขากลับของ B ยังไม่มีทางไปและชนกฎ error body |
คำตัดสินรวมของ advisor
เลือกทาง A — แต่ ไม่ใช่เพราะนับแต้ม (A ชนะ 6 · B ชนะ 5 · เสมอ 1 ซึ่งแทบเท่ากัน)
เหตุผลคือ น้ำหนักของมิติที่แพ้ไม่เท่ากัน
- มิติที่ B แพ้ เป็น dealbreaker เชิงปฏิบัติ — ข้อ 3 (หักทุกคนที่เรียกอยู่วันนี้) กับข้อ 12 (ขากลับยังไม่มีทางไป) สองข้อนี้ไม่มี mitigation ราคาถูก
- มิติที่ A แพ้ มี ทางปิดที่ถูกกว่ามาก — ข้อ 2 (ลืม mark) ปิดด้วยด่านตรวจอัตโนมัติตาม §4 · ข้อ 1 กับ 11 พึ่งการจำแนกข้อมูลกับชั้นลายเซ็นที่มีอยู่แล้ว
สรุปผลกระทบเป็นภาพเดียว
- ทาง A เจ็บช้าแต่เจ็บเรื่อย ๆ — วันแรกทุกอย่างราบรื่น แต่ความปลอดภัยขึ้นกับวินัยของคนที่มา mark field ในอีกหลายปีข้างหน้า
- ทาง B เจ็บหนักวันแรกแต่จบ — ต้องนัดทุกคนเปลี่ยนพร้อมกัน ต้องยอมมืดตอน debug ต้องออกแบบขากลับใหม่ แต่พอผ่านแล้วไม่มีช่องให้ลืม
แล้วข้างนอกเขาทำกันแบบไหน
| แบบ | ใครทำ | หมายเหตุ |
|---|---|---|
| ไม่ทำเลย — พึ่ง TLS + การยืนยันตัวตนอย่างเดียว | ส่วนใหญ่ที่สุด — API สาธารณะระดับโลกแทบทั้งหมด (Stripe · GitHub · ผู้ให้บริการคลาวด์เจ้าใหญ่) | ถือว่า TLS พอแล้ว และไม่ยอมแลกความซับซ้อนกับ gateway/observability ที่ต้องเสียไป |
| รายฟิลด์ (ทาง A) | เมื่อ มีข้อบังคับ ให้ปกป้องข้อมูลบางตัวเป็นพิเศษ — ต้นแบบคือ มาตรฐานบัตรเครดิต (PCI) ที่เข้ารหัสเลขบัตรตั้งแต่เครื่องรูดจนถึงผู้ประมวลผล เพื่อไม่ให้ระบบของร้านค้าเห็นเลขบัตรเลย | เป็นรูปแบบที่พบบ่อยที่สุดเมื่อมีการทำจริง เพราะ gateway กับเครื่องมือ monitoring ยังทำงานได้ |
| ทั้งก้อน (ทาง B) | งาน ธนาคารคุยกับธนาคาร และ payment gateway ฝั่งเอเชียบางเจ้า (อินเดีย/จีน/เอเชียตะวันออกเฉียงใต้) มักบังคับให้เข้ารหัสทั้ง payload | มักเป็นกรณีที่ “API” จริง ๆ แล้วคือท่อส่งข้อความ ไม่ใช่ REST API ที่มีคนอื่นมาใช้ต่อ |
| ผสม — เซ็นทั้งข้อความ + เข้ารหัสเฉพาะ field สำคัญ | 🔵 รูปแบบที่นิยมที่สุดเมื่อองค์กรตัดสินใจทำจริง | ตรงกับที่ระบบนี้มีอยู่แล้ว — เรามีชั้นลายเซ็นครอบทั้ง body อยู่แล้ว บวกเข้ารหัสรายฟิลด์ = แบบผสมพอดี |
ข้อมูลที่น่าสนใจที่สุดในตารางนี้: มาตรฐาน FAPI 2.0 ของวงการธนาคารเปิด ตัดการเซ็นและเข้ารหัส request แบบเดิมออกจากตัวมาตรฐานหลัก แล้วใช้วิธีส่งคำขอทางช่องหลังบ้าน (PAR) แทน — แต่ยังเก็บการเซ็นไว้เป็น profile เสริมสำหรับกรณีที่ต้องการปฏิเสธความรับผิดชอบไม่ได้ · และ profile เสริมนั้นระบุชัดว่าไม่ต้องเข้ารหัส · รายละเอียดเต็มอยู่ใน §2.1 ข้างล่าง
⇒ สรุป: ถ้าเทียบกับข้างนอก การที่เราทำ ลายเซ็น + เข้ารหัสรายฟิลด์ ถือว่าอยู่ในกลุ่มที่ทำมากกว่าค่าเฉลี่ยอยู่แล้ว · การขยับไป ทั้งก้อน จะทำให้เราอยู่ในกลุ่มที่แคบลงมาก ซึ่งส่วนใหญ่เป็นงานท่อส่งข้อความระหว่างธนาคาร ไม่ใช่ API ที่มีหลายฝ่ายมาต่อ
2.1 TLS กับ mTLS ต่างกันยังไง
| TLS (ที่ใช้กันทั่วไป) | mTLS (TLS สองทาง) | |
|---|---|---|
| ใครพิสูจน์ตัวตน | ฝั่ง server ฝ่ายเดียว — เบราว์เซอร์ตรวจว่าใบรับรองของเว็บถูกต้องไหม | ทั้งสองฝั่ง — server ตรวจใบรับรองของ client ด้วย |
| client เป็นใครรู้ตอนไหน | รู้ทีหลัง ที่ชั้นแอป (รหัสผ่าน · token · API key) | รู้ตั้งแต่ก่อนส่งข้อมูลแม้แต่ไบต์เดียว |
| ใช้กับอะไร | เว็บทั่วไป — เพราะแจกใบรับรองให้ผู้ใช้ทุกคนไม่ไหว | ระบบคุยกับระบบ — จำนวนคู่สนทนาจำกัด แจกใบรับรองได้ |
| ได้อะไรเพิ่ม | เข้ารหัสระหว่างทาง + รู้ว่าคุยกับ server ตัวจริง | เพิ่มการยืนยันตัวตน client + ใช้ ผูก token เข้ากับใบรับรอง ได้ ⇒ ขโมย token ไปใช้ต่อไม่ได้ถ้าไม่มีกุญแจส่วนตัวคู่กัน |
2.2 FAPI 2.0 มีอะไรบ้าง และเราขาดอะไร
FAPI (Financial-grade API) เป็นชุดมาตรฐานของ OpenID Foundation สำหรับ API ที่ต้องการความปลอดภัยระดับการเงิน มี 3 เอกสาร
| เอกสาร | สถานะ | เนื้อหา |
|---|---|---|
| FAPI 2.0 Security Profile | Final · 22 ก.พ. 2025 | ตัวหลัก — ข้อบังคับเรื่องการขอและใช้ token |
| FAPI 2.0 Attacker Model | — | นิยามว่ากันภัยแบบไหนบ้าง |
| FAPI 2.0 Message Signing | Final · ก.ย. 2025 | เสริม ไม่บังคับ — เซ็นข้อความเพื่อให้ปฏิเสธความรับผิดชอบไม่ได้ · ระบุชัดว่าไม่ต้องเข้ารหัส |
เทียบข้อบังคับกับของที่เรามี
| # | FAPI 2.0 Security Profile บังคับ | เรามีไหม | ช่องว่าง |
|---|---|---|---|
| 1 | token ต้องผูกกับผู้ถือ (sender-constrained) ด้วย mTLS หรือ DPoP — ห้ามออก bearer token ธรรมดา | ❌ เป็น bearer token | 🔴 ช่องว่างใหญ่สุด — แต่ดูหมายเหตุข้างล่าง |
| 2 | client ยืนยันตัวตนด้วย private_key_jwt หรือ mTLS เท่านั้น | ❌ ใช้ client secret (ยืนยันแล้วในโค้ด Sentinel — TokenRefreshService กับ GraphTokenProvider ส่ง client_secret) | 🔴 ต้องเปลี่ยนไปใช้กุญแจแทนรหัสลับ |
| 3 | PAR — ส่งคำขอ authorize ทางช่องหลังบ้านก่อน และต้องปฏิเสธคำขอที่ไม่ผ่าน PAR | ❓ ยังไม่ยืนยัน | ขึ้นกับว่า Entra รองรับไหม — เช็คที่ .well-known/openid-configuration ของ tenant ว่ามี pushed_authorization_request_endpoint หรือเปล่า |
| 4 | PKCE ด้วยวิธี S256 | ✅ มีอยู่แล้ว — Sentinel ใช้ AddOpenIdConnect มาตรฐานและไม่ได้ปิด UsePkce (ค่าตั้งต้นของ framework คือเปิด) | ไม่มีช่องว่าง |
| 5 | iss ในคำตอบ (กันสลับผู้ให้บริการ) | ❓ ยังไม่ยืนยัน | เช็คว่า Entra ส่ง iss กลับมาไหม และเราตรวจหรือเปล่า |
| 6 | TLS 1.2 ขึ้นไป + ชุดรหัสตาม BCP 195 | ❓ น่าจะผ่าน แต่ยังไม่ได้ตรวจ cipher จริง | ตรวจที่ระดับ gateway/ingress |
| 7 | ใช้ authorization code flow ห้าม implicit | ✅ ใช้ code flow อยู่แล้ว | ไม่มีช่องว่าง |
หมายเหตุสำคัญของข้อ 1 — อย่าอ่านว่า “เราสอบตกข้อใหญ่สุด”
สถาปัตยกรรมของเราเก็บ token ไว้ฝั่ง server ทั้งหมด (คุกกี้เป็นแค่ตัวชี้ ไม่มี token อยู่ข้างใน) ⇒ ภัยที่ข้อ 1 ตั้งใจจะกัน คือ “token ถูกขโมยจากเบราว์เซอร์แล้วเอาไปใช้ต่อ” ถูกตัดออกไปเยอะแล้วโดยโครงสร้าง เพราะ token ไม่เคยไปถึงเบราว์เซอร์เลย
สิ่งที่ยังเหลือคือ คุกกี้ถูกขโมย ไม่ใช่ token ถูกขโมย ⇒ ช่องว่างจริงเล็กกว่าที่ตารางทำให้ดู · แนวทางที่เราใช้ (เก็บ token ที่ server แล้วให้เบราว์เซอร์ถือแค่ตัวชี้) ตรงกับข้อเสนอแนะของเอกสาร OAuth 2.0 for Browser-Based Applications (ยังเป็นร่าง) อยู่แล้ว
ถ้าจะเดินตาม FAPI จริง เรียงตามความคุ้ม
- เปลี่ยน client secret เป็น
private_key_jwt(ข้อ 2) — ทำได้ฝั่งเราเป็นหลัก ไม่ต้องรอ Entra รองรับอะไรใหม่ และตัดความเสี่ยงรหัสลับรั่วออกไปเลย - เช็คว่า Entra รองรับ PAR ไหม (ข้อ 3) — ถ้ารองรับ เปิดได้ค่อนข้างตรงไปตรงมา
- ผูก token กับผู้ถือด้วย DPoP (ข้อ 1) — งานใหญ่สุด และคุ้มน้อยสุดสำหรับเรา เพราะโครง server-side token ตัดภัยหลักไปแล้ว
แหล่งอ้างอิง
- FAPI 2.0 Security Profile — Final, 22 ก.พ. 2025 (ข้อบังคับข้อ 1-7 ในตารางมาจากเอกสารนี้)
- FAPI 2.0 Message Signing — Final, ก.ย. 2025 (profile เสริม ที่ระบุว่าไม่ต้องเข้ารหัส)
- OpenID Foundation — FAPI Working Group
- RFC ที่ FAPI อ้างถึง: RFC 8705 (mTLS + token ผูกใบรับรอง) · RFC 9449 (DPoP) · RFC 9126 (PAR) · RFC 7636 (PKCE) · RFC 9207 (
iss) · BCP 195 (การใช้ TLS อย่างปลอดภัย)
⚠️ สิ่งที่ยังไม่ได้ยืนยัน: สถานะการรองรับ PAR และ DPoP ของ Microsoft Entra — ค้นแล้วยังไม่เจอเอกสารทางการที่ชี้ขาด ต้องตรวจจาก .well-known/openid-configuration ของ tenant เราเอง
3. เคสที่ต้องคิดเผื่อ
ไม่ได้มีแต่เบราว์เซอร์ของเราเองที่เรียก API — เคสพวกนี้ต้องมีคำตอบก่อนตัดสิน
| เคส | ทาง A เจออะไร | ทาง B เจออะไร | ทางออกที่เสนอ |
|---|---|---|---|
| service อื่นในบ้านเรียกกันเอง (ไม่มีเบราว์เซอร์ ไม่มี WebCrypto) | ส่ง plaintext มาได้เลย ไม่ต้องทำอะไร | ต้องเขียน crypto ฝั่ง .NET เองทุก service ที่เรียก | แยกเป็นเส้นของภายในต่างหาก — ดู §3.1 ข้างล่าง |
| partner ภายนอกเรียกเข้ามา | บอกเขาว่าต้องเข้ารหัส field ไหนบ้าง + ต้องตามเมื่อรายการเปลี่ยน | บอกว่า “เข้ารหัสทั้งก้อน” จบ อธิบายง่ายกว่ามาก | แยกเป็นเส้นของ partner ต่างหาก — แต่ “ไม่เข้ารหัส” มีเงื่อนไข ดู §3.2 |
| QA · Postman · ทดสอบมือ | ทดสอบ field ที่ไม่เข้ารหัสได้ตามปกติ | ต้องมีเครื่องมือเข้ารหัสก่อนถึงจะยิงได้เลยสักครั้ง | เตรียม collection ที่เข้ารหัสให้อัตโนมัติ · ทาง B ต้องมีตั้งแต่วันแรก ไม่งั้น QA ทำงานไม่ได้ |
| งานเบื้องหลัง · batch · sync จากระบบ HR | ไม่กระทบ ส่ง plaintext ตามเดิม | ต้องแก้ทุกตัวที่ยิงเข้า endpoint นั้น | หา caller ให้ครบก่อนเปิด — ตัวที่มักลืมคือ cronjob กับ script ของ ops |
| หน้าเว็บเวอร์ชันเก่าที่ยังค้างในเครื่องคน | ยังใช้งานได้ | พังทันทีที่เปิด | ทาง B ต้องมีช่วงรับทั้ง 2 แบบอยู่ดี ⇒ ข้อดีเรื่อง “ลืมไม่ได้” หายไปในช่วงเปลี่ยนผ่าน |
| เพิ่ม field อ่อนไหวใหม่ในอนาคต | ต้องมีคนไป mark · ลืมแล้วไม่มีสัญญาณเตือน | ครอบเอง | ทาง A ต้องมี ด่านตรวจอัตโนมัติ ว่า field ที่จำแนกว่าอ่อนไหวถูก mark ครบ ไม่ใช่ฝากไว้กับ reviewer |
| เกิดปัญหาบน production ต้องสืบ | ตามรอยจาก field ที่ไม่อ่อนไหวได้ | ต้องพึ่ง trace id อย่างเดียว | ทาง B ต้องลง correlation id ที่ไม่ได้เข้ารหัส ไว้ที่ header ให้ครบก่อน ไม่งั้นสืบไม่ได้เลย |
3.1 แยกเส้นสำหรับภายใน — เห็นด้วย แต่ยังไม่ใช่ตอนนี้
ท่าที่เสนอ: แยก route เป็น 2 เส้น (เส้นข้างนอกสำหรับหน้าเว็บ · เส้นข้างในสำหรับ service ด้วยกัน) แล้ววิ่งเข้า business logic ตัวเดียวกัน
ปรับนิดเดียวจากที่คุณเสนอ: ควรเป็น 2 action บาง ๆ แยกกัน แล้วส่งคำสั่งตัวเดียวกันเข้าไป ไม่ใช่ action เดียวแปะ 2 route — เพราะสิ่งที่ทำให้ 2 เส้นต่างกัน (ใครเข้าได้ · ต้องเซ็นไหม · field ไหนเข้ารหัส) เป็นของที่ผูกกับ action ไม่ใช่ผูกกับ route ⇒ action เดียวจะตั้งนโยบายต่างกันไม่ได้เลย · ส่วน business logic ยังใช้ตัวเดียวกัน ไม่มีโค้ดซ้ำ
| ได้อะไร | ต้องระวังอะไร |
|---|---|
| เส้นข้างนอก บังคับเข้ารหัสได้จริง ไม่ต้องเปิดรับ plaintext ค้างไว้ตลอดกาล | 🔴 เส้นข้างในต้องออกไปข้างนอกไม่ได้จริง ๆ — ต้องกันด้วยระดับเครือข่าย (ไม่ลงทะเบียนใน gateway เลย) ไม่ใช่กันด้วยความตั้งใจ · ถ้าหลุด = มีประตูหลังที่ข้ามการเข้ารหัสทั้งหมด |
| ตั้งวิธียืนยันตัวตนคนละแบบได้ (ข้างใน = API key/mTLS · ข้างนอก = token ของผู้ใช้ + ลายเซ็น) | มี 2 เส้นให้ดูแล — การตรวจความถูกต้องของข้อมูลต้องไม่หลุดจากกัน |
| แยก log แยกสถิติได้ว่า traffic มาจากทางไหน | เอกสารต้องแยกกลุ่ม ไม่งั้นคนนอกไปเจอเส้นข้างใน |
คำตอบสำหรับ “เอาแบบง่ายสุด”: ตอนนี้ยังไม่ต้องแยก — ใช้เส้นเดียวที่รับได้ทั้ง plaintext และแบบเข้ารหัสไปก่อน เพราะยังไม่มี service ไหนในบ้านยิงเส้นนี้จริง อย่าสร้างประตูรอคนที่ยังไม่มา
แยกเมื่อไหร่: ตอนที่จะ บังคับ ให้ทุก request เข้ารหัส เพราะ “บังคับ” กับ “รับ plaintext ด้วยก็ได้” อยู่บนเส้นเดียวกันไม่ได้
3.2 แยกเส้นสำหรับ partner — เห็นด้วยกับโครง แต่ “ไม่เข้ารหัส” ต้องมีเงื่อนไข
เห็นด้วย ว่าควรมีเส้นของ partner แยกต่างหาก — คนละสัญญา คนละรอบเวลาปล่อยของ คนละวิธียืนยันตัวตน
แต่เหตุผล “เป็น service-to-service อยู่แล้ว เลยไม่ต้องเข้ารหัส” ยังไม่พอ — เพราะ partner อยู่ข้างนอก ⇒ traffic ของเขา วิ่งผ่าน gateway ตัวเดียวกับที่เราไม่ไว้ใจจนต้องสร้างชั้นนี้ขึ้นมา ถ้าเขาส่งเลขบัตร/วันเกิดเป็น plaintext ผ่านเส้นนั้น เท่ากับเจาะรูข้าง ๆ ประตูที่เพิ่งล็อกไป
จะให้เส้น partner เป็น plaintext ได้ ต้องจริงข้อใดข้อหนึ่ง:
| # | เงื่อนไข | แปลว่า |
|---|---|---|
| ก | เส้นนั้น ไม่ผ่านคนกลางที่เราไม่ไว้ใจ | ต่อกันแบบ private link / mTLS ที่ไม่ถูกถอดกลางทาง — ไม่ใช่วิ่งผ่าน gateway ปกติ · 🔴 “เปิด mTLS ที่ gateway” ไม่นับ ดูเหตุผลใน §2.1 |
| ข | ข้อมูลที่ partner ส่ง ไม่ใช่ชุดอ่อนไหวเดียวกัน | ถ้าเขาส่งแค่รหัสอ้างอิง/สถานะ ไม่มีเลขบัตรหรือวันเกิด ก็ไม่มีอะไรให้ปกป้อง |
⇒ ถ้าข้อใดข้อหนึ่งจริง = ไม่ต้องเข้ารหัส จบ · ถ้าไม่จริงทั้งคู่ เส้น partner ก็ต้องเข้ารหัสเหมือนกัน (ซึ่งทาง B อธิบายให้ partner ฟังง่ายกว่า — ดูตารางข้างบน)
4. ข้อเสนอ
ทาง A (รายฟิลด์) เหมาะกว่าถ้าโจทย์คือ “เริ่มใช้จริงได้เร็ว โดยไม่หักคนที่เรียกอยู่” — โดยเฉพาะถ้ามี partner ภายนอกหรือ service อื่นที่เราบังคับตารางเวลาเขาไม่ได้ · และเป็นแบบที่ตรงกับแนวปฏิบัติของอุตสาหกรรมมากกว่า (ดู §2)
ทาง B (ทั้งก้อน) เหมาะกว่าถ้าโจทย์คือ “ต้องไม่มีช่องให้ลืม และยอมจ่ายค่าเปลี่ยนผ่าน” — แต่ต้องยอมรับ 3 เรื่องก่อน: มืดตอน debug · ขากลับต้องออกแบบใหม่ · ทุกคนที่เรียกต้องเปลี่ยนพร้อมกัน
ถ้าเลือกทาง A ต้องปิดช่องนี้ให้ได้ ไม่งั้นไม่ควรเลือก: ด่านตรวจอัตโนมัติว่า field อ่อนไหวถูก mark ครบทุกตัว — เพราะจุดอ่อนเดียวของทาง A คือ “ลืม” ซึ่งจะเกิดแน่นอนถ้าฝากไว้กับคน
5. บันทึกการถก — เพิ่มข้างล่างนี้
| วันที่ | ประเด็น | ข้อสรุป |
|---|---|---|
| 27/08 | เข้ารหัสทั้งก้อน หรือ เฉพาะบาง field | เลือก เฉพาะบาง field (ทาง A) |
| 27/08 | หลังบ้านถอดตรงไหน | ถอดให้เสร็จก่อนถึงโค้ด business ไม่ให้คนเขียน handler ต้องรู้เรื่อง |
| 01/09 | ทบทวนอีกรอบว่าจะกลับไปทาง B ไหม | ยังไม่ตัดสิน — หน้านี้คือของที่เอาไว้ตัดสิน |
| 01/09 | ความเห็น advisor รายมิติ | ทาง A (ชนะ 6 · แพ้ 5 · เสมอ 1) แต่ชี้ว่าน้ำหนักสำคัญกว่าจำนวน — ข้อ 3 กับ 12 ของ B ไม่มีทางแก้ราคาถูก |
| 01/09 | ตัดเคสแอปมือถือออก | Owner สั่งตัด — ไม่อยู่ในแผน ⇒ ลบแถวใน §3 และลบคำถามเปิดที่เคยอ้างมือถือเป็นตัวตัดสิน |
| 01/09 | service ในบ้านเรียกกันเอง ควรแยกเป็น 2 เส้นวิ่งเข้า logic เดียวกันไหม | เห็นด้วยกับโครง ปรับเป็น 2 action บาง ๆ → คำสั่งเดียวกัน (นโยบายผูกกับ action ไม่ใช่ route) · แต่ยังไม่ต้องทำตอนนี้ เพราะยังไม่มี caller จริง — แยกตอนจะบังคับเข้ารหัส ดู §3.1 |
| 01/09 | partner ภายนอก แยก API แล้วไม่เข้ารหัสเพราะเป็น s2s | โครงเห็นด้วย · เหตุผลยังไม่พอ — partner วิ่งผ่าน gateway ตัวเดียวกับที่เราไม่ไว้ใจ ⇒ ต้องมีเงื่อนไข ก หรือ ข ใน §3.2 รองรับก่อนถึงจะปล่อย plaintext ได้ |
| 01/09 | Owner ขอเพิ่ม TLS vs mTLS + FAPI 2.0 ขาดอะไร + แหล่งอ้างอิง | เพิ่มเป็น §2.1 กับ §2.2 · อ่านจาก spec จริง · ข้อค้นพบสำคัญ 2 ข้อ: mTLS ไม่ได้แก้ปัญหาไม่ไว้ใจคนกลาง (ถอดที่ไหนก็เห็นที่นั่น) และ FAPI ไม่ได้คุมการเข้ารหัส payload ธุรกิจเลย ⇒ เดินตาม FAPI ไม่ได้ตอบคำถาม A/B ของหน้านี้ |
| 01/09 | ✅ ท้วงแถว 8 — Swagger ไม่ได้วิ่งผ่าน middleware แล้วทำไมทาง B ถึงจะมองไม่เห็น | ท้วงถูก ตรวจโค้ดจริงแล้ว: Swagger สร้างจากการอ่านโครง DTO ตอนสตาร์ท ไม่เกี่ยวกับ middleware · เหตุผลเดิม (“ทาง B ไม่เหลือโครงให้บอก”) เป็นของยุคที่ยังถอด [FromBody] ออก ซึ่งเลิกใช้ไปแล้ว · แก้เหตุผลแถว 8 ใหม่เป็น “เอกสารโกหกแค่ไหน” · คำตัดสินยังเป็นทาง A แต่ชนะห่างน้อยลง · คะแนนรวมไม่เปลี่ยน |
คำถามที่ยังเปิดอยู่
- ยอมรับได้ไหมว่าคนกลางเห็นโครง request กับค่าที่ไม่ได้ mark (ข้อ 1)
- ความเสี่ยง “ลืม mark field ใหม่” ปิดด้วยด่านตรวจอัตโนมัติพอไหม หรือต้องการการรับประกันเชิงโครงสร้าง
- มี partner ภายนอก หรือ service อื่น ที่จะมาเรียก endpoint กลุ่มนี้ในอีก 6-12 เดือนไหม
- 🔴 สำหรับเส้น partner — เงื่อนไข ก หรือ ข ใน §3.2 ข้อไหนเป็นจริง ถ้าไม่จริงสักข้อ เส้น partner ก็ต้องเข้ารหัสด้วย จะปล่อย plaintext ไม่ได้