From 82c756006bfe2c67ead4e0f6bdc15d14fcd805e5 Mon Sep 17 00:00:00 2001 From: LBF38 Date: Tue, 20 Jan 2026 15:26:05 +0100 Subject: [PATCH] Add support for session-cookie-expires nginx annotation --- .../routing-configuration/kubernetes/ingress-nginx.md | 2 +- pkg/config/dynamic/http_config.go | 4 ++++ pkg/provider/kubernetes/ingress-nginx/annotations.go | 1 + .../fixtures/ingresses/06-ingress-with-sticky.yml | 1 + pkg/provider/kubernetes/ingress-nginx/kubernetes.go | 1 + pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go | 1 + pkg/server/service/loadbalancer/sticky.go | 6 ++++++ pkg/server/service/loadbalancer/sticky_test.go | 8 ++++++-- 8 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index 31a11d6e6..7bbee8b69 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -288,6 +288,7 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/session-cookie-domain` | | | `nginx.ingress.kubernetes.io/session-cookie-samesite` | | | `nginx.ingress.kubernetes.io/session-cookie-max-age` | | +| `nginx.ingress.kubernetes.io/session-cookie-expires` | | ### Load Balancing & Backend @@ -416,7 +417,6 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/server-alias` | | | `nginx.ingress.kubernetes.io/server-snippet` | | | `nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none` | | -| `nginx.ingress.kubernetes.io/session-cookie-expires` | | | `nginx.ingress.kubernetes.io/session-cookie-change-on-failure` | | | `nginx.ingress.kubernetes.io/ssl-ciphers` | | | `nginx.ingress.kubernetes.io/ssl-prefer-server-ciphers` | | diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 32ecff3f6..a209f6b10 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -298,6 +298,10 @@ type Cookie struct { // Domain defines the host to which the cookie will be sent. // More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value Domain string `json:"domain,omitempty" toml:"domain,omitempty" yaml:"domain,omitempty"` + + // Expires defines the number of seconds to add to the current time to calculate the expiration date of the cookie. + // This option is exposed only for the Ingress NGINX provider. + Expires int `json:"-" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"` } // SetDefaults set the default values for a Cookie. diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index 2b5b2f919..e37ee659c 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -33,6 +33,7 @@ type ingressConfig struct { SessionCookieDomain *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-domain"` SessionCookieSameSite *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-samesite"` SessionCookieMaxAge *int `annotation:"nginx.ingress.kubernetes.io/session-cookie-max-age"` + SessionCookieExpires *int `annotation:"nginx.ingress.kubernetes.io/session-cookie-expires"` ServiceUpstream *bool `annotation:"nginx.ingress.kubernetes.io/service-upstream"` diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml index ac56745cc..5390535d1 100644 --- a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/06-ingress-with-sticky.yml @@ -12,6 +12,7 @@ metadata: nginx.ingress.kubernetes.io/session-cookie-domain: "foo.localhost" nginx.ingress.kubernetes.io/session-cookie-samesite: "None" nginx.ingress.kubernetes.io/session-cookie-max-age: "42" + nginx.ingress.kubernetes.io/session-cookie-expires: "42" spec: ingressClassName: nginx diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index 046873d76..2da2bef13 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -555,6 +555,7 @@ func (p *Provider) buildService(namespace string, backend netv1.IngressBackend, HTTPOnly: true, // Default value in Nginx. SameSite: strings.ToLower(ptr.Deref(cfg.SessionCookieSameSite, "")), MaxAge: ptr.Deref(cfg.SessionCookieMaxAge, 0), + Expires: ptr.Deref(cfg.SessionCookieExpires, 0), Path: ptr.To(ptr.Deref(cfg.SessionCookiePath, "/")), Domain: ptr.Deref(cfg.SessionCookieDomain, ""), }, diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index 115693ca9..c48bae016 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -492,6 +492,7 @@ func TestLoadIngresses(t *testing.T) { Domain: "foo.localhost", HTTPOnly: true, MaxAge: 42, + Expires: 42, Path: ptr.To("/foobar"), SameSite: "none", Secure: true, diff --git a/pkg/server/service/loadbalancer/sticky.go b/pkg/server/service/loadbalancer/sticky.go index 380234e18..abd002982 100644 --- a/pkg/server/service/loadbalancer/sticky.go +++ b/pkg/server/service/loadbalancer/sticky.go @@ -9,6 +9,7 @@ import ( "net/http" "strconv" "sync" + "time" "github.com/traefik/traefik/v3/pkg/config/dynamic" ) @@ -27,6 +28,7 @@ type stickyCookie struct { httpOnly bool sameSite http.SameSite maxAge int + expires time.Time path string domain string } @@ -59,6 +61,9 @@ func NewSticky(cookieConfig dynamic.Cookie) *Sticky { if cookieConfig.Path != nil { cookie.path = *cookieConfig.Path } + if cookieConfig.Expires > 0 { + cookie.expires = time.Now().Add(time.Duration(cookieConfig.Expires) * time.Second) + } return &Sticky{ cookie: cookie, @@ -137,6 +142,7 @@ func (s *Sticky) WriteStickyCookie(rw http.ResponseWriter, name string) error { Secure: s.cookie.secure, SameSite: s.cookie.sameSite, MaxAge: s.cookie.maxAge, + Expires: s.cookie.expires, } http.SetCookie(rw, cookie) diff --git a/pkg/server/service/loadbalancer/sticky_test.go b/pkg/server/service/loadbalancer/sticky_test.go index d94992224..55b37c571 100644 --- a/pkg/server/service/loadbalancer/sticky_test.go +++ b/pkg/server/service/loadbalancer/sticky_test.go @@ -4,6 +4,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -103,15 +104,17 @@ func TestSticky_StickyHandler(t *testing.T) { } func TestSticky_WriteStickyCookie(t *testing.T) { - sticky := NewSticky(dynamic.Cookie{ + cookieConfig := dynamic.Cookie{ Name: "test", Secure: true, HTTPOnly: true, SameSite: "none", MaxAge: 42, + Expires: 10, Path: pointer("/foo"), Domain: "foo.com", - }) + } + sticky := NewSticky(cookieConfig) // Should return an error if the handler does not exist. res := httptest.NewRecorder() @@ -133,6 +136,7 @@ func TestSticky_WriteStickyCookie(t *testing.T) { assert.True(t, cookie.HttpOnly) assert.Equal(t, http.SameSiteNoneMode, cookie.SameSite) assert.Equal(t, 42, cookie.MaxAge) + assert.WithinDuration(t, time.Now(), cookie.Expires, time.Duration(cookieConfig.Expires)*time.Second) assert.Equal(t, "/foo", cookie.Path) assert.Equal(t, "foo.com", cookie.Domain) }