16.2 Wave 0 — Stop-the-bleed
Wave 0 เป็นสิ่งที่ทำทันที — ลด Critical security/availability risk โดยไม่เปลี่ยน architecture
เป้าหมายของ Wave 0
- ลด Critical debt จาก 13 → 3 รายการ
- Effort: 2 สัปดาห์
- ไม่เปลี่ยน architecture, ไม่ upgrade runtime, ไม่เปลี่ยน database
ทีมที่ต้องการ
| บทบาท | จำนวน | หน้าที่ |
|---|---|---|
| Backend engineer | 1-2 | แก้ code ใน vtrc-api, centralize-api, cu-central-api |
| Ops engineer | 1 | หมุนเวียน secret, ปิด phpMyAdmin, ตั้ง monitor |
| Reviewer | 1 | review change ทุกตัว |
Task breakdown
Sprint 1 (สัปดาห์ที่ 1)
Task 0.1 · JWT_SECRET refuse to start (SEC-4)
Debt: SEC-4 Effort: 30 นาที ไฟล์: vtrc-api/api/src/config.js:12
งาน:
- แก้
JWT_SECRET = process.env.JWT_SECRET || 'secret'เป็น
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET === 'secret') {
console.error('JWT_SECRET must be set to a non-default value');
process.exit(1);
}- ตรวจสอบว่าทุก environment (prod, UAT, dev) มี
JWT_SECRETที่ไม่ใช่ค่า default
Definition of Done:
- [ ] service refuse to start ถ้า JWT_SECRET ว่างหรือเป็น
'secret' - [ ] deploy ไป UAT แล้วระบบยังทำงานได้
- [ ] deploy ไป prod สำเร็จ
Rollback: เปลี่ยน code กลับเป็นค่า default (ไม่ควรทำ — ควรแก้ env แทน)
Task 0.2 · ปิด playground/introspection ใน prod (SEC-2)
Debt: SEC-2 Effort: 30 นาที ไฟล์: vtrc-api/api/src/index.js:68-69
งาน:
playground: !IS_PRODUCTION,
introspection: !IS_PRODUCTION,Definition of Done:
- [ ] เข้า
https://vtrcapi.redcross.or.th/api/graphqlใน browser ไม่เห็น playground - [ ] introspection query ส่ง error ใน prod
- [ ] UAT ยังมี playground ได้
Task 0.3 · centralize CORS allow-list (SEC-5)
Debt: SEC-5 Effort: 30 นาที ไฟล์: centralize-api/src/main.ts:31-35
งาน:
app.enableCors({
origin: [
'https://vtrc.redcross.or.th',
'https://vtrcbackoffice.redcross.or.th',
'https://uat-vtrc.redcross.or.th',
'https://uat-vtrcbackoffice.redcross.or.th'
],
allowedHeaders: ['Content-Type', 'Authorization', 'apiKey'],
methods: ['GET', 'POST']
});Definition of Done:
- [ ] request จาก origin อื่นถูกปฏิเสธด้วย CORS error
- [ ] request จาก frontend ที่อนุญาตยังทำงานได้
Task 0.4 · ปิด syncModelToTable ใน prod (CORR-6)
Debt: CORR-6 Effort: 1 ชั่วโมง ไฟล์: vtrc-api/api/src/index.js:104
งาน:
if (!IS_PRODUCTION) {
syncModelToTable();
}สิ่งที่ต้องทำเพิ่ม: ตรวจสอบว่ามี migration script สำหรับ schema change ถ้าไม่มี ต้องเขียน (ก่อน deploy feature ที่เปลี่ยน model ครั้งต่อไป)
Definition of Done:
- [ ] production boot ไม่เรียก syncModelToTable
- [ ] UAT ยังเรียกเพื่อ dev convenience
Task 0.5 · ลบ phpMyAdmin จาก prod compose (PERF-7)
Debt: PERF-7 Effort: 1 ชั่วโมง ไฟล์: vtrc-api/docker-compose.yml:35-46
งาน:
- ลบ service
vtrc-pmaออกจาก production docker-compose.yml - สร้าง
docker-compose.dev.ymlสำหรับ dev/UAT ที่ยังมี phpMyAdmin
Definition of Done:
- [ ] port 8093 ไม่ตอบใน prod
- [ ] dev/UAT ยังใช้ phpMyAdmin ได้
Rollback: เพิ่ม service กลับเข้า compose (แต่ควรทำความเข้าใจว่าทำไมต้อง rollback ก่อน)
Task 0.6 · ลบ typosquat packages (LEG-13, SEC-9)
Debt: LEG-13, SEC-9 Effort: 1 วัน ไฟล์: package.json ใน vtrc-api, vtrc-rc-backoffice
งาน:
- ลบ
s,stream,finditจาก vtrc-api - ลบ
undefined-service-web,i,npm,saveจาก vtrc-rc-backoffice - ลบ
axoisจาก cu-central-api (เพิ่มaxios@^1.xแทนถ้ามี caller — ตรวจก่อน) - ลบ
error-exgit URL dependency จาก cu-central-api - regenerate lockfile
- รัน
npm audit --productionแล้วบันทึกผล
Definition of Done:
- [ ] ทุก package.json ไม่มี typosquat
- [ ]
npm installสำเร็จ - [ ] service start ได้ปกติ
Task 0.7 · commit centralize-api yarn.lock + เพิ่ม engines (LEG-15, LEG-18)
Debt: LEG-15, LEG-18 Effort: 30 นาที ไฟล์: centralize-api/yarn.lock (สร้างใหม่), package.json ทุกตัว
งาน:
- รัน
yarn installใน centralize-api เพื่อ generate yarn.lock แล้ว commit - เพิ่ม
"engines": { "node": ">=12.0.0" }ในทุก package.json (ปรับเป็น>=20.0.0ใน Wave 1) - ลบ
package-lock.jsonออกจาก vtrc-web (ใช้ yarn classic ตามที่เคย)
Definition of Done:
- [ ] centralize-api มี yarn.lock ใน repo
- [ ] vtrc-web มี lockfile เดียว
- [ ] ทุก package.json มี engines
Sprint 2 (สัปดาห์ที่ 2)
Task 0.8 · แก้ qpdf command injection (SEC-1, PERF-1 — mitigation ระยะสั้น)
Debt: SEC-1, PERF-1 Effort: 2-3 วัน ไฟล์: vtrc-api/api/src/routes.js:73,120,210,260,312
งาน:
- เปลี่ยน
execSyncเป็นexecFile(async ที่รับ arg array)
// เดิม (อันตราย)
execSync(`qpdf --encrypt ${password} ${password} 256 -- in.pdf out.pdf`);
// ใหม่ (ปลอดภัย)
await new Promise((resolve, reject) => {
execFile('qpdf', [
'--encrypt', password, password, '256', '--',
'in.pdf', 'out.pdf'
], (err) => err ? reject(err) : resolve());
});- เพิ่ม input validation: password ต้องเป็น alphanumeric เท่านั้น (defense in depth)
- เขียน test สำหรับกรณี password มี shell metachar
Definition of Done:
- [ ] ไม่มี
execSyncใน routes.js - [ ] test กรณี password
'; rm -rf / #'ไม่ execute shell - [ ] PDF payslip ยัง generate ได้ปกติ
Task 0.9 · หมุนเวียน device key + ย้ายไป env (SEC-3)
Debt: SEC-3 Effort: 1-2 วัน ไฟล์: vtrc-api/api/src/config.js:42-148
งาน:
- generate device key ใหม่ทั้งหมด (7 prod + 10 UAT)
- ย้ายไป environment variable
DEVICE_KEYS(JSON string) - แจ้งทีม frontend/mobil ปรับ apiKey ใหม่ในแต่ละ client
- หลังย้ายครบ ลบ device key เดิมจาก source
Definition of Done:
- [ ] source code ไม่มี device key ใน config.js
- [ ] client ทุกตัวใช้ key ใหม่ได้
- [ ] key เดิมถูก revoke
Rollback: คืนค่า config.js เดิม (key เดิมยังอยู่ใน git history แต่ควร rotate ทิ้งหมด)
Task 0.10 · หมุนเวียน GCP SA key (SEC-11)
Debt: SEC-11 Effort: 1-2 วัน ไฟล์: vtrc-api/vtrc-gcp-sa-key.json (ลบ), env ของ CI/CD
งาน:
- สร้าง SA key ใหม่ใน GCP console
- rotate key เดิม (disable + delete)
- เก็บ key ใหม่ใน secret manager ของ Bitbucket Pipelines
- ลบ
vtrc-gcp-sa-key.jsonออกจาก repo (และ git history ถ้าจำเป็น — ใช้git filter-repo) - แจ้งทีมว่าไม่ commit secret ใน repo อีก
Definition of Done:
- [ ] key เดิม disabled ใน GCP
- [ ] ไฟล์ไม่อยู่ใน repo (และ history)
- [ ] CI/CD ยัง push image ได้
Task 0.11 · แก้ typosquat รายการเล็ก (LEG-9, LEG-27, SEC-7, CORR-9)
Debt: LEG-9, LEG-27, SEC-7, CORR-9 Effort: รวม 1 วัน
งาน:
- ยกระดับ
jsonwebtokenv8 → v9 (ตรวจ breaking changes ใน changelog) - เปลี่ยน
xlsxcommunity → ใช้ exceljs หรือ CDN build - แก้
axoistypo →axios - แก้ httpRequest.js (ตรวจ caller ก่อนว่ามีจริง)
Definition of Done:
- [ ] ทุก dependency ไม่มี typosquat
- [ ] test pass
Task 0.12 · ลบ column ซ้ำ + แก้ log query (CORR-7, CORR-5)
Debt: CORR-7, CORR-5 Effort: 1 ชั่วโมง
งาน:
- ลบ declaration ซ้ำใน
vtrc-api/api/src/models/core/withdraw/hospital.js:14,138,159,189 - เปลี่ยน
centralize-api/src/app.module.ts:34,53ให้logging: ['error']ใน prod
Definition of Done:
- [ ] hospital.js ไม่มี column ซ้ำ
- [ ] centralize-api prod log ไม่มี SQL query
Task 0.13 · ตั้ง uptime monitor + alert (พื้นฐาน operations)
Debt: (ไม่มี ID — แต่สำคัญ) Effort: 1 วัน
งาน:
- สมัคร UptimeRobot (ฟรี) หรือใช้ service ที่มี
- เพิ่ม monitor สำหรับทุก domain (Volume 13.1)
- ตั้ง alert ส่ง LINE/อีเมลเมื่อ down
- ตั้ง disk alert ที่ 80% บนทุก host
Definition of Done:
- [ ] monitor ครอบคลุมทุก domain
- [ ] alert ทดสอบว่าทำงานจริง (ดึง service ออกแล้วรอ alert)
เกณฑ์การตัดสินใจว่า Wave 0 เสร็จ
ก่อนประกาศ Wave 0 เสร็จ ต้องตอบ "ใช่" กับทุกข้อ:
- [ ] Critical debt เหลือ ≤ 3 รายการ (ตรวจสอบใน Volume 14)
- [ ] penetration test ใหม่ผ่าน (หรืออย่างน้อยไม่พบ Critical ใหม่)
- [ ] uptime monitor + alert ทำงาน
- [ ] deployment guide (Volume 13.3) อัปเดตสำหรับการเปลี่ยนแปลง
- [ ] team retrospective — บันทึก lesson learned
สิ่งที่ต้องระวัง
1. อย่าทำ task ทุกตัวในวันเดียว
แม้แต่ task เล็ก ๆ อาจมีผลข้างเคียง — deploy ทีละ task แล้วสังเกต 24 ชั่วโมงก่อน task ถัดไป
2. อย่าลืม UAT
ทุก task ต้องทดสอบใน UAT ก่อน ไม่ใช่ deploy ตรงไป prod
3. บันทึกการกระทำ
แต่ละ task บันทึก:
- วันเวลาที่ deploy
- ผู้ deploy
- สิ่งที่เปลี่ยน
- ผลลัพธ์
ในกรณีเกิด incident ทีมต้อง track ได้ว่า deploy อะไรไปล่าสุด
4. ห้าม skip review
ถึงแม้ task จะเล็ก review ห้าม skip — มี task ที่ดูเล็กแต่ทำให้ prod พัง (เช่น env var ผิด)
สรุป Wave 0
Wave 0 ใช้เวลา 2 สัปดาห์ ลด Critical debt ได้ 10+ รายการ โดยไม่เปลี่ยน architecture
เมื่อ Wave 0 เสร็จ ทีมพร้อมเริ่ม Wave 1 (stabilize runtime และ dependency)
บทถัดไปจะลงรายละเอียด Wave 1