Skip to content

13.5 nginx reverse proxy & SSL

บทนี้อธิบายการตั้งค่า nginx ที่เป็น reverse proxy หลักของระบบ รวมถึง SSL termination, upstream load balancing, และ security headers

ภาพรวม nginx ในระบบ

ไฟล์ configบทบาทServer block หลัก
vtrc-api/nginx/default.confReverse proxy สำหรับ vtrc-api + serve vtrc-webvtrcAPI.redcross.or.th, vtrc.redcross.or.th
cu-central-api/nginx/default.confReverse proxy สำหรับ cu-central-api (GraphQL) เท่านั้น — ไม่มี upstream NestJS :3000centralizeapi.redcross.or.th
vtrc-rc-backoffice/nginx/default.confServe static ของ backofficelocalhost
vtrc-web/nginx.confconfig ภายใน Docker image ของ vtrc-weblisten 80

vtrc-api nginx — upstream และ load balancing

vtrc-api/nginx/default.conf:1-37 กำหนด upstream ไว้หลายกลุ่ม:

nginx
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;
}

จุดสังเกต:

  1. โหลด balancer แบบ round-robin (default ของ nginx) กระจายภาระระหว่าง localhost:8081, 8082, 8083 — สอดคล้องกับ APP_NO=1, 2, 3 ใน docker-compose
  2. vtrc-image-nodes ที่ port 8085 — แยก traffic การอัปโหลด/ดาวน์โหลดไฟล์ออกจาก API หลัก บ่งชี้ว่ามีการวาง replica ที่ 5 (APP_NO=5) สำหรับจัดการไฟล์โดยเฉพาะ
  3. มี upstream หลายตัวที่ไม่ใช่ vtrc-api — logEvent, stimul, workforce, incident-management, 2way, meeting ล้วนเป็น service อื่นที่ mount ผ่าน nginx ของ vtrc-api ทำให้ nginx ตัวนี้เป็นจุดรวม traffic หลักของระบบทั้งหมด
  4. รองรับเพียง host networkserver 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;
    }
}

จุดสังเกต:

  1. server_tokens off — ปิดการแสดงเวอร์ชัน nginx ใน response header
  2. more_set_headers 'Server: vtrc-server' — เปลี่ยนชื่อ server ใน header เพื่อปกปิดเทคโนโลยีที่ใช้ (security through obscurity)
  3. Strict-Transport-Security — บังคับให้ browser ใช้ HTTPS ต่อไปเรื่อย ๆ (1 ปี) รวม subdomain และ preload list
  4. proxy_read_timeout 600 — 10 นาที รองรับ PDF generation ที่อาจช้า (รายละเอียด Volume 11 บทที่ 4 — execSync block event loop)
  5. X-Frame-Options ถูก comment ไว้ สำหรับ vtrcAPI.redcross.or.th — แต่เปิดไว้ใน vtrc.redcross.or.th (บรรทัดที่ 214) ความไม่สอดคล้องนี้น่าสนใจ
  6. 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
    }
}

จุดสังเกต:

  1. /adn/ — directory html-app-dn mount แยก — docker-compose.yml:23 บอกว่าเป็น vtrc-mobile-app บ่งชี้ว่าเป็น static file ของ mobile app ที่ถูก serve ผ่าน nginx เดียวกับเว็บ
  2. try_files $uri $uri/ /index.html — SPA fallback มาตรฐานสำหรับ React Router ฝั่ง client-side
  3. /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:

nginx
upstream centralized-api {
    server localhost:8000;
    server localhost:8002;
    server localhost:8003;
}

จุดสังเกต:

  • upstream ชื่อ centralized-api — แม้ codebase จะเรียก service นี้ว่า cu-central-api config ยังใช้ชื่อเดิม สะท้อนการเปลี่ยนชื่อในอดีต
  • 3 replica สอดคล้องกับ cu-central-api/docker-compose.yml:38-91 (localhost:8000, 8002, 8003)
  • ไม่มี server localhost:3000 ใน upstream — NestJS centralize-api ไม่ได้อยู่ใน path ของ nginx นี้ (ดู Volume 12.1 E-08) อย่าสมมติว่า centralizeapi.redcross.or.th proxy ไป NestJS

cu-central-api nginx — server block หลาย port

cu-central-api/nginx/default.conf:64-151 มีสาม server block:

  1. Port 80 (default.conf:7-63) — serve static ไม่มี TLS ใช้สำหรับ health check ภายใน
  2. Port 25563 ssl (default.conf:64-107) — server_name centralizeapi.redcross.or.th บน port ไม่มาตรฐาน
  3. Port 443 ssl (default.conf:108-151) — server_name centralizeapi.redcross.or.th บน port มาตรฐาน แต่ proxy ไปที่ localhost:8082 (ไม่ใช่ upstream centralized-api)

จุดสังเกตที่สำคัญ:

  • server block บน 443 proxy ไป localhost:8082default.conf:147 proxy_pass http://localhost:8082; ทำให้เส้นทางนี้ไม่ใช้ load balancer ของ upstream แต่เป็น direct connection
  • server block บน 25563 proxy ไป centralized-api upstreamdefault.conf:103 proxy_pass http://centralized-api; ใช้ load balancer ปกติ
  • ความแตกต่างนี้บ่งชี้ว่าอาจมี service ที่รันบน port 8082 นอกเหนือจาก cu-central-api — เป็นประเด็นที่ต้องสอบถามทีมปฏิบัติการ

vtrc-web nginx (ใน Docker image)

vtrc-web/nginx.conf:1-9:

nginx
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 — สรุป

Headervtrc-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 ต้องการ iframe
  • Content-Security-Policy ของ cu-central-api ระบุ default-src https: เฉย ๆ ไม่มี source list ทำให้ browser ใช้ default ที่เข้มงวดกว่าที่คาด

ปัญหาเชิงปฏิบัติการ

ปัญหาที่มาผลกระทบ
upstream ตั้ง localhost:PORT ทำงานได้เพราะ host networkdefault.conf:1-37ย้ายไป bridge network ไม่ได้โดยไม่แก้ nginx config
proxy_read_timeout 600 บน location /default.conf:88request ที่ช้าจะยึด nginx worker ไว้นาน — ลด concurrency
/db-management/ ถูก comment แต่ port 8093 ยังเปิดdefault.conf:186-193phpMyAdmin เข้าถึงได้จากภายนอก
ชื่อ upstream centralized-api ผิดจากชื่อจริงcu-central-api/nginx/default.conf:1สับสนตอน debug
ใบรับรอง SSL อยู่ใน volume mountdocker-compose + nginxต้อง renew บน host เอง — ไม่มี automation ที่เห็น

ข้อแนะนำเพื่อปรับปรุง

  1. เปลี่ยน upstream จาก localhost:PORT เป็นชื่อ container — เตรียมพร้อมสำหรับการย้ายไป bridge network หรือ Kubernetes
  2. ใช้ nginx -t เป็น step บังคับใน CI — ตรวจสอบ syntax ก่อน deploy
  3. เพิ่ม request rate limit ผ่าน limit_req_zone — ป้องกัน brute force login (รายละเอียด Volume 9)
  4. ใช้ certbot + Let's Encrypt หรือ certificate manager อัตโนมัติ
  5. ลบ phpMyAdmin container ออกจาก production compose หรือจำกัด IP เข้มข้นขึ้น