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

Two location blocks are needed to handle PutFS:

# Trailing slash → listing via API
location ~ /$ {
    proxy_pass http://api$request_uri;
}

# Everything else
location / {
    # PUT, DELETE → 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:

  • location ~ /$ catches paths ending in / (e.g. /my-dataset/) and proxies them to the API, which streams back the key listing.
  • location / catches everything else. limit_except GET HEAD means: for any method other than GET/HEAD, proxy to the API. 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.

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 ~ /$ {
            proxy_pass http://api$request_uri;
        }

        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