แนวปฏิบัติด้านความปลอดภัย
กุญแจ 1 ใบอ่านประวัติการขายของร้านและรายชื่อสมาชิกได้ แต่แก้อะไรไม่ได้เลย เพราะทุกเส้นอ่านอย่างเดียว ⇒ ความเสี่ยงคือ “ข้อมูลรั่ว” ไม่ใช่ “ของพัง” ถึงอย่างนั้นก็ยังต้องดูแลกุญแจให้เท่ากับรหัสผ่านฐานข้อมูล
ความจริง 2 ข้อที่กำหนดทุกอย่างในหน้านี้:
- กุญแจโชว์ครั้งเดียว — เราเก็บไว้เป็นค่าที่ย้อนกลับไม่ได้ (hash) เท่านั้น ⇒ ไม่มีใครใน ScanFood อ่านกลับมาให้คุณได้ · ทำหายคือออกใบใหม่ ไม่ใช่ขอดูของเดิม
- กุญแจเดินทางได้ที่เดียว — เราอ่านจาก
Authorization: Bearer <key>เท่านั้น ไม่อ่านจาก query string ไม่อ่านจาก body
เก็บกุญแจไว้ฝั่งเซิร์ฟเวอร์
หัวข้อที่มีชื่อว่า “เก็บกุญแจไว้ฝั่งเซิร์ฟเวอร์”เก็บกุญแจใน secret manager หรือ environment variable ที่ฉีดให้ตอน deploy อย่าเก็บใน repo อย่าใส่ไฟล์ที่ commit อย่าวางในตั๋วงานหรือห้องแชท
# อ่านเข้า shell โดยไม่ให้ตกค้างใน history…read -rs SCANFOOD_API_KEY && export SCANFOOD_API_KEY
curl -s -H "Authorization: Bearer $SCANFOOD_API_KEY" \ "https://api.scanfood.co/ext/v1/shops"
# …และห้ามทำแบบนี้: กุญแจจะไปโผล่ใน history ใน access log# และในทุกตัวกลางระหว่างคุณกับเรา# curl "https://api.scanfood.co/ext/v1/shops?key=sf_live_…" ← API ไม่อ่านช่องนี้อยู่แล้ว// อ่านจาก environment · อย่า hard-code · อย่าส่งไปฝั่ง clientconst KEY = process.env.SCANFOOD_API_KEY;if (!KEY) throw new Error('ยังไม่ได้ตั้ง SCANFOOD_API_KEY');
const res = await fetch('https://api.scanfood.co/ext/v1/shops', { headers: { Authorization: `Bearer ${KEY}` },});
// ปิดบังกุญแจก่อนบันทึกอะไรก็ตามconst safe = (s) => String(s).replace(/sf_live_[a-z0-9]+_[a-f0-9]+/g, 'sf_live_***');console.log(safe(`GET /v1/shops -> ${res.status}`));import os, re, requests
KEY = os.environ.get("SCANFOOD_API_KEY")if not KEY: raise SystemExit("ยังไม่ได้ตั้ง SCANFOOD_API_KEY")
res = requests.get( "https://api.scanfood.co/ext/v1/shops", headers={"Authorization": f"Bearer {KEY}"}, timeout=30,)
REDACT = re.compile(r"sf_live_[a-z0-9]+_[a-f0-9]+")print(REDACT.sub("sf_live_***", f"GET /v1/shops -> {res.status_code}"))เรื่องที่ควรรู้เพิ่ม:
- วิ่งบน HTTPS เท่านั้น · อย่าปิดการตรวจใบรับรอง “แค่ตอนเทส” เพราะนั่นคือคำขอที่มักหลุดไปวิ่งบนของจริง
- เราไม่เคยบันทึกกุญแจ — ทั้งตัวเต็มและค่า hash ไม่โผล่ในบันทึกการใช้งานของเราสักไบต์ ·
สิ่งที่เราเก็บคือ
keyId(ท่อนกลาง) ซึ่งบอกกันได้อย่างปลอดภัยเวลาติดต่อฝ่ายสนับสนุน
หนึ่งระบบ หนึ่งกุญแจ
หัวข้อที่มีชื่อว่า “หนึ่งระบบ หนึ่งกุญแจ”ขอกุญแจแยกใบให้ทุกระบบที่เรียกเรา — งานซิงก์บัญชีตอนกลางคืน แดชบอร์ด สมุดงานของนักวิเคราะห์ ผู้ขายที่มาทำ POC
| ทำไม | ได้อะไร |
|---|---|
| เพิกถอนได้ตรงจุด | ปิดสิทธิ์ผู้ขายได้โดยงานซิงก์บัญชีตอนตี 2 ไม่หยุด |
| รู้ว่าใครใช้ | คำถาม “เมื่อวานใครยิง 40,000 ครั้ง” มีคำตอบ ไม่ใช่ยักไหล่ |
| เพดานแยกกัน | การทดลองกินโควตาวันของกุญแจงานจริงไม่ได้ |
| วงความเสียหายจำกัด | กุญแจหลุด 1 ใบ = เปิดเฉพาะขอบเขตของระบบนั้น ไม่ใช่ทุกอย่างที่คุณมี |
กุญแจทุกใบต้องมีป้ายชื่อ · ใช้มันให้เป็นประโยชน์ เช่น accounting-nightly-sync, bi-dashboard, vendor-poc-2026-09
คือชื่อที่เพื่อนร่วมงานอ่านแล้วตัดสินใจได้เองในอีก 1 ปีข้างหน้าโดยไม่ต้องถามใคร
ให้สิทธิ์เท่าที่จำเป็น
หัวข้อที่มีชื่อว่า “ให้สิทธิ์เท่าที่จำเป็น”มีปุ่ม 2 ตัวที่กำหนดว่ากุญแจเห็นได้แค่ไหน · ขอค่าที่เล็กที่สุดที่ยังทำงานได้ แล้วค่อยขอกว้างขึ้นทีหลังก็ได้
ขอบเขต — เห็นร้านไหนบ้าง · กุญแจออกให้ได้ทั้งแบบร้านเดียวและแบบทั้งแฟรนไชส์ กุญแจแฟรนไชส์เห็นทุกสาขาใต้แฟรนไชส์นั้น ทั้งตอนนี้และในอนาคต รวมสาขาที่เปิดหลังออกกุญแจไปแล้ว ถ้าระบบแตะแค่ 2 สาขา กุญแจแฟรนไชส์ไม่ใช่เครื่องมือที่ถูก
ข้อมูลส่วนบุคคล — เบอร์โทรและอีเมล · เบอร์โทรและอีเมลของสมาชิก ไม่ถูกส่งออก เป็นค่าตั้งต้น
กุญแจธรรมดาจะเห็น hasTel และ hasEmail เป็นจริง/เท็จ ซึ่งพอตอบคำถาม “ติดต่อสมาชิกคนนี้ได้ไหม”
โดยที่ข้อมูลส่วนบุคคลไม่ต้องออกจาก ScanFood · กุญแจที่คืนค่าจริงมีอยู่ แต่ออกให้แบบตั้งใจ
โดยระบุวัตถุประสงค์ และมาพร้อมภาระหน้าที่ด้านการคุ้มครองข้อมูลส่วนบุคคล
อีกส่วนที่อยู่นอกมือ API โดยการออกแบบ: ต้นทุน สูตร และข้อมูลวัตถุดิบไม่ออกทางนี้ และไม่มีเส้นไหนเขียนอะไรกลับเข้า ScanFood ได้
บันทึกและเฝ้าระวัง
หัวข้อที่มีชื่อว่า “บันทึกและเฝ้าระวัง”- ปิดบังก่อนบันทึก — บันทึก path, สถานะ, จำนวนแถว และเวลา · อย่าบันทึกหัว
Authorization· ฟังก์ชันช่วยแบบข้างบนที่ใส่ไว้ตรงขอบเพียงจุดเดียว มีค่ากว่ากฎที่ต้องอาศัยให้คนจำ - บันทึก
keyIdไม่ใช่ตัวกุญแจ — ในsf_live_<keyId>_<secret>ท่อนkeyIdเป็นค่าที่เปิดเผยได้ · บันทึกไว้ทุกรอบ = คำถามฝ่ายสนับสนุนตอบได้ในไม่กี่นาที - จับรูปแบบของการถูกใช้ผิด — คำขอที่คุณไม่ได้ตั้งไว้ ในเวลาที่คุณไม่ได้รัน คือสัญญาณว่ากุญแจหลุด · บันทึกฝั่งคุณจะเห็นก่อนใครเพื่อน
- ตั้งเตือนที่
401— ระบบที่ทำงานปกติไม่ผลิตข้อผิดพลาดการยืนยันตัวตน · ถ้าโผล่มาเป็นชุด แปลว่ากุญแจถูกเพิกถอน ถูกหมุนโดยไม่มีใครบอก หรือมีคนกำลังลองกุญแจที่ได้ไปไม่ครบ - เฝ้าโควตาวัน — การใช้งานที่กระโดดโดยอธิบายไม่ได้ คือเครื่องจับการรั่วที่ถูกที่สุดที่คุณมี ·
ตัวเลขที่ต้องตามคือ
X-RateLimit-Remainingที่ติดมากับทุกคำตอบ
หมุนกุญแจ
หัวข้อที่มีชื่อว่า “หมุนกุญแจ”กุญแจไม่มีวันหมดอายุในตัวเอง ⇒ การหมุนเป็นสิ่งที่คุณกำหนดตารางเอง ไม่ใช่สิ่งที่ถูกบังคับ และเพราะกุญแจโชว์ครั้งเดียว การหมุนจึงเป็น “ออกใบใหม่ แล้วปลดใบเก่า” — ไม่มีทางอ่านใบปัจจุบันซ้ำ
ลำดับสำคัญ และถ้าทำตามลำดับนี้จะไม่มีช่วงที่ระบบล่ม:
- ขอกุญแจใบใหม่ สำหรับระบบเดิมและขอบเขตเดิม
- เอาไปติดตั้ง ใน secret store แล้วรีสตาร์ตหรือโหลดใหม่ฝั่งผู้เรียก
- ยืนยันว่าใบใหม่คือใบที่ทำงานอยู่ — เห็นการใช้งานของใบใหม่ ส่วนใบเก่าเงียบลง
- แจ้งให้เราเพิกถอนใบเก่า — ก่อนหน้านั้นทั้ง 2 ใบใช้ได้พร้อมกัน จึงไม่มีช่วงที่ไม่มีใบไหนใช้ได้เลย
หลังเพิกถอน กุญแจใบนั้นจะตอบ 401 พร้อม "error": "key_revoked" และเป็นแบบนั้นถาวร ·
สั่งเพิกถอนซ้ำไม่มีผลเสีย
ถ้ากุญแจหลุด
หัวข้อที่มีชื่อว่า “ถ้ากุญแจหลุด”กุญแจที่ไปอยู่ใน repo สาธารณะ ในภาพหน้าจอ ในล็อกที่วางแชร์ หรือในเครื่องของอดีตพนักงาน = กุญแจที่หลุดแล้ว ไม่แน่ใจก็ให้ถือว่าหลุด
- แจ้งเราทันที และขอให้เพิกถอนใบนั้น · บอก
keyId(ท่อนกลางของกุญแจ) ไม่ใช่กุญแจทั้งใบ - ออกใบแทน แล้วติดตั้ง · ถ้าระบบหยุดไม่ได้ ให้ทำใบใหม่ก่อนตามลำดับการหมุนข้างบน แล้วค่อยเพิกถอน
- ให้ถือว่าช่วง 5 นาทีนั้นถูกใช้ไปแล้ว — ระหว่างที่การเพิกถอนยังกระจายไม่ครบ กุญแจยังใช้ได้ · ถามเราได้ว่ากุญแจใบนั้นอ่านอะไรไปบ้างและเมื่อไหร่ เราเก็บบันทึกการใช้งานรายกุญแจ พร้อม path สถานะ และเวลา
- ปิดรูที่ทำให้มันหลุด — กุญแจใหม่ที่กลับไปอยู่ใน commit history เดิม ห้องแชทเดิม หรือไดรฟ์แชร์เดิม จะหลุดอีก
- ดูว่าขอบเขตของมันเปิดอะไรไว้ — กุญแจร้านเดียวที่ไม่มีสิทธิ์ข้อมูลส่วนบุคคลและหลุดไป 5 นาที เป็นคนละเรื่องกับกุญแจแฟรนไชส์ที่คืนเบอร์โทรได้
เพราะ API อ่านอย่างเดียว กุญแจที่หลุดจึงยกเลิกบิลไม่ได้ แก้ราคาไม่ได้ ลบสมาชิกไม่ได้ สิ่งที่มันทำได้คืออ่าน — ซึ่งเป็นเหตุผลว่าทำไมขอบเขตที่คุณขอไว้ตั้งแต่ต้น คือสิ่งที่จำกัดความเสียหายในตอนท้าย