Release v2.15.0
Summary
Hardening at the edge - what the proxy logs, believes, forwards and reveals - plus two opt-in surfaces a deployment had no way to ask for. Every change is additive or default-preserving.
Added
- An opt-in capability-gated admission mode. A closed deployment admits a caller only after a verifier it names accepts a capability the caller presents once; every refusal answers one byte-identical 404, so a wrong capability, an unknown route and an unreachable verifier are indistinguishable. The capability is never stored and never becomes an identity - what is stored is a sealed entry transaction proving a verifier said yes.
Publicremains the default. An entry outlives a provider handshake by default, so a slow sign-in does not come back to a refusal (#81) - An opt-in private management health listener, off until a deployment names a port and bound to loopback unless told otherwise. Liveness consults nothing, so a storage outage cannot restart the fleet; readiness round-trips the data-protection key ring and nothing else, so a deployment whose backends are all down still becomes ready. The two management paths are refused on the public socket, and everything else is refused on the management one (#83)
- Aspire builders for capability-gated admission and the management listener, as new methods so an app host compiled against an earlier package keeps working (#81, #83)
- An opt-in per-service identity verification mode. In
Required, an identity service that cannot give an unambiguous positive denies the request instead of authorizing it, clears the sealed authorization record, the identity cookie and the cached result, and logs a bounded reason code.BestEffortremains the default and behaves exactly as before, denying only on403(#77) - A configurable verification timeout, applied where a missing answer is a denial. An unstated timeout stays unbounded in
BestEffort(#77) - A configurable positive-result cache duration, including a value meaning never cache (#77)
- An explicit trusted-proxy boundary: forwarding headers are believed only from configured addresses or networks, with a configurable forward limit and startup validation that names an unusable entry (#79)
- Aspire builders for identity verification, trusted proxies and the forward limit, as new methods so an app host compiled against an earlier package keeps working (#77, #79)
- A
link-failedpage a deployment can customize (#80)
Changed
- A failed credential-link exchange no longer redirects to the completion path. Only an explicit success signals completion; every failure answers one generic page, identical for all six causes, so the response is not an oracle for whether a provider identity or an application account exists (#80)
- The client address and scheme are resolved once, from normalized connection state. Geo headers are answered empty unless the immediate peer is trusted (#79)
- A deployment that configures no trusted proxies keeps today's behavior and gets a startup warning naming the key that changes it. Restoring the framework default outright would break sign-in behind every ingress, and would silently drop the secure flag from session cookies while sessions kept working (#79)
Requiredverification refuses an authenticated request that resolves no tenant, and a deployment that requires verification without configuring tenant resolution now fails to start rather than starting with the mode inert (#77)
Fixed
- Forwarded identity headers stay within ASCII. A provider display name outside it is encoded per RFC 8187 and announced on a starred sibling header; an ASCII name is forwarded byte for byte. Previously such a name was not mojibake downstream but a hard transport failure - a proxied request never reached the application and identity enrichment failed silently (#84)
- A multi-hop deployment that never set a forward limit previously recorded an address the caller could choose. It now records the peer the connection came from; deployments wanting the original client address should set the forward limit to their real chain depth (#79)
Security
- Invitation capabilities, capability-bearing request paths and raw provider subjects no longer reach any log sink. The offending parameters are gone from the source-generated messages rather than filtered, so passing one is no longer expressible. Downstream response bodies are no longer logged (#78)
- A display name that already looks like an encoded value can no longer carry a line break through to whatever reads the header (#84)
- Waiting on another request's verification is bounded and released when the caller goes away, so an unreachable verifier cannot queue work indefinitely (#77)
- Loopback-only trust states its own boundary rather than inheriting one that a common container setting silently clears, and that setting is now warned about at startup (#79)
- Admission is decided before anything serves a byte, ahead of routing and static files, so a closed deployment does not hand out its sign-in pages, their assets or the list of identity providers it trusts (#81)
- An admission mode that is not recognized closes the deployment instead of opening it. A mode given as a number outside the enum previously bound silently and left the gate inert, so a deployment meant to be closed would have served everyone (#81)
- The management listener decides from the port a socket was accepted on rather than the Host header, which a caller writes (#83)
- No outbound call follows a redirect. A redirect re-sends the body to the host it names, so an endpoint that answered one could turn a call this proxy makes into a request somewhere else carrying what it was sent - the client credentials being verified, the client principal, an invite token or a subject. Each address is configured by the deployment, so a redirect is a non-answer and is now read as failure (#77, #81)
- An entry is admitted only if it carries the values it is meant to prove, so a record sealed for another purpose that happens to read back in the same shape is refused (#81)
- A refusal names no server, and the health paths refuse on the public listener exactly as every other path does, so neither distinguishes a closed deployment from one that is not there (#81, #83)
- A refusal is never stored, and an admitted answer varies by the entry it was admitted with, so an intermediary cannot serve one caller's pages to someone who was never admitted (#81)