Nginx from Zero to Production · Part 4 — TLS, Security & Performance
Production-grade Nginx: HTTPS with a local self-signed cert (and Let's Encrypt), HTTP/2, gzip/brotli, response caching with proxy_cache, rate limiting, and the security headers every site needs — with exercises.
Proxy của bạn từ Phần 3 chạy được, nhưng nó là HTTP trần. Phần này thêm bốn thứ tách một đồ chơi khỏi production: mã hoá, nén, caching, và bảo vệ.
Ta sẽ thực hành tất cả cục bộ với một chứng chỉ tự ký.
1. HTTPS với chứng chỉ cục bộ
Trên production bạn sẽ lấy chứng chỉ miễn phí từ Let’s Encrypt (phần sau). Để học cục bộ, tạo một cái tự ký:
sudo mkdir -p /etc/nginx/certs
sudo openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout /etc/nginx/certs/local.key \
-out /etc/nginx/certs/local.crt \
-subj "/CN=localhost"
Giờ là một server block HTTPS:
server {
listen 443 ssl;
http2 on; # enable HTTP/2 (Nginx 1.25.1+ syntax)
server_name localhost;
ssl_certificate /etc/nginx/certs/local.crt;
ssl_certificate_key /etc/nginx/certs/local.key;
# Modern, safe defaults
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m; # reuse handshakes → faster reconnects
location / {
proxy_pass http://app_pool;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo nginx -t && sudo nginx -s reload
curl -k https://localhost/ # -k: accept the self-signed cert locally
-k bỏ qua xác thực tin cậy vì chứng chỉ tự ký không có trong kho tin cậy — điều này bình thường ở local, không bao giờ ở production.
Chuyển hết HTTP sang HTTPS
Giữ một block cổng 80 nhỏ với nhiệm vụ duy nhất là đẩy khách sang HTTPS:
server {
listen 80;
server_name localhost;
return 301 https://$host$request_uri; # permanent redirect to https
}
2. Let’s Encrypt cho domain thật
Trên server công khai với domain thật, Certbot tự động hoá mọi thứ — cấp, cài, và gia hạn chứng chỉ miễn phí:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# edits your Nginx config, obtains the cert, and sets up auto-renewal
sudo certbot renew --dry-run # verify renewal works
Chứng chỉ sống 90 ngày; Certbot cài một timer để gia hạn tự động. Bạn không phải đụng tới chúng nữa.
3. Nén — gzip & brotli
Nén response dạng text cắt 60–80% byte trên đường truyền. Bật gzip trong context http:
http {
gzip on;
gzip_comp_level 5; # 1=fast/large … 9=slow/small; 5 is a good balance
gzip_min_length 1024; # don't bother compressing tiny responses
gzip_vary on; # add "Vary: Accept-Encoding" for caches
gzip_types
text/plain text/css application/json
application/javascript text/xml application/xml image/svg+xml;
}
Lưu ý: gzip_types không nên gồm các định dạng đã nén (JPEG, PNG, MP4, woff2) — nén lại chúng tốn CPU mà gần như không lợi.
Brotli nén tốt hơn gzip ~15–20% và được mọi trình duyệt hiện đại hỗ trợ. Nó cần module ngx_brotli (đi kèm trong nhiều distro / các biến thể image chính thức):
brotli on;
brotli_comp_level 5;
brotli_types text/css application/javascript application/json image/svg+xml;
Kiểm tra response dùng cái nào:
curl -H 'Accept-Encoding: gzip, br' -I https://localhost/app.css -k
# → Content-Encoding: br (or gzip)
4. Header cache cho file tĩnh
Bảo trình duyệt cache asset có fingerprint thật lâu và HTML thì không. (Lý thuyết đầy đủ xem bài Frontend Caching.)
# Hashed assets (app.a3f9.js) never change → cache for a year
location ~* \.(js|css|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off; # don't log every asset hit
}
# HTML is the entry point → must revalidate so deploys take effect
location = /index.html {
add_header Cache-Control "no-cache";
}
5. Cache response với proxy_cache
Nginx có thể cache response backend để request lặp lại không chạm app — biến một API chậm thành tức thì.
# 1) define the cache store (in http context)
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=api_cache:10m max_size=1g inactive=60m;
server {
location /api/ {
proxy_pass http://app_pool;
proxy_cache api_cache; # use the store above
proxy_cache_valid 200 302 60s; # cache OK responses 60s
proxy_cache_valid 404 10s; # cache misses briefly too
proxy_cache_use_stale error timeout updating; # serve stale if backend dies
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / EXPIRED
}
}
Xem nó hoạt động:
curl -I https://localhost/api/products -k # X-Cache-Status: MISS (first hit)
curl -I https://localhost/api/products -k # X-Cache-Status: HIT (served from cache)
proxy_cache_use_stale là người hùng production: nếu backend chết hoặc chậm, Nginx phục vụ response tốt cuối cùng thay vì lỗi — site của bạn vẫn sống khi backend trục trặc.
Đừng bao giờ cache response cá nhân hoá (mọi thứ sau đăng nhập) trong cache Nginx dùng chung, kẻo user này thấy data của user khác. Bỏ qua cache cho route theo-user:
proxy_cache_bypass $cookie_session;.
6. Giới hạn tốc độ
Bảo vệ endpoint đăng nhập và API khỏi brute force và lụt request. Định nghĩa một vùng “thùng rò”, rồi áp dụng:
# define zones in the http context, keyed by client IP
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; # 10 req/sec/IP
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # 5 req/min/IP
server {
location /api/ {
limit_req zone=api burst=20 nodelay; # allow short bursts up to 20
proxy_pass http://app_pool;
}
location /login {
limit_req zone=login burst=3; # strict: brute-force defense
proxy_pass http://app_pool;
}
}
Cách đọc:
- tốc độ cho phép ổn định mỗi IP.
- một hàng đợi hấp thụ spike ngắn vượt tốc độ.
- phục vụ burst ngay thay vì giãn đều; thiếu nó, request trong hàng đợi bị trì hoãn cho khớp tốc độ.
Vượt giới hạn, Nginx trả 503 (đổi được qua limit_req_status 429;). Cũng có thể giới hạn số kết nối đồng thời mỗi IP bằng limit_conn nếu cần.
7. Header bảo mật
Vài header làm cứng mọi response. (Nền tảng chi tiết trong series Web Security.)
# HSTS: force HTTPS for 2 years (only add once HTTPS truly works!)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always; # no MIME sniffing
add_header X-Frame-Options "SAMEORIGIN" always; # anti-clickjacking
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# A starter Content-Security-Policy — tailor to your app
add_header Content-Security-Policy "default-src 'self'" always;
Từ khoá always quan trọng: thiếu nó, Nginx bỏ header trên response lỗi (4xx/5xx) — mà đó chính là những response kẻ tấn công dò.
Chỉ thêm HSTS sau khi HTTPS đã chạy hoàn chỉnh. Nó bảo trình duyệt từ chối HTTP thường suốt hai năm — ship khi còn lỗi là bạn khoá luôn user ở ngoài.
8. Tóm tắt
- Phục vụ HTTPS (tự ký ở local, Let’s Encrypt ở prod), bật HTTP/2, và 301 redirect HTTP→HTTPS.
- Nén text bằng gzip/brotli; đừng nén lại media.
- Cache asset tĩnh lâu, HTML không bao giờ; cache response backend bằng
proxy_cachevà sống sót qua sự cố nhờproxy_cache_use_stale. - Giới hạn tốc độ API và đặc biệt
/login. - Thêm header bảo mật kèm
always.
Tiếp — Phần 5: production & debug: một config thực tế hoàn chỉnh (SPA + API + cache + TLS), triển khai Docker/Compose, và cheat-sheet debug.
Bài tập
- HTTPS cục bộ: tạo chứng chỉ tự ký, phục vụ
https://localhost, và xác nhận bằngcurl -k -Irằng bạn nhậnHTTP/2 200. - Ép HTTPS: thêm block redirect cổng 80 và xác minh
curl -I http://localhosttrả301tới URLhttps://. - Nén: bật gzip, rồi so
Content-Lengthcủa một file CSS khi có và không cóAccept-Encoding: gzip. - proxy_cache: cache
/api/trong 60s, thêmX-Cache-Status, và xem nó chuyểnMISS → HITở request thứ hai. - Giới hạn tốc độ: đặt
/loginthành5r/m, rồi nã nó bằng vòng lặp và xem các 503 xuất hiện. - Nâng cao: thêm cả năm header bảo mật kèm
always, rồi xác minh chúng xuất hiện cả trên response404.