Skip to content

0.12.0

Choose a tag to compare

@github-actions github-actions released this 03 Jun 01:06
· 142 commits to develop since this release
a476d9d

Security-focused release with the following improvements and bugfixes.

Important Changes

  • Breaking: add trusted_forwarded_proxies global option. This supports deployments where rpxy runs behind another load balancer or reverse proxy that adds X-Forwarded-For, Forwarded, and related forwarding headers, and those headers should be trusted only when the immediate peer is within explicitly trusted proxy ranges. From this version, no proxy is trusted by default, so requests forwarded from rpxy to backend applications are rebuilt from the immediate peer only. When trusted_forwarded_proxies is configured with trusted CIDR blocks, rpxy preserves and normalizes forwarding information learned through those trusted proxies, rewrites outgoing X-Forwarded-For and related headers from that normalized chain, and falls back safely when the incoming forwarding view is malformed, inconsistent, or cannot be represented safely.
  • Add cloudflare, fastly and cloudfront as a built-in trusted_forwarded_proxies alias and add the rpxy-trusted-proxies snapshot updater command for explicit provider range refreshes.
  • Breaking: harden default_app fallback against untrusted Host headers. When a request matches the default_app fallback (i.e., its Host does not match any configured server_name), rpxy now force-overwrites the outgoing Host header with the default app's configured server_name regardless of the keep_original_host / set_upstream_host upstream options. In addition, the default_app fallback is now strictly limited to plaintext HTTP; TLS requests with an unknown server name are rejected unconditionally (independent of sni_consistency).
  • Sticky cookie security attributes. The Set-Cookie issued by the sticky-session load balancer now always carries HttpOnly and SameSite=Lax, and additionally carries Secure when the client-visible request scheme is HTTPS. Operator-visible behavior changes:
    • Applications that previously read rpxy's sticky cookie from JavaScript (document.cookie) will no longer see it.
    • When rpxy itself terminates TLS, Secure is set automatically.
    • When rpxy runs behind an external TLS terminator (ALB, CloudFront, Nginx, HAProxy, etc.), the terminator's address must be listed in trusted_forwarded_proxies for Secure to be applied; rpxy honors X-Forwarded-Proto: https (or Forwarded: proto=https) only from trusted peers.
    • Operator requirement. Any proxy listed in trusted_forwarded_proxies must overwrite or normalize incoming X-Forwarded-Proto rather than appending a client-supplied value (e.g. Nginx proxy_set_header X-Forwarded-Proto $scheme;). Otherwise an attacker upstream of the trusted proxy can spoof the forwarded scheme. ALB and CloudFront satisfy this by default. This is the same operator requirement that 0.12.0 introduced for X-Forwarded-For chains.
  • Breaking: sticky cookie values are now opaque AEAD blobs. Deployments using load_balance = "sticky" must configure the new global sticky_cookie_secret option as an unpadded base64url-encoded 32-byte secret. The default cookie name changed from rpxy_srv_id to rpxy_sticky_token; the old name is no longer treated as rpxy's sticky cookie. The sealed token contains the backend identifier and an expiration timestamp mirrored with the cookie expires / Max-Age attributes; expired, malformed, plaintext, or wrong-secret cookies are ignored and reissued automatically. Rotating the secret intentionally resets sticky-session affinity. Replay remains possible only within the sealed expiration window, so sticky cookies must not be used for authentication decisions.
  • Dependency note: the sticky-cookie AEAD implementation currently pins aes-gcm = 0.11.0-rc.3 intentionally for the 0.11 AEAD nonce-generation API. This pre-release dependency must be re-evaluated, replaced with a final 0.11.x release, or explicitly re-approved before the release dependency freeze.
  • Rebuild X-Forwarded-Host as part of the general forwarding-header policy. rpxy no longer forwards a client-supplied X-Forwarded-Host value as-is; instead it rebuilds X-Forwarded-Host from the original client-visible host, alongside the other authoritative X-Forwarded-* headers. As with Forwarded: host=, this value is observational only and must not be used for security decisions.
  • Harden TLS private key file permissions on Unix-like systems. Newly-created ACME cache files are now created with mode 0600, newly-created ACME cache directories with mode 0700, and existing cache artifacts keep their current modes. Manually provisioned TLS private key files are also checked at load time; rpxy emits a warn! log when any group or other permission bit is set, while still loading the key for backward compatibility.
  • Redact sensitive headers in DEBUG request logs. The debug! line that logs the request to be forwarded now masks the values of Authorization, Cookie, and Proxy-Authorization with a <redacted> placeholder (header names stay visible). For troubleshooting, redaction can be disabled by setting the environment variable RPXY_UNSAFE_DEBUG_HEADERS to 1, true, or yes; the variable is read once at startup and emits a warn! when enabled. Do not leave it enabled in production. The unredacted values still only appear when RUST_LOG=debug.
  • Fix: preserve the case of the sticky cookie path attribute. The sticky-session Set-Cookie previously lowercased its path, which could mis-scope the cookie and silently break stickiness on case-sensitive route paths. The path is now emitted verbatim (the cookie domain is still lowercased). Because the path is bound into the sealed token, sticky cookies issued for a mixed-case path before the upgrade are ignored once and reissued; all-lowercase paths are unaffected.
  • Validate server_name as a hostname. Each app's server_name is now validated at startup and must be a syntactically valid hostname: dot-separated labels of 1-63 characters, each starting and ending with an alphanumeric and otherwise containing only alphanumerics and -, with a total length up to 253 ASCII characters. This is defense-in-depth, in particular for the ACME on-disk paths derived from server_name. Valid hostnames are unaffected, but a server_name that is not a valid hostname (containing path separators, .., wildcards *, underscores _, IPv6 literals, or non-ASCII characters) is now rejected at startup where it was previously accepted (IPv4 literals are still accepted).
  • Add optional per-IP connection limit. A new global max_clients_per_ip option caps the number of concurrent connections from a single source IP, in addition to the existing global max_clients, so one source cannot exhaust the connection pool. It defaults to 0 (disabled), preserving existing behavior. The source IP is the immediate TCP/QUIC peer, or the real client address recovered from an inbound PROXY protocol header; it is not derived from X-Forwarded-For / Forwarded, so the limit is only meaningful when rpxy is the edge or inbound PROXY protocol is enabled (behind a bare L7 load balancer every connection collapses to the balancer's IP). For HTTP/1.1 and HTTP/2 the slot is reserved before the TLS handshake so handshake floods are bounded too; for HTTP/3 it caps QUIC connections per source IP, and a single IP's concurrent HTTP/3 request streams are then bounded by max_clients_per_ip times [experimental.h3] max_concurrent_bidistream.
  • Structured audit logging for TLS / mTLS handshake failures. TLS handshake failures, including mTLS client-certificate verification failures, are now logged as structured records carrying the source IP, the SNI, a stable failure category, and (for negotiation failures) whether the vhost enforces mutual TLS. The category is one of client_cert (a missing or invalid client certificate — the mTLS authentication failure case, determined from the rustls error, not from received TLS alerts), handshake, acceptor, no_sni, unknown_sni, acme_no_config, or timeout. client_cert and handshake failures are logged at warn!; routine misdirected/scanner cases (no_sni, unknown_sni, acme_no_config) at info!. Previously these were logged without the source IP or SNI, and an mTLS verification failure was misreported under a "Failed to build TLS acceptor" message.
  • Retain the last known-good certificate when a hot-reload read fails. During certificate hot-reload, if a configured server_name's certificate or key temporarily fails to read (for example, the file is missing or being rewritten at the moment of reload), rpxy now keeps serving that domain's previously loaded certificate instead of dropping the domain from the active SNI map. Previously a single transient read error took that domain's TLS offline until the next reload cycle that happened to read it successfully. The retained-certificate case is logged at warn!, and the never-loaded case (a server_name that has not yet loaded successfully since startup, where there is nothing to retain) remains a hard error!; both logs now include the target server_name. A domain whose files stay invalid therefore keeps serving its last-good certificate until the process restarts or a later reload succeeds.

Improvement

  • Document that connection_handling_timeout = 0 (the default) means no forced timeout, and recommend a non-zero value in production unless long-lived connections (e.g. WebSocket) are required.
  • Document the HTTP/3 request_max_body_size default of 256 MiB and recommend setting a lower explicit value in production when large uploads are not required.
  • Add an optional global redact_query_in_access_log setting. When enabled, query-string values in the access log (both the request path+query and the upstream URL) are masked as <redacted> while the parameter keys and the path are kept, so URLs that carry tokens or PII (e.g. ?token=..., ?email=...) are not logged verbatim. It defaults to false, preserving the current full-query access-log behavior. Redaction is applied when the access-log record is built, so the record itself does not store the raw query values (the underlying request/upstream http::Uri still holds them in memory during request handling).
  • deps and refactor

What's Changed

  • chore(deps): bump aws-lc-rs from 1.16.1 to 1.16.2 by @dependabot[bot] in #510
  • chore(deps): bump rustls-webpki from 0.103.9 to 0.103.10 by @dependabot[bot] in #511
  • chore(deps): bump toml from 1.0.7+spec-1.1.0 to 1.1.0+spec-1.1.0 by @dependabot[bot] in #512
  • chore(deps): bump sha2 from 0.10.9 to 0.11.0 by @dependabot[bot] in #513
  • chore(deps): bump hyper from 1.8.1 to 1.9.0 by @dependabot[bot] in #515
  • chore(deps): bump toml from 1.1.0+spec-1.1.0 to 1.1.1+spec-1.1.0 by @dependabot[bot] in #514
  • chore(deps): bump toml from 1.1.1+spec-1.1.0 to 1.1.2+spec-1.1.0 by @dependabot[bot] in #516
  • chore(deps): bump tokio from 1.50.0 to 1.51.0 by @dependabot[bot] in #517
  • chore(deps): bump tokio from 1.51.0 to 1.51.1 by @dependabot[bot] in #518
  • chore(deps): bump rustls-webpki from 0.103.10 to 0.103.11 by @dependabot[bot] in #519
  • chore(deps): bump tokio from 1.51.1 to 1.52.0 by @dependabot[bot] in #524
  • chore(deps): bump hyper-rustls from 0.27.8 to 0.27.9 by @dependabot[bot] in #525
  • chore(deps): bump rustls-webpki from 0.103.11 to 0.103.12 by @dependabot[bot] in #526
  • Feat: redesign forwarding header handlings (X-Forwarded-* and Forwarded) by @junkurihara in #531
  • chore(deps): bump actions/github-script from 7 to 9 by @dependabot[bot] in #532
  • chore(deps): bump actions/cache from 4 to 5 by @dependabot[bot] in #533
  • Feat: redesign default app behavior by @junkurihara in #534
  • chore(deps): bump mimalloc from 0.1.48 to 0.1.49 by @dependabot[bot] in #535
  • chore(deps): bump tokio from 1.52.0 to 1.52.1 by @dependabot[bot] in #536
  • chore(deps): bump rustls-webpki from 0.103.12 to 0.103.13 by @dependabot[bot] in #537
  • chore(deps): bump lru from 0.17.0 to 0.18.0 by @dependabot[bot] in #542
  • chore(deps): bump reqwest from 0.13.2 to 0.13.3 by @dependabot[bot] in #543
  • chore(deps): bump rustls from 0.23.39 to 0.23.40 by @dependabot[bot] in #544
  • chore(deps): bump tokio from 1.52.1 to 1.52.2 by @dependabot[bot] in #552
  • chore(deps): bump ubuntu from 24.04 to 26.04 in /docker by @dependabot[bot] in #553
  • Feat: key file permission by @junkurihara in #555
  • Feat: sticky cookie flags by @junkurihara in #556
  • feat: support AEAD-encryoted sticky token for avoiding attacks by @junkurihara in #558
  • chore(deps): bump mimalloc from 0.1.50 to 0.1.51 by @dependabot[bot] in #561
  • chore(deps): bump serde_json from 1.0.149 to 1.0.150 by @dependabot[bot] in #562
  • chore(deps): bump mimalloc from 0.1.51 to 0.1.52 by @dependabot[bot] in #563
  • chore(deps): bump aes-gcm from 0.11.0-rc.3 to 0.11.0-rc.4 by @dependabot[bot] in #566
  • chore(deps): bump reqwest from 0.13.3 to 0.13.4 by @dependabot[bot] in #564
  • chore(deps): bump http from 1.4.0 to 1.4.1 by @dependabot[bot] in #565
  • chore(deps): bump hyper from 1.9.0 to 1.10.0 by @dependabot[bot] in #567
  • chore(deps): bump socket2 from 0.6.3 to 0.6.4 by @dependabot[bot] in #568
  • feat: redact sensitive debug headers by @junkurihara in #571
  • feat: security hardening. sticky cookie path case and server_name validation by @junkurihara in #572
  • feat: per-ip connection limit for avoiding connection exhausting attack by @junkurihara in #573
  • feat: mTLS logging audit by @junkurihara in #574
  • feat: retain last known-good certificate on hot-reload read failure by @junkurihara in #575
  • feat: redact access log query when specified by @junkurihara in #576
  • 0.12.0 by @junkurihara in #577

Full Changelog: 0.11.3...0.12.0