[WIP] GEP-1619: Define session name conflict resolution rules#4649
[WIP] GEP-1619: Define session name conflict resolution rules#4649gcs278 wants to merge 4 commits into
Conversation
|
Skipping CI for Draft Pull Request. |
59ca705 to
701ab68
Compare
| implementations, such as cloud-based global load balancers, don't have the capability to configure the cookie name. | ||
|
|
||
| The use case for modifying the cookie name using `SessionName` is that certain users might need to align it with an | ||
| **Cookie names MUST be unique** across all session persistence configurations within a Gateway. This includes: |
There was a problem hiding this comment.
I see the note about controllers rejecting and setting a PartiallyInvalid status below, but it this something that can/should be checked earlier in a ValidatingAdmissionPolicy? Should this be considered now that we're actually distributing a VAP alongside the Gateway API CRDs?
There was a problem hiding this comment.
Even if we did do that, controllers will still need to check for it, because users could delete that VAP. I probably don't favor auto-including a VAP for that reason, although we could allow it (via a MAY or similar).
There was a problem hiding this comment.
I'm assuming you mean that implementations can implement their own VAP (rather than including it in a standard VAP that we ship). Added a MAY line about this for clarity.
|
Looks like a good direction and useful clarifications so far to me! |
| Use a single route rule with multiple path matches. The cookie path will be computed as the longest common prefix: | ||
|
|
||
| ```yaml | ||
| rules: | ||
| - matches: | ||
| - path: /api/upload | ||
| - path: /api/download | ||
| sessionPersistence: | ||
| type: Cookie | ||
| cookie: | ||
| name: file-session | ||
| # Cookie path computed as /api (longest common prefix) | ||
| # Result: /api/upload and /api/download share the same session | ||
| ``` |
There was a problem hiding this comment.
Is this a standard way to do this? Any prior art on this kind of thing?
Feels a bit suboptimal that we don't have the computed path shown anywhere, a bit too magical for me.
There was a problem hiding this comment.
Doing some more review: no, I don't think there is prior art for computed cookie path. I did a survey about how existing implementations and dataplanes handle cookie path (prior to GEP-1619), and here's what I found:
| Implementation | API | Cookie Path | Source |
|---|---|---|---|
| Istio | DestinationRule httpCookie.path |
User-configured (default: omitted) | source (L1487) |
| Contour | HTTPProxy loadBalancerPolicy |
Hardcoded / |
source (L717) |
| HAProxy Ingress | Annotations | Hardcoded / |
template (L740); #528 requests it |
| Traefik | IngressRoute sticky.cookie |
User-configured (default: /) |
source (L58) |
| Kong | upstream hash_on_cookie_path |
User-configured (default: /) |
source (L194) |
| NGINX (dataplane) | sticky cookie path= |
User-configured (default: omitted) | docs |
| HAProxy (dataplane) | cookie directive |
Hardcoded / |
docs |
| Envoy (dataplane) | hash_policy cookie.path |
User-configured (default: omitted) | API ref |
So this is a good point. We are paving a new path for...path. There are pros and cons to each option:
Computed path: Scopes cookies more precisely, avoids cookie proliferation, but no prior art, can break when edge proxies rewrite the request path (#4713), and is a bit "magical".
Default /: Matches what most implementations already do (5 of 8), predictable, universally implementable, but broader scope than needed.
Computed path is a more elegant solution, but this is one of those complexity tradeoffs and we can re-evaluate.
Not defaulting and requiring an explicit cookieConfig.path field isn't a great option for users who just want to configure the bare minimum. Alternatively, omitting path entirely (letting the browser set it) feels like bad UX because the browser scopes the cookie to the parent path of the request URL, which potentially breaks shared sessions inside a multi-path route rule.
We can also take this conversation to #4713 - it is very relevant there. Thoughts on these options?
There was a problem hiding this comment.
+1 for defaulting to /
It would also be helpful to allow an optional cookieConfig.path for explicit configuration.
| This is an invalid configuration. Cookie and header names MUST be unique across all route rules, both within a single | ||
| HTTPRoute and across multiple HTTPRoutes serving the same Gateway. If multiple route rules configure session persistence | ||
| with the same cookie or header name, implementations MUST reject the configuration and set the route condition to | ||
| `PartiallyInvalid`. See [Name](#name) for the rationale behind this requirement. |
There was a problem hiding this comment.
Unfortunately, we need to explicitly specify these rules. I assume they are something like:
- Within the same resource, all conflicting rules must be rejected, with the whole Route being rejected if that means there are no valid rules left. Otherwise, a
PartiallyInvalidor similar Condition must be set on the Route in reference to that parentref. (I can't remember what the Reason should be, but that should be specified as well). - Across multiple resources, conflicting rules mean that the Route with the oldest creation time wins for that rule only. For all other Routes with conflicting rules, those rules must be rejected, and the Routes with conflicting, rejected rules MUST have a
PartiallyInvalidCondition added. (This really sucks for implementors, because it means that you have to do per-rule comparisons across the entire Gateway set of attached routes as part of conflict resolution. However, we already specify this for path, so it's not completely unprecedented).
The second one also means that it's possible to have Routes with multiple Rules where some are valid in the older one, and some in the newer one, because the newer one contains non-conflicting rules.
There was a problem hiding this comment.
Good point - I see now that saying "just reject" isn't enough.
However, I realize we can check session name uniqueness within the same xRoute via CEL validation (I see precedent for rule name uniqueness is enforced). That's a win-win - removes the foot gun of accidentally breaking two rules by adding a duplicate cookie name, and implementations don't have to handle this special logic at all. Let me know if you know why CEL won't work.
But we can't use CEL across multiple resources, so I will add those details.
gcs278
left a comment
There was a problem hiding this comment.
@youngnick @mikemorris Thank you for the reviews - my apologies for the gap in responses, but I'm allocating more time to getting this moving again.
Changes are here: 1e39b5d
| implementations, such as cloud-based global load balancers, don't have the capability to configure the cookie name. | ||
|
|
||
| The use case for modifying the cookie name using `SessionName` is that certain users might need to align it with an | ||
| **Cookie names MUST be unique** across all session persistence configurations within a Gateway. This includes: |
There was a problem hiding this comment.
I'm assuming you mean that implementations can implement their own VAP (rather than including it in a standard VAP that we ship). Added a MAY line about this for clarity.
| Use a single route rule with multiple path matches. The cookie path will be computed as the longest common prefix: | ||
|
|
||
| ```yaml | ||
| rules: | ||
| - matches: | ||
| - path: /api/upload | ||
| - path: /api/download | ||
| sessionPersistence: | ||
| type: Cookie | ||
| cookie: | ||
| name: file-session | ||
| # Cookie path computed as /api (longest common prefix) | ||
| # Result: /api/upload and /api/download share the same session | ||
| ``` |
There was a problem hiding this comment.
Doing some more review: no, I don't think there is prior art for computed cookie path. I did a survey about how existing implementations and dataplanes handle cookie path (prior to GEP-1619), and here's what I found:
| Implementation | API | Cookie Path | Source |
|---|---|---|---|
| Istio | DestinationRule httpCookie.path |
User-configured (default: omitted) | source (L1487) |
| Contour | HTTPProxy loadBalancerPolicy |
Hardcoded / |
source (L717) |
| HAProxy Ingress | Annotations | Hardcoded / |
template (L740); #528 requests it |
| Traefik | IngressRoute sticky.cookie |
User-configured (default: /) |
source (L58) |
| Kong | upstream hash_on_cookie_path |
User-configured (default: /) |
source (L194) |
| NGINX (dataplane) | sticky cookie path= |
User-configured (default: omitted) | docs |
| HAProxy (dataplane) | cookie directive |
Hardcoded / |
docs |
| Envoy (dataplane) | hash_policy cookie.path |
User-configured (default: omitted) | API ref |
So this is a good point. We are paving a new path for...path. There are pros and cons to each option:
Computed path: Scopes cookies more precisely, avoids cookie proliferation, but no prior art, can break when edge proxies rewrite the request path (#4713), and is a bit "magical".
Default /: Matches what most implementations already do (5 of 8), predictable, universally implementable, but broader scope than needed.
Computed path is a more elegant solution, but this is one of those complexity tradeoffs and we can re-evaluate.
Not defaulting and requiring an explicit cookieConfig.path field isn't a great option for users who just want to configure the bare minimum. Alternatively, omitting path entirely (letting the browser set it) feels like bad UX because the browser scopes the cookie to the parent path of the request URL, which potentially breaks shared sessions inside a multi-path route rule.
We can also take this conversation to #4713 - it is very relevant there. Thoughts on these options?
| This is an invalid configuration. Cookie and header names MUST be unique across all route rules, both within a single | ||
| HTTPRoute and across multiple HTTPRoutes serving the same Gateway. If multiple route rules configure session persistence | ||
| with the same cookie or header name, implementations MUST reject the configuration and set the route condition to | ||
| `PartiallyInvalid`. See [Name](#name) for the rationale behind this requirement. |
There was a problem hiding this comment.
Good point - I see now that saying "just reject" isn't enough.
However, I realize we can check session name uniqueness within the same xRoute via CEL validation (I see precedent for rule name uniqueness is enforced). That's a win-win - removes the foot gun of accidentally breaking two rules by adding a duplicate cookie name, and implementations don't have to handle this special logic at all. Let me know if you know why CEL won't work.
But we can't use CEL across multiple resources, so I will add those details.
1e39b5d to
1a3b58f
Compare
1a3b58f to
754a8b0
Compare
353447e to
002a7d0
Compare
002a7d0 to
1f24567
Compare
1f24567 to
cf86b00
Compare
| // +listType=atomic | ||
| // <gateway:experimental:validation:XValidation:message="Rule name must be unique within the route",rule="self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name))"> | ||
| // <gateway:experimental:validation:XValidation:message="Session persistence cookie name must be unique across all rules within the route",rule="self.filter(l, has(l.sessionPersistence) && has(l.sessionPersistence.cookie) && has(l.sessionPersistence.cookie.name)).all(l1, self.exists_one(l2, has(l2.sessionPersistence) && has(l2.sessionPersistence.cookie) && has(l2.sessionPersistence.cookie.name) && l1.sessionPersistence.cookie.name == l2.sessionPersistence.cookie.name))"> | ||
| // <gateway:experimental:validation:XValidation:message="Session persistence header name must be unique across all rules within the route",rule="self.filter(l, has(l.sessionPersistence) && has(l.sessionPersistence.header) && has(l.sessionPersistence.header.name)).all(l1, self.exists_one(l2, has(l2.sessionPersistence) && has(l2.sessionPersistence.header) && has(l2.sessionPersistence.header.name) && l1.sessionPersistence.header.name == l2.sessionPersistence.header.name))"> |
There was a problem hiding this comment.
Nit: This uniqueness CEL rule compares header names with ==. should we normalize the headers to avoid mixed-case duplicates?
There was a problem hiding this comment.
Yes - good catch - I'll fix on my next round of updates.
| // | ||
| // +optional | ||
| // +kubebuilder:validation:MaxLength=128 | ||
| Name *string `json:"name,omitempty"` |
There was a problem hiding this comment.
I’m not sure cookie.name / header.name uniqueness should be a hard MUST across all route rules. This seems to prohibit intentional session sharing between routes, for example two route rules that deliberately use the same cookie or header name so clients keep the same backend affinity across related paths. For cookies, name alone is also not the full browser cookie identity because path/domain affect whether sessions are actually shared.
Would it make sense to soften this to a recommendation, or limit it to cases where duplicate names introduce ambiguity or conflicting behavior, rather than applying it to all duplicates? This would let Gateway users decide how cookies should be shared, without constraining how Routes and RouteRules are structured.
There was a problem hiding this comment.
#4268 (comment) suggested to use the following for shared session.
kind: HTTPRoute
spec:
rules:
- matches:
- path:
type: PathPrefix
value: /cart
- path:
type: PathPrefix
value: /checkout
backendRefs:
- name: svc-a
port: 80
sessionPersistence:
name: shop-session
However, this may unnecessarily limit the flexibility of how Routes / RouteRules can be constructed. A single rule with multiple matches works when all matched paths have identical routing behavior, but it does not cover some cases, for example, where paths need different rule-level behavior while intentionally sharing stickiness. For example:
kind: HTTPRoute
spec:
hostnames: ["shop.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /cart
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Flow
value: cart
backendRefs:
- name: shop-app
port: 80
sessionPersistence:
type: Cookie
cookie:
name: shop-session
- matches:
- path:
type: PathPrefix
value: /checkout
filters:
- type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: X-Flow
value: checkout
backendRefs:
- name: shop-app
port: 80
sessionPersistence:
type: Cookie
cookie:
name: shop-session
Or where different HTTPRoutes are needed for separate policy attachment.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-public
spec:
hostnames: ["shop.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /products
backendRefs:
- name: shop-app
port: 80
sessionPersistence:
type: Cookie
cookie:
name: shop-session
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: shop-account
spec:
hostnames: ["shop.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /account
backendRefs:
- name: shop-app
port: 80
sessionPersistence:
type: Cookie
cookie:
name: shop-session
---
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: account-oidc
spec:
targetRef:
kind: HTTPRoute
name: shop-account
oidc:
provider:
issuer: https://idp.example.com/realms/shop
clientID: shop-web
clientSecret:
name: shop-oidc-client-secretThere was a problem hiding this comment.
Right, the cookie tuple is (name, domain, path). I agree that enforcing unique session names limits the ability to share sessions for route-inline session persistence.
For softening, I suppose you are suggesting that we allow duplicate session names for route rules that:
- Target the same service
- Have the same session persistence configuration
If you don't target the same service (# 1), then you have a collision with potential breakage (encoded pod IP is not common between services). If you don't have the same configuration (# 2), then you get a thrashing cookie configuration (set-cookies fighting).
It's valid - but the tradeoff is:
- It feels a bit magical with implicit behavior - applying two same name sessions shares them
- Complexity increase for implemenations that have to compare API configurations for conflicts.
Alternatively, I suppose we could go hands off - and don't do any validation. That'd be the most simple for implementation, while allowing sharing. The tradeoff there, is that users now have foot guns for setting conflicting configuration.
For context - originally, I designed the uniqueness requirement with the assumption that session sharing would be handled by BackendTrafficPolicy, where it works naturally without nearly as much collision concerns (sharing sessions across services doesn't make as much sense like sharing across routes to the same service). However, supporting both attachment points simultaneously in the API is becoming hard to justify.
There's a broader discussion about this in #4462 - would love your input there if you get a chance. If BackendTrafficPolicy is the right end solution - then our debates on conflicts get much simpler.
Did I capture your suggestion correctly?
gcs278
left a comment
There was a problem hiding this comment.
@zhaohuabing thanks for the comments, really appreciated! Feel free to keep the discussion going - I'm going to hold on any major changes until I get more input from the community on the direction we are taking.
| // | ||
| // +optional | ||
| // +kubebuilder:validation:MaxLength=128 | ||
| Name *string `json:"name,omitempty"` |
There was a problem hiding this comment.
Right, the cookie tuple is (name, domain, path). I agree that enforcing unique session names limits the ability to share sessions for route-inline session persistence.
For softening, I suppose you are suggesting that we allow duplicate session names for route rules that:
- Target the same service
- Have the same session persistence configuration
If you don't target the same service (# 1), then you have a collision with potential breakage (encoded pod IP is not common between services). If you don't have the same configuration (# 2), then you get a thrashing cookie configuration (set-cookies fighting).
It's valid - but the tradeoff is:
- It feels a bit magical with implicit behavior - applying two same name sessions shares them
- Complexity increase for implemenations that have to compare API configurations for conflicts.
Alternatively, I suppose we could go hands off - and don't do any validation. That'd be the most simple for implementation, while allowing sharing. The tradeoff there, is that users now have foot guns for setting conflicting configuration.
For context - originally, I designed the uniqueness requirement with the assumption that session sharing would be handled by BackendTrafficPolicy, where it works naturally without nearly as much collision concerns (sharing sessions across services doesn't make as much sense like sharing across routes to the same service). However, supporting both attachment points simultaneously in the API is becoming hard to justify.
There's a broader discussion about this in #4462 - would love your input there if you get a chance. If BackendTrafficPolicy is the right end solution - then our debates on conflicts get much simpler.
Did I capture your suggestion correctly?
| // +listType=atomic | ||
| // <gateway:experimental:validation:XValidation:message="Rule name must be unique within the route",rule="self.all(l1, !has(l1.name) || self.exists_one(l2, has(l2.name) && l1.name == l2.name))"> | ||
| // <gateway:experimental:validation:XValidation:message="Session persistence cookie name must be unique across all rules within the route",rule="self.filter(l, has(l.sessionPersistence) && has(l.sessionPersistence.cookie) && has(l.sessionPersistence.cookie.name)).all(l1, self.exists_one(l2, has(l2.sessionPersistence) && has(l2.sessionPersistence.cookie) && has(l2.sessionPersistence.cookie.name) && l1.sessionPersistence.cookie.name == l2.sessionPersistence.cookie.name))"> | ||
| // <gateway:experimental:validation:XValidation:message="Session persistence header name must be unique across all rules within the route",rule="self.filter(l, has(l.sessionPersistence) && has(l.sessionPersistence.header) && has(l.sessionPersistence.header.name)).all(l1, self.exists_one(l2, has(l2.sessionPersistence) && has(l2.sessionPersistence.header) && has(l2.sessionPersistence.header.name) && l1.sessionPersistence.header.name == l2.sessionPersistence.header.name))"> |
There was a problem hiding this comment.
Yes - good catch - I'll fix on my next round of updates.
cf86b00 to
f34055d
Compare
If we clarify that |
|
Hi @gcs278, just checking in — is there anything blocking this PR? We’re hoping the default cookie path / in this PR can help unblock the edge proxy rewrite use case in Envoy Gateway. |
|
Hey @zhaohuabing - yea recently there has been discussions about Backend (#4488) - which is being considered as an alternative to policy attachment. I was waiting for those discussions to finalize before making too many more modifications to this GEP. It sounds like there's general agreement on Session Persistence going experimental in Backend and also agreement that it shouldn't live in multiple places (e.g. route inline, backend, and BackendTrafficPolicy). I'm going to try to finalize the direction on that soon. I'll be advocating for Backend, but I need to discuss with the implementations that already shipped it as experimental in route-inline first. I'm planning to refactor this draft GEP soon - slim it down and address only a couple things at a time. Stay tuned. |
Cookie and header names must be unique across all route rules. Implementations must reject duplicates with PartiallyInvalid condition. Adds rationale and examples.
Add CEL validation for session name uniqueness within xRoutes, specify conflict resolution rules and status conditions for cross-resource conflicts.
Header names are case-insensitive per RFC 7230. Use lowerAscii() in the CEL validation to catch mixed-case duplicates. Cookie names remain case-sensitive per RFC 6265.
c06a348 to
20209a3
Compare
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: gcs278 The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
🚧 WIP (Depends on #4870, #4873, #4876).
What type of PR is this?
/kind gep
What this PR does / why we need it:
This PR updates GEP-1619 to define session name conflict resolution rules, resolving ambiguities documented in #4268.
Context: Design Doc
Require unique cookie and header names
Cookie and header names MUST be unique across all session persistence configurations within a Gateway. This includes across route rules, across BackendTrafficPolicies, and between route-level and BackendTrafficPolicy configurations. Within the same xRoute, uniqueness is enforced by CEL validation at admission time. Across xRoutes, the oldest route wins (
PartiallyInvalid/UnsupportedValue). Across BackendTrafficPolicies, the oldest policy wins (Accepted: False/Conflicted).Which issue(s) this PR fixes:
Fixes #4268
Does this PR introduce a user-facing change?: