Key 101 — กุญแจทุกดอกในระบบ: สร้างที่ไหน เก็บที่ไหน ใช้ตอนไหน
แผนที่กุญแจฉบับเดียวจบ — ระบบเรามีกุญแจ 6 ตระกูล อยู่คนละที่ ทำคนละหน้าที่ · หน้านี้ตอบว่าดอกไหนของใคร สร้างเมื่อไหร่ เก็บที่ไหน และเวลามันพังจะรู้ได้ยังไง
อัปเดต: 2026-08-27
Key 101 — กุญแจทุกดอกในระบบ
หน้านี้เป็นแผนที่ ไม่ใช่ตำรา — แต่ละกลไกมีเล่ม 101 ของตัวเองอยู่แล้ว หน้านี้ตอบคำถามที่เล่มพวกนั้นตอบไม่ได้: “ตกลงเรามีกุญแจกี่ดอก ดอกไหนของใคร แล้วทำไมบางดอกอยู่ในเบราว์เซอร์ บางดอกอยู่ใน Key Vault”
กุญแจทั้งหมดในระบบ — 6 ตระกูล
| # | กุญแจ | เป็นของใคร | สร้างเมื่อไหร่ | ส่วนตัวเก็บที่ไหน | ส่วนสาธารณะเก็บที่ไหน | ใช้ทำอะไร |
|---|---|---|---|---|---|---|
| 1 | ลายเซ็นผู้ใช้ (ECDSA P-256) | ผู้ใช้ 1 คน ต่อ 1 เครื่อง | เบราว์เซอร์สร้างเองครั้งแรกที่ต้องใช้ | IndexedDB ในเบราว์เซอร์ (ดึงค่าออกมาไม่ได้) | Redis ฝั่ง server | พิสูจน์ว่า ใครส่ง และเนื้อหาไม่ถูกแก้กลางทาง |
| 2 | เข้ารหัส payload (RSA-OAEP-256) | server — ทั้งระบบมีคู่เดียว | ทีมสร้างแล้ววางไว้ใน Key Vault | Key Vault | Key Vault + ประกาศออกทาง GET v1/security/jwks | ล็อกเนื้อคำขอ ไม่ให้คนกลางที่ถอด TLS อ่านได้ |
| 3 | เข้ารหัสข้อมูลใน DB (AES-256-GCM) | server | ทีมสร้าง | Key Vault (บนเครื่อง dev = user-secrets) | ไม่มี — ดอกเดียวทั้งล็อกและไข ห้ามออกจาก server เด็ดขาด | เข้ารหัส field อ่อนไหวก่อนลง DB |
| 4 | ลิงก์ยกเลิกรับข่าว (HMAC) | server | ทีมสร้าง | Key Vault | ไม่มี — ดอกเดียวเหมือน #3 | ทำ token ในลิงก์ให้ปลอมไม่ได้ |
| 5 | API key / Subscription key | service ↔ service | ทีมออกให้ | Key Vault / APIM | ไม่มี — เป็น string ลับ | กันไม่ให้ service ปลอมเรียกกันเอง · รายละเอียด |
| 6 | กุญแจเซ็น JWT | Entra ID — ไม่ใช่ของเรา | Microsoft จัดการ | ไม่เคยอยู่ในมือเรา | Entra JWKS สาธารณะ | backend เอา public ของ Entra มาตรวจ token · Sentinel แค่เก็บ/ส่งต่อ token ไม่ได้เซ็นเอง |
#1-2 คือกุญแจคู่ (2 ดอกที่เกิดมาคู่กัน) · #3-5 คือกุญแจดอกเดียว (ล็อกและไขด้วยดอกเดียวกัน) — ความต่างนี้คือรากของทุกกฎด้านล่าง
กฎเดียวที่ต้องจำ
ส่วนตัวอยู่ฝั่งที่ต้อง “ลงมือทำ” · ส่วนสาธารณะอยู่ฝั่งที่แค่ “ตรวจ” หรือ “ล็อก”
- เซ็น = ผู้ส่งลงมือ ⇒ ส่วนตัวต้องอยู่ที่ เบราว์เซอร์ ถ้า server ถือด้วย server ก็ปลอมลายเซ็นแทนผู้ใช้ได้ ⇒ ลายเซ็นหมดค่าทันที
- ถอดรหัส = ผู้รับลงมือ ⇒ ส่วนตัวต้องอยู่ที่ server ถ้าเบราว์เซอร์ถือด้วย เท่ากับแจกให้ทุกคน (โค้ดหน้าเว็บใครก็เปิดอ่านได้) ⇒ เข้ารหัสไปก็ไม่มีความหมาย
- กุญแจดอกเดียว (#3-5) ⇒ ห้ามให้เบราว์เซอร์เห็นทุกกรณี เพราะเห็น = ทั้งล็อกและไขได้
เทียบเป็นของจริง: กุญแจ #2 คือ แม่กุญแจของ server (แจกตัวล็อกให้ทุกคน แต่คนไขมีคนเดียว) · กุญแจ #1 คือ ตราประทับของผู้ใช้ (ผู้ใช้ประทับเอง server แค่ดูว่าลายตรงไหม)
ในคำขอเดียวใช้ 2 ดอกพร้อมกัน
เบราว์เซอร์ ① ล็อกเนื้อหาด้วยส่วนสาธารณะของ server (ดึงจาก JWKS)
② ประทับตราด้วยส่วนตัวของตัวเอง (IndexedDB)
server ③ ตรวจตราด้วยส่วนสาธารณะของผู้ใช้ (Redis) ← ใครส่ง
④ ไขกล่องด้วยส่วนตัวของตัวเอง (Key Vault) ← อ่านเนื้อหา
🔴 ลำดับ ③ ก่อน ④ ห้ามสลับ — ลายเซ็นเซ็นทับ “ก้อนที่ล็อกแล้ว” ตามที่วิ่งมาบนสาย ถ้าไขก่อนไบต์จะเปลี่ยน ตรวจลายเซ็นไม่ผ่านทันที และยังเปิดให้คนที่ยังไม่ผ่านการตรวจสั่งให้ server ไขกล่องได้ด้วย
ที่เก็บกุญแจมี 5 ที่ — และมันทับกันเป็นชั้น
| ที่เก็บ | เก็บอะไร | ใครอ่านได้ |
|---|---|---|
| Azure Key Vault | กุญแจและความลับฝั่ง server ทุกดอก | service ที่ตั้งค่าให้ต่อตู้ |
| CSI ของ AKS | ตัวส่งค่าจากตู้เข้า pod เป็น environment variable | pod นั้น |
| user-secrets | ของบนเครื่อง dev เท่านั้น ไม่เคยขึ้น environment จริง | เครื่องคนเขียนโค้ด |
| IndexedDB | ส่วนตัวของผู้ใช้ดอกเดียว (#1) | เบราว์เซอร์เครื่องนั้น |
| Redis | ส่วนสาธารณะของผู้ใช้ + token ของ session | service |
เส้นทางค่าเข้าแอป:
- บนเครื่อง dev — ใส่ URL ของตู้ไว้ใน appsettings → ตอน service start มันดูดค่าจากตู้เข้ามาเป็น config เอง
- บน environment จริง — ไม่ใส่ URL ตู้ ปล่อยให้ CSI เอาค่าจากตู้เดียวกันมา mount เป็น environment variable แทน
⚠️ ลำดับทับซ้อนบนเครื่อง dev: user-secrets ทับ Key Vault ทับ environment variable ทับ appsettings
คีย์ชื่อเดียวกันที่ยังค้างใน user-secrets จะ ชนะค่าที่อยู่ในตู้เสมอ — เป็นสาเหตุอันดับหนึ่งของอาการ “แก้ที่ตู้แล้วไม่มีผล”
การตั้งชื่อ: ชื่อในตู้ dev--user-service--Section--Key จะกลายเป็น config Section:Key ในแอป (-- → :) · ทาง CSI จะมาเป็น Section__Key (__ → :) — คนละหน้าตาแต่ปลายทางคือคีย์เดียวกัน
กุญแจผู้ใช้ (#1) — จุดที่คนเข้าใจผิดบ่อยที่สุด
❌ “login ใหม่ 1 ครั้ง = สร้างกุญแจคู่ใหม่ 1 ครั้ง” ✅ สร้างครั้งเดียวต่อเครื่อง — login รอบต่อไปใช้คู่เดิม
- โค้ดฝั่งเบราว์เซอร์ทำ “มีอยู่แล้วใช้เลย ไม่มีค่อยสร้าง” — อ่านจาก IndexedDB ก่อนเสมอ
- สร้างแบบ ดึงค่าออกมาไม่ได้ (
extractable: false) ⇒ แม้แต่โค้ดของเราเองก็อ่านส่วนตัวไม่ได้ สั่งได้อย่างเดียวคือ “เซ็นให้หน่อย” - ผูกกับ ตัวตนของผู้ใช้ (oid) ไม่ใช่ session ⇒ login ใหม่ไม่ต้องผูกใหม่
- ตอนฝากส่วนสาธารณะ: คนแรกชนะ — ส่งดอกเดิมซ้ำผ่านเงียบ ๆ แต่ส่งคนละดอกทั้งที่มีของเดิมอยู่จะถูกปฏิเสธด้วย
KEY_CONFLICT - ฝั่ง server เก็บส่วนสาธารณะไว้ 7 วัน — ไม่ได้ใช้เกินนั้นมันหายไป แต่กุญแจในเบราว์เซอร์ยังอยู่ จึงแค่ฝากใหม่ได้เลย
- การฝากเกิดขึ้นแบบ ตอนที่จำเป็นเท่านั้น: interceptor เซ็นคำขอ → ถ้า server ตอบว่ายังไม่รู้จักกุญแจนี้ ถึงค่อยฝากแล้วยิงซ้ำ (ไม่ใช่ฝากตอน login)
🔴 ช่องที่ยังไม่ปิด: 1 ผู้ใช้ = 1 กุญแจ ⇒ เปิดเครื่องที่สอง/เบราว์เซอร์ที่สองภายใน 7 วันจะชน KEY_CONFLICT และยังไม่มีวิธีถอนกุญแจเก่า — ใช้หลายเครื่องพร้อมกันยังไม่รองรับ
พื้นฐานว่ากุญแจคู่เซ็น/ตรวจยังไง: Body Signature 101
กุญแจ payload ของ server (#2) — เปลี่ยนดอกยังไงไม่ให้ระบบล่ม
- ไม่ต้องแจ้งใครล่วงหน้า — ทุกดอกมีชื่อกำกับ (
kid) เบราว์เซอร์ดึงรายการจาก JWKS ใหม่ทุกครั้งอยู่แล้ว - เปลี่ยนดอก = วางดอกใหม่ในตู้ด้วยชื่อใหม่ แล้วเอาดอกเก่าออกจากรายการ
- ⚠️ ตัวหน้าบ้านหยิบ ดอกแรกในรายการ ⇒ ถ้าปล่อยให้มี 2 ดอกพร้อมกันมันอาจหยิบดอกเก่าเงียบ ๆ · ตอนเปลี่ยนต้องเหลือดอกเดียว
- ⚠️ แก้ค่าในตู้แล้ว ต้อง restart service — ตัวดูดค่าตั้งรอบรีเฟรชไว้ 30 นาที ไม่ใช่ทันที
- ⚠️ service อื่นที่ตั้งให้ต่อตู้เดียวกันจะโหลดความลับทั้งตู้เข้าหน่วยความจำของตัวเอง ไม่ได้กรองเฉพาะของตัวเอง — เรื่องนี้มีบันทึกแยกในเอกสารภายใน
เหตุผลว่าทำไมต้องเข้ารหัสทั้งที่มี HTTPS แล้ว: Encryption Field 101 · สเปคเต็มของชั้นนี้: สเปคชั้น 3
กุญแจข้อมูลใน DB (#3) — คนละเรื่องกับ #2 คนละความเสี่ยง
- #2 ปกป้อง “ระหว่างทาง” · #3 ปกป้อง “ตอนนอนอยู่ใน DB” — คนละช่วงชีวิตของข้อมูล
- เป็นกุญแจ ดอกเดียว ⇒ ใครได้ไปคือถอดได้ทั้งหมดย้อนหลัง — ความเสี่ยงหนักกว่า #2 มาก เพราะ #2 หลุดแค่ทำให้อ่านคำขอใหม่ได้ แต่ #3 หลุดคืออ่านข้อมูลเก่าทั้งฐาน
- เปลี่ยนดอกต้อง เก็บดอกเก่าไว้ด้วย ไม่งั้นข้อมูลเดิมที่ล็อกด้วยดอกเก่าจะอ่านไม่ออก — ระบบจึงเก็บหลายดอกพร้อมกันแล้วชี้ว่า “ดอกไหนใช้ล็อกของใหม่”
รายละเอียดว่าเข้ารหัสกับปิดบัง (masking) ต่างกันยังไง: Data Encryption & Masking 101
คำถามที่ถามกันบ่อย
ส่วนสาธารณะหลุดเป็นไรไหม? ไม่ — มันถูกออกแบบมาให้แจก · ของ server ประกาศทางเน็ตอยู่แล้ว ใครโหลดก็ได้ · แต่ กุญแจดอกเดียว (#3-5) ไม่มีคำว่า “ส่วนสาธารณะ” หลุดคือจบ
ทำไมกุญแจของ server ต้องอยู่ Key Vault ไม่เก็บไว้ในโปรเจกต์? ของในโปรเจกต์ติดไปกับเครื่องคนเขียนโค้ดและอาจหลุดขึ้น git ได้ · ตู้เก็บให้ที่เดียว ควบคุมสิทธิ์ได้ เปลี่ยนได้โดยไม่ต้องแก้โค้ดหรือ deploy ใหม่ · และ environment จริงก็ดึงจากตู้เดียวกันผ่าน CSI อยู่แล้ว ⇒ ใช้กลไกเดียวกันทั้งเครื่อง dev และของจริง
ผู้ใช้ล้างข้อมูลเบราว์เซอร์แล้วเกิดอะไรขึ้น?
กุญแจ #1 หายไปพร้อมกัน เบราว์เซอร์จะสร้างคู่ใหม่ให้เอง · ถ้าของเดิมฝั่ง server ยังไม่หมดอายุ 7 วัน จะชน KEY_CONFLICT — ดูช่องที่ยังไม่ปิดด้านบน
เข้ารหัส payload แล้วกัน XSS ได้ไหม? ไม่ได้ และห้ามอ้างแบบนั้น — สคริปต์ที่ฝังอยู่ในหน้าเว็บเห็นข้อมูลตั้งแต่ก่อนถูกเข้ารหัสอยู่แล้ว · XSS ต้องแก้ที่หน้าบ้าน: Frontend Hardening 101
กุญแจแต่ละดอกอายุเท่าไหร่? #1 ฝั่ง server เก็บ 7 วัน (ฝั่งเบราว์เซอร์อยู่จนกว่าจะล้าง) · #2-#5 ไม่มีวันหมดอายุอัตโนมัติ เปลี่ยนเมื่อเราสั่งเปลี่ยนเท่านั้น · ส่วนอายุ session เป็นคนละเรื่อง: Session Lifetime 101