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; MSSQLsapassword เดียวกันใน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
| Member | Scorecard ID | Where secret appears | Branch |
|---|---|---|---|
benefit-api | SEC-BENEFIT-01 | .env (+ JWT with DB/SFTP) | prod |
recruitment-api | SEC-RECRUIT-01 | .env SECRET_KEY + REFRESH_TOKEN_SECRET_KEY | prod |
ai-recruitment-api | SEC-AIRECRUIT-01 | .env | prod |
pms-api | SEC-PMS-02 | .env:41 — explicitly shares with the three above | uat |
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 keyImpact summary
| Capability after forge | Tied to |
|---|---|
| Call Nest JWT-guarded routes as arbitrary user | All four services |
Use AI generation without role (SEC-AIRECRUIT-02) | Any valid signature |
Combine with @Public() salary/medical | Even 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)
| Member | Scorecard ID | Evidence |
|---|---|---|
chat-center-api | SEC-CHAT-04 | .env:10-11,26,28-29 (prod) |
job-scheduler-api | SEC-JOBSCHED-01 | .env:9-11,18 — same pattern; .env even copies BASE_PATH=benefit-api/ (QUAL-JOBSCHED-02) |
vtrc-common | SEC-COMMON-03 | .env:11,17 + Docker bake Dockerfile:19 |
workflow-api | SEC-WORKFLOW-02 | real 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):
| Repo | ID | Note |
|---|---|---|
2way-vtrc-api | SEC-2WAYVTRC-03b | util/env.ts:17-32 Prod block hardcodes sa + host 10.188.128.61 |
benefit-api | SEC-BENEFIT-01 | Multiple 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 family | Repos | Evidence |
|---|---|---|
SECRET_JWT_TWOWAY | 2way-api, 2way-batch, 2way-line-service (canonical-truths) | Issued by backdoor path SEC-2WAYAPI-01; verified in middleware patterns |
Hardcoded JWT SECRET ≠ TWOWAY | 2way-vtrc-api | SEC-2WAYVTRC-04 · util/jwt.config.ts:2 vs checkbypasstoken.ts:65 |
| SSO AES key/IV + SSO password fallbacks | 2way-api | SEC-2WAYAPI-02 · auth.service.ts:20-26 |
| Same SSO AES literals in loginSSO | 2way-vtrc-api | SEC-2WAYVTRC-08 · loginSSO.ts:21-27 (also referenced in 02-core-pipeline) |
Blast after reading source:
- Use backdoor → get TWOWAY JWT 48h (
SEC-2WAYAPI-01) - Or forge if
SECRET_JWT_TWOWAYleaked from env files - Decrypt/forge SSO payloads if AES fallbacks used (
SEC-2WAYAPI-02) - Confusion between two JWT secrets on
2way-vtrc-apiincreases 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)
| Files | ID | Contents (names only) |
|---|---|---|
.envdev, .envuat, .envprod | SEC-MTVTRC-01 | API_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
| Repo | ID | Mechanism | Blast |
|---|---|---|---|
vtrc-rc-backoffice | SEC-VTRC-RC-01 | Tracked .env + CRA REACT_APP_* inline | Stimulsoft MSSQL connection string in browser bundle (legacy C2) |
recruitment-web | SEC-RCWEB-01 | .env/.env.uat tracked; cryptoJs.js:1-32; Dockerfile stage-2 ENV ineffective for CRA bake | AES keys decrypt candidate PII; also in downloadable JS |
meeting-backoffice | SEC-MTBO-01 / 02 | Hardcoded X-API-Key; .env UAT URLs | Client 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)
| Repo | ID | Evidence |
|---|---|---|
vtrc-mobile | SEC-MOBILE-01 | src/utils/login.js:16-42 PEM in bundle |
new-vtrc-mobile | SEC-NEW-01 | src/constants/RSAKey.ts:11-38 same key copied |
vtrc-mobile Android | SEC-MOBILE-02 | build.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
| Repo | ID | Keys (types) | Abuse |
|---|---|---|---|
ai-recruitment-api | SEC-AIRECRUIT-01 | OpenAI, remove.bg, HCTI | Direct financial abuse |
recruitment-api | SEC-RECRUIT-01 | SMTP, reCAPTCHA, GCP SA JSON | Email spam / captcha bypass / cloud |
pms-api | SEC-PMS-02 | SMTP + GCP SA | Same |
benefit-api | SEC-BENEFIT-01 | SFTP password ×3 sites in code | FMIS file channel |
Set provider budget alerts when rotating (benefit-api / ai-recruitment scorecard remediation).
3.9 Cluster H — Device apiKeys / hardcoded config
| Location | ID | Note |
|---|---|---|
cu-central-api | SEC-CU-02 | Two apiKeys hardcoded in auth controller |
pms-api | SEC-PMS-02 (related) | Device keys in configuration.ts:69-211 |
recruitment-api | SEC-RECRUIT-01 | Device keys configuration.ts:75-104 |
vtrc-mobile | SEC-MOBILE-03 | Prod 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 git | benefit + recruit + ai-recruit + pms JWT APIs | SEC-*-01/02 cluster A |
MSSQL sa from common/chat/jobsched/workflow | Entire SQL instance (network permitting) | Cluster B |
2way-api source | Admin JWT via backdoor without secret rotate | SEC-2WAYAPI-01 |
"testnaja" string | 2way-vtrc middleware skip | SEC-2WAYVTRC-01 |
Meeting .envprod | Prod meeting JWT/SMTP/DB | SEC-MTVTRC-01 |
| RC-backoffice bundle | Stimulsoft MSSQL string | SEC-VTRC-RC-01 |
| Mobile APK | RSA private + (if gradle) signing secrets | SEC-MOBILE-01/02, SEC-NEW-01 |
OpenAI key from ai-recruit .env | Paid API burn | SEC-AIRECRUIT-01 |
3.11 Coordinated rotation playbook
Phase 0 — Inventory (day 0)
- Clone each repo on canonical branch (prod/uat/prod-meeting-vtrc-api as applicable)
- List env names only (never paste values into chat):
grep -E '^(SECRET_|DB_|SMTP_|SSO_|MAIL_|API_)' .env | cut -d= -f1 - Attach matrix rows from §3.2–§3.8 to the IR ticket
Phase 1 — Generate new secrets
| Cluster | Action |
|---|---|
| A | New per-service JWT secrets (preferred) or one shared temporary then split |
| B | New sa password + create least-privilege logins |
| C | New SECRET_JWT_TWOWAY, AES, SSO app password |
| D | New meeting JWT + MAIL_PASS + API_KEY per env |
| E–H | New AES/apiKeys/RSA/GCP/OpenAI/SFTP |
Phase 2 — Cutover window
- Deploy env injection (k8s Secret / Vault / CI) before removing old values from running pods
- Restart all cluster members in the same window
- Invalidate sessions (short JWT TTL helps;
ignoreExpiration: trueon 2way —SEC-2WAYAPI-05— blocks clean invalidate until fixed)
Phase 3 — History hygiene
.gitignore.env*(except placeholder.env.example)git filter-repo/ BFG per repo policy- Revoke GCP SA keys and provider API keys (cannot “filter-repo” cloud keys)
Phase 4 — Prove old secrets dead
| Test | Expect |
|---|---|
| Forge JWT with old Domain key | 401 on all four services |
Connect MSSQL with old sa | Login failed |
2way login i3admin | Fail (R02 done) |
Meeting call with old SECRET_JWT_* | 401 |
3.12 Systemic pattern reinforcement (Vol 17.4)
.envproduction commit — ≥15 repos- Shared JWT
SECRET_KEY— benefit/recruit/ai-recruit/pms @Public()on write/PII — NestJS services- Backdoor strings — 2way-api, 2way-vtrc-api, cu-central
- Raw SQL template literals — benefit, 2way, chat, recruit, vtrc-common, vtrc-api
- No secret manager anywhere
Patterns 1–2 + 6 are why this chapter exists as its own volume section.
3.13 What is NOT claimed
| Claim | Status |
|---|---|
| Exact identical byte match of every password across all hosts in live prod | Documented from git evidence; live vault may differ — still rotate what was ever committed |
vtrc-api definitely verifies Domain SECRET_KEY today | Flagged 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 |