Skip to content

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 engineer1-2แก้ code ใน vtrc-api, centralize-api, cu-central-api
Ops engineer1หมุนเวียน secret, ปิด phpMyAdmin, ตั้ง monitor
Reviewer1review 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' เป็น
javascript
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

งาน:

javascript
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

งาน:

typescript
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

งาน:

javascript
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-ex git 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)
javascript
// เดิม (อันตราย)
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 วัน

งาน:

  • ยกระดับ jsonwebtoken v8 → v9 (ตรวจ breaking changes ใน changelog)
  • เปลี่ยน xlsx community → ใช้ exceljs หรือ CDN build
  • แก้ axois typo → 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