Giải pháp · 03/10/2026

NGINX X-Accel-Redirect thực chiến: Tải file riêng tư an toàn

Khi ứng dụng tự đọc rồi stream một file vài gigabyte, mỗi lượt tải giữ worker, bộ nhớ và kết nối của runtime trong thời gian dài. X-Accel-Redirect tách đúng trách nhiệm: ứng dụng xác thực và quyết định quyền truy cập, còn NGINX truyền file từ vùng lưu trữ riêng tư.

NGINX X-Accel-Redirect thực chiến: Tải file riêng tư an toàn

Khi ứng dụng tự đọc rồi stream một file vài gigabyte, mỗi lượt tải giữ worker, bộ nhớ và kết nối của runtime trong thời gian dài. X-Accel-Redirect tách đúng trách nhiệm: ứng dụng xác thực và quyết định quyền truy cập, còn NGINX truyền file từ vùng lưu trữ riêng tư.

Bài này xây dựng luồng tải file production với location internal, alias, header phản hồi từ backend, tên file an toàn, Range request, giới hạn tốc độ, logging và kiểm thử. Cơ chế được đối chiếu với tài liệu NGINX chính thức về internal request, alias, sendfile và xử lý X-Accel header.

1. Kiến trúc: ứng dụng giữ quyền, NGINX giữ đường truyền

  1. Client gọi endpoint nghiệp vụ, ví dụ GET /downloads/8f2....
  2. Ứng dụng xác thực user, tải bản ghi file và kiểm tra tenant, gói dịch vụ, trạng thái hoặc thời hạn.
  3. Nếu bị từ chối, ứng dụng trả 401/403/404 như bình thường.
  4. Nếu được phép, ứng dụng trả một response nhỏ có header X-Accel-Redirect trỏ tới URI nội bộ.
  5. NGINX thực hiện internal redirect, ánh xạ URI tới file và gửi nội dung cho client.

Client không nhận đường dẫn filesystem và không cần URL public tới kho file. Ứng dụng vẫn là nơi duy nhất quyết định quyền; NGINX chỉ thực thi quyết định cho request đó.

2. Cấu hình NGINX tối thiểu

server {
    listen 443 ssl;
    server_name files.example.com;

    location / {
        proxy_pass http://app;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Request-Id $request_id;
    }

    location /_protected/ {
        internal;
        alias /srv/private-downloads/;
        sendfile on;

        add_header Cache-Control "private, no-store" always;
        add_header X-Content-Type-Options "nosniff" always;
    }
}

Với cấu hình trên, URI nội bộ /_protected/tenant-7/report.pdf ánh xạ thành /srv/private-downloads/tenant-7/report.pdf. Dấu gạch chéo cuối của cả location và alias phải nhất quán. Directive internal khiến request trực tiếp từ client tới URI này nhận 404, nhưng internal redirect sinh bởi X-Accel-Redirect vẫn hợp lệ.

3. Backend chỉ trả quyết định và metadata

// Pseudocode PHP/Laravel-style
function download(string $publicId, User $user): Response
{
    $file = FileRecord::findByPublicId($publicId);

    if (!$file || !$user->can('download', $file)) {
        abort(404); // Không tiết lộ file có tồn tại hay không.
    }

    $internalUri = '/_protected/' . $file->storage_key;
    $filename = safeDownloadName($file->display_name);

    return response('', 200)
        ->header('X-Accel-Redirect', $internalUri)
        ->header('Content-Type', $file->mime_type)
        ->header('Content-Disposition',
            'attachment; filename="download"; filename*=UTF-8\'\''
            . rawurlencode($filename));
}

storage_key phải do server tạo và lấy từ database, chẳng hạn tenant-7/9a/9a71...bin. Không ghép trực tiếp tên file, query parameter hay đường dẫn tuyệt đối do client gửi vào X-Accel-Redirect.

4. Chặn path traversal từ thiết kế dữ liệu

Kiểm tra chuỗi ../ là cần thiết nhưng chưa đủ nếu hệ thống còn nhiều kiểu mã hóa, separator hoặc symlink. Thiết kế an toàn hơn là:

  • Client chỉ gửi public ID không mang ý nghĩa đường dẫn.
  • Database ánh xạ ID tới storage key bất biến do server sinh.
  • Storage key chỉ dùng tập ký tự hẹp, không có segment ., .., NUL, backslash hay dấu gạch chéo đầu.
  • File nằm ngoài document root và user chạy NGINX chỉ có quyền đọc cần thiết.
  • Pipeline upload tự sinh tên vật lý; tên gốc chỉ là metadata hiển thị.
  • Nếu cho phép symlink, phải có policy rõ ràng; nếu không, cân nhắc disable_symlinks và kiểm thử chi phí trên filesystem thực tế.

Đừng gửi absolute path trong header. X-Accel-Redirect nhận một URI nội bộ, sau đó NGINX mới ánh xạ URI ấy bằng cấu hình do operator kiểm soát.

5. Content-Disposition không được trở thành header injection

Tên hiển thị có thể chứa dấu tiếng Việt, dấu ngoặc kép hoặc ký tự xuống dòng. Backend phải loại CR/LF và ký tự điều khiển, giới hạn độ dài, tạo fallback ASCII đơn giản, rồi mã hóa tham số filename* theo UTF-8. Không nội suy tên gốc chưa chuẩn hóa vào header.

function safeDownloadName(string $name): string
{
    $name = preg_replace('/[\x00-\x1F\x7F]/u', '', $name) ?? '';
    $name = trim(str_replace(['/', '\\'], '-', $name));
    return mb_substr($name !== '' ? $name : 'download', 0, 180);
}

MIME type cũng nên lấy từ metadata đã xác minh khi upload hoặc từ allowlist. Header X-Content-Type-Options: nosniff giúp trình duyệt không tự suy đoán kiểu nội dung ngoài ý muốn.

6. Range request, resume và file lớn

Khi file được phục vụ như tài nguyên tĩnh, NGINX có thể xử lý byte ranges và các request HEAD theo cơ chế HTTP của nó. Điều này phù hợp với tải tiếp tục, trình phát media và download manager mà không bắt ứng dụng tự viết logic seek/chunk.

sendfile on cho phép NGINX dùng cơ chế sendfile() của hệ điều hành khi phù hợp. Tuy nhiên, hiệu năng thật còn phụ thuộc filesystem, kích thước file, page cache và kernel; hãy benchmark thay vì coi một directive là bảo đảm tuyệt đối.

Quyền phải được kiểm tra ở mỗi request đi qua endpoint nghiệp vụ. Một Range request mới không nên có đường tắt truy cập thẳng location nội bộ.

7. Giới hạn tốc độ và bảo vệ tài nguyên

location /_protected/ {
    internal;
    alias /srv/private-downloads/;
    sendfile on;

    limit_rate_after 10m;
    limit_rate 8m;
}

limit_rate được áp dụng theo request, nên nhiều kết nối đồng thời có thể cộng dồn băng thông. NGINX cũng có thể nhận X-Accel-Limit-Rate từ upstream trong những integration được hỗ trợ, nhưng giá trị động chỉ nên do backend tin cậy quyết định.

Rate limit đường truyền không thay thế giới hạn số lượt authorize, số kết nối, quota tenant hoặc bảo vệ endpoint khỏi bot. Với file rất lớn, cần thêm timeout phù hợp và kế hoạch capacity cho disk/network.

8. Signed URL và one-time token: dùng đúng vai trò

Signed URL hữu ích khi cần chia sẻ link có hạn dùng mà không duy trì session trình duyệt. Token nên ràng buộc ít nhất với file ID, thời hạn, phạm vi hành động và nonce; chữ ký phải được so sánh constant-time. Tuy nhiên, chữ ký hợp lệ không tự động chứng minh user hiện vẫn có quyền nếu chính sách yêu cầu kiểm tra trạng thái tài khoản hay tài liệu.

“One-time download” cần một phép đổi token nguyên tử trong datastore. Dù vậy, mạng có thể đứt sau khi token đã được đổi nhưng trước khi người dùng nhận đủ file. Hãy xác định rõ semantics: token một lần authorize, hay hệ thống cho phép retry trong một cửa sổ ngắn. Không thể suy ra “đã tải hoàn tất” chỉ từ việc backend cấp quyền.

9. Cache và dữ liệu riêng tư

  • Mặc định hợp lý cho tài liệu nhạy cảm là Cache-Control: private, no-store.
  • Nếu cho phép cache, cache key phải bao phủ identity/authorization context hoặc dùng URL bất biến, đã ký và scope đúng.
  • Không để CDN cache một response riêng tư rồi phục vụ chéo user.
  • Nếu file có version, storage key bất biến giúp tránh race khi nội dung bị thay thế giữa lúc authorize và lúc NGINX mở file.
  • Quy định rõ chính sách thu hồi: link đã phát hành còn hiệu lực bao lâu và việc xóa file vật lý diễn ra khi nào.

10. Log đúng: authorization không phải completion

Ứng dụng nên log quyết định cấp quyền với request ID, user/tenant, file ID, policy result và token ID nếu có; tuyệt đối không log secret hoặc signed URL đầy đủ. NGINX access log ghi status cuối, số byte gửi và thời gian request. Ghép hai phía bằng X-Request-Id hoặc correlation ID.

log_format downloads '$request_id $remote_addr $status '
                     '$body_bytes_sent $request_time $uri';

access_log /var/log/nginx/downloads.log downloads;

Một status 200/206 và số byte đã gửi là tín hiệu vận hành, nhưng vẫn cần định nghĩa nghiệp vụ cẩn thận trước khi gọi đó là “người dùng đã tải xong”. Client có thể hủy kết nối, proxy có thể buffer, hoặc file có thể chỉ được lấy một range.

11. Kiểm thử trước khi mở traffic

# URI nội bộ không được truy cập trực tiếp
curl -i https://files.example.com/_protected/tenant-7/example.pdf
# Mong đợi: 404

# Endpoint được authorize
curl -I -H 'Authorization: Bearer ...' \
  https://files.example.com/downloads/8f2...

# Range request
curl -i -H 'Authorization: Bearer ...' \
  -H 'Range: bytes=0-1023' \
  https://files.example.com/downloads/8f2...

Test matrix phải có: anonymous, user sai tenant, file bị thu hồi, token hết hạn, Range hợp lệ/không hợp lệ, HEAD, tên Unicode, file không tồn tại, symlink, concurrent request và client ngắt giữa chừng. Kiểm tra cả response cuối mà client thấy, không chỉ response thô từ application server.

12. Triển khai và rollback an toàn

  1. Tạo thư mục riêng, ownership và permission tối thiểu.
  2. Thêm location nội bộ; chạy nginx -t trước khi reload.
  3. Kiểm thử trực tiếp phải 404 và internal redirect phải tải đúng file trên staging.
  4. Deploy backend sinh storage key an toàn và header metadata.
  5. Mở dần traffic, theo dõi 403/404/5xx, 206, throughput, open files và disk latency.
  6. Giữ feature flag để quay lại đường tải cũ trong cửa sổ chuyển đổi, nhưng không mở public location làm rollback.

Checklist production

  • Ứng dụng kiểm tra identity, tenant và policy trước khi cấp internal URI.
  • Location chứa file có internal và request trực tiếp trả 404.
  • Client không điều khiển filesystem path; storage key do server sinh.
  • File nằm ngoài web root, permission đọc tối thiểu và policy symlink rõ ràng.
  • Tên download được làm sạch CR/LF và mã hóa an toàn trong Content-Disposition.
  • Cache policy phù hợp dữ liệu riêng tư; CDN không phục vụ chéo user.
  • Đã test Range, HEAD, Unicode, revoke, expiry, disconnect và concurrent download.
  • Log ứng dụng và NGINX có correlation ID nhưng không chứa secret.
  • Dashboard theo dõi status, byte, latency, disk/network và file-not-found.

Kết luận

X-Accel-Redirect tạo một ranh giới kiến trúc sạch: backend giữ logic phân quyền, còn NGINX đảm nhiệm việc truyền file. Giá trị không chỉ nằm ở hiệu năng; location internal, storage key do server kiểm soát và file ngoài web root còn giúp giảm bề mặt lộ đường dẫn. Một triển khai production tốt phải xử lý đồng thời path traversal, Content-Disposition, Range, cache, token semantics, logging và rollback — rồi chứng minh bằng kiểm thử từ góc nhìn client.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.