Skip to content

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-apibenefit-api/.env (branch: prod):64ใช้ secret เดียวกันตรวจ JWT
recruitment-apirecruitment-api/.env (branch: prod):44 — ยืนยันเท่ากับ benefit-api ตัวต่อตัวอักษรใช้ secret เดียวกันตรวจ JWT + แยก issuer เพื่อรู้ว่า token มาจาก vtrc-api หรือ recruitment-api เอง
ai-recruitment-apiai-recruitment-api/.env (branch: prod):11 — ยืนยันเท่ากับ benefit-api/recruitment-api ตัวต่อตัวอักษร (ai-recruitment-api/04-auth-integrations)ใช้ secret เดียวกันตรวจ JWT ผ่าน middleware verifyJWT
pms-apipms-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-commonvtrc-common/.env:11SEC-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-commonSECRET_KEY เป็น RSA public-key format, vtrc-common/.env:17SEC-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.md2way-vtrc-api (แม้ชื่อคล้าย) มี secret ที่สองแยกต่างหาก hardcode ใน util/jwt.config.ts ไม่ได้อยู่ใน cluster นี้ — เป็นจุดที่ต้องระวังเพราะชื่อ repo ทำให้เข้าใจผิดว่าเป็นกลุ่มเดียวกันด้าน secret ด้วย


Cluster 5 — RSA private key ข้าม 2 mobile app

Appหลักฐาน
vtrc-mobileRSA private key hardcode ในซอร์ส คอมไพล์เข้า JS bundle — SEC-MOBILE-01
new-vtrc-mobileRSA private key เดียวกัน copy มาจาก vtrc-mobileSEC-NEW-01

ผลกระทบ — key ตัวเดียวถอด/แก้ payload ที่เข้ารหัสด้วย key นี้ได้จากทั้งสองแอป และเพราะเป็นไฟล์ต้นฉบับที่ compile เข้า bundle (ไม่ใช่ env var) การหมุน key ต้อง rebuild + force-update ทั้งสองแอปพร้อมกัน


Parallel pattern (ไม่ใช่ shared secret แต่รูปแบบความเสี่ยงเดียวกัน)

PatternRepo ที่พบหมายเหตุ
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