10.5 · Credentials + secrets
บทนี้ catalog secrets ที่ commit อยู่ใน repo ทั้ง 5 ตัว — เป็นหมวดที่รุนแรงที่สุดเพราะทุก entry เป็น credential ที่ใช้งานได้จริง รายละเอียดของ secret values จะไม่แสดงในบทนี้ — อ้างเฉพาะ file:line ที่ commit อยู่
Severity ranking ของ secret
| Type | Severity | เหตุผล |
|---|---|---|
| GCP service account key | 🔴 Critical | impersonate service account ได้ |
| Production device keys | 🔴 Critical | bypass "is this a known app?" check |
| JWT secret default | 🔴 Critical | forge token ได้ถ้า env unset |
.env ที่มี prod DB password | 🔴 Critical | direct DB access |
.env ที่มี AD/SFTP/HRMI password | 🔴 Critical | lateral movement |
REACT_APP_* ใน frontend bundle | 🔴 Critical | shipped ไปทุก browser |
Integration keys (KEY_PLASMA ฯลฯ) | 🟡 High | impersonate cross-system caller |
.env.example placeholder | 🟢 Medium | อาจมีคนใช้จริง |
Inventory 1 · GCP service account key
{
"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
- Rotate key ทันทีใน GCP Console → IAM → Service Accounts → Keys
- Purge file ออกจาก git history (
git filter-repo --path vtrc-api/vtrc-gcp-sa-key.json --invert-paths) - Force-push และ reclone ทุก worktree
- ใช้ Workload Identity หรือ short-lived token แทน long-lived key
Inventory 2 · Production device keys
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
const mapKey = API_DEVICE_KEY[API_ACCESS].filter(value => value.key === apiKey)filter() เป็น linear scan ไม่มี hash map — performance ไม่ critical (เพราะ array สั้น) แต่บ่งบอกว่าไม่ได้ออกแบบให้ keys หมุนเวียนบ่อย
Required action
- Rotate keys ทั้งหมด — สร้าง UUID ใหม่แต่ละตัว
- ย้ายไป secret manager (Vault, GCP Secret Manager, AWS Secrets Manager)
- Hash ด้วย Argon2id ใน DB + compare ด้วย constant-time
- App identity ควรเป็น claim ใน signed JWT ไม่ใช่ static header
Inventory 3 · JWT secret default
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
// ตัวอย่าง 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
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.!) สำหรับ userCENTRALIZE_APP_UATต่อ10.188.128.58:1433 SECRET_KEYที่เป็น public portion ของ SSH key (abuse ของ field)
Inventory 5 · cu-central-api .env.develop-test committed
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_KEY | signing ภายใน cu-central-api |
JWT_SECRET | JWT signing (ความยาว RSA public key แสดงว่าใช้ผิดประเภท) |
DB_PASSWORD | MSSQL password |
AD_PASSWORD | Active Directory service account password |
| Conicle SFTP password | SFTP ไป Conicle |
| HRMI password | HR master data |
| SSO password | SSO service |
| SMS gateway key | SMS notification |
AES_SECRET_KEY + AES_INIT_VECTOR | AES encryption key + IV |
single file นี้เป็นลายเซ็นเข้าถึงระบบต้นน้ำเกือบทั้งหมด
Inventory 6 · REACT_APP_* ใน frontend bundle
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 ไปทุก clientREACT_APP_STIMUL_CONN_STRING— MSSQLsapassword (h@ckm3SA?) สำหรับ10.188.128.61shipped ไปทุก browser ที่เปิด backoffice
ใครก็ที่เปิด SPA สามารถ extract credentials จาก JS bundle ได้ทันที — DevTools → Sources หรือ grep ใน minified bundle
Required action
- ย้ายทุก
REACT_APP_*ที่ sensitive ออก — ใช้ server-side proxy แทน - Rotate
sapassword ทันที - ใช้ BFF (Backend-for-Frontend) pattern — frontend เรียกผ่าน server ที่ถือ secret
Inventory 7 · Integration keys
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 integrationKEY_PLASMA— plasma subsystemKEY_HR— HR-side callers (2 GUIDs)
ค่าพวกนี้ commit อยู่ใน repo — ใครเห็น source สามารถ impersonate cross-system caller ได้
Inventory 8 · .env.example placeholders
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)
| # | Action | Secret |
|---|---|---|
| 1 | Rotate GCP SA key + purge from git | Inventory 1 |
| 2 | Rotate prod device keys (ทั้ง 7) + ย้ายไป secret manager | Inventory 2 |
| 3 | Hard-fail ที่ boot ถ้า JWT_SECRET missing ใน prod | Inventory 3 |
| 4 | Rotate centralize-api DB password + ลบ .env ออกจาก repo | Inventory 4 |
| 5 | Rotate cu-central-api credentials ทั้งหมด + ลบ .env.develop-test | Inventory 5 |
| 6 | Rotate MSSQL sa password + ย้ายออกจาก REACT_APP_* | Inventory 6 |
ทุก action ต้องทำควบคู่กับการ purge git history (git filter-repo) — ไม่งั้น secret ยังอยู่ใน reflog และ commit เก่า
อ่านต่อ → 9.6 Injection + RCE