feature(cookie-affinity): Add partitioned cookie support (#10428)

This commit is contained in:
avif 2024-04-02 21:39:49 +03:00
parent ecb38de6db
commit c1d3a92fe9
11 changed files with 134 additions and 4 deletions

View file

@ -17,6 +17,7 @@ Session affinity can be configured using the following annotations:
|nginx.ingress.kubernetes.io/session-cookie-domain|Domain that will be set on the cookie|string| |nginx.ingress.kubernetes.io/session-cookie-domain|Domain that will be set on the cookie|string|
|nginx.ingress.kubernetes.io/session-cookie-samesite|`SameSite` attribute to apply to the cookie|Browser accepted values are `None`, `Lax`, and `Strict`| |nginx.ingress.kubernetes.io/session-cookie-samesite|`SameSite` attribute to apply to the cookie|Browser accepted values are `None`, `Lax`, and `Strict`|
|nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none|Will omit `SameSite=None` attribute for older browsers which reject the more-recently defined `SameSite=None` value|`"true"` or `"false"` |nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none|Will omit `SameSite=None` attribute for older browsers which reject the more-recently defined `SameSite=None` value|`"true"` or `"false"`
|nginx.ingress.kubernetes.io/session-cookie-partitioned|Will set `Partitioned` attribute on the cookie|`"true"` or `"false"` (defaults to false)|
|nginx.ingress.kubernetes.io/session-cookie-max-age|Time until the cookie expires, corresponds to the `Max-Age` cookie directive|number of seconds| |nginx.ingress.kubernetes.io/session-cookie-max-age|Time until the cookie expires, corresponds to the `Max-Age` cookie directive|number of seconds|
|nginx.ingress.kubernetes.io/session-cookie-expires|Legacy version of the previous annotation for compatibility with older browsers, generates an `Expires` cookie directive by adding the seconds to the current date|number of seconds| |nginx.ingress.kubernetes.io/session-cookie-expires|Legacy version of the previous annotation for compatibility with older browsers, generates an `Expires` cookie directive by adding the seconds to the current date|number of seconds|
|nginx.ingress.kubernetes.io/session-cookie-change-on-failure|When set to `false` nginx ingress will send request to upstream pointed by sticky cookie even if previous attempt failed. When set to `true` and previous attempt failed, sticky cookie will be changed to point to another upstream.|`true` or `false` (defaults to `false`)| |nginx.ingress.kubernetes.io/session-cookie-change-on-failure|When set to `false` nginx ingress will send request to upstream pointed by sticky cookie even if previous attempt failed. When set to `true` and previous attempt failed, sticky cookie will be changed to point to another upstream.|`true` or `false` (defaults to `false`)|

View file

@ -0,0 +1,49 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cookie-partitioned
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "PARTITIONEDCOOKIENAME"
nginx.ingress.kubernetes.io/session-cookie-secure: "true"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
nginx.ingress.kubernetes.io/session-cookie-partitioned: "true"
spec:
ingressClassName: nginx
rules:
- host: stickyingress-partitioned.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-svc
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cookie-partitioned-false
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "PARTITIONEDFALSECOOKIENAME"
nginx.ingress.kubernetes.io/session-cookie-secure: "true"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
nginx.ingress.kubernetes.io/session-cookie-partitioned: "false"
spec:
ingressClassName: nginx
rules:
- host: stickyingress-partitioned-false.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: http-svc
port:
number: 80

View file

@ -104,6 +104,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/session-cookie-path](#cookie-affinity)|string| |[nginx.ingress.kubernetes.io/session-cookie-path](#cookie-affinity)|string|
|[nginx.ingress.kubernetes.io/session-cookie-samesite](#cookie-affinity)|string|"None", "Lax" or "Strict"| |[nginx.ingress.kubernetes.io/session-cookie-samesite](#cookie-affinity)|string|"None", "Lax" or "Strict"|
|[nginx.ingress.kubernetes.io/session-cookie-secure](#cookie-affinity)|string| |[nginx.ingress.kubernetes.io/session-cookie-secure](#cookie-affinity)|string|
|[nginx.ingress.kubernetes.io/session-cookie-partitioned](#cookie-affinity)|"true" or "false"|
|[nginx.ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"| |[nginx.ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
|[nginx.ingress.kubernetes.io/ssl-passthrough](#ssl-passthrough)|"true" or "false"| |[nginx.ingress.kubernetes.io/ssl-passthrough](#ssl-passthrough)|"true" or "false"|
|[nginx.ingress.kubernetes.io/stream-snippet](#stream-snippet)|string| |[nginx.ingress.kubernetes.io/stream-snippet](#stream-snippet)|string|
@ -192,6 +193,8 @@ Use `nginx.ingress.kubernetes.io/session-cookie-domain` to set the `Domain` attr
Use `nginx.ingress.kubernetes.io/session-cookie-samesite` to apply a `SameSite` attribute to the sticky cookie. Browser accepted values are `None`, `Lax`, and `Strict`. Some browsers reject cookies with `SameSite=None`, including those created before the `SameSite=None` specification (e.g. Chrome 5X). Other browsers mistakenly treat `SameSite=None` cookies as `SameSite=Strict` (e.g. Safari running on OSX 14). To omit `SameSite=None` from browsers with these incompatibilities, add the annotation `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true"`. Use `nginx.ingress.kubernetes.io/session-cookie-samesite` to apply a `SameSite` attribute to the sticky cookie. Browser accepted values are `None`, `Lax`, and `Strict`. Some browsers reject cookies with `SameSite=None`, including those created before the `SameSite=None` specification (e.g. Chrome 5X). Other browsers mistakenly treat `SameSite=None` cookies as `SameSite=Strict` (e.g. Safari running on OSX 14). To omit `SameSite=None` from browsers with these incompatibilities, add the annotation `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true"`.
Use `nginx.ingress.kubernetes.io/session-cookie-partitioned` to apply a `Partitioned` attribute to the sticky cookie.
Use `nginx.ingress.kubernetes.io/session-cookie-expires` to control the cookie expires, its value is a number of seconds until the cookie expires. Use `nginx.ingress.kubernetes.io/session-cookie-expires` to control the cookie expires, its value is a number of seconds until the cookie expires.
Use `nginx.ingress.kubernetes.io/session-cookie-path` to control the cookie path when use-regex is set to true. Use `nginx.ingress.kubernetes.io/session-cookie-path` to control the cookie path when use-regex is set to true.

View file

@ -71,8 +71,8 @@ export LUA_RESTY_CACHE=99e7578465b40f36f596d099b82eab404f2b42ed
# Check for recent changes: https://github.com/openresty/lua-resty-core/compare/v0.1.27...master # Check for recent changes: https://github.com/openresty/lua-resty-core/compare/v0.1.27...master
export LUA_RESTY_CORE=v0.1.28 export LUA_RESTY_CORE=v0.1.28
# Check for recent changes: https://github.com/cloudflare/lua-resty-cookie/compare/f418d77082eaef48331302e84330488fdc810ef4...master # Check for recent changes: https://github.com/utix/lua-resty-cookie/compare/9533f47...master
export LUA_RESTY_COOKIE_VERSION=f418d77082eaef48331302e84330488fdc810ef4 export LUA_RESTY_COOKIE_VERSION=9533f479371663107b515590fc9daf00d61ebf11
# Check for recent changes: https://github.com/openresty/lua-resty-dns/compare/8bb53516e2933e61c317db740a9b7c2048847c2f...master # Check for recent changes: https://github.com/openresty/lua-resty-dns/compare/8bb53516e2933e61c317db740a9b7c2048847c2f...master
export LUA_RESTY_DNS=8bb53516e2933e61c317db740a9b7c2048847c2f export LUA_RESTY_DNS=8bb53516e2933e61c317db740a9b7c2048847c2f
@ -249,8 +249,8 @@ get_src 39baab9e2b31cc48cecf896cea40ef6e80559054fd8a6e440cc804a858ea84d4 \
get_src a77b9de160d81712f2f442e1de8b78a5a7ef0d08f13430ff619f79235db974d4 \ get_src a77b9de160d81712f2f442e1de8b78a5a7ef0d08f13430ff619f79235db974d4 \
"https://github.com/openresty/lua-cjson/archive/$LUA_CJSON_VERSION.tar.gz" "lua-cjson" "https://github.com/openresty/lua-cjson/archive/$LUA_CJSON_VERSION.tar.gz" "lua-cjson"
get_src 5ed48c36231e2622b001308622d46a0077525ac2f751e8cc0c9905914254baa4 \ get_src a404c790553617424d743b82a9f01feccd0d2930b306b370c665ca3b7c09ccb6 \
"https://github.com/cloudflare/lua-resty-cookie/archive/$LUA_RESTY_COOKIE_VERSION.tar.gz" "lua-resty-cookie" "https://github.com/utix/lua-resty-cookie/archive/$LUA_RESTY_COOKIE_VERSION.tar.gz" "lua-resty-cookie"
get_src 573184006b98ccee2594b0d134fa4d05e5d2afd5141cbad315051ccf7e9b6403 \ get_src 573184006b98ccee2594b0d134fa4d05e5d2afd5141cbad315051ccf7e9b6403 \
"https://github.com/openresty/lua-resty-lrucache/archive/$LUA_RESTY_CACHE.tar.gz" "lua-resty-lrucache" "https://github.com/openresty/lua-resty-lrucache/archive/$LUA_RESTY_CACHE.tar.gz" "lua-resty-lrucache"

View file

@ -61,6 +61,9 @@ const (
// This is used to control whether SameSite=None should be conditionally applied based on the User-Agent // This is used to control whether SameSite=None should be conditionally applied based on the User-Agent
annotationAffinityCookieConditionalSameSiteNone = "session-cookie-conditional-samesite-none" annotationAffinityCookieConditionalSameSiteNone = "session-cookie-conditional-samesite-none"
// This is used to set the Partitioned flag on the cookie
annotationAffinityCookiePartitioned = "session-cookie-partitioned"
// This is used to control the cookie change after request failure // This is used to control the cookie change after request failure
annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure" annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure"
@ -141,6 +144,12 @@ var sessionAffinityAnnotations = parser.Annotation{
Risk: parser.AnnotationRiskLow, Risk: parser.AnnotationRiskLow,
Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`, Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`,
}, },
annotationAffinityCookiePartitioned: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation sets the cookie as Partitioned`,
},
annotationAffinityCookieChangeOnFailure: { annotationAffinityCookieChangeOnFailure: {
Validator: parser.ValidateBool, Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeIngress, Scope: parser.AnnotationScopeIngress,
@ -184,6 +193,8 @@ type Cookie struct {
SameSite string `json:"samesite"` SameSite string `json:"samesite"`
// Flag that conditionally applies SameSite=None attribute on cookie if user agent accepts it. // Flag that conditionally applies SameSite=None attribute on cookie if user agent accepts it.
ConditionalSameSiteNone bool `json:"conditional-samesite-none"` ConditionalSameSiteNone bool `json:"conditional-samesite-none"`
// Partitioned flag to be set
Partitioned bool `json:"partitioned"`
} }
type affinity struct { type affinity struct {
@ -241,6 +252,11 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone) klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone)
} }
cookie.Partitioned, err = parser.GetBoolAnnotation(annotationAffinityCookiePartitioned, ing, a.annotationConfig.Annotations)
if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePartitioned)
}
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations) cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations)
if err != nil { if err != nil {
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure) klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure)

View file

@ -82,6 +82,7 @@ func TestIngressAffinityCookieConfig(t *testing.T) {
data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSameSite)] = "Strict" data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSameSite)] = "Strict"
data[parser.GetAnnotationWithPrefix(annotationAffinityCookieChangeOnFailure)] = "true" data[parser.GetAnnotationWithPrefix(annotationAffinityCookieChangeOnFailure)] = "true"
data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSecure)] = "true" data[parser.GetAnnotationWithPrefix(annotationAffinityCookieSecure)] = "true"
data[parser.GetAnnotationWithPrefix(annotationAffinityCookiePartitioned)] = "true"
ing.SetAnnotations(data) ing.SetAnnotations(data)
affin, err := NewParser(&resolver.Mock{}).Parse(ing) affin, err := NewParser(&resolver.Mock{}).Parse(ing)
@ -133,4 +134,8 @@ func TestIngressAffinityCookieConfig(t *testing.T) {
if !nginxAffinity.Cookie.Secure { if !nginxAffinity.Cookie.Secure {
t.Errorf("expected secure parameter set to true but returned %v", nginxAffinity.Cookie.Secure) t.Errorf("expected secure parameter set to true but returned %v", nginxAffinity.Cookie.Secure)
} }
if !nginxAffinity.Cookie.Partitioned {
t.Errorf("expected partitioned parameter set to true but returned %v", nginxAffinity.Cookie.Partitioned)
}
} }

View file

@ -868,6 +868,7 @@ func (n *NGINXController) getBackendServers(ingresses []*ingress.Ingress) ([]*in
ups.SessionAffinity.CookieSessionAffinity.Domain = anns.SessionAffinity.Cookie.Domain ups.SessionAffinity.CookieSessionAffinity.Domain = anns.SessionAffinity.Cookie.Domain
ups.SessionAffinity.CookieSessionAffinity.SameSite = anns.SessionAffinity.Cookie.SameSite ups.SessionAffinity.CookieSessionAffinity.SameSite = anns.SessionAffinity.Cookie.SameSite
ups.SessionAffinity.CookieSessionAffinity.ConditionalSameSiteNone = anns.SessionAffinity.Cookie.ConditionalSameSiteNone ups.SessionAffinity.CookieSessionAffinity.ConditionalSameSiteNone = anns.SessionAffinity.Cookie.ConditionalSameSiteNone
ups.SessionAffinity.CookieSessionAffinity.Partitioned = anns.SessionAffinity.Cookie.Partitioned
ups.SessionAffinity.CookieSessionAffinity.ChangeOnFailure = anns.SessionAffinity.Cookie.ChangeOnFailure ups.SessionAffinity.CookieSessionAffinity.ChangeOnFailure = anns.SessionAffinity.Cookie.ChangeOnFailure
locs := ups.SessionAffinity.CookieSessionAffinity.Locations locs := ups.SessionAffinity.CookieSessionAffinity.Locations

View file

@ -162,6 +162,7 @@ type CookieSessionAffinity struct {
Domain string `json:"domain,omitempty"` Domain string `json:"domain,omitempty"`
SameSite string `json:"samesite,omitempty"` SameSite string `json:"samesite,omitempty"`
ConditionalSameSiteNone bool `json:"conditional_samesite_none,omitempty"` ConditionalSameSiteNone bool `json:"conditional_samesite_none,omitempty"`
Partitioned bool `json:"partitioned,omitempty"`
ChangeOnFailure bool `json:"change_on_failure,omitempty"` ChangeOnFailure bool `json:"change_on_failure,omitempty"`
} }

View file

@ -187,6 +187,9 @@ func (csa1 *CookieSessionAffinity) Equal(csa2 *CookieSessionAffinity) bool {
if csa1.ConditionalSameSiteNone != csa2.ConditionalSameSiteNone { if csa1.ConditionalSameSiteNone != csa2.ConditionalSameSiteNone {
return false return false
} }
if csa1.Partitioned != csa2.Partitioned {
return false
}
return true return true
} }

View file

@ -99,6 +99,7 @@ function _M.set_cookie(self, value)
httponly = true, httponly = true,
samesite = cookie_samesite, samesite = cookie_samesite,
secure = cookie_secure, secure = cookie_secure,
partitioned = self.cookie_session_affinity.partitioned,
} }
if self.cookie_session_affinity.expires and self.cookie_session_affinity.expires ~= "" then if self.cookie_session_affinity.expires and self.cookie_session_affinity.expires ~= "" then

View file

@ -503,6 +503,56 @@ describe("Sticky", function()
end) end)
end) end)
describe("Partitioned settings", function()
local mocked_cookie_new = cookie.new
before_each(function()
reset_sticky_balancer()
end)
after_each(function()
cookie.new = mocked_cookie_new
end)
local function test_set_cookie_with(sticky_balancer_type, expected_path, partitioned, expected_partitioned)
local s = {}
cookie.new = function(self)
local cookie_instance = {
set = function(self, payload)
assert.equal(payload.key, test_backend.sessionAffinityConfig.cookieSessionAffinity.name)
assert.equal(payload.path, expected_path)
assert.equal(payload.domain, nil)
assert.equal(payload.httponly, true)
assert.equal(payload.secure, true)
assert.equal(payload.partitioned, expected_partitioned)
return true, nil
end,
get = function(k) return false end,
}
s = spy.on(cookie_instance, "set")
return cookie_instance, false
end
local b = get_test_backend()
b.sessionAffinityConfig.cookieSessionAffinity.locations = {}
b.sessionAffinityConfig.cookieSessionAffinity.locations["test.com"] = {"/"}
b.sessionAffinityConfig.cookieSessionAffinity.secure = true
b.sessionAffinityConfig.cookieSessionAffinity.partitioned = partitioned
local sticky_balancer_instance = sticky_balancer_type:new(b)
assert.has_no.errors(function() sticky_balancer_instance:balance() end)
assert.spy(s).was_called()
end
it("returns a secure cookie with Partitioned when user specifies partitioned=true", function()
test_set_cookie_with(sticky_balanced, "/", true, true)
end)
it("returns a secure cookie with without Partitioned when user specifies partitioned=false", function()
test_set_cookie_with(sticky_balanced, "/", false, false)
end)
it("returns a secure cookie with without Partitioned when user does not specify partitioned", function()
test_set_cookie_with(sticky_balanced, "/", nil, false)
end)
end)
describe("get_cookie()", function() describe("get_cookie()", function()
describe("legacy cookie value", function() describe("legacy cookie value", function()