05
Phase 1: Basic • Foundations

Day 5: Service-to-Service Security & Client Credentials Grant

Phase 1 Index ↗
Module 05 • First Principles เมื่อระบบคุยกับระบบ: Machine-to-Machine (M2M) คืออะไร?

1. Client Credentials Grant คืออะไร? เมื่อไม่มีมนุษย์มาล็อกอิน ระบบยืนยันตัวตนกันอย่างไร?

ใน Day 2 และ Day 4 เราได้เรียนรู้การยืนยันตัวตนสำหรับผู้ใช้งาน (Human End-User) ผ่านทางหน้าจอเบราว์เซอร์ด้วย Authorization Code Flow + PKCE กันไปแล้ว แต่ในระบบซอฟต์แวร์ระดับองค์กร มีงานอีกประเภทหนึ่งที่ไม่เคยมีมนุษย์มานั่งหน้าจอเลย เช่น:

  • Cron Job / Scheduled Worker: สคริปต์ที่ตื่นขึ้นมาตอนตี 2 เพื่อดึงรายการสั่งซื้อไปสรุปยอดบัญชี
  • Event Consumer Daemon: เซอร์วิสที่คอยดึงข้อความจาก Message Queue แล้วยิงอัปเดตสต็อกสินค้า
  • Microservice-to-Microservice (M2M): บริการชำระเงิน (Payment Service) เรียกตัดเงินที่บริการธนาคาร (Ledger Service)

คำถามคือ: เมื่อไม่มีมนุษย์มาพิมพ์ Username และ Password แล้วใครจะเป็นผู้ยืนยันตัวตน? คำตอบคือ มาตรฐาน OAuth 2.0 ได้ออกแบบโฟลว์เฉพาะสำหรับเครื่องจักร เรียกว่า Client Credentials Grant (RFC 6749 Section 4.4) ซึ่งแอปพลิเคชันจะใช้ "ตัวตนของตัวซอฟต์แวร์เอง" (Client Identity) โดยส่ง client_id และ client_secret ตรงไปยัง Keycloak เพื่อขอรับ Access Token สำหรับเรียกใช้งาน API เบื้องหลัง

👤 User Interactive Flow (Auth Code + PKCE) Human Involved

"มีมนุษย์เป็นผู้ถือครองสิทธิ์และรหัสผ่าน"

  • มีขั้นตอน Front-channel ผ่าน Browser 302 Redirect เพื่อให้คนกรอกรหัสผ่านและ 2FA
  • มีพารามิเตอร์ PKCE (code_challenge / code_verifier) เพื่อป้องกันการดักฟังบน Client
  • ได้รับ Refresh Token เพื่อรักษา Session เมื่อคนใช้งานต่อเนื่องโดยไม่ต้องพิมพ์รหัสผ่านใหม่
🤖 Machine-to-Machine Flow (Client Credentials) Autonomous Server

"เครื่องจักรคุยกันโดยตรง ไม่มีมนุษย์เกี่ยวข้อง"

  • ไร้ Browser และไร้ Redirect: ส่ง HTTP POST ตรงไปยัง Token Endpoint (Back-channel 100%)
  • ไม่ต้องใช้ PKCE: เพราะทำงานบนเซิร์ฟเวอร์หลังบ้านที่มีความปลอดภัย (Confidential Client)
  • ไม่มี Refresh Token เด็ดขาด: เมื่อ Access Token หมดอายุ เครื่องจักรมี Client Secret อยู่แล้ว จึงยิงขอ Token ใบใหม่ได้ทันที
นิยามระดับรากฐาน (Root Technical Definitions): • Client Credentials Grant (RFC 6749 Sec 4.4): โฟลว์การมอบอำนาจที่แอปพลิเคชันใช้ Credentials ประจำตัวซอฟต์แวร์ในการยืนยันสิทธิ์กับ Authorization Server โดยไม่มี End-user เกี่ยวข้อง • Service Account: บัญชีผู้ใช้พิเศษแบบไร้ตัวตนมนุษย์ (Synthetic User Entity) ที่ Keycloak สร้างผูกติดกับ Confidential Client เพื่อใช้เป็นภาชนะสำหรับถือครองสิทธิ์ (Roles & Scopes) • Direct Backchannel POST: การสื่อสารผ่าน HTTPS ระหว่าง Server ต่อ Server โดยตรง ไม่ผ่านเบราว์เซอร์และไม่มีความเสี่ยงเรื่อง Browser History Leaks • Zero-Downtime Secret Rotation: กลยุทธ์การหมุนเวียนรหัสลับประจำแอปพลิเคชันอย่างปลอดภัย โดยรองรับการมีกุญแจสองดอกขนานกันชั่วคราว เพื่อไม่ให้ Service ล่มระหว่างเปลี่ยนคีย์

📚 ศัพท์เทคนิคสำคัญประจำโปรโตคอล M2M & Keycloak Service Accounts

ตารางนิยามคำศัพท์ที่เป็นรากฐานของข้อสอบและการเชื่อมต่อระหว่าง Microservices:

ศัพท์เทคนิค นิยามทางสถาปัตยกรรม หน้าที่ & ตัวอย่างการใช้งาน ข้อกำหนดความปลอดภัย
grant_type=client_credentials ชนิดคำขอใน OAuth 2.0 พารามิเตอร์ที่ส่งใน HTTP POST Body ไปที่ /token เพื่อระบุว่าเป็นการขอรับ Token ระดับเครื่องจักร ต้องใช้ POST Back-channel เท่านั้น
Service Accounts Enabled สวิตช์เปิดใช้บัญชีเครื่องจักรใน Keycloak เมื่อเปิดเป็น ON ในหน้า Client Keycloak จะสร้าง User เสมือนชื่อ service-account-<clientId> ใน Realm ให้อัตโนมัติ เปิดได้เฉพาะ Confidential Client
Service Account Roles สิทธิ์ของ Service Account แท็บสำหรับมอบ Roles ให้กับตัวแอปพลิเคชัน เช่น กำหนดสิทธิ์ billing:charge หรือ inventory:read ห้ามให้ realm-admin เด็ดขาด
Absence of Refresh Token คุณสมบัติการไม่แจก Refresh Token Keycloak จะไม่แนบ Refresh Token ใน Response ของ Client Credentials เพราะเครื่องจักรครอบครอง Client Secret อยู่แล้ว จึงไม่มีความจำเป็นต้องมี Session Token ซ้ำซ้อน RFC 6749 มาตรฐานสากล
Private Key JWT (Signed JWT) การยืนยันตัวตนไร้ Shared Secret แอปพลิเคชันใช้ Private Key เซ็น JWT สดๆ แล้วส่งไปให้ Keycloak ตรวจสอบด้วย Public Key แทนการส่ง Password ผ่านสายไฟ ปลอดภัยสูงสุด (Zero Shared Secret)
Short-Lived Access Token อายุการใช้งานสั้นของ M2M Token Token สำหรับเครื่องจักรควรมีอายุสั้นมาก (เช่น 60–300 วินาที) เพื่อลดความเสี่ยงหาก Token หลุดจากการดักฟังในระบบเครือข่ายภายใน แนะนำ 1–5 นาทีใน Production
Mental Model #05 ภาพเปรียบเทียบเชิงกายภาพในชีวิตประจำวัน

The Autonomous Night-Shift Security Drone vs. The Human Employee

เปรียบเทียบการยืนยันตัวตนในระบบ IAM เสมือนการเปิดประตูเข้าอาคารสำนักงานใหญ่:
• User Flow (Human Employee): เปรียบเหมือน "พนักงานมนุษย์" ที่ต้องเดินมาหน้าป้อมยาม แสดงหน้าตา สแกนใบหน้าและกรอกรหัส 2FA ยามจึงแจกบัตรผ่านประตูพร้อมให้กุญแจสำรอง (Refresh Token) เผื่อเดินเข้าออกในวันนั้น
• Machine Flow (Autonomous Drone / Background Worker): เปรียบเหมือน "โดรนรักษาความปลอดภัยอัตโนมัติ" ที่บินอยู่ตอนตี 3 ไม่มีหน้าตามนุษย์ให้สแกน โดรนจะบินตรงไปยังแท่นชาร์จกลาง (Keycloak IdP) แล้วยื่นรหัสลับประจำชิปประมวลผล (Client Secret) ผ่านสัญญาณคลื่นเข้ารหัสส่วนตัว แท่นชาร์จจะปล่อยสติกเกอร์บาร์โค้ดผ่านประตูที่มีอายุ 5 นาที (Access Token) ให้โดรนบินไปตรวจห้องเก็บของ (Resource Server) หากสติกเกอร์หมดอายุ โดรนก็บินกลับมายื่นรหัสลับของชิปเพื่อขอสติกเกอร์แผ่นใหม่ได้เองทันทีโดยไม่ต้องปลุกใคร!

Worker / Daemon Service Confidential Client ("billing-job") Keycloak IdP Server Service Account Engine Protected Resource API Target Service ("payment-api") 1. POST /token (grant_type=client_credentials, client_id="billing-job", client_secret="Sec_99...") Keycloak Authenticates Client: ✅ Secret Matches Client Hash ✅ Resolves Service Account Roles 2. HTTP 200 OK: { access_token: "eyJ...", expires_in: 300 } ⚠️ NO refresh_token returned (RFC 6749 Compliant) 3. POST /api/v1/invoices (Authorization: Bearer <access_token>) Stateless Verification: Verify Signature via Keycloak /certs 4. HTTP 201 Created: { status: "processed", invoice_id: "INV-9021" } 🔄 Token หมดอายุใน 5 นาที: Worker ยิงขอ Token ใหม่ได้เองทันที Worker calls Step 1 again directly using stored Secret
Case Study #05 Security Incident Post-Mortem

The $2.4M Orphan Service Account & Hardcoded Secret Breach

Direct Financial Loss
$2,450,000

มูลค่าความเสียหายจากการถูกยึด Database และค่าปรับด้านความปลอดภัยจากการรั่วไหลของข้อมูลลูกค้า

Vulnerability Vector
Overprivileged Role & Git-Leaked Secret

ตั้ง Service Account ให้ถือสิทธิ์ realm-admin และฝัง Client Secret ไว้ใน Repository สาธารณะ

Architectural Takeaway
Least Privilege & Zero-Downtime Rotation

ห้ามให้สิทธิ์แอดมินแก่ Service Account, ใช้ Vault จัดเก็บ และวางระบบหมุนเวียนคีย์ลับอัตโนมัติ

สถานการณ์จริง: บริษัทผู้ให้บริการระบบโลจิสติกส์แห่งหนึ่ง ได้สร้าง Microservice ชื่อ tracking-sync เพื่อทำหน้าที่ซิงก์พิกัดรถบรรทุกแบบอัตโนมัติ ในขั้นตอนการติดตั้งบน Keycloak เพื่อความรวดเร็วในการทดสอบ วิศวกรได้เข้าไปที่แท็บ Service Account Roles แล้วมอบบทบาท realm-admin (สิทธิ์ผู้ดูแลระบบสูงสุดของ Realm) ให้กับ Service Account นี้ โดยตั้งใจว่าจะกลับมาลดสิทธิ์ในภายหลังแต่ลืมทำ

ต่อมา นักพัฒนาฝึกงานคนหนึ่งได้เผลอ Push โค้ดที่มีไฟล์คอนฟิก application.yaml ซึ่งมีค่า client_secret บันทึกอยู่ขึ้นไปยัง Public GitHub Repository ภายในเวลาไม่ถึง 3 นาที บอทสแกนโค้ดของแฮกเกอร์ได้ตรวจพบคีย์ลับนี้ และยิงคำขอ HTTP POST ไปยัง Token Endpoint ของ Keycloak:

curl -X POST https://auth.logistics.th/realms/corp/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=tracking-sync" \
  -d "client_secret=Leaked_Secret_Value_XYZ"

เนื่องจาก Service Account นี้ถือครองสิทธิ์ realm-admin แฮกเกอร์จึงได้รับ Token ที่มีพลังอำนาจเบ็ดเสร็จ แฮกเกอร์สามารถใช้ Token นี้เรียก Keycloak Admin REST API เพื่อดาวน์โหลดข้อมูลพนักงานทุกคน, เปลี่ยน Password ของผู้บริหาร, และสั่งลบ Client ทั้งหมดในระบบทิ้งจนบริการอัมพาตทั้งประเทศ

Defense Argument สำหรับคุยกับทีม: "ในโลกของ M2M Architecture กฎเหล็กข้อสำคัญที่สุดคือ หลักการให้สิทธิ์เท่าที่จำเป็น (Principle of Least Privilege) ห้ามมอบสิทธิ์ realm-admin หรือสิทธิ์ระดับผู้ดูแลระบบให้กับ Service Account เป็นอันขาด! แต่ละ Worker ควรได้รับเฉพาะ Fine-Grained Client Roles เท่าที่มันจำเป็นต้องใช้ (เช่น tracking:write เท่านั้น) และ Secret ต้องถูกดึงจาก Secret Manager (เช่น HashiCorp Vault หรือ AWS Secrets Manager) พร้อมมีวงจร Zero-Downtime Secret Rotation สม่ำเสมอ!"
Hands-on Breakdown แกะโครงสร้างคำสั่ง CLI & ลงมือสร้างจริง 3 เลเวล

Anatomy of Service Account Provisioning via kcadm.sh CLI

การสร้าง M2M Client และการกำหนดสิทธิ์ให้ Service Account สามารถทำได้ผ่าน Keycloak Admin CLI (kcadm.sh) ตามขั้นตอนแบบทีละขั้น:

LEVEL 1: Provisioning a Confidential Client with Service Account Enabled Confidential Client

สร้าง Confidential Client ชื่อ billing-daemon โดยเปิดใช้งาน serviceAccountsEnabled=true

# 1. เข้าสู่ Container และล็อกอินผ่าน kcadm
docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://localhost:8080 --realm master --user superadmin --password AdminSuperSecret2026!

# 2. สร้าง Confidential Client พร้อมเปิด Service Accounts
docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh create clients -r app-dev \
  -s clientId=billing-daemon \
  -s name="Automated Billing Background Daemon" \
  -s enabled=true \
  -s publicClient=false \
  -s serviceAccountsEnabled=true \
  -s standardFlowEnabled=false \
  -s directAccessGrantsEnabled=false

# 3. ตรวจสอบว่า Client สร้างสำเร็จและดึง Client Secret
CLIENT_UUID=$(docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh get clients -r app-dev -q clientId=billing-daemon --fields id --format csv | tail -n 1 | tr -d '\r')
docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh get clients/$CLIENT_UUID/client-secret -r app-dev
LEVEL 2: Assigning Scoped Roles to Service Account & Token Exchange ⚡ Role Binding & cURL Test

ดึง User ID ของ Service Account ที่ Keycloak สร้างให้โดยอัตโนมัติ แล้วผูกสิทธิ์ Role เฉพาะทาง พร้อมทดสอบยิงแลก Token ด้วย cURL

# 1. ค้นหา User Entity เสมือนของ Service Account
SA_USER_ID=$(docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh get clients/$CLIENT_UUID/service-account-user -r app-dev --fields id --format csv | tail -n 1 | tr -d '\r')
echo "Service Account User ID: $SA_USER_ID"

# 2. สร้าง Realm Role สำหรับงานเฉพาะทาง: 'invoice-writer'
docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh create roles -r app-dev -s name=invoice-writer -s description="Permission to generate and update invoices"

# 3. มอบสิทธิ์ Role 'invoice-writer' ให้กับ Service Account User
docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh add-roles -r app-dev --uid $SA_USER_ID --rolename invoice-writer

# 4. ทดสอบยิง HTTP POST แลก M2M Access Token ผ่าน cURL (Back-channel 100%)
CLIENT_SECRET=$(docker exec -it keycloak-core /opt/keycloak/bin/kcadm.sh get clients/$CLIENT_UUID/client-secret -r app-dev | jq -r '.value')

curl -s -X POST http://localhost:8080/realms/app-dev/protocol/openid-connect/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=billing-daemon" \
  -d "client_secret=$CLIENT_SECRET" | jq .
LEVEL 3: Architectural M2M Hardening (Anti-Pattern vs Production-Ready) Secret Lifecycle Architecture
❌ Flawed & Dangerous Setup
  • มอบสิทธิ์ realm-admin หรือ Service Account Admin ให้บอทเพื่อง่ายต่อการทดสอบ
  • ตั้งอายุ Access Token ยาวเป็นชั่วโมงหรือเป็นวันสำหรับ M2M Token
  • ฝัง client_secret ไว้ใน Environment Variable ของ Container โดยตรงแบบ Plaintext
  • ไม่เคยเปลี่ยน Secret เลยตลอดอายุการใช้งาน (Static Credentials)
  • เปิดใช้งาน Standard Flow หรือ Direct Access Grants ค้างไว้ใน M2M Client
# เสี่ยง: เสี่ยงต่อการยึดครองทั้ง Realm เมื่อคีย์หลุด และขยาย Blast Radius สูงสุด
✅ Production-Grade Hardened M2M
  • ปฏิบัติตาม Least Privilege: ให้เฉพาะ Client Roles สำหรับ Resource นั้นๆ
  • กำหนด Short Lifespan (เช่น 60–300 วินาที) สำหรับ M2M Token
  • หมุนเวียน Secret ผ่าน Secret Manager แบบ Zero-Downtime Overlapping Rotation
  • สำหรับบริการที่ต้องการความปลอดภัยขั้นสูงสุด ให้ยกระดับไปใช้ Private Key JWT (RFC 7523)
  • ปิดโฟลว์ที่ไม่เกี่ยวข้องทั้งหมด (เปิดเฉพาะ Service Accounts Enabled)
# ปลอดภัยสูง: สอดคล้องกับมาตรฐาน Zero-Trust Architecture และข้อกำหนด FAPI
Simulator #05 Live M2M Token Exchanger & Secret Rotation Sandbox

Live Client Credentials Exchanger & Zero-Downtime Secret Rotation

ทดลองจำลองการยิงคำขอของ Worker Service เพื่อขอ Access Token และสังเกตโครงสร้าง Payload ของ JWT ที่ได้ พร้อมทดลองหมุนเวียน Secret แบบ Zero-Downtime เพื่อดูว่าระบบจัดการคีย์สลับกันอย่างไร:

2. Active Client Secret
คีย์ปัจจุบัน: Version 1 (Active)
Response Status
200 OK
Token Issued Instantly
Refresh Token Field
NULL (NONE)
RFC 6749 Compliant
Decoded Access Token Payload (JWT Claims) SECURE M2M
{
  "iss": "http://localhost:8080/realms/app-dev",
  "sub": "sa-uuid-99124-billing-daemon",
  "client_id": "billing-daemon",
  "resource_access": {
    "billing-daemon": {
      "roles": ["invoice-writer"]
    }
  },
  "scope": "email profile",
  "exp": 1791629100,
  "iat": 1791628800
}
Key takeaway: สังเกตว่าใน Claim sub ของ Token จะไม่ใช่ ID ของมนุษย์ แต่เป็น UUID ของ Service Account User ที่ Keycloak สร้างให้โดยอัตโนมัติ และไม่มีการคืน refresh_token เพราะเครื่องจักรสามารถหมุนเวียน Access Token ได้เอง!
Active Recall Comprehensive Exam 12 Production Scenarios (100% Constructive Alignment)

Day 5 Service-to-Service & Client Credentials Exam

ครอบคลุม Client Credentials Flow, Service Accounts, Zero-Downtime Rotation พร้อมข้อสอบ Spaced Repetition จาก Day 1, Day 2, Day 3 และ Day 4

Live Score
0/12 answered
0 / 12
🎯

Day 5 Practical Deliverable (ภารกิจประจำวัน)

ก่อนก้าวไปสู่ Days 06-07 (Checkpoint Lab #1: Entity Modeling Audit & CLI Handshake Suite) คุณต้องทำภารกิจต่อไปนี้ให้สำเร็จบน Keycloak ของคุณ: