11.4 · Shared secrets & credentials — blast radius
ทุก volume ก่อนหน้าพบ "secret เดียวกันใช้ข้ามหลาย repo" เป็นข้อค้นพบแยกกัน บทนี้รวมทุกจุดเข้าเป็นภาพเดียว พร้อม "cluster" ที่บอกว่าถ้า secret ตัวใดตัวหนึ่งรั่ว จะกระทบ service อะไรบ้างพร้อมกัน
นิยาม — "shared secret" ในบทนี้หมายถึงกรณีที่ยืนยันแล้วว่าค่าเดียวกันตัวต่อตัวอักษร ถูก copy ไปหลาย repo (ไม่ใช่แค่ pattern ความเสี่ยงคล้ายกัน) ส่วนกรณีที่เป็นแค่ pattern เดียวกันแต่ค่าไม่ยืนยันว่าเท่ากัน จะระบุว่าเป็น "parallel pattern" แยกไว้ต่างหาก
Cluster 1 — JWT secret ข้าม legacy core + 4 domain microservice ("shared-secret SSO")
| Service | หลักฐาน | Role |
|---|---|---|
vtrc-api (legacy core) | issuer redcross.or.th:vtrc ใน JWT payload — token ที่ออกโดย vtrc-api verify ผ่านที่ recruitment-api ได้ตรง เพราะ secret เดียวกัน (recruitment-api/src/modules/auth/auth.service.ts:152,170,194,263) | ผู้ออก token ตัวจริง (legacy identity provider โดยพฤตินัย) |
benefit-api | benefit-api/.env (branch: prod):64 | ใช้ secret เดียวกันตรวจ JWT |
recruitment-api | recruitment-api/.env (branch: prod):44 — ยืนยันเท่ากับ benefit-api ตัวต่อตัวอักษร | ใช้ secret เดียวกันตรวจ JWT + แยก issuer เพื่อรู้ว่า token มาจาก vtrc-api หรือ recruitment-api เอง |
ai-recruitment-api | ai-recruitment-api/.env (branch: prod):11 — ยืนยันเท่ากับ benefit-api/recruitment-api ตัวต่อตัวอักษร (ai-recruitment-api/04-auth-integrations) | ใช้ secret เดียวกันตรวจ JWT ผ่าน middleware verifyJWT |
pms-api | pms-api/.env (branch: uat):41 — ยืนยันเท่ากับอีก 3 service (pms-api/04-auth-integrations) | ใช้ secret เดียวกันตรวจ JWT — แม้เป็นค่า UAT ไม่ใช่ prod แต่ค่าตรงกับ prod ของอีก 3 ตัว |
ข้อค้นพบใหม่ที่ manual รุ่นก่อนไม่ได้เน้น — เพราะ recruitment-api ตรวจ decodedToken.iss === 'redcross.or.th:vtrc' เพื่อแยก token จาก vtrc-api ได้สำเร็จ (ไม่ signature error) แปลว่า secret ของ vtrc-api เองก็อยู่ใน cluster เดียวกันนี้ด้วย — ทำให้ blast radius จริงคือ 5 service (vtrc-api + 4 domain microservice) ไม่ใช่ 4 ตามที่ canonical-truths.md/Volume 6 index.md ระบุไว้ ควรเพิ่ม fact นี้เข้า canonical-truths.md — ดู 11.5 Scorecard CORR-DATA-04
ผลกระทบ — ใครมี secret นี้ (รั่วอยู่แล้วผ่าน .env ที่ commit เข้า git ของทั้ง benefit-api, recruitment-api, ai-recruitment-api, pms-api) สร้าง JWT ปลอมที่ ทั้ง 5 service เชื่อพร้อมกัน ได้ทันที รวมถึงปลอมเป็น vtrc-api (ระบบที่มีข้อมูลพนักงานทั้งองค์กร) ได้ด้วย
Cluster 2 — MSSQL sa password ข้าม 4 domain microservice
| Service | หลักฐาน |
|---|---|
chat-center-api | .env commit พร้อม password sa จริง — SEC-CHAT-04 |
job-scheduler-api | .env commit พร้อม password sa เดียวกัน — SEC-JOBSCHED-01 |
vtrc-common | vtrc-common/.env:11 — SEC-COMMON-03 |
workflow-api | .env.example มี password จริงของ MSSQL sa (DB_PMS_PASSWORD) — SEC-WORKFLOW-02 |
ค่า password ยืนยันตรงกันตัวต่อตัวอักษรจาก canonical-truths.md: h@ckm3SA?
ผลกระทบ — password ระดับ sa (database admin เต็มสิทธิ์) ตัวเดียวเปิดทางเข้าได้ทั้ง MSSQL instance ที่ทั้ง 4 service ใช้ ไม่ใช่แค่ schema ของตัวเอง — ถ้า network ไม่ได้ segment ตาม service, การรั่วของ 1 .env เท่ากับรั่วสิทธิ์ admin ของ database ทั้งก้อน
Cluster 3 — JWT secret รูปแบบ RSA-key ข้าม 3 domain microservice (แยกจาก Cluster 1)
| Service | หลักฐาน |
|---|---|
vtrc-common | SECRET_KEY เป็น RSA public-key format, vtrc-common/.env:17 — SEC-COMMON-03 |
chat-center-api | ใช้ค่าเดียวกัน ตาม SEC-COMMON-03 ที่ระบุว่า "ใช้ร่วมกับ chat-center-api/job-scheduler-api" |
job-scheduler-api | ใช้ค่าเดียวกัน |
สำคัญ — นี่คือ secret คนละตัวกับ Cluster 1 (Cluster 1 เป็น string ธรรมดา, Cluster 3 เป็น RSA-key format) แม้ 3 service นี้จะทับซ้อนกับ Cluster 2 ในเรื่อง MSSQL sa password ก็ตาม — แปลว่า vtrc-common/chat-center-api/job-scheduler-api แชร์กัน 2 ชนิด secret พร้อมกัน (DB password + JWT secret) ส่วน workflow-api ยืนยันแค่ Cluster 2 (DB password) — ไม่มีหลักฐานยืนยันว่า workflow-api ใช้ RSA secret เดียวกันด้วย (ไม่กระทบมากเพราะ workflow-api ไม่มี global JWT guard อยู่แล้ว — SEC-WORKFLOW-01)
libs/jwt/jwt-auth.guard.ts, libs/log/logs.middleware.ts เหมือนกันไบต์ต่อไบต์ระหว่าง 3 service นี้ (QUAL-COMMON-02) — ยืนยันว่าเป็นการ copy-paste ทั้ง code และ secret จาก template เดียวกัน ไม่ใช่ shared library ที่ import จริง
Cluster 4 — SECRET_JWT_TWOWAY ข้าม 3 repo ใน 2way Platform
| Service | หลักฐาน |
|---|---|
2way-api | ใช้ SECRET_JWT_TWOWAY |
2way-batch | ใช้ค่าเดียวกัน |
2way-line-service | ใช้ค่าเดียวกัน |
ตาม canonical-truths.md — 2way-vtrc-api (แม้ชื่อคล้าย) มี secret ที่สองแยกต่างหาก hardcode ใน util/jwt.config.ts ไม่ได้อยู่ใน cluster นี้ — เป็นจุดที่ต้องระวังเพราะชื่อ repo ทำให้เข้าใจผิดว่าเป็นกลุ่มเดียวกันด้าน secret ด้วย
Cluster 5 — RSA private key ข้าม 2 mobile app
| App | หลักฐาน |
|---|---|
vtrc-mobile | RSA private key hardcode ในซอร์ส คอมไพล์เข้า JS bundle — SEC-MOBILE-01 |
new-vtrc-mobile | RSA private key เดียวกัน copy มาจาก vtrc-mobile — SEC-NEW-01 |
ผลกระทบ — key ตัวเดียวถอด/แก้ payload ที่เข้ารหัสด้วย key นี้ได้จากทั้งสองแอป และเพราะเป็นไฟล์ต้นฉบับที่ compile เข้า bundle (ไม่ใช่ env var) การหมุน key ต้อง rebuild + force-update ทั้งสองแอปพร้อมกัน
Parallel pattern (ไม่ใช่ shared secret แต่รูปแบบความเสี่ยงเดียวกัน)
| Pattern | Repo ที่พบ | หมายเหตุ |
|---|---|---|
AES key เข้ารหัส PII ฝัง hardcode ใน browser bundle (REACT_APP_*) | vtrc-web (S-3), vtrc-rc-backoffice (C1/C2), recruitment-web (SEC-RCWEB-01) | ค่าจริงไม่ยืนยันว่าเท่ากันข้าม 3 repo (แต่ละ repo ใช้เข้ารหัส PII ของฟอร์มตัวเอง) — เป็น pattern เดียวกัน (REACT_APP_* build-time env ถูก compile เข้า bundle ที่ browser ดาวน์โหลดได้) ไม่ใช่ secret เดียวกัน |
| Backdoor/bypass credential (ไม่ใช่ secret ที่แชร์ แต่เป็น credential ที่ hardcode ให้ bypass auth ทั้งระบบ) | 2way-api (SEC-2WAYAPI-01, username/password hardcode), 2way-vtrc-api (SEC-2WAYVTRC-01, static token "testnaja"), cu-central-api (SEC-CU-04, /auth/signinbypass) | รายละเอียด trust-boundary ดู Volume 12 · 02-shared-trust-boundaries |
.env/.env.* production ทั้งไฟล์ commit เข้า git | อย่างน้อย 15 repo — ดูรายการเต็มที่ Volume 12 · 04-scorecard | เป็น root cause ของ Cluster 1-5 ทั้งหมด — ถ้าไม่มี .env หลุด secret เหล่านี้จะไม่ verify ได้จากภายนอก repo |
Blast radius รวม — ASCII
┌───────────────────────────────────────┐
│ Cluster 1 — JWT secret (string) │
│ vtrc-api ── benefit-api ── recruitment-api │
│ └── ai-recruitment-api │
│ └── pms-api │
└───────────────────────────────────────┘
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Cluster 2 — MSSQL sa password│ │ Cluster 3 — JWT secret (RSA) │
│ chat-center-api │◄────┤ vtrc-common │
│ job-scheduler-api │◄────┤ chat-center-api │
│ vtrc-common │◄────┤ job-scheduler-api │
│ workflow-api │ └─────────────────────────────┘
└─────────────────────────────┘
(3 ใน 4 service ซ้อนกันทั้ง cluster 2 และ 3)
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ Cluster 4 — SECRET_JWT_TWOWAY│ │ Cluster 5 — RSA private key │
│ 2way-api │ │ vtrc-mobile │
│ 2way-batch │ │ new-vtrc-mobile │
│ 2way-line-service │ └─────────────────────────────┘
└─────────────────────────────┘
(2way-vtrc-api อยู่นอก cluster นี้ — มี secret แยกของตัวเอง)รวม 5 cluster ที่ยืนยันแล้วว่าค่าเดียวกันจริง ครอบคลุม 14 repo ที่ไม่ซ้ำกัน: vtrc-api, benefit-api, recruitment-api, ai-recruitment-api, pms-api (cluster 1) · chat-center-api, job-scheduler-api, vtrc-common, workflow-api (cluster 2) · chat-center-api, job-scheduler-api, vtrc-common ซ้ำกับ cluster 2 (cluster 3) · 2way-api, 2way-batch, 2way-line-service (cluster 4) · vtrc-mobile, new-vtrc-mobile (cluster 5) — chat-center-api/job-scheduler-api/vtrc-common ปรากฏทั้ง cluster 2 และ 3 จึงนับตัวไม่ซ้ำได้ 14 ชื่อ repo จากทั้งหมด ~25 repo ในแพลตฟอร์ม
debt ID สำหรับ data-model risk ที่รวมประเด็นนี้ไว้ ดู 11.5 Scorecard →