Skip to content

3 · Shared-secret blast radius

บทนี้ตอบคำถาม: ถ้า secret หนึ่งหลุด กระทบกี่ระบบ? — Deep Redo ยืนยันว่าแพลตฟอร์มไม่มี secret manager กลาง และมีหลาย “cluster” ที่ค่าซ้ำข้าม repo

English: Blast radius ≠ one repo. A single committed SECRET_KEY or MSSQL sa password can forge tokens or open databases across services. Cite only scorecard/addendum/canonical-truths evidence.

กลับ Runbook · ถัดไป Auth failures


3.1 Canonical statement (do not weaken)

จาก docs/_audit-method/canonical-truths.md:

JWT secret เดียวกันใน benefit-api / recruitment-api / ai-recruitment-api / pms-api; MSSQL sa password เดียวกันใน chat-center-api / job-scheduler-api / vtrc-common / workflow-api — ไม่มี central auth/secret-management service

Vol 12.5 เสริม: rotate ของ shared JWT ต้องพร้อมกัน

Vol 11 / Vol 12 note: vtrc-api อาจอยู่ใน JWT blast ที่เคยไม่ถูกนับครบ (CORR-SEC-02 / CORR-DATA-04) — ตรวจ runtime verify ก่อน cutover ทุกครั้ง


3.2 Cluster A — Domain JWT SECRET_KEY

MemberScorecard IDWhere secret appearsBranch
benefit-apiSEC-BENEFIT-01.env (+ JWT with DB/SFTP)prod
recruitment-apiSEC-RECRUIT-01.env SECRET_KEY + REFRESH_TOKEN_SECRET_KEYprod
ai-recruitment-apiSEC-AIRECRUIT-01.envprod
pms-apiSEC-PMS-02.env:41 — explicitly shares with the three aboveuat

Exploit chain (from pms / benefit / recruit scorecards):

git clone → read SECRET_KEY from any one .env
        → jwt.sign({…}, SECRET_KEY)
        → call benefit / recruitment / ai-recruitment / pms APIs that trust that key

Impact summary

Capability after forgeTied to
Call Nest JWT-guarded routes as arbitrary userAll four services
Use AI generation without role (SEC-AIRECRUIT-02)Any valid signature
Combine with @Public() salary/medicalEven without forge — but forge expands remaining guarded routes

Remediation rule: One rotation ticket covering all four (plus any runtime consumer of the same value, including possible vtrc-api verify — verify before cutover). Then split per-service secrets long-term (Vol 13 SEC-API-02).

ไทย: อย่า rotate แค่ pms-api แล้วปิด ticket — token ที่ forge จาก .env ของ benefit ยังใช้กับ pms ได้จนกว่าทั้งคลัสเตอร์จะหมุน


3.3 Cluster B — MSSQL sa password (platform copy-paste)

MemberScorecard IDEvidence
chat-center-apiSEC-CHAT-04.env:10-11,26,28-29 (prod)
job-scheduler-apiSEC-JOBSCHED-01.env:9-11,18 — same pattern; .env even copies BASE_PATH=benefit-api/ (QUAL-JOBSCHED-02)
vtrc-commonSEC-COMMON-03.env:11,17 + Docker bake Dockerfile:19
workflow-apiSEC-WORKFLOW-02real password in .env.example:11,31,44 (uat)

Canonical-truths equates these as the same sa password cluster.

Blast: sa = instance superadmin — not limited to one schema. Attacker with network path to host gets read/write/drop across databases on that instance.

Also in sa / hardcode family (related but listed separately in scorecards):

RepoIDNote
2way-vtrc-apiSEC-2WAYVTRC-03butil/env.ts:17-32 Prod block hardcodes sa + host 10.188.128.61
benefit-apiSEC-BENEFIT-01Multiple MSSQL passwords (main/hrmi/vtrc) — rotate all three

Remediation: Rotate sa once coordinated; migrate app connections to least-privilege users; never commit .env.example with real passwords.


3.4 Cluster C — 2way JWT / SSO crypto

Secret familyReposEvidence
SECRET_JWT_TWOWAY2way-api, 2way-batch, 2way-line-service (canonical-truths)Issued by backdoor path SEC-2WAYAPI-01; verified in middleware patterns
Hardcoded JWT SECRET ≠ TWOWAY2way-vtrc-apiSEC-2WAYVTRC-04 · util/jwt.config.ts:2 vs checkbypasstoken.ts:65
SSO AES key/IV + SSO password fallbacks2way-apiSEC-2WAYAPI-02 · auth.service.ts:20-26
Same SSO AES literals in loginSSO2way-vtrc-apiSEC-2WAYVTRC-08 · loginSSO.ts:21-27 (also referenced in 02-core-pipeline)

Blast after reading source:

  1. Use backdoor → get TWOWAY JWT 48h (SEC-2WAYAPI-01)
  2. Or forge if SECRET_JWT_TWOWAY leaked from env files
  3. Decrypt/forge SSO payloads if AES fallbacks used (SEC-2WAYAPI-02)
  4. Confusion between two JWT secrets on 2way-vtrc-api increases misconfiguration risk (SEC-2WAYVTRC-04)

Rotate order: Delete backdoor (R02) → rotate SECRET_JWT_TWOWAY → remove SSO fallbacks → rotate AES/SSO app password → unify 2way-vtrc JWT to env-only.


3.5 Cluster D — Meeting JWT + SMTP + apiKey (three env files)

FilesIDContents (names only)
.envdev, .envuat, .envprodSEC-MTVTRC-01API_KEY, SECRET_JWT_*, MAIL_PASS, MSSQL PASSWORD/HOST on uat/dev

Blast: One git clone → credentials for dev + UAT + prod meeting stack. Attacker chooses easiest environment.

Additional hardcode: SEC-MTVTRC-08 cdb.connector.ts:3-26 api-key + Bearer in source; SEC-MTVTRC-04 Tinify key.


3.6 Cluster E — Frontend / CRA baked secrets

RepoIDMechanismBlast
vtrc-rc-backofficeSEC-VTRC-RC-01Tracked .env + CRA REACT_APP_* inlineStimulsoft MSSQL connection string in browser bundle (legacy C2)
recruitment-webSEC-RCWEB-01.env/.env.uat tracked; cryptoJs.js:1-32; Dockerfile stage-2 ENV ineffective for CRA bakeAES keys decrypt candidate PII; also in downloadable JS
meeting-backofficeSEC-MTBO-01 / 02Hardcoded X-API-Key; .env UAT URLsClient can call API with key

English: Frontend secrets are dual-leaked: git history and every user who loads the SPA.


3.7 Cluster F — Mobile RSA private key (cross-app)

RepoIDEvidence
vtrc-mobileSEC-MOBILE-01src/utils/login.js:16-42 PEM in bundle
new-vtrc-mobileSEC-NEW-01src/constants/RSAKey.ts:11-38 same key copied
vtrc-mobile AndroidSEC-MOBILE-02build.gradle:174-179 keystore passwords

Blast: Private key in any shipped APK/IPA is permanently compromised for that keypair. Copying into the new app expands victims. Keystore password leak enables signing malware as the org app.


3.8 Cluster G — Billable third-party API keys

RepoIDKeys (types)Abuse
ai-recruitment-apiSEC-AIRECRUIT-01OpenAI, remove.bg, HCTIDirect financial abuse
recruitment-apiSEC-RECRUIT-01SMTP, reCAPTCHA, GCP SA JSONEmail spam / captcha bypass / cloud
pms-apiSEC-PMS-02SMTP + GCP SASame
benefit-apiSEC-BENEFIT-01SFTP password ×3 sites in codeFMIS file channel

Set provider budget alerts when rotating (benefit-api / ai-recruitment scorecard remediation).


3.9 Cluster H — Device apiKeys / hardcoded config

LocationIDNote
cu-central-apiSEC-CU-02Two apiKeys hardcoded in auth controller
pms-apiSEC-PMS-02 (related)Device keys in configuration.ts:69-211
recruitment-apiSEC-RECRUIT-01Device keys configuration.ts:75-104
vtrc-mobileSEC-MOBILE-03Prod apiKey hardcode ×3 sites

Device keys map to APP_TYPE (workspace rules) — leaking them weakens the first gate before JWT.


3.10 Blast-radius matrix (quick IR)

If attacker obtains…Can reach…Primary IDs
Any Domain SECRET_KEY from gitbenefit + recruit + ai-recruit + pms JWT APIsSEC-*-01/02 cluster A
MSSQL sa from common/chat/jobsched/workflowEntire SQL instance (network permitting)Cluster B
2way-api sourceAdmin JWT via backdoor without secret rotateSEC-2WAYAPI-01
"testnaja" string2way-vtrc middleware skipSEC-2WAYVTRC-01
Meeting .envprodProd meeting JWT/SMTP/DBSEC-MTVTRC-01
RC-backoffice bundleStimulsoft MSSQL stringSEC-VTRC-RC-01
Mobile APKRSA private + (if gradle) signing secretsSEC-MOBILE-01/02, SEC-NEW-01
OpenAI key from ai-recruit .envPaid API burnSEC-AIRECRUIT-01

3.11 Coordinated rotation playbook

Phase 0 — Inventory (day 0)

  1. Clone each repo on canonical branch (prod/uat/prod-meeting-vtrc-api as applicable)
  2. List env names only (never paste values into chat): grep -E '^(SECRET_|DB_|SMTP_|SSO_|MAIL_|API_)' .env | cut -d= -f1
  3. Attach matrix rows from §3.2–§3.8 to the IR ticket

Phase 1 — Generate new secrets

ClusterAction
ANew per-service JWT secrets (preferred) or one shared temporary then split
BNew sa password + create least-privilege logins
CNew SECRET_JWT_TWOWAY, AES, SSO app password
DNew meeting JWT + MAIL_PASS + API_KEY per env
E–HNew AES/apiKeys/RSA/GCP/OpenAI/SFTP

Phase 2 — Cutover window

  1. Deploy env injection (k8s Secret / Vault / CI) before removing old values from running pods
  2. Restart all cluster members in the same window
  3. Invalidate sessions (short JWT TTL helps; ignoreExpiration: true on 2way — SEC-2WAYAPI-05blocks clean invalidate until fixed)

Phase 3 — History hygiene

  1. .gitignore .env* (except placeholder .env.example)
  2. git filter-repo / BFG per repo policy
  3. Revoke GCP SA keys and provider API keys (cannot “filter-repo” cloud keys)

Phase 4 — Prove old secrets dead

TestExpect
Forge JWT with old Domain key401 on all four services
Connect MSSQL with old saLogin failed
2way login i3adminFail (R02 done)
Meeting call with old SECRET_JWT_*401

3.12 Systemic pattern reinforcement (Vol 17.4)

  1. .env production commit — ≥15 repos
  2. Shared JWT SECRET_KEY — benefit/recruit/ai-recruit/pms
  3. @Public() on write/PII — NestJS services
  4. Backdoor strings — 2way-api, 2way-vtrc-api, cu-central
  5. Raw SQL template literals — benefit, 2way, chat, recruit, vtrc-common, vtrc-api
  6. No secret manager anywhere

Patterns 1–2 + 6 are why this chapter exists as its own volume section.


3.13 What is NOT claimed

ClaimStatus
Exact identical byte match of every password across all hosts in live prodDocumented from git evidence; live vault may differ — still rotate what was ever committed
vtrc-api definitely verifies Domain SECRET_KEY todayFlagged as reconciliation item (CORR-DATA-04) — verify before including in cutover list
Network path from internet to 10.188.128.*UNVERIFIED; assume internal attacker / VPN / pivot

ไป 4 · Auth mechanism failures