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)
}