13.5 nginx reverse proxy & SSL
บทนี้อธิบายการตั้งค่า nginx ที่เป็น reverse proxy หลักของระบบ รวมถึง SSL termination, upstream load balancing, และ security headers
ภาพรวม nginx ในระบบ
| ไฟล์ config | บทบาท | Server block หลัก |
|---|---|---|
vtrc-api/nginx/default.conf | Reverse proxy สำหรับ vtrc-api + serve vtrc-web | vtrcAPI.redcross.or.th, vtrc.redcross.or.th |
cu-central-api/nginx/default.conf | Reverse proxy สำหรับ cu-central-api (GraphQL) เท่านั้น — ไม่มี upstream NestJS :3000 | centralizeapi.redcross.or.th |
vtrc-rc-backoffice/nginx/default.conf | Serve static ของ backoffice | localhost |
vtrc-web/nginx.conf | config ภายใน Docker image ของ vtrc-web | listen 80 |
vtrc-api nginx — upstream และ load balancing
vtrc-api/nginx/default.conf:1-37 กำหนด upstream ไว้หลายกลุ่ม:
upstream vtrc-api-nodes {
server localhost:8081;
server localhost:8082;
server localhost:8083;
#server localhost:8084; # ปิดการใช้งาน — บ่งชี้ว่าเคยมี replica 4
}
upstream vtrc-image-nodes {
server localhost:8085;
#server localhost:8086;
}
upstream logEvent-api-nodes {
server localhost:7100;
}
upstream stimul-api-nodes {
server localhost:6101;
}
upstream workforce-api-nodes {
server localhost:6102;
}
upstream incident-management-api-nodes {
server localhost:7201;
}
upstream incident-management-nodes {
server localhost:7200;
}
upstream 2way-nodes {
server localhost:8001;
}
upstream meeting-nodes {
server localhost:8000;
}
upstream 2way-backoffice-nodes {
server localhost:3000;
}
upstream 2way-api-nodes {
server localhost:8200;
}จุดสังเกต:
- โหลด balancer แบบ round-robin (default ของ nginx) กระจายภาระระหว่าง
localhost:8081,8082,8083— สอดคล้องกับ APP_NO=1, 2, 3 ใน docker-compose vtrc-image-nodesที่ port 8085 — แยก traffic การอัปโหลด/ดาวน์โหลดไฟล์ออกจาก API หลัก บ่งชี้ว่ามีการวาง replica ที่ 5 (APP_NO=5) สำหรับจัดการไฟล์โดยเฉพาะ- มี upstream หลายตัวที่ไม่ใช่ vtrc-api — logEvent, stimul, workforce, incident-management, 2way, meeting ล้วนเป็น service อื่นที่ mount ผ่าน nginx ของ vtrc-api ทำให้ nginx ตัวนี้เป็นจุดรวม traffic หลักของระบบทั้งหมด
- รองรับเพียง host network —
server localhost:PORTทำงานได้เพราะ nginx รันด้วยnetwork_mode: host
vtrc-api nginx — server block หลัก
vtrc-api/nginx/default.conf:54-207 — server block สำหรับ vtrcAPI.redcross.or.th บน port 443:
server {
listen 443 ssl;
server_name vtrcAPI.redcross.or.th;
server_tokens off;
more_set_headers 'Server: vtrc-server';
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
ssl_certificate /etc/sslcert/vtrcApi.crt;
ssl_certificate_key /etc/sslcert/vtrcApi.key;
ssl_protocols TLSv1.2 TLSv1.3;
# ... ciphers ...
location / {
proxy_pass http://vtrc-api-nodes; # → API หลัก
client_max_body_size 50m;
proxy_read_timeout 600; # 10 นาที — รองรับ PDF generation ช้า
}
location /graphql/file/ {
proxy_pass http://vtrc-image-nodes; # → file API แยก
client_max_body_size 50m;
proxy_read_timeout 60;
}
}จุดสังเกต:
server_tokens off— ปิดการแสดงเวอร์ชัน nginx ใน response headermore_set_headers 'Server: vtrc-server'— เปลี่ยนชื่อ server ใน header เพื่อปกปิดเทคโนโลยีที่ใช้ (security through obscurity)Strict-Transport-Security— บังคับให้ browser ใช้ HTTPS ต่อไปเรื่อย ๆ (1 ปี) รวม subdomain และ preload listproxy_read_timeout 600— 10 นาที รองรับ PDF generation ที่อาจช้า (รายละเอียด Volume 11 บทที่ 4 —execSyncblock event loop)X-Frame-Optionsถูก comment ไว้ สำหรับvtrcAPI.redcross.or.th— แต่เปิดไว้ในvtrc.redcross.or.th(บรรทัดที่ 214) ความไม่สอดคล้องนี้น่าสนใจContent-Security-Policyมีเฉพาะฝั่ง web (default.conf:218) —default-src https: data: 'unsafe-inline' 'unsafe-eval'ค่อนข้างตึง แต่ก็เปิดunsafe-evalซึ่งอาจจำเป็นสำหรับ Webpack dev mode
vtrc-api nginx — server block สำหรับ vtrc.redcross.or.th
vtrc-api/nginx/default.conf:208-268:
server {
listen 443 ssl;
server_name vtrc.redcross.or.th;
...
add_header X-Frame-Options "SAMEORIGIN" always;
...
location /2way-backoffice/ {
proxy_pass http://2way-backoffice-nodes; # → port 3000
}
location /incident-management/ {
proxy_pass http://incident-management-nodes; # → port 7200
}
location /adn/ {
root /usr/share/nginx/html-app-dn;
index index.html index.htm;
}
location / {
root /usr/share/nginx/html; # → serve static ของ vtrc-web
try_files $uri $uri/ /index.html; # → SPA fallback
}
}จุดสังเกต:
/adn/— directoryhtml-app-dnmount แยก —docker-compose.yml:23บอกว่าเป็นvtrc-mobile-appบ่งชี้ว่าเป็น static file ของ mobile app ที่ถูก serve ผ่าน nginx เดียวกับเว็บtry_files $uri $uri/ /index.html— SPA fallback มาตรฐานสำหรับ React Router ฝั่ง client-side/2way-backoffice/และ/incident-management/— เส้นทางสำหรับระบบย่อยอื่นที่ไม่ได้อยู่ใน scope ของคู่มือนี้
vtrc-api nginx — HTTP redirect
vtrc-api/nginx/default.conf:38-53 — server block สองตัวบน port 80 ที่เปลี่ยนเส้นทางไป HTTPS:
server {
listen 80;
server_name vtrcAPI.redcross.or.th;
return 301 https://vtrcapi.redcross.or.th$request_uri;
}
server {
listen 80;
server_name vtrc.redcross.or.th;
return 301 https://vtrc.redcross.or.th$request_uri;
}จุดสังเกต: ชื่อ vtrcAPI.redcross.or.th (ตัว A ใหญ่) ถูกเปลี่ยนเส้นทางไปยัง vtrcapi.redcross.or.th (ตัว a เล็ก) — เพื่อรองรับผู้ใช้ที่พิมพ์ผิด case
cu-central-api nginx — upstream สองชั้น
cu-central-api/nginx/default.conf:1-5:
upstream centralized-api {
server localhost:8000;
server localhost:8002;
server localhost:8003;
}จุดสังเกต:
- upstream ชื่อ
centralized-api— แม้ codebase จะเรียก service นี้ว่าcu-central-apiconfig ยังใช้ชื่อเดิม สะท้อนการเปลี่ยนชื่อในอดีต - 3 replica สอดคล้องกับ
cu-central-api/docker-compose.yml:38-91(localhost:8000,8002,8003) - ไม่มี
server localhost:3000ใน upstream — NestJScentralize-apiไม่ได้อยู่ใน path ของ nginx นี้ (ดู Volume 12.1 E-08) อย่าสมมติว่าcentralizeapi.redcross.or.thproxy ไป NestJS
cu-central-api nginx — server block หลาย port
cu-central-api/nginx/default.conf:64-151 มีสาม server block:
- Port 80 (
default.conf:7-63) — serve static ไม่มี TLS ใช้สำหรับ health check ภายใน - Port 25563 ssl (
default.conf:64-107) — server_namecentralizeapi.redcross.or.thบน port ไม่มาตรฐาน - Port 443 ssl (
default.conf:108-151) — server_namecentralizeapi.redcross.or.thบน port มาตรฐาน แต่ proxy ไปที่localhost:8082(ไม่ใช่ upstreamcentralized-api)
จุดสังเกตที่สำคัญ:
- server block บน 443 proxy ไป
localhost:8082—default.conf:147proxy_pass http://localhost:8082;ทำให้เส้นทางนี้ไม่ใช้ load balancer ของ upstream แต่เป็น direct connection - server block บน 25563 proxy ไป
centralized-apiupstream —default.conf:103proxy_pass http://centralized-api;ใช้ load balancer ปกติ - ความแตกต่างนี้บ่งชี้ว่าอาจมี service ที่รันบน port 8082 นอกเหนือจาก cu-central-api — เป็นประเด็นที่ต้องสอบถามทีมปฏิบัติการ
vtrc-web nginx (ใน Docker image)
vtrc-web/nginx.conf:1-9:
server {
listen 80;
location / {
root /usr/share/nginx/html/;
include /etc/nginx/mime.types;
try_files $uri $uri/ /index.html;
}
}config นี้ฝังใน Docker image ของ vtrc-web เพื่อ serve static file ในกรณีที่ไม่ผ่าน nginx หลักของ vtrc-api — แต่ใน production จริง vtrc-web static ถูก serve ผ่าน nginx หลักของ vtrc-api (location / ใน server block ของ vtrc.redcross.or.th)
SSL/TLS configuration
ทุก server block ที่ใช้ SSL มีค่าคล้ายกัน:
ssl_certificate /etc/sslcert/<name>.crt;
ssl_certificate_key /etc/sslcert/<name>.key;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;จุดสังเกต:
- เฉพาะ TLS 1.2 และ 1.3 — ปิด TLS 1.0/1.1 ที่ไม่ปลอดภัย
ssl_session_tickets off— ปิด session resumption ผ่าน ticket ทำให้ความปลอดภัยสูงขึ้น (ป้องกัน session hijacking ถ้า key รั่ว)- ใบรับรองอยู่ที่
/etc/sslcert/— mount ผ่าน volume./cert:/etc/sslcertใน docker-compose ทำให้การ renew ใบรับรองต้องดำเนินการที่ host
Security headers — สรุป
| Header | vtrc-api (443 api) | vtrc-api (443 web) | cu-central-api (25563) | cu-central-api (443) |
|---|---|---|---|---|
| X-XSS-Protection | ✅ | ✅ | ✅ | ✅ |
| X-Content-Type-Options | ✅ | ✅ | ✅ | ✅ |
| Strict-Transport-Security | ✅ | ✅ | ✅ | ✅ |
| X-Frame-Options | ❌ (comment) | ✅ | ❌ | ❌ |
| Content-Security-Policy | ❌ | ✅ (unsafe-eval) | ✅ (default-src https:) | ✅ (default-src https:) |
จุดที่ขัดแย้งกัน:
X-Frame-Optionsเปิดที่ web block แต่ปิดที่ api block — อาจเพราะ GraphQL playground ต้องการ iframeContent-Security-Policyของ cu-central-api ระบุdefault-src https:เฉย ๆ ไม่มี source list ทำให้ browser ใช้ default ที่เข้มงวดกว่าที่คาด
ปัญหาเชิงปฏิบัติการ
| ปัญหา | ที่มา | ผลกระทบ |
|---|---|---|
upstream ตั้ง localhost:PORT ทำงานได้เพราะ host network | default.conf:1-37 | ย้ายไป bridge network ไม่ได้โดยไม่แก้ nginx config |
proxy_read_timeout 600 บน location / | default.conf:88 | request ที่ช้าจะยึด nginx worker ไว้นาน — ลด concurrency |
/db-management/ ถูก comment แต่ port 8093 ยังเปิด | default.conf:186-193 | phpMyAdmin เข้าถึงได้จากภายนอก |
ชื่อ upstream centralized-api ผิดจากชื่อจริง | cu-central-api/nginx/default.conf:1 | สับสนตอน debug |
| ใบรับรอง SSL อยู่ใน volume mount | docker-compose + nginx | ต้อง renew บน host เอง — ไม่มี automation ที่เห็น |
ข้อแนะนำเพื่อปรับปรุง
- เปลี่ยน upstream จาก
localhost:PORTเป็นชื่อ container — เตรียมพร้อมสำหรับการย้ายไป bridge network หรือ Kubernetes - ใช้
nginx -tเป็น step บังคับใน CI — ตรวจสอบ syntax ก่อน deploy - เพิ่ม request rate limit ผ่าน
limit_req_zone— ป้องกัน brute force login (รายละเอียด Volume 9) - ใช้ certbot + Let's Encrypt หรือ certificate manager อัตโนมัติ
- ลบ phpMyAdmin container ออกจาก production compose หรือจำกัด IP เข้มข้นขึ้น