13.6 Environment configuration
บทนี้สำรวจวิธีที่แต่ละ service อ่าน configuration, ตัวแปร environment ที่ใช้, และจุดที่มีค่า default อันตราย
กลไกการอ่าน configuration
| Service | กลไก | ไฟล์ที่เกี่ยวข้อง |
|---|---|---|
| vtrc-api | require('dotenv').config() + process.env | api/src/config.js:1 |
| cu-central-api | require('dotenv').config() + process.env | main-api/src/config.js |
| centralize-api | @nestjs/config ConfigService | src/app.module.ts และ src/config/ |
| vtrc-web | build-time — เลือก endpoint ที่ runtime ผ่าน window.location.href | src/config/apollo.js |
| vtrc-rc-backoffice | build-time — เช่นเดียวกัน | src/config/apollo.js |
รูปแบบทั่วไปของ backend คืออ่าน env ตอน start แล้ว export เป็น object ที่ใช้ทั่วระบบ — vtrc-api/api/src/config.js เป็นตัวอย่างชัดเจน
รายการ environment variable หลัก
ตารางรวบรวม env var จาก .env.example ของทั้ง 3 backend services:
vtrc-api (จาก vtrc-api/api/.env.example)
| หมวด | Variable | ค่า default | หมายเหตุ |
|---|---|---|---|
| Runtime | NODE_ENV | development | ใช้ตรวจสอบ IS_PRODUCTION |
| Runtime | APP_NO | 0 | ระบุ replica |
| Runtime | APP_PORT | 8081 | port ที่ service ฟัง |
| Runtime | TIME_ZONE | Asia/Bangkok | |
| Database | DB_SCHEMA | '' (ว่าง) | mariadb หรือ mysql |
| Database | DB_HOST | '' (ว่าง) | |
| Database | DB_NAME | '' (ว่าง) | vtrc |
| Database | DB2_NAME | — | vtrc-centralize |
| Secrets | DATA_ENCRYPT_KEY | — | คีย์เข้ารหัสข้อมูล |
| Secrets | JWT_SECRET | 'secret' ⚠️ | ค่า default อันตราย — ถ้า env ไม่ตั้ง ทุก JWT จะเซ็นด้วย 'secret' |
| External | END_POINT_CENTRALIZE | — | ชี้ไป cu-central-api |
| External | END_POINT_CENTRALIZE_AUTH | — | ชี้ไป cu-central-api auth |
| External | END_POINT_CONICLE | — | Conicle LMS |
| External | API_CENTRALIZE_KEY | — | API key สำหรับเรียก cu-central-api |
| External | CLIENT_ID_CENTRALIZE | — | client identifier |
| File storage | FILE_STORAGE_PATH | /root_path/storage | |
| File storage | LOG_PATH | /root_path/logs | |
| Transfer | TRANSFER_PROTOTAL (typo) | — | scp |
| Transfer | TRANSFER_HOST | — | host สำหรับ scp |
| Transfer | TRANSFER_USERNAME | — | |
| Transfer | TRANSFER_PASSWORD | — |
cu-central-api (จาก cu-central-api/main-api/.env.example)
| หมวด | Variable | ค่า default | หมายเหตุ |
|---|---|---|---|
| Runtime | APP_PORT | 8080 | |
| Runtime | TIME_ZONE | +07:00 | |
| Secrets | HASH_KEY | — | คีย์สำหรับ hash |
| Secrets | JWT_SECRET | 'JWT_SECRET_KEY' ⚠️ | ค่า default ที่ไม่ใช่ค่าจริง — ต้อง override |
| Database | DB_SCHEMA, DB_HOST, DB_NAME, ... | '' (ว่าง) | |
| Redis | REDIS_HOST | localhost | |
| Redis | REDIS_PASSWORD | 'REDIS_PASSWORD' | placeholder |
| LDAP | AD_URL | ldap://localhost | LDAP ขององค์กร |
| LDAP | AD_BASE_DN | dc=redcross,dc=or,dc=th | |
| LDAP | AD_USERNAME | — | bind DN |
| LDAP | AD_PASSWORD | — | bind password |
| SMTP | SMTP_HOST, SMTP_PORT, ... | — | ส่ง email |
| External | HRMI_END_POINT | — | HRMI service |
| External | HRMI_USERNAME, HRMI_PASSWORD | — |
centralize-api (จาก src/config/)
centralize-api ใช้ NestJS ConfigService ที่อ่านจาก .env โดยอัตโนมัติ — variable ที่ใช้มีโครงสร้างคล้าย cu-central-api (DB, MSSQL, JWT_ISSUER, JWT_SECRET)
จุดสังเกตจาก centralize-api/src/main.ts:18-19:
const config = app.get<ConfigService>(ConfigService);
const port = config.get('port') || 3000;ค่า default ที่อันตราย
vtrc-api — JWT_SECRET
vtrc-api/api/src/config.js:12:
JWT_SECRET = 'secret',ถ้าตัวแปร environment JWT_SECRET ไม่ถูกตั้งในขณะที่ service start, JWT ทุกตัวจะถูกเซ็นด้วย string 'secret' ซึ่งเป็นความลับที่ไม่เป็นความลับ — ผู้โจมตีที่รู้สามารถปลอมแปลง JWT ของผู้ใช้คนใดก็ได้
ปัญหานี้ถูกรายงานใน docs/audit/05-tech-debt-register.md (SEC-4) ว่าเป็น "Critical"
vtrc-api — DB credentials ใน .env.example
vtrc-api/api/.env.example:13-14:
DB_USERNAME="root"
DB_PASSWORD="1q2w3e4r00"แม้เป็น .env.example ค่า 1q2w3e4r00 อาจเป็น password จริงที่ทีมเคยใช้และลืมเปลี่ยน — ควรเป็น <your-password-here> แทน
vtrc-api — DATA_ENCRYPT_KEY ใน .env.example
vtrc-api/api/.env.example:3:
DATA_ENCRYPT_KEY="79E1289D21753BA7C9FEA0BD0F6CB14F5AD65D6B9B6C7501E9B291F9D1819E4F"ค่านี้เป็น 64 หลัก hex ที่ดูเหมือน production key — ถ้าใช้จริง การรั่วไหลของ .env.example (ซึ่งอยู่ใน repository) เท่ากับเปิดเผยคีย์เข้ารหัส
cu-central-api — NODE_ENV=development ใน docker-compose
cu-central-api/docker-compose.yml:45,64,81:
environment:
- NODE_ENV=developmentcompose นี้ใช้ใน production แต่ตั้ง NODE_ENV=development ทำให้ code ที่ตรวจ IS_PRODUCTION ทำงานผิดพลาด — เช่น GraphQL playground จะเปิดทำงาน (vtrc-api/api/src/index.js:68-69)
การจัดการ secrets ในปัจจุบัน
จากการตรวจสอบ repository พบว่า:
- ไม่มี secret manager — ไม่มี HashiCorp Vault, GCP Secret Manager, หรือ AWS Secrets Manager
- ไม่มี KMS encryption — ไม่มีการเข้ารหัส secret ที่เก็บใน env
.envcommit ใน centralize-api Dockerfile —centralize-api/Dockerfile:19คัดลอก.envจาก build stage เข้า image production หมายความว่า secret ทุกตัวอยู่ใน image- GCP service account key commit ใน repo —
vtrc-api/vtrc-gcp-sa-key.jsonอยู่ใน version control (รายละเอียด Volume 9 บทที่ 5)
vtrc-api config.js — ที่เก็บข้อมูลที่ละเอียดอ่อน
vtrc-api/api/src/config.js:41-148 มีข้อมูลที่เก็บแบบ hard-coded:
API_DEVICE_KEY = {
prod: [
{
id: '93361f06-6f55-11eb-9439-0242ac130002',
key: 'fd77b56f-1489-4fb1-a094-a614f0d16198',
APP_TYPE: 'WEB',
APP_NAME: 'VTRC-WEB'
},
// ... 7 prod keys + 10 UAT keys
],
uat: [ ... ]
}Device key เหล่านี้ใช้เป็น apiKey header ในทุก GraphQL request — ค่า key ทั้งหมดอยู่ใน code ที่ commit ลง repository (รายละเอียด Volume 9 บทที่ 5 — SEC-3)
config ที่ควรอยู่ใน env แต่อยู่ใน code
นอกจาก API_DEVICE_KEY ยังมี config อื่นที่ฝังใน config.js แทน env:
JWT_EXP = '15m'— TTL ของ JWTJWT_ISSUER = 'redcross.or.th:vtrc'— issuer claimDB_MORE_DATABASE = ['vtrc-centralize']— schema list- สถานะทั้งหมดของ workflow (statusLeave, statusHrmiName, ...) — แม้จะไม่ใช่ secret แต่การฝังใน code ทำให้เปลี่ยนไม่ได้โดยไม่ deploy ใหม่
การตรวจสอบ IS_PRODUCTION
vtrc-api/api/src/config.js กำหนดค่าคงที่:
export const IS_PRODUCTION = (NODE_ENV === 'production')IS_PRODUCTION ถูกใช้ใน code เพื่อ:
- เปิด/ปิด GraphQL playground (แต่
index.js:68-69ไม่ได้ gate — เปิดเสมอ) - ตัดสินใจเรื่อง logging level
- ตัดสินใจเรื่อง schema sync
ปัญหาคือ NODE_ENV=development ใน docker-compose ของ cu-central-api ทำให้ตรรกะเหล่านี้ทำงานผิด
ปัญหาเชิงปฏิบัติการ
| ปัญหา | ที่มา | ผลกระทบ |
|---|---|---|
JWT_SECRET = 'secret' default | config.js:12 | ถ้า env ว่าง, JWT ทุกตัวเซ็นด้วย 'secret' |
.env.example มีค่าที่ดูเป็น production | .env.example:3,14 | รั่วไหลถ้า push ไป public repo |
NODE_ENV=development ใน compose ของ cu-central-api | docker-compose.yml:45,64,81 | IS_PRODUCTION = false ในทุกที่ |
.env คัดลอกใน centralize-api Docker build | Dockerfile:19 | secret อยู่ใน image ที่ push ไป GCR |
| Device keys อยู่ใน config.js | config.js:42-148 | ผู้ที่อ่าน repo ได้ = ปลอมตัวเป็น app |
| ไม่มี rotation mechanism | ทั้งระบบ | key ใช้มานาน, ไม่มี log การใช้งาน |
ชื่อ env var TRANSFER_PROTOTAL (typo ของ PROTOCOL) | .env.example:26 | ใช้มานานแล้ว เปลี่ยนทีหลังทำให้เสีย compatibility |
ข้อแนะนำเพื่อปรับปรุง
- ยกเลิกค่า default ที่อันตราย — service ต้อง crash ตอน start ถ้า
JWT_SECRETหรือDB_PASSWORDว่าง - ย้าย secret ไป secret manager — GCP Secret Manager สำหรับ GCP-hosted services
- เปลี่ยน
.env.exampleให้เป็น placeholder —<your-jwt-secret-here>แทนค่าจริง - แยก
.env.productionจาก.env.development— และไม่ commit.env.production - อย่าคัดลอก
.envใน Dockerfile — inject ผ่านdocker run --env-fileหรือ secret mount - เพิ่ม pre-commit hook ที่ block การ commit ค่าที่ดูเป็น secret (regex ของ UUID, ความยาว ≥ 32)