Skip to content

Nginx configuration

The core routing logic that makes PutFS work. Nginx decides what to handle itself (reads) and what to proxy to the API (writes, listings).

Routing

A single location block handles PutFS – the split is purely by method:

location / {
    # PUT, DELETE, QUERY → proxy to API (raw $request_uri preserves percent-encoding)
    limit_except GET HEAD {
        proxy_pass http://api$request_uri;
    }
    # GET, HEAD → serve directly from disk
    root /data;
    try_files $uri =404;
}

How it works:

  • limit_except GET HEAD means: for any method other than GET/HEAD, proxy to the API. That covers PUT, DELETE and the QUERY listing method – GET means "get a file", QUERY means "list", so nginx needs no URI-shape rules (QUERY is not named anywhere; it proxies simply by not being GET/HEAD).
  • GET and HEAD fall through to try_files, which serves the file directly from disk via sendfile – Python is never involved in the read path. A GET on a trailing-slash path is simply not a file and answers 404 right here (the bare API would answer 405 + Allow: QUERY).

Forward $request_uri, not $uri

$uri is nginx's normalised, percent-decoded form. Forwarding it to the upstream breaks any path containing characters that need encoding in the HTTP request line – literal spaces, unicode, etc. (the proxied request line becomes malformed and the upstream returns 400). Forward $request_uri (raw, percent-encoded) so the upstream sees the URI as the client sent it. Auth still operates on $uri (so .. and %2F.. variants resolve to the same canonical form), and safe_path on the Python side rejects any .. segment that survived the round-trip. The two together close the auth-bypass class without breaking encoded paths.

Minimal working config

worker_processes auto;
events {}
http {
    include mime.types;
    default_type application/octet-stream;
    sendfile on;
    access_log off;

    upstream api {
        server unix:/run/putfs/putfs.sock;
    }

    server {
        listen 80;

        location / {
            limit_except GET HEAD {
                proxy_pass http://api$request_uri;
            }
            root /data;
            try_files $uri =404;
        }
    }
}

This is unauthenticated and unhardened. For production, add the items below.

What to add

Concern Where
Authentication Auth – map-based key+secret with path/method scoping, or auth_request
Stored-XSS hardening Static file servingContent-Disposition: attachment, nosniff, CSP, type reset. Mandatory unless storage runs on its own dedicated origin.
Upload size limits client_max_body_size 0; in the server block (default is 1MB)
Upload streaming proxy_request_buffering off; – avoids double disk I/O
Rate limiting limit_req_zone – see Nginx tuning
Timeouts client_body_timeout 300s; proxy_read_timeout 300s; for large files
Performance Nginx tuning – keepalive, gzip, high-concurrency settings