Skip to content

10.5 · Credentials + secrets

บทนี้ catalog secrets ที่ commit อยู่ใน repo ทั้ง 5 ตัว — เป็นหมวดที่รุนแรงที่สุดเพราะทุก entry เป็น credential ที่ใช้งานได้จริง รายละเอียดของ secret values จะไม่แสดงในบทนี้ — อ้างเฉพาะ file:line ที่ commit อยู่


Severity ranking ของ secret

TypeSeverityเหตุผล
GCP service account key🔴 Criticalimpersonate service account ได้
Production device keys🔴 Criticalbypass "is this a known app?" check
JWT secret default🔴 Criticalforge token ได้ถ้า env unset
.env ที่มี prod DB password🔴 Criticaldirect DB access
.env ที่มี AD/SFTP/HRMI password🔴 Criticallateral movement
REACT_APP_* ใน frontend bundle🔴 Criticalshipped ไปทุก browser
Integration keys (KEY_PLASMA ฯลฯ)🟡 Highimpersonate cross-system caller
.env.example placeholder🟢 Mediumอาจมีคนใช้จริง

Inventory 1 · GCP service account key

1:12:vtrc-api/vtrc-gcp-sa-key.json
{
  "type": "service_account",
  "project_id": "vtrc-nonprod",
  "private_key_id": "b05e73c8e621ea878718ce25ba37770a2627e1cc",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "vtrc-api-sa@vtrc-nonprod.iam.gserviceaccount.com",

🔴 Critical — full RSA-2048 private key ของ service account vtrc-api-sa@vtrc-nonprod.iam.gserviceaccount.com commit อยู่ใน repo คนที่มี repo read สามารถ

  • impersonate service account บน GCP
  • เข้าถึง GCP resources ที่ SA นี้มีสิทธิ์ (Cloud Storage, Pub/Sub, Secret Manager ถ้ามี)
  • ถ้า SA มี broad IAM scope impact เป็น project-wide

Required action

  1. Rotate key ทันทีใน GCP Console → IAM → Service Accounts → Keys
  2. Purge file ออกจาก git history (git filter-repo --path vtrc-api/vtrc-gcp-sa-key.json --invert-paths)
  3. Force-push และ reclone ทุก worktree
  4. ใช้ Workload Identity หรือ short-lived token แทน long-lived key

Inventory 2 · Production device keys

41:85:vtrc-api/api/src/config.js
  API_DEVICE_KEY = {
    prod: [
      {
        id: '93361f06-6f55-11eb-9439-0242ac130002',
        key: 'fd77b56f-1489-4fb1-a094-a614f0d16198',
        APP_TYPE: 'WEB',
        APP_NAME: 'VTRC-WEB'
      },
      ... 6 more prod keys (MOBILE, WEB_ADMIN, INTEGRATE_APP × 2, TWOWAY, MEETING)

🔴 Critical — production device keys ทั้ง 7 ตัว (prod) + 7 ตัว (UAT ที่ config.js:86-148) commit ในรูป plaintext

apiKey header เป็นสิ่งที่ frontend ส่งมากับทุก GraphQL request — vtrc-api lookup ใน API_DEVICE_KEY array เพื่อ resolve APP_TYPE (WEB, WEB_ADMIN, MOBILE, ...) ตามที่อธิบายในบทที่ 9.2

เมื่อ keys รั่ว

  • ผู้โจมตีสามารถส่ง request ที่ฝังตัวเป็นแอป VTRC-WEB, VTRC-BACKOFFICE ฯลฯ ได้ทันที
  • Gate 1 ของ signInApp (apiKey validation) ผ่านโดยไม่มีอุปสรรค
  • Authentication ที่เหลืออยู่ที่ cu-central-api LDAP/AD bind อย่างเดียว (ไม่ใช่ NestJS centralize-api / ไม่ใช่ MSSQL password check)

Lookup เป็น linear filter

34:34:vtrc-api/api/src/index.js
  const mapKey = API_DEVICE_KEY[API_ACCESS].filter(value => value.key === apiKey)

filter() เป็น linear scan ไม่มี hash map — performance ไม่ critical (เพราะ array สั้น) แต่บ่งบอกว่าไม่ได้ออกแบบให้ keys หมุนเวียนบ่อย

Required action

  1. Rotate keys ทั้งหมด — สร้าง UUID ใหม่แต่ละตัว
  2. ย้ายไป secret manager (Vault, GCP Secret Manager, AWS Secrets Manager)
  3. Hash ด้วย Argon2id ใน DB + compare ด้วย constant-time
  4. App identity ควรเป็น claim ใน signed JWT ไม่ใช่ static header

Inventory 3 · JWT secret default

12:12:vtrc-api/api/src/config.js
  JWT_SECRET = 'secret',

🔴 Critical — ถ้า env JWT_SECRET ไม่ถูก set ใน deployment ใด (เช่น container ใหม่ ลืม env, staging ที่ copy config มาก่อน set env) vtrc-api จะ boot ด้วย secret 'secret' โดยไม่ fail

ผู้โจมตีที่อ่าน source (ซึ่งเป็น public repo) สามารถ forge JWT ที่ valid ได้ทันที — payload structure ({id, uid}) เป็นที่รู้กันจาก auth.js:118-120

Required action

javascript
// ตัวอย่าง guard ที่ควรมี
if (IS_PRODUCTION && (JWT_SECRET === 'secret' || !JWT_SECRET)) {
  throw new Error('JWT_SECRET must be set in production')
}

Inventory 4 · centralize-api .env committed

1:15:centralize-api/.env
NODE_ENV=prod
PROJECT_NAME=Centralize Api
SERVER_URL=http://10.188.128.160
PORT=3000

DB_TYPE=mssql
DB_HOST=10.188.128.58
DB_PORT=1433
DB_USER=CENTRALIZE_APP_UAT
DB_PASSWORD=Xfwo_gawPp.!
DB_NAME=dbHRMI_Center

DB_NAME_NBC=dbHRMI_Center_NBC

SECRET_KEY=AAAAB3NzaC1yc2EAAAADAQABAAABAQCMH5xOB6PH3INC7ScxkR2b3IChoJJDpCcQqgwwtzKxqkXBTUNqb+qE20Z2WJcnca9rPTjiuahlOChjRo8mELybKZt65NrWHYbj2hv4Yliybz8xPZJC9KIT3hlcwDv5jqqUmeDRNk1MUEn1SNXJDquaerv1vQU5EZYw0/hXYNc2Y+hqltWcdPCJ12cpBO9VXnOvtGWLzufMLOHTsupaPqcjRpid5xI7tx/cQZvSKw8gUA/9UNlX/XhSToU72NEQLpxBFyGhRqPYJQprvFpqdGOmhIzjsSTut5cKdxqYmsWUbGZgVVgEzLBdA/JXrCLWXoiLaohNciqvG/xdnzyKldJb

🔴 Critical.env (ไม่ใช่ .env.example) ของ centralize-api ถูก commit โดยมี

  • NODE_ENV=prod
  • live MSSQL password (Xfwo_gawPp.!) สำหรับ user CENTRALIZE_APP_UAT ต่อ 10.188.128.58:1433
  • SECRET_KEY ที่เป็น public portion ของ SSH key (abuse ของ field)

Inventory 5 · cu-central-api .env.develop-test committed

10:13:cu-central-api/main-api/.env.develop-test
HASH_KEY="8fd70d89d726bcea39ee6bbfa711634ee908e788114afbb1252140f09370e362"

JWT_ISSUER="Red Cross Centralize API - UAT"
JWT_SECRET="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7RsavIFFAjNeDFuE9pcieDRaWTrVQkHmrjont4owoLrKMMuciB3Wocp5g3fyvjR6FXOrP5ysHp/k25CB5VvgRKg2Ahb2vZwD3C9vgpOpN19GY2BSsU6oy4k0Afvf1j87i/qt2m7z2mGEl0gLvDe3gY9+A9RrZ95HS1ZtT4PFFwvQ8DTQ4Swz5NF2VGHcwQF1dF/sX4ZwUFGdshD1WPNw/s/qqa+11AIRRjbWIVtMB6gvvfoqPyDXzQi1dZU0JTPvKla9Mfl50n64Rg7omimmcHKBQ1ERrVhkHqycH8pVB9DSllT8IaOCIpoWN0sMRGa31YBdMf8PPGCp6698SpHmEQIDAQAB"

🔴 Critical — ไฟล์นี้ leak credentials ของระบบต้นน้ำ ~7 ระบบ

Secretบริการที่เกี่ยวข้อง
HASH_KEYsigning ภายใน cu-central-api
JWT_SECRETJWT signing (ความยาว RSA public key แสดงว่าใช้ผิดประเภท)
DB_PASSWORDMSSQL password
AD_PASSWORDActive Directory service account password
Conicle SFTP passwordSFTP ไป Conicle
HRMI passwordHR master data
SSO passwordSSO service
SMS gateway keySMS notification
AES_SECRET_KEY + AES_INIT_VECTORAES encryption key + IV

single file นี้เป็นลายเซ็นเข้าถึงระบบต้นน้ำเกือบทั้งหมด


Inventory 6 · REACT_APP_* ใน frontend bundle

1:6:vtrc-rc-backoffice/.env
REACT_APP_END_POINT_RECRUITMENT_API_ATTACHFILE='https://vtrcapi.redcross.or.th/recruitment-api'
REACT_APP_PRIVATE_KEY='aue3uNdEfInEdb565dec6cOtHdn95e8d'
REACT_APP_END_POINT_EXPORT='https://vtrcapi.redcross.or.th/workforce-api'
REACT_APP_AI_RECRUITMENT_API='https://vtrcapi.redcross.or.th/ai-recruitment-api/api'
REACT_APP_PMS_API_END_POINT='https://vtrcapi.redcross.or.th/pms-api/api'
REACT_APP_STIMUL_CONN_STRING="Integrated Security=False;Data Source=10.188.128.61;Initial Catalog=:db:;User ID=sa; Password=h@ckm3SA?"

🔴 Critical — Create React App embed ทุก REACT_APP_* env var เข้าไปใน JS bundle ที่ shipped ไป browser ทุกตัว

  • REACT_APP_PRIVATE_KEY — shipped ไปทุก client
  • REACT_APP_STIMUL_CONN_STRINGMSSQL sa password (h@ckm3SA?) สำหรับ 10.188.128.61 shipped ไปทุก browser ที่เปิด backoffice

ใครก็ที่เปิด SPA สามารถ extract credentials จาก JS bundle ได้ทันที — DevTools → Sources หรือ grep ใน minified bundle

Required action

  1. ย้ายทุก REACT_APP_* ที่ sensitive ออก — ใช้ server-side proxy แทน
  2. Rotate sa password ทันที
  3. ใช้ BFF (Backend-for-Frontend) pattern — frontend เรียกผ่าน server ที่ถือ secret

Inventory 7 · Integration keys

152:154:vtrc-api/api/src/config.js
  KEY_CU_H = 'CU-RE-STRUCTURE-ID-02',
  KEY_PLASMA = '47837e2d-7788-4a1a-8e24-a90abf6742f4',
  KEY_HR = ['305A814A-CEAC-449B-BF92-CCAEC3475B1A', '5A764C0F-6C0D-46C5-83E0-9B94C755BFE2'],

🟡 High — static identifiers ที่ใช้เป็น trust token ระหว่าง vtrc-api กับระบบต้นน้ำ

  • KEY_CU_H — CU restructure integration
  • KEY_PLASMA — plasma subsystem
  • KEY_HR — HR-side callers (2 GUIDs)

ค่าพวกนี้ commit อยู่ใน repo — ใครเห็น source สามารถ impersonate cross-system caller ได้


Inventory 8 · .env.example placeholders

14:17:vtrc-api/api/.env.example
DB_USERNAME="root"
DB_PASSWORD="1q2w3e4r00"
DB_HOST=vtrc-database

🟢 Medium — placeholder password 1q2w3e4r00 เป็น pattern ที่คนมักใช้จริง ใน deployment ต่าง ๆ ควร verify ว่าไม่มี env ไหนยังใช้ password นี้อยู่


ทำไม secrets ถึงเป็น Critical

repo read (1 secret leak)

   ├──▶ prod device keys → impersonate VTRC app
   │       │
   │       └──▶ GraphQL mutation ทุกตัว → privilege escalation

   ├──▶ JWT 'secret' default → forge token (ถ้า env unset)
   │       │
   │       └──▶ เข้าในฐานะ user ใดก็ได้

   ├──▶ .env DB password → direct DB access
   │       │
   │       └──▶ dump HR data (IdentityCard, salary, ...)

   ├──▶ .env AD/SFTP/HRMI password → lateral movement
   │       │
   │       └──▶ pivot ไประบบต้นน้ำทั้งหมด

   ├──▶ REACT_APP_* in bundle → client-side leak
   │       │
   │       └──▶ ทุกคนที่เปิด SPA ได้ secret

   └──▶ GCP SA key → impersonate on cloud

           └──▶ access GCP resources ที่ SA มีสิทธิ์

Wave 0 stop-the-bleed checklist

ตาม audit/05-tech-debt-register.md Wave 0 (Weeks 0-2)

#ActionSecret
1Rotate GCP SA key + purge from gitInventory 1
2Rotate prod device keys (ทั้ง 7) + ย้ายไป secret managerInventory 2
3Hard-fail ที่ boot ถ้า JWT_SECRET missing ใน prodInventory 3
4Rotate centralize-api DB password + ลบ .env ออกจาก repoInventory 4
5Rotate cu-central-api credentials ทั้งหมด + ลบ .env.develop-testInventory 5
6Rotate MSSQL sa password + ย้ายออกจาก REACT_APP_*Inventory 6

ทุก action ต้องทำควบคู่กับการ purge git history (git filter-repo) — ไม่งั้น secret ยังอยู่ใน reflog และ commit เก่า


อ่านต่อ → 9.6 Injection + RCE