0.12.0
·
142 commits
to develop
since this release
Security-focused release with the following improvements and bugfixes.
Important Changes
- Breaking: add
trusted_forwarded_proxiesglobal option. This supports deployments where rpxy runs behind another load balancer or reverse proxy that addsX-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. Whentrusted_forwarded_proxiesis configured with trusted CIDR blocks, rpxy preserves and normalizes forwarding information learned through those trusted proxies, rewrites outgoingX-Forwarded-Forand 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,fastlyandcloudfrontas a built-intrusted_forwarded_proxiesalias and add therpxy-trusted-proxiessnapshot updater command for explicit provider range refreshes. - Breaking: harden
default_appfallback against untrustedHostheaders. When a request matches thedefault_appfallback (i.e., itsHostdoes not match any configuredserver_name), rpxy now force-overwrites the outgoingHostheader with the default app's configuredserver_nameregardless of thekeep_original_host/set_upstream_hostupstream options. In addition, thedefault_appfallback is now strictly limited to plaintext HTTP; TLS requests with an unknown server name are rejected unconditionally (independent ofsni_consistency). - Sticky cookie security attributes. The
Set-Cookieissued by the sticky-session load balancer now always carriesHttpOnlyandSameSite=Lax, and additionally carriesSecurewhen 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,
Secureis 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_proxiesforSecureto be applied; rpxy honorsX-Forwarded-Proto: https(orForwarded: proto=https) only from trusted peers. - Operator requirement. Any proxy listed in
trusted_forwarded_proxiesmust overwrite or normalize incomingX-Forwarded-Protorather than appending a client-supplied value (e.g. Nginxproxy_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 forX-Forwarded-Forchains.
- Applications that previously read rpxy's sticky cookie from JavaScript (
- Breaking: sticky cookie values are now opaque AEAD blobs. Deployments using
load_balance = "sticky"must configure the new globalsticky_cookie_secretoption as an unpadded base64url-encoded 32-byte secret. The default cookie name changed fromrpxy_srv_idtorpxy_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 cookieexpires/Max-Ageattributes; 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.3intentionally 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-Hostas part of the general forwarding-header policy. rpxy no longer forwards a client-suppliedX-Forwarded-Hostvalue as-is; instead it rebuildsX-Forwarded-Hostfrom the original client-visible host, alongside the other authoritativeX-Forwarded-*headers. As withForwarded: 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 mode0700, and existing cache artifacts keep their current modes. Manually provisioned TLS private key files are also checked at load time; rpxy emits awarn!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 ofAuthorization,Cookie, andProxy-Authorizationwith a<redacted>placeholder (header names stay visible). For troubleshooting, redaction can be disabled by setting the environment variableRPXY_UNSAFE_DEBUG_HEADERSto1,true, oryes; the variable is read once at startup and emits awarn!when enabled. Do not leave it enabled in production. The unredacted values still only appear whenRUST_LOG=debug. - Fix: preserve the case of the sticky cookie
pathattribute. The sticky-sessionSet-Cookiepreviously lowercased itspath, which could mis-scope the cookie and silently break stickiness on case-sensitive route paths. Thepathis now emitted verbatim (the cookiedomainis 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_nameas a hostname. Each app'sserver_nameis 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 fromserver_name. Valid hostnames are unaffected, but aserver_namethat 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_ipoption caps the number of concurrent connections from a single source IP, in addition to the existing globalmax_clients, so one source cannot exhaust the connection pool. It defaults to0(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 fromX-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 bymax_clients_per_iptimes[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, ortimeout.client_certandhandshakefailures are logged atwarn!; routine misdirected/scanner cases (no_sni,unknown_sni,acme_no_config) atinfo!. 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 atwarn!, and the never-loaded case (aserver_namethat has not yet loaded successfully since startup, where there is nothing to retain) remains a harderror!; both logs now include the targetserver_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_sizedefault 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_logsetting. 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 tofalse, 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/upstreamhttp::Uristill 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