Back to Rustfs

Running RustFS behind a reverse proxy

docs/operations/reverse-proxy.md

1.0.0-rc.17.2 KB
Original Source

Running RustFS behind a reverse proxy

RustFS speaks plain S3 over HTTP/1.1 and HTTP/2 and works behind reverse proxies (Caddy, Nginx, HAProxy) and CDNs (Cloudflare). Most proxy problems are not RustFS storage bugs — the same request sent directly to :9000 succeeds, while the proxied request fails. This page documents the request semantics RustFS expects from the proxy layer and gives known-good configurations.

Rule of thumb: if a request works against http://<host>:9000 directly but fails through the proxy, the fault is in the proxy/CDN request forwarding, not in RustFS object handling. Use the checklist below to find which forwarding behavior broke.

What RustFS requires from the proxy

S3 clients sign requests with AWS SigV4. RustFS (via s3s) re-derives the signature from the forwarded request, and streams the request body to storage. For this to succeed the proxy must forward the request byte-for-byte with respect to the signed material and the body:

  1. Do not alter the body. No transparent compression, no re-encoding, no truncation. If the client sent Content-Length: N, exactly N body bytes must reach RustFS. If fewer bytes arrive, RustFS waits for the rest per the HTTP spec and the request appears to hang until the client aborts.
  2. Do not rewrite signed headers. Host and any x-amz-* / signed headers must reach RustFS unchanged. Rewriting Host is fine only if the client signed with that same host.
  3. Preserve Content-Length; avoid re-chunking large bodies. Some CDNs drop Content-Length and switch to Transfer-Encoding: chunked, or buffer the whole request body before forwarding — both change the timing and framing RustFS sees.
  4. Keep upstream idle keep-alive shorter than RustFS's, or vice-versa (see next section) so the proxy never reuses a connection RustFS has already closed.
  5. Do not strip ETag from responses (breaks multipart completion).

Idle keep-alive: the #1 cause of socket hang up on writes

RustFS closes idle upstream HTTP/1.1 keep-alive connections after RUSTFS_HTTP1_HEADER_READ_TIMEOUT seconds (default 75s; see crates/config/src/constants/tls.rs). Reverse proxies keep a pool of upstream connections and reuse them. If the proxy's upstream idle-keepalive window is longer than RustFS's timeout, the proxy can pick a connection that RustFS has already FIN'd, write a request onto the dead socket, and the client sees:

TimeoutError: socket hang up          # ECONNRESET
AbortError: Request aborted

This is most visible on large PutObject uploads because:

  • PUT is non-idempotent, so proxies will not transparently retry it; and
  • a larger body keeps the connection in use longer, widening the race window, so small uploads on the same path often succeed.

Fix — make the two windows agree

Pick either side; doing both is safest:

  • RustFS side: keep RUSTFS_HTTP1_HEADER_READ_TIMEOUT (default 75s) above the proxy's upstream idle-keepalive. To harden slowloris protection on a directly-exposed node instead, lower it — but then also lower the proxy keepalive below it.
  • Proxy side: lower the proxy's upstream idle-keepalive below RustFS's timeout, or disable upstream keep-alive entirely.

Known-good Caddy configuration

caddy
your-domain.example.com {
	reverse_proxy http://127.0.0.1:9000 {
		transport http {
			# Talk HTTP/1.1 to RustFS.
			versions 1.1

			# Keep the proxy's upstream idle-keepalive BELOW RustFS's
			# RUSTFS_HTTP1_HEADER_READ_TIMEOUT (default 75s) so Caddy never
			# reuses a connection RustFS already closed. Set to 0 to disable
			# upstream keep-alive entirely (simplest, slightly less efficient).
			keepalive 30s
			keepalive_idle_conns_per_host 0

			# Never let the proxy compress/transform the request body.
			compression off

			# Generous timeouts for multi-MB single-request PUTs.
			dial_timeout 30s
			read_timeout 300s
			write_timeout 300s
		}

		# Forward the body untouched; do not negotiate compression upstream.
		header_up Accept-Encoding identity

		# Preserve the host the client signed with.
		header_up Host {upstream_hostport}

		# Stream immediately instead of buffering.
		flush_interval -1
	}
}

Nginx equivalent (essentials)

nginx
location / {
    proxy_pass http://127.0.0.1:9000;
    proxy_http_version 1.1;

    # Nginx default upstream keepalive is 60s; keep it under RustFS's 75s.
    # (set `keepalive` in the matching `upstream {}` block)
    proxy_set_header Connection "";

    proxy_set_header Host $host;
    proxy_set_header Accept-Encoding "identity";

    # Do not buffer/limit large uploads.
    proxy_request_buffering off;
    client_max_body_size 0;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
}

Cloudflare (orange-cloud) caveats

Cloudflare's proxy (orange cloud) may buffer the entire request body before forwarding, and can rewrite requests to Transfer-Encoding: chunked, dropping the client's Content-Length. Symptoms match this pattern exactly: tiny uploads succeed, larger uploads fail with socket hang up.

  • For large object writes, prefer DNS-only (grey cloud) for the S3 endpoint, or a Cloudflare plan/tunnel configuration that does not buffer/re-chunk the request body.
  • Force Accept-Encoding: identity so nothing in the path negotiates compression (see issues #609, #1492).
  • Ensure Content-Length reaches RustFS; disable chunked re-encoding in tunnel settings (see issue #934).

Diagnosis checklist

Run each step and note where behavior diverges:

  1. Bypass the proxy. Send the failing request to http://<host>:9000 directly. Success here confirms the fault is in the proxy/CDN path.
  2. Bypass the CDN, keep the proxy. Point the proxy straight at the origin (Cloudflare grey cloud / direct DNS). If it now works, the CDN was buffering/re-chunking the body.
  3. Check idle reuse. If failures are intermittent and correlate with upload size, it is almost always the keep-alive mismatch above. Lower the proxy keepalive (or disable it) and retry.
    • If instead the upload hangs indefinitely (rather than resetting), the proxy is likely forwarding a partial body and then going silent without closing the connection. RustFS bounds this wait with RUSTFS_HTTP_REQUEST_BODY_READ_TIMEOUT (default 300s; 0 disables) and, on timeout, logs a put_object_body_read_stalled event with the received/expected byte counts — grep the server log for it to confirm a truncated-body forwarding problem.
  4. Compare bytes. Confirm the proxy forwards exactly Content-Length body bytes with no compression/transformation.
  5. Confirm signed headers survive. Host and x-amz-* headers must reach RustFS unchanged; a SignatureDoesNotMatch (rather than a hang) points here.
  • #3076 — Large single-request PutObject fails behind Caddy (this document)
  • #609 — Bucket inaccessible via Cloudflare proxied DNS (Accept-Encoding)
  • #1492 — SigV4 SignatureDoesNotMatch on Cloudflare tunnel (Accept-Encoding)
  • #934 — Console fails behind Cloudflare tunnels (chunked / Content-Length)
  • #1766 — Large multipart upload fails through Nginx (ETag stripping)