jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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_cache và 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

  1. HTTPS cục bộ: tạo chứng chỉ tự ký, phục vụ https://localhost, và xác nhận bằng curl -k -I rằng bạn nhận HTTP/2 200.
  2. Ép HTTPS: thêm block redirect cổng 80 và xác minh curl -I http://localhost trả 301 tới URL https://.
  3. Nén: bật gzip, rồi so Content-Length của một file CSS khi có và không có Accept-Encoding: gzip.
  4. proxy_cache: cache /api/ trong 60s, thêm X-Cache-Status, và xem nó chuyển MISS → HIT ở request thứ hai.
  5. Giới hạn tốc độ: đặt /login thành 5r/m, rồi nã nó bằng vòng lặp và xem các 503 xuất hiện.
  6. 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 response 404.